357 lines
8.0 KiB
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)
|
|
}
|