mirror of https://github.com/Xhofe/alist
feat: sftp server support (#7643)
* feat: sftp server support * fix(sftp-server): try fix build failed * fix: sftp download lackpull/7655/head
parent
201e25c17f
commit
33ba7f1521
|
@ -15,6 +15,7 @@ import (
|
|||
func Init() {
|
||||
bootstrap.InitConfig()
|
||||
bootstrap.Log()
|
||||
bootstrap.InitHostKey()
|
||||
bootstrap.InitDB()
|
||||
data.InitData()
|
||||
bootstrap.InitIndex()
|
||||
|
|
|
@ -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
12
go.mod
|
@ -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
27
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue