feat: sftp server support (#7643)

* feat: sftp server support

* fix(sftp-server): try fix build failed

* fix: sftp download lack
pull/7655/head
KirCute_ECT 2024-12-12 20:51:43 +08:00 committed by GitHub
parent 201e25c17f
commit 33ba7f1521
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 584 additions and 32 deletions

View File

@ -15,6 +15,7 @@ import (
func Init() {
bootstrap.InitConfig()
bootstrap.Log()
bootstrap.InitHostKey()
bootstrap.InitDB()
data.InitData()
bootstrap.InitIndex()

View File

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
ftpserver "github.com/KirCute/ftpserverlib-pasvportmap"
"github.com/KirCute/sftpd-alist"
"net"
"net/http"
"os"
@ -131,6 +132,24 @@ the address is defined in config file`,
}()
}
}
var sftpDriver *server.SftpDriver
var sftpServer *sftpd.SftpServer
if conf.Conf.SFTP.Listen != "" && conf.Conf.SFTP.Enable {
var err error
sftpDriver, err = server.NewSftpDriver()
if err != nil {
utils.Log.Fatalf("failed to start sftp driver: %s", err.Error())
} else {
utils.Log.Infof("start sftp server on %s", conf.Conf.SFTP.Listen)
go func() {
sftpServer = sftpd.NewSftpServer(sftpDriver)
err = sftpServer.RunServer()
if err != nil {
utils.Log.Fatalf("problem sftp server listening: %s", err.Error())
}
}()
}
}
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 1 second.
quit := make(chan os.Signal, 1)
@ -181,6 +200,15 @@ the address is defined in config file`,
}
}()
}
if conf.Conf.SFTP.Listen != "" && conf.Conf.SFTP.Enable && sftpServer != nil && sftpDriver != nil {
wg.Add(1)
go func() {
defer wg.Done()
if err := sftpServer.Close(); err != nil {
utils.Log.Fatal("SFTP server shutdown err: ", err)
}
}()
}
wg.Wait()
utils.Log.Println("Server exit")
},

12
go.mod
View File

@ -3,6 +3,8 @@ module github.com/alist-org/alist/v3
go 1.22.4
require (
github.com/KirCute/ftpserverlib-pasvportmap v1.25.0
github.com/KirCute/sftpd-alist v0.0.11
github.com/SheltonZhu/115driver v1.0.32
github.com/Xhofe/go-cache v0.0.0-20240804043513-b1a71927bc21
github.com/Xhofe/rateg v0.0.0-20230728072201-251a4e1adad4
@ -60,7 +62,7 @@ require (
github.com/xhofe/tache v0.1.3
github.com/xhofe/wopan-sdk-go v0.1.3
github.com/zzzhr1990/go-common-entity v0.0.0-20221216044934-fd1c571e3a22
golang.org/x/crypto v0.27.0
golang.org/x/crypto v0.30.0
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e
golang.org/x/image v0.19.0
golang.org/x/net v0.28.0
@ -76,7 +78,6 @@ require (
require (
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/KirCute/ftpserverlib-pasvportmap v0.0.0-20241208190057-c9a7bf2571e2 // indirect
github.com/blevesearch/go-faiss v1.0.20 // indirect
github.com/blevesearch/zapx/v16 v16.1.5 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
@ -90,6 +91,7 @@ require (
github.com/hekmon/cunits/v2 v2.1.0 // indirect
github.com/ipfs/boxo v0.12.0 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/taruti/bytepool v0.0.0-20160310082835-5e3a9ea56543 // indirect
)
require (
@ -223,10 +225,10 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.etcd.io/bbolt v1.3.8 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/term v0.24.0 // indirect
golang.org/x/text v0.18.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/tools v0.24.0 // indirect
google.golang.org/api v0.169.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect

27
go.sum
View File

@ -4,8 +4,10 @@ cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2Qx
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/KirCute/ftpserverlib-pasvportmap v0.0.0-20241208190057-c9a7bf2571e2 h1:P3MoQ1kDfbCjL6+MPd5K7wPdKB4nqMuLU6Mv0+tdWDA=
github.com/KirCute/ftpserverlib-pasvportmap v0.0.0-20241208190057-c9a7bf2571e2/go.mod h1:v0NgMtKDDi/6CM6r4P+daCljCW3eO9yS+Z+pZDTKo1E=
github.com/KirCute/ftpserverlib-pasvportmap v1.25.0 h1:ikwCzeqoqN6wvBHOB9OI6dde/jbV7EoTMpUcxtYl5Po=
github.com/KirCute/ftpserverlib-pasvportmap v1.25.0/go.mod h1:v0NgMtKDDi/6CM6r4P+daCljCW3eO9yS+Z+pZDTKo1E=
github.com/KirCute/sftpd-alist v0.0.11 h1:BGInXmmLBI+v6S9WZCwvY0DRK1vDprGNcTv/57p2GSo=
github.com/KirCute/sftpd-alist v0.0.11/go.mod h1:pPFzr6GrKqXvFXLr46ZpoqmtSpwH8DKTYloSp/ybzKQ=
github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd h1:nzE1YQBdx1bq9IlZinHa+HVffy+NmVRoKr+wHN8fpLE=
github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd/go.mod h1:C8yoIfvESpM3GD07OCHU7fqI7lhwyZ2Td1rbNbTAhnc=
github.com/RoaringBitmap/roaring v1.9.3 h1:t4EbC5qQwnisr5PrP9nt0IRhRTb9gMUgQF4t4S2OByM=
@ -492,12 +494,13 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7 h1:Jtcrb09q0AVWe3BGe8qtuuGxNSHWGkTWr43kHTJ+CpA=
github.com/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7/go.mod h1:suDIky6yrK07NnaBadCB4sS0CqFOvUK91lH7CR+JlDA=
github.com/taruti/bytepool v0.0.0-20160310082835-5e3a9ea56543 h1:6Y51mutOvRGRx6KqyMNo//xk8B8o6zW9/RVmy1VamOs=
github.com/taruti/bytepool v0.0.0-20160310082835-5e3a9ea56543/go.mod h1:jpwqYA8KUVEvSUJHkCXsnBRJCSKP1BMa81QZ6kvRpow=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4=
github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=
@ -571,8 +574,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@ -614,8 +617,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -647,8 +650,6 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
@ -661,8 +662,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -676,8 +677,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=

101
internal/bootstrap/ssh.go Normal file
View File

@ -0,0 +1,101 @@
package bootstrap
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"github.com/alist-org/alist/v3/cmd/flags"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/pkg/utils"
"golang.org/x/crypto/ssh"
"os"
"path/filepath"
)
func InitHostKey() {
sshPath := filepath.Join(flags.DataDir, "ssh")
if !utils.Exists(sshPath) {
err := utils.CreateNestedDirectory(sshPath)
if err != nil {
utils.Log.Fatalf("failed to create ssh directory: %+v", err)
return
}
}
conf.SSHSigners = make([]ssh.Signer, 0, 4)
if rsaKey, ok := LoadOrGenerateRSAHostKey(sshPath); ok {
conf.SSHSigners = append(conf.SSHSigners, rsaKey)
}
// TODO Add keys for other encryption algorithms
}
func LoadOrGenerateRSAHostKey(parentDir string) (ssh.Signer, bool) {
privateKeyPath := filepath.Join(parentDir, "ssh_host_rsa_key")
publicKeyPath := filepath.Join(parentDir, "ssh_host_rsa_key.pub")
privateKeyBytes, err := os.ReadFile(privateKeyPath)
if err == nil {
var privateKey *rsa.PrivateKey
privateKey, err = rsaDecodePrivateKey(privateKeyBytes)
if err == nil {
var ret ssh.Signer
ret, err = ssh.NewSignerFromKey(privateKey)
if err == nil {
return ret, true
}
}
}
_ = os.Remove(privateKeyPath)
_ = os.Remove(publicKeyPath)
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
utils.Log.Fatalf("failed to generate RSA private key: %+v", err)
return nil, false
}
publicKey, err := ssh.NewPublicKey(&privateKey.PublicKey)
if err != nil {
utils.Log.Fatalf("failed to generate RSA public key: %+v", err)
return nil, false
}
ret, err := ssh.NewSignerFromKey(privateKey)
if err != nil {
utils.Log.Fatalf("failed to generate RSA signer: %+v", err)
return nil, false
}
privateBytes := rsaEncodePrivateKey(privateKey)
publicBytes := ssh.MarshalAuthorizedKey(publicKey)
err = os.WriteFile(privateKeyPath, privateBytes, 0600)
if err != nil {
utils.Log.Fatalf("failed to write RSA private key to file: %+v", err)
return nil, false
}
err = os.WriteFile(publicKeyPath, publicBytes, 0644)
if err != nil {
_ = os.Remove(privateKeyPath)
utils.Log.Fatalf("failed to write RSA public key to file: %+v", err)
return nil, false
}
return ret, true
}
func rsaEncodePrivateKey(privateKey *rsa.PrivateKey) []byte {
privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey)
privateBlock := &pem.Block{
Type: "RSA PRIVATE KEY",
Headers: nil,
Bytes: privateKeyBytes,
}
return pem.EncodeToMemory(privateBlock)
}
func rsaDecodePrivateKey(bytes []byte) (*rsa.PrivateKey, error) {
block, _ := pem.Decode(bytes)
if block == nil {
return nil, fmt.Errorf("failed to parse PEM block containing the key")
}
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
return privateKey, nil
}

View File

@ -84,6 +84,11 @@ type FTP struct {
EnablePasvConnIPCheck bool `json:"enable_pasv_conn_ip_check" env:"ENABLE_PASV_CONN_IP_CHECK"`
}
type SFTP struct {
Enable bool `json:"enable" env:"ENABLE"`
Listen string `json:"listen" env:"LISTEN"`
}
type Config struct {
Force bool `json:"force" env:"FORCE"`
SiteURL string `json:"site_url" env:"SITE_URL"`
@ -104,6 +109,7 @@ type Config struct {
Cors Cors `json:"cors" envPrefix:"CORS_"`
S3 S3 `json:"s3" envPrefix:"S3_"`
FTP FTP `json:"ftp" envPrefix:"FTP_"`
SFTP SFTP `json:"sftp" envPrefix:"SFTP_"`
}
func DefaultConfig() *Config {
@ -185,5 +191,9 @@ func DefaultConfig() *Config {
EnableActiveConnIPCheck: true,
EnablePasvConnIPCheck: true,
},
SFTP: SFTP{
Enable: true,
Listen: ":5222",
},
}
}

View File

@ -1,6 +1,7 @@
package conf
import (
"golang.org/x/crypto/ssh"
"net/url"
"regexp"
)
@ -32,3 +33,5 @@ var (
ManageHtml string
IndexHtml string
)
var SSHSigners []ssh.Signer

View File

@ -70,7 +70,7 @@ func NewMainDriver() (*FtpMainDriver, error) {
Banner: setting.GetStr(conf.Announcement),
TLSRequired: tlsRequired,
DisableLISTArgs: false,
DisableSite: true,
DisableSite: false,
DisableActiveMode: conf.Conf.FTP.DisableActiveMode,
EnableHASH: false,
DisableSTAT: false,
@ -79,6 +79,9 @@ func NewMainDriver() (*FtpMainDriver, error) {
DefaultTransferType: transferType,
ActiveConnectionsCheck: activeConnCheck,
PasvConnectionsCheck: pasvConnCheck,
SiteHandlers: map[string]ftpserver.SiteHandler{
"SIZE": ftp.HandleSIZE,
},
},
proxyHeader: header,
clients: make(map[uint32]ftpserver.ClientContext),
@ -128,7 +131,7 @@ func (d *FtpMainDriver) AuthUser(cc ftpserver.ClientContext, user, pass string)
}
}
if userObj.Disabled || !userObj.CanFTPAccess() {
return nil, errors.New("user not allowed to access FTP")
return nil, errors.New("user is not allowed to access via FTP")
}
ctx := context.Background()

View File

@ -5,13 +5,15 @@ import (
"errors"
ftpserver "github.com/KirCute/ftpserverlib-pasvportmap"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/fs"
"github.com/spf13/afero"
"os"
"time"
)
type AferoAdapter struct {
ctx context.Context
ctx context.Context
nextFileSize int64
}
func NewAferoAdapter(ctx context.Context) *AferoAdapter {
@ -78,14 +80,36 @@ func (a *AferoAdapter) ReadDir(name string) ([]os.FileInfo, error) {
}
func (a *AferoAdapter) GetHandle(name string, flags int, offset int64) (ftpserver.FileTransfer, error) {
fileSize := a.nextFileSize
a.nextFileSize = 0
if offset != 0 {
return nil, errors.New("offset")
return nil, errs.NotSupport
}
if (flags & os.O_APPEND) > 0 {
return nil, errors.New("append")
if (flags & os.O_SYNC) != 0 {
return nil, errs.NotSupport
}
if (flags & os.O_WRONLY) > 0 {
return OpenUpload(a.ctx, name)
if (flags & os.O_APPEND) != 0 {
return nil, errs.NotSupport
}
_, err := fs.Get(a.ctx, name, &fs.GetArgs{})
exists := err == nil
if (flags&os.O_CREATE) == 0 && !exists {
return nil, errs.ObjectNotFound
}
if (flags&os.O_EXCL) != 0 && exists {
return nil, errors.New("file already exists")
}
if (flags & os.O_WRONLY) != 0 {
trunc := (flags & os.O_TRUNC) != 0
if fileSize > 0 {
return OpenUploadWithLength(a.ctx, name, trunc, fileSize)
} else {
return OpenUpload(a.ctx, name, trunc)
}
}
return OpenDownload(a.ctx, name)
}
func (a *AferoAdapter) SetNextFileSize(size int64) {
a.nextFileSize = size
}

11
server/ftp/const.go Normal file
View File

@ -0,0 +1,11 @@
package ftp
// From leffss/sftpd
const (
SSH_FXF_READ = 0x00000001
SSH_FXF_WRITE = 0x00000002
SSH_FXF_APPEND = 0x00000004
SSH_FXF_CREAT = 0x00000008
SSH_FXF_TRUNC = 0x00000010
SSH_FXF_EXCL = 0x00000020
)

View File

@ -1,6 +1,7 @@
package ftp
import (
"bytes"
"context"
ftpserver "github.com/KirCute/ftpserverlib-pasvportmap"
"github.com/alist-org/alist/v3/internal/conf"
@ -23,29 +24,38 @@ type FileUploadProxy struct {
buffer *os.File
path string
ctx context.Context
trunc bool
}
func OpenUpload(ctx context.Context, path string) (*FileUploadProxy, error) {
func uploadAuth(ctx context.Context, path string) error {
user := ctx.Value("user").(*model.User)
path, err := user.JoinPath(path)
if err != nil {
return nil, err
return err
}
meta, err := op.GetNearestMeta(stdpath.Dir(path))
if err != nil {
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
return nil, err
return err
}
}
if !(common.CanAccess(user, meta, path, ctx.Value("meta_pass").(string)) &&
((user.CanFTPManage() && user.CanWrite()) || common.CanWrite(meta, stdpath.Dir(path)))) {
return nil, errs.PermissionDenied
return errs.PermissionDenied
}
return nil
}
func OpenUpload(ctx context.Context, path string, trunc bool) (*FileUploadProxy, error) {
err := uploadAuth(ctx, path)
if err != nil {
return nil, err
}
tmpFile, err := os.CreateTemp(conf.Conf.TempDir, "file-*")
if err != nil {
return nil, err
}
return &FileUploadProxy{buffer: tmpFile, path: path, ctx: ctx}, nil
return &FileUploadProxy{buffer: tmpFile, path: path, ctx: ctx, trunc: trunc}, nil
}
func (f *FileUploadProxy) Read(p []byte) (n int, err error) {
@ -77,6 +87,9 @@ func (f *FileUploadProxy) Close() error {
if _, err := f.buffer.Seek(0, io.SeekStart); err != nil {
return err
}
if f.trunc {
_ = fs.Remove(f.ctx, f.path)
}
s := &stream.FileStream{
Obj: &model.Object{
Name: name,
@ -84,10 +97,113 @@ func (f *FileUploadProxy) Close() error {
Modified: time.Now(),
},
Mimetype: contentType,
WebPutAsTask: false,
WebPutAsTask: true,
}
s.SetTmpFile(f.buffer)
s.Closers.Add(f.buffer)
_, err = fs.PutAsTask(f.ctx, dir, s)
return err
}
type FileUploadWithLengthProxy struct {
ftpserver.FileTransfer
ctx context.Context
path string
length int64
first512Bytes [512]byte
pFirst int
pipeWriter io.WriteCloser
errChan chan error
}
func OpenUploadWithLength(ctx context.Context, path string, trunc bool, length int64) (*FileUploadWithLengthProxy, error) {
err := uploadAuth(ctx, path)
if err != nil {
return nil, err
}
if trunc {
_ = fs.Remove(ctx, path)
}
return &FileUploadWithLengthProxy{ctx: ctx, path: path, length: length}, nil
}
func (f *FileUploadWithLengthProxy) Read(p []byte) (n int, err error) {
return 0, errs.NotSupport
}
func (f *FileUploadWithLengthProxy) Write(p []byte) (n int, err error) {
if f.pipeWriter != nil {
select {
case e := <-f.errChan:
return 0, e
default:
return f.pipeWriter.Write(p)
}
} else if len(p) < 512-f.pFirst {
copy(f.first512Bytes[f.pFirst:], p)
f.pFirst += len(p)
return len(p), nil
} else {
copy(f.first512Bytes[f.pFirst:], p[:512-f.pFirst])
contentType := http.DetectContentType(f.first512Bytes[:])
dir, name := stdpath.Split(f.path)
reader, writer := io.Pipe()
f.errChan = make(chan error, 1)
s := &stream.FileStream{
Obj: &model.Object{
Name: name,
Size: f.length,
Modified: time.Now(),
},
Mimetype: contentType,
WebPutAsTask: false,
Reader: reader,
}
go func() {
e := fs.PutDirectly(f.ctx, dir, s, true)
f.errChan <- e
close(f.errChan)
}()
f.pipeWriter = writer
n, err = writer.Write(f.first512Bytes[:])
if err != nil {
return n, err
}
n1, err := writer.Write(p[512-f.pFirst:])
if err != nil {
return n1 + 512 - f.pFirst, err
}
f.pFirst = 512
return len(p), nil
}
}
func (f *FileUploadWithLengthProxy) Seek(offset int64, whence int) (int64, error) {
return 0, errs.NotSupport
}
func (f *FileUploadWithLengthProxy) Close() error {
if f.pipeWriter != nil {
err := f.pipeWriter.Close()
if err != nil {
return err
}
err = <-f.errChan
return err
} else {
data := f.first512Bytes[:f.pFirst]
contentType := http.DetectContentType(data)
dir, name := stdpath.Split(f.path)
s := &stream.FileStream{
Obj: &model.Object{
Name: name,
Size: int64(f.pFirst),
Modified: time.Now(),
},
Mimetype: contentType,
WebPutAsTask: false,
Reader: bytes.NewReader(data),
}
return fs.PutDirectly(f.ctx, dir, s, true)
}
}

122
server/ftp/sftp.go Normal file
View File

@ -0,0 +1,122 @@
package ftp
import (
"github.com/KirCute/sftpd-alist"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"os"
)
type SftpDriverAdapter struct {
FtpDriver *AferoAdapter
}
func (s *SftpDriverAdapter) OpenFile(_ string, _ uint32, _ *sftpd.Attr) (sftpd.File, error) {
// See also GetHandle
return nil, errs.NotImplement
}
func (s *SftpDriverAdapter) OpenDir(_ string) (sftpd.Dir, error) {
// See also GetHandle
return nil, errs.NotImplement
}
func (s *SftpDriverAdapter) Remove(name string) error {
return s.FtpDriver.Remove(name)
}
func (s *SftpDriverAdapter) Rename(old, new string, _ uint32) error {
return s.FtpDriver.Rename(old, new)
}
func (s *SftpDriverAdapter) Mkdir(name string, attr *sftpd.Attr) error {
return s.FtpDriver.Mkdir(name, attr.Mode)
}
func (s *SftpDriverAdapter) Rmdir(name string) error {
return s.Remove(name)
}
func (s *SftpDriverAdapter) Stat(name string, _ bool) (*sftpd.Attr, error) {
stat, err := s.FtpDriver.Stat(name)
if err != nil {
return nil, err
}
return fileInfoToSftpAttr(stat), nil
}
func (s *SftpDriverAdapter) SetStat(_ string, _ *sftpd.Attr) error {
return errs.NotSupport
}
func (s *SftpDriverAdapter) ReadLink(_ string) (string, error) {
return "", errs.NotSupport
}
func (s *SftpDriverAdapter) CreateLink(_, _ string, _ uint32) error {
return errs.NotSupport
}
func (s *SftpDriverAdapter) RealPath(path string) (string, error) {
return utils.FixAndCleanPath(path), nil
}
func (s *SftpDriverAdapter) GetHandle(name string, flags uint32, _ *sftpd.Attr, offset uint64) (sftpd.FileTransfer, error) {
return s.FtpDriver.GetHandle(name, sftpFlagToOpenMode(flags), int64(offset))
}
func (s *SftpDriverAdapter) ReadDir(name string) ([]sftpd.NamedAttr, error) {
dir, err := s.FtpDriver.ReadDir(name)
if err != nil {
return nil, err
}
ret := make([]sftpd.NamedAttr, len(dir))
for i, d := range dir {
ret[i] = *fileInfoToSftpNamedAttr(d)
}
return ret, nil
}
// From leffss/sftpd
func sftpFlagToOpenMode(flags uint32) int {
mode := 0
if (flags & SSH_FXF_READ) != 0 {
mode |= os.O_RDONLY
}
if (flags & SSH_FXF_WRITE) != 0 {
mode |= os.O_WRONLY
}
if (flags & SSH_FXF_APPEND) != 0 {
mode |= os.O_APPEND
}
if (flags & SSH_FXF_CREAT) != 0 {
mode |= os.O_CREATE
}
if (flags & SSH_FXF_TRUNC) != 0 {
mode |= os.O_TRUNC
}
if (flags & SSH_FXF_EXCL) != 0 {
mode |= os.O_EXCL
}
return mode
}
func fileInfoToSftpAttr(stat os.FileInfo) *sftpd.Attr {
ret := &sftpd.Attr{}
ret.Flags |= sftpd.ATTR_SIZE
ret.Size = uint64(stat.Size())
ret.Flags |= sftpd.ATTR_MODE
ret.Mode = stat.Mode()
ret.Flags |= sftpd.ATTR_TIME
ret.ATime = stat.Sys().(model.Obj).CreateTime()
ret.MTime = stat.ModTime()
return ret
}
func fileInfoToSftpNamedAttr(stat os.FileInfo) *sftpd.NamedAttr {
return &sftpd.NamedAttr{
Name: stat.Name(),
Attr: *fileInfoToSftpAttr(stat),
}
}

21
server/ftp/site.go Normal file
View File

@ -0,0 +1,21 @@
package ftp
import (
"fmt"
ftpserver "github.com/KirCute/ftpserverlib-pasvportmap"
"strconv"
)
func HandleSIZE(param string, client ftpserver.ClientDriver) (int, string) {
fs, ok := client.(*AferoAdapter)
if !ok {
return ftpserver.StatusNotLoggedIn, "Unexpected exception (driver is nil)"
}
size, err := strconv.ParseInt(param, 10, 64)
if err != nil {
return ftpserver.StatusSyntaxErrorParameters, fmt.Sprintf(
"Couldn't parse file size, given: %s, err: %v", param, err)
}
fs.SetNextFileSize(size)
return ftpserver.StatusOK, "Accepted next file size"
}

109
server/sftp.go Normal file
View File

@ -0,0 +1,109 @@
package server
import (
"context"
"github.com/KirCute/sftpd-alist"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/internal/setting"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/ftp"
"github.com/pkg/errors"
"golang.org/x/crypto/ssh"
"net/http"
)
type SftpDriver struct {
proxyHeader *http.Header
config *sftpd.Config
}
func NewSftpDriver() (*SftpDriver, error) {
header := &http.Header{}
header.Add("User-Agent", setting.GetStr(conf.FTPProxyUserAgent))
return &SftpDriver{
proxyHeader: header,
}, nil
}
func (d *SftpDriver) GetConfig() *sftpd.Config {
if d.config != nil {
return d.config
}
serverConfig := ssh.ServerConfig{
NoClientAuth: true,
NoClientAuthCallback: d.NoClientAuth,
PasswordCallback: d.PasswordAuth,
AuthLogCallback: d.AuthLogCallback,
BannerCallback: d.GetBanner,
}
for _, k := range conf.SSHSigners {
serverConfig.AddHostKey(k)
}
d.config = &sftpd.Config{
ServerConfig: serverConfig,
HostPort: conf.Conf.SFTP.Listen,
ErrorLogFunc: utils.Log.Error,
//DebugLogFunc: utils.Log.Debugf,
}
return d.config
}
func (d *SftpDriver) GetFileSystem(sc *ssh.ServerConn) (sftpd.FileSystem, error) {
userObj, err := op.GetUserByName(sc.User())
if err != nil {
return nil, err
}
ctx := context.Background()
ctx = context.WithValue(ctx, "user", userObj)
ctx = context.WithValue(ctx, "meta_pass", "")
ctx = context.WithValue(ctx, "client_ip", sc.RemoteAddr().String())
ctx = context.WithValue(ctx, "proxy_header", d.proxyHeader)
return &ftp.SftpDriverAdapter{FtpDriver: ftp.NewAferoAdapter(ctx)}, nil
}
func (d *SftpDriver) Close() {
}
func (d *SftpDriver) NoClientAuth(conn ssh.ConnMetadata) (*ssh.Permissions, error) {
if conn.User() != "guest" {
return nil, errors.New("only guest is allowed to login without authorization")
}
guest, err := op.GetGuest()
if err != nil {
return nil, err
}
if guest.Disabled || !guest.CanFTPAccess() {
return nil, errors.New("user is not allowed to access via SFTP")
}
return nil, nil
}
func (d *SftpDriver) PasswordAuth(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
userObj, err := op.GetUserByName(conn.User())
if err != nil {
return nil, err
}
passHash := model.StaticHash(string(password))
if err = userObj.ValidatePwdStaticHash(passHash); err != nil {
return nil, err
}
if userObj.Disabled || !userObj.CanFTPAccess() {
return nil, errors.New("user is not allowed to access via SFTP")
}
return nil, nil
}
func (d *SftpDriver) AuthLogCallback(conn ssh.ConnMetadata, method string, err error) {
ip := conn.RemoteAddr().String()
if err == nil {
utils.Log.Infof("[SFTP] %s(%s) logged in via %s", conn.User(), ip, method)
} else if method != "none" {
utils.Log.Infof("[SFTP] %s(%s) tries logging in via %s but with error: %s", conn.User(), ip, method, err)
}
}
func (d *SftpDriver) GetBanner(_ ssh.ConnMetadata) string {
return setting.GetStr(conf.Announcement)
}