Files
ZFile/backend/protocol_ftp.go

357 lines
8.0 KiB
Go

package main
import (
"database/sql"
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strconv"
"strings"
ftpserver "goftp.io/server/v2"
)
func startProtocolServers(cfg Config, db *sql.DB) error {
if cfg.SFTPEnabled {
return fmt.Errorf("SFTP_ENABLED=true is configured, but SFTP server is not implemented")
}
if cfg.FTPEnabled {
ftpSrv, err := buildFTPServer(cfg, db)
if err != nil {
return fmt.Errorf("build ftp server: %w", err)
}
go runFTPServer("FTP", ftpSrv)
}
if cfg.FTPSEnabled {
ftpsSrv, err := buildFTPSServer(cfg, db)
if err != nil {
return fmt.Errorf("build ftps server: %w", err)
}
go runFTPServer("FTPS", ftpsSrv)
}
return nil
}
func runFTPServer(name string, srv *ftpserver.Server) {
log.Printf("%s listening on %s:%d", name, srv.Hostname, srv.Port)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, ftpserver.ErrServerClosed) {
log.Printf("%s server stopped: %v", name, err)
}
}
type ftpUserDriver struct {
db *sql.DB
root string
}
func buildFTPServer(cfg Config, db *sql.DB) (*ftpserver.Server, error) {
return buildFileZFTPServer(ftpServerOptions{
name: "FileZ FTP",
host: cfg.FTPHost,
port: cfg.FTPPort,
publicIP: cfg.FTPPublicIP,
passivePorts: cfg.FTPPassivePorts,
tls: false,
}, db, cfg.StorageRoot)
}
func buildFTPSServer(cfg Config, db *sql.DB) (*ftpserver.Server, error) {
return buildFileZFTPServer(ftpServerOptions{
name: "FileZ FTPS",
host: cfg.FTPSHost,
port: cfg.FTPSPort,
publicIP: cfg.FTPSPublicIP,
passivePorts: cfg.FTPSPassivePorts,
tls: true,
certFile: cfg.FTPSCertFile,
keyFile: cfg.FTPSKeyFile,
explicitFTPS: cfg.FTPSExplicit,
forceTLS: cfg.FTPSForceTLS,
}, db, cfg.StorageRoot)
}
type ftpServerOptions struct {
name string
host string
port int
publicIP string
passivePorts string
tls bool
certFile string
keyFile string
explicitFTPS bool
forceTLS bool
}
func buildFileZFTPServer(opts ftpServerOptions, db *sql.DB, storageRoot string) (*ftpserver.Server, error) {
drv := &ftpUserDriver{db: db, root: storageRoot}
serverOpts := &ftpserver.Options{
Name: opts.name,
Hostname: opts.host,
Port: opts.port,
PublicIP: opts.publicIP,
PassivePorts: opts.passivePorts,
WelcomeMessage: opts.name + " ready",
Driver: drv,
Auth: drv,
Perm: ftpserver.NewSimplePerm("filez", "filez"),
TLS: opts.tls,
CertFile: opts.certFile,
KeyFile: opts.keyFile,
ExplicitFTPS: opts.explicitFTPS,
ForceTLS: opts.forceTLS,
}
return ftpserver.NewServer(serverOpts)
}
func (d *ftpUserDriver) CheckPasswd(ctx *ftpserver.Context, username, password string) (bool, error) {
username = strings.TrimSpace(username)
if username == "" {
return false, nil
}
var userID int64
var hash string
err := d.db.QueryRow(`SELECT id, password_hash FROM users WHERE email = ?`, username).Scan(&userID, &hash)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
return false, err
}
if !verifyPasswordHash(hash, password) {
return false, nil
}
if err := os.MkdirAll(d.userRoot(userID), 0o755); err != nil {
return false, err
}
d.setUserID(ctx, userID)
return true, nil
}
func (d *ftpUserDriver) userRoot(userID int64) string {
return filepath.Join(d.root, strconv.FormatInt(userID, 10))
}
func (d *ftpUserDriver) setUserID(ctx *ftpserver.Context, userID int64) {
if ctx == nil || ctx.Sess == nil {
return
}
if ctx.Sess.Data == nil {
ctx.Sess.Data = make(map[string]interface{})
}
ctx.Sess.Data["filez_user_id"] = userID
}
func (d *ftpUserDriver) userIDFromCtx(ctx *ftpserver.Context) (int64, error) {
if ctx == nil || ctx.Sess == nil {
return 0, fmt.Errorf("missing ftp session")
}
if v, ok := ctx.Sess.Data["filez_user_id"]; ok {
switch id := v.(type) {
case int64:
if id > 0 {
return id, nil
}
case int:
if id > 0 {
return int64(id), nil
}
}
}
username := strings.TrimSpace(ctx.Sess.LoginUser())
if username == "" {
return 0, fmt.Errorf("missing ftp username")
}
var userID int64
if err := d.db.QueryRow(`SELECT id FROM users WHERE email = ?`, username).Scan(&userID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return 0, fmt.Errorf("user not found")
}
return 0, err
}
d.setUserID(ctx, userID)
return userID, nil
}
func (d *ftpUserDriver) fullPath(ctx *ftpserver.Context, rel string) (string, error) {
userID, err := d.userIDFromCtx(ctx)
if err != nil {
return "", err
}
root := d.userRoot(userID)
if err := os.MkdirAll(root, 0o755); err != nil {
return "", err
}
clean := filepath.FromSlash(strings.TrimPrefix(normalizePath(rel), "/"))
full := filepath.Clean(filepath.Join(root, clean))
if full != root && !strings.HasPrefix(full, root+string(os.PathSeparator)) {
return "", fmt.Errorf("invalid path")
}
return full, nil
}
func (d *ftpUserDriver) Stat(ctx *ftpserver.Context, rel string) (os.FileInfo, error) {
full, err := d.fullPath(ctx, rel)
if err != nil {
return nil, err
}
return os.Stat(full)
}
func (d *ftpUserDriver) ListDir(ctx *ftpserver.Context, rel string, callback func(os.FileInfo) error) error {
full, err := d.fullPath(ctx, rel)
if err != nil {
return err
}
entries, err := os.ReadDir(full)
if err != nil {
return err
}
for _, entry := range entries {
info, err := entry.Info()
if err != nil {
continue
}
if err := callback(info); err != nil {
return err
}
}
return nil
}
func (d *ftpUserDriver) DeleteDir(ctx *ftpserver.Context, rel string) error {
if normalizePath(rel) == "/" {
return fmt.Errorf("cannot remove root directory")
}
full, err := d.fullPath(ctx, rel)
if err != nil {
return err
}
st, err := os.Stat(full)
if err != nil {
return err
}
if !st.IsDir() {
return fmt.Errorf("not a directory")
}
return os.RemoveAll(full)
}
func (d *ftpUserDriver) DeleteFile(ctx *ftpserver.Context, rel string) error {
full, err := d.fullPath(ctx, rel)
if err != nil {
return err
}
st, err := os.Stat(full)
if err != nil {
return err
}
if st.IsDir() {
return fmt.Errorf("not a file")
}
return os.Remove(full)
}
func (d *ftpUserDriver) Rename(ctx *ftpserver.Context, fromPath, toPath string) error {
if normalizePath(fromPath) == "/" {
return fmt.Errorf("cannot rename root directory")
}
oldPath, err := d.fullPath(ctx, fromPath)
if err != nil {
return err
}
newPath, err := d.fullPath(ctx, toPath)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(newPath), 0o755); err != nil {
return err
}
return os.Rename(oldPath, newPath)
}
func (d *ftpUserDriver) MakeDir(ctx *ftpserver.Context, rel string) error {
full, err := d.fullPath(ctx, rel)
if err != nil {
return err
}
return os.MkdirAll(full, 0o755)
}
func (d *ftpUserDriver) GetFile(ctx *ftpserver.Context, rel string, offset int64) (int64, io.ReadCloser, error) {
full, err := d.fullPath(ctx, rel)
if err != nil {
return 0, nil, err
}
f, err := os.Open(full)
if err != nil {
return 0, nil, err
}
defer func() {
if err != nil {
_ = f.Close()
}
}()
st, err := f.Stat()
if err != nil {
return 0, nil, err
}
if offset < 0 {
offset = 0
}
if _, err := f.Seek(offset, io.SeekStart); err != nil {
return 0, nil, err
}
sz := st.Size() - offset
if sz < 0 {
sz = 0
}
return sz, f, nil
}
func (d *ftpUserDriver) PutFile(ctx *ftpserver.Context, rel string, data io.Reader, offset int64) (int64, error) {
full, err := d.fullPath(ctx, rel)
if err != nil {
return 0, err
}
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
return 0, err
}
if offset < 0 {
f, err := os.Create(full)
if err != nil {
return 0, err
}
defer f.Close()
return io.Copy(f, data)
}
flags := os.O_CREATE | os.O_WRONLY
if offset == 0 {
flags |= os.O_TRUNC
}
f, err := os.OpenFile(full, flags, 0o644)
if err != nil {
return 0, err
}
defer f.Close()
if _, err := f.Seek(offset, io.SeekStart); err != nil {
return 0, err
}
return io.Copy(f, data)
}