allow to disable some hooks on a per-user basis

This way you can, for example, mix external and internal users
This commit is contained in:
Nicola Murino 2021-04-04 22:32:25 +02:00
parent d92861a8e8
commit fdf3f23df5
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
15 changed files with 211 additions and 66 deletions

View file

@ -1601,23 +1601,25 @@ func checkUserAndPass(user *User, password, ip, protocol string) (User, error) {
if user.Password == "" {
return *user, errors.New("credentials cannot be null or empty")
}
hookResponse, err := executeCheckPasswordHook(user.Username, password, ip, protocol)
if err != nil {
providerLog(logger.LevelDebug, "error executing check password hook: %v", err)
return *user, errors.New("unable to check credentials")
}
switch hookResponse.Status {
case -1:
// no hook configured
case 1:
providerLog(logger.LevelDebug, "password accepted by check password hook")
return *user, nil
case 2:
providerLog(logger.LevelDebug, "partial success from check password hook")
password = hookResponse.ToVerify
default:
providerLog(logger.LevelDebug, "password rejected by check password hook, status: %v", hookResponse.Status)
return *user, ErrInvalidCredentials
if !user.Filters.Hooks.CheckPasswordDisabled {
hookResponse, err := executeCheckPasswordHook(user.Username, password, ip, protocol)
if err != nil {
providerLog(logger.LevelDebug, "error executing check password hook: %v", err)
return *user, errors.New("unable to check credentials")
}
switch hookResponse.Status {
case -1:
// no hook configured
case 1:
providerLog(logger.LevelDebug, "password accepted by check password hook")
return *user, nil
case 2:
providerLog(logger.LevelDebug, "partial success from check password hook")
password = hookResponse.ToVerify
default:
providerLog(logger.LevelDebug, "password rejected by check password hook, status: %v", hookResponse.Status)
return *user, ErrInvalidCredentials
}
}
match, err := isPasswordOK(user, password)
@ -2142,6 +2144,9 @@ func executePreLoginHook(username, loginMethod, ip, protocol string) (User, erro
if err != nil {
return u, err
}
if u.Filters.Hooks.PreLoginDisabled {
return u, nil
}
startTime := time.Now()
out, err := getPreLoginHookResponse(loginMethod, ip, protocol, userAsJSON)
if err != nil {
@ -2328,11 +2333,16 @@ func updateUserFromExtAuthResponse(user *User, password, pkey string) {
func doExternalAuth(username, password string, pubKey []byte, keyboardInteractive, ip, protocol string, tlsCert *x509.Certificate) (User, error) {
var user User
pkey, err := utils.GetSSHPublicKeyAsString(pubKey)
u, userAsJSON, err := getUserAndJSONForHook(username)
if err != nil {
return user, err
}
u, userAsJSON, err := getUserAndJSONForHook(username)
if u.Filters.Hooks.ExternalAuthDisabled {
return u, nil
}
pkey, err := utils.GetSSHPublicKeyAsString(pubKey)
if err != nil {
return user, err
}
@ -2397,7 +2407,6 @@ func getUserAndJSONForHook(username string) (User, []byte, error) {
ID: 0,
Username: username,
}
return u, userAsJSON, nil
}
userAsJSON, err = json.Marshal(u)
if err != nil {

View file

@ -116,7 +116,15 @@ type PatternsFilter struct {
DeniedPatterns []string `json:"denied_patterns,omitempty"`
}
// HooksFilter defines user specific overrides for global hooks
type HooksFilter struct {
ExternalAuthDisabled bool `json:"external_auth_disabled"`
PreLoginDisabled bool `json:"pre_login_disabled"`
CheckPasswordDisabled bool `json:"check_password_disabled"`
}
// UserFilters defines additional restrictions for a user
// TODO: rename to UserOptions in v3
type UserFilters struct {
// only clients connecting from these IP/Mask are allowed.
// IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291
@ -142,6 +150,8 @@ type UserFilters struct {
// For FTP clients it must match the name provided using the
// "USER" command
TLSUsername TLSUsername `json:"tls_username,omitempty"`
// user specific hook overrides
Hooks HooksFilter `json:"hooks,omitempty"`
}
// User defines a SFTPGo user
@ -1069,6 +1079,9 @@ func (u *User) getACopy() User {
copy(filters.FilePatterns, u.Filters.FilePatterns)
filters.DeniedProtocols = make([]string, len(u.Filters.DeniedProtocols))
copy(filters.DeniedProtocols, u.Filters.DeniedProtocols)
filters.Hooks.ExternalAuthDisabled = u.Filters.Hooks.ExternalAuthDisabled
filters.Hooks.PreLoginDisabled = u.Filters.Hooks.PreLoginDisabled
filters.Hooks.CheckPasswordDisabled = u.Filters.Hooks.CheckPasswordDisabled
return User{
ID: u.ID,

View file

@ -42,4 +42,6 @@ You can also restrict the hook scope using the `check_password_scope` configurat
You can combine the scopes. For example, 6 means FTP and WebDAV.
You can disable the hook on a per-user basis.
An example check password program allowing 2FA using password + one time token can be found inside the source tree [checkpwd](../examples/OTP/authy/checkpwd) directory.

View file

@ -35,6 +35,8 @@ If an error happens while executing the hook then login will be denied.
"Dynamic user creation or modification" and "External Authentication" are mutually exclusive, they are quite similar, the difference is that "External Authentication" returns an already authenticated user while using "Dynamic users modification" you simply create or update a user. The authentication will be checked inside SFTPGo.
In other words while using "External Authentication" the external program receives the credentials of the user trying to login (for example the cleartext password) and it needs to validate them. While using "Dynamic users modification" the pre-login program receives the user stored inside the dataprovider (it includes the hashed password if any) and it can modify it, after the modification SFTPGo will check the credentials of the user trying to login.
You can disable the hook on a per-user basis.
Let's see a very basic example. Our sample program will grant access to the existing user `test_user` only in the time range 10:00-18:00. Other users will not be modified since the program will terminate with no output.
```shell

View file

@ -68,6 +68,8 @@ fi
The structure for SFTPGo users can be found within the [OpenAPI schema](../httpd/schema/openapi.yaml).
You can disable the hook on a per-user basis so that you can mix external and internal users.
An example authentication program allowing to authenticate against an LDAP server can be found inside the source tree [ldapauth](../examples/ldapauth) directory.
An example server, to use as HTTP authentication hook, allowing to authenticate against an LDAP server can be found inside the source tree [ldapauthserver](../examples/ldapauthserver) directory.

18
go.mod
View file

@ -3,23 +3,23 @@ module github.com/drakkan/sftpgo
go 1.16
require (
cloud.google.com/go v0.80.0 // indirect
cloud.google.com/go v0.81.0 // indirect
cloud.google.com/go/storage v1.14.0
github.com/Azure/azure-storage-blob-go v0.13.0
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 // indirect
github.com/alexedwards/argon2id v0.0.0-20210326052512-e2135f7c9c77
github.com/aws/aws-sdk-go v1.38.7
github.com/aws/aws-sdk-go v1.38.12
github.com/cockroachdb/cockroach-go/v2 v2.1.0
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect
github.com/eikenb/pipeat v0.0.0-20200430215831-470df5986b6d
github.com/fclairamb/ftpserverlib v0.13.0
github.com/frankban/quicktest v1.11.3 // indirect
github.com/go-chi/chi/v5 v5.0.2
github.com/go-chi/jwtauth/v5 v5.0.0
github.com/go-chi/jwtauth/v5 v5.0.1
github.com/go-chi/render v1.0.1
github.com/go-ole/go-ole v1.2.5 // indirect
github.com/go-sql-driver/mysql v1.5.0
github.com/go-sql-driver/mysql v1.6.0
github.com/golang/snappy v0.0.3 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.2.0 // indirect
@ -31,7 +31,7 @@ require (
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
github.com/klauspost/cpuid/v2 v2.0.6 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
github.com/lestrrat-go/jwx v1.1.6
github.com/lestrrat-go/jwx v1.1.7
github.com/lib/pq v1.10.0
github.com/magiconair/properties v1.8.5 // indirect
github.com/mattn/go-sqlite3 v1.14.6
@ -46,10 +46,10 @@ require (
github.com/prometheus/client_golang v1.10.0
github.com/prometheus/common v0.20.0 // indirect
github.com/rs/cors v1.7.1-0.20200626170627-8b4a00bd362b
github.com/rs/xid v1.2.1
github.com/rs/xid v1.3.0
github.com/rs/zerolog v1.21.0
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shirou/gopsutil/v3 v3.21.2
github.com/shirou/gopsutil/v3 v3.21.3
github.com/spf13/afero v1.6.0
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/cobra v1.1.3
@ -65,8 +65,8 @@ require (
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
golang.org/x/mod v0.4.2 // indirect
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 // indirect
golang.org/x/sys v0.0.0-20210326220804-49726bf1d181
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 // indirect
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
google.golang.org/api v0.43.0
gopkg.in/ini.v1 v1.62.0 // indirect

48
go.sum
View file

@ -21,8 +21,8 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.80.0 h1:kAdyAMrj9CjqOSGiluseVjIgAyQ3uxADYtUYR6MwYeY=
cloud.google.com/go v0.80.0/go.mod h1:fqpb6QRi1CFGAMXDoE72G+b+Ybv7dMB/T1tbExDHktI=
cloud.google.com/go v0.81.0 h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@ -118,8 +118,8 @@ github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo
github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.36.1/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.38.7 h1:uOu2IrTiNhcSNAjBmA21t48lTx5mgGdcFKamDjXMscA=
github.com/aws/aws-sdk-go v1.38.7/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.38.12 h1:khtODkUna3iF53Cg3dCF4e6oWgrAEbZDU4x1aq+G0WY=
github.com/aws/aws-sdk-go v1.38.12/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@ -219,8 +219,8 @@ github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwv
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
github.com/go-chi/chi/v5 v5.0.2 h1:4xKeALZdMEsuI5s05PU2Bm89Uc5iM04qFubUCl5LfAQ=
github.com/go-chi/chi/v5 v5.0.2/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/jwtauth/v5 v5.0.0 h1:FjyoBQ0sH6/OSBCTXdRMMd7Eis3UjmsX2wKTSucFn+g=
github.com/go-chi/jwtauth/v5 v5.0.0/go.mod h1:wdYCsXCBuihmcGwLdfVgZ4LhDLOZHfyF+Fd5mEjiGPM=
github.com/go-chi/jwtauth/v5 v5.0.1 h1:eyJ6Yx5VphEfjkqpZ7+LJEWThzyIcF5aN2QVpgqSIu0=
github.com/go-chi/jwtauth/v5 v5.0.1/go.mod h1:+JtcRYGZsnA4+ur1LFlb4Bei3O9WeUzoMfDZWfUJuoY=
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@ -246,14 +246,14 @@ github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEK
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/goccy/go-json v0.3.5/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.4.8 h1:TfwOxfSp8hXH+ivoOk36RyDNmXATUETRdaNWDaZglf8=
github.com/goccy/go-json v0.4.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
@ -517,15 +517,16 @@ github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgx
github.com/lestrrat-go/backoff/v2 v2.0.7/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A=
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4=
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
github.com/lestrrat-go/codegen v1.0.0/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM=
github.com/lestrrat-go/httpcc v1.0.0 h1:FszVC6cKfDvBKcJv646+lkh4GydQg2Z29scgUfkOpYc=
github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
github.com/lestrrat-go/iter v1.0.0/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A=
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
github.com/lestrrat-go/jwx v1.1.0/go.mod h1:vn9FzD6gJtKkgYs7RTKV7CjWtEka8F/voUollhnn4QE=
github.com/lestrrat-go/jwx v1.1.6 h1:VfyUo2PAU4lO/liwhdwiSZ55/QZDLTT3EYY5z9KfwZs=
github.com/lestrrat-go/jwx v1.1.6/go.mod h1:c+R8G7qsaFNmTzYjU98A+sMh8Bo/MJqO9GnpqR+X024=
github.com/lestrrat-go/jwx v1.1.7 h1:+PNt2U7FfrK4xn+ZCG+9jPRq5eqyG30gwpVwcekrCjA=
github.com/lestrrat-go/jwx v1.1.7/go.mod h1:Tg2uP7bpxEHUDtuWjap/PxroJ4okxGzkQznXiG+a5Dc=
github.com/lestrrat-go/option v0.0.0-20210103042652-6f1ecfceda35/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
@ -688,8 +689,9 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/cors v1.7.1-0.20200626170627-8b4a00bd362b h1:LeFDRLtjhSBjIezNZvfN0CHsu2GfDS2CJAiEGaWBJ34=
github.com/rs/cors v1.7.1-0.20200626170627-8b4a00bd362b/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM=
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/rs/zerolog v1.21.0 h1:Q3vdXlfLNT+OftyBHsU0Y445MD+8m8axjKgf2si0QcM=
@ -706,8 +708,8 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY=
github.com/shirou/gopsutil/v3 v3.21.2 h1:fIOk3hyqV1oGKogfGNjUZa0lUbtlkx3+ZT0IoJth2uM=
github.com/shirou/gopsutil/v3 v3.21.2/go.mod h1:ghfMypLDrFSWN2c9cDYFLHyynQ+QUht0cv/18ZqVczw=
github.com/shirou/gopsutil/v3 v3.21.3 h1:wgcdAHZS2H6qy4JFewVTtqfiYxFzCeEJod/mLztdPG8=
github.com/shirou/gopsutil/v3 v3.21.3/go.mod h1:ghfMypLDrFSWN2c9cDYFLHyynQ+QUht0cv/18ZqVczw=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
@ -857,8 +859,8 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 h1:D7nTwh4J0i+5mW4Zjzn5omvlr6YBcWywE6KOcatyNxY=
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 h1:0Ja1LBD+yisY6RWM/BH7TJVXWsSjs2VwBSmvSX4HdBc=
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -932,11 +934,10 @@ golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210314195730-07df6a141424/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210326220804-49726bf1d181 h1:64ChN/hjER/taL4YJuA+gpLfIMT+/NFherRZixbxOhg=
golang.org/x/sys v0.0.0-20210326220804-49726bf1d181/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -1053,7 +1054,6 @@ google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.42.0/go.mod h1:+Oj4s6ch2SEGtPjGqfUfZonBH0GjQH89gTeKKAEGZKI=
google.golang.org/api v0.43.0 h1:4sAyIHT6ZohtAQDoxws+ez7bROYmUlOVvsUscYCDTqA=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@ -1112,10 +1112,9 @@ google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210312152112-fc591d9ea70f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210323160006-e668133fea6a h1:XVaQ1+BDKvrRcgppHhtAaniHCKyV5xJAvymwsPHHFaE=
google.golang.org/genproto v0.0.0-20210323160006-e668133fea6a/go.mod h1:f2Bd7+2PlaVKmvKQ52aspJZXIDaRQBVdOOBfJ5i8OEs=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1 h1:E7wSQBXkH3T3diucK+9Z1kjn4+/9tNG7lZLr75oOhh8=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@ -1140,8 +1139,9 @@ google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0 h1:o1bcQ6imQMIOpdrO3SWf2z5RV72WbDwdXuK0MDlc8As=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1 h1:cmUfbeGKnz9+2DD/UYsMQXeqbHZqZDs4eQwW0sFOpBY=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=

View file

@ -974,6 +974,7 @@ func TestUpdateUser(t *testing.T) {
u.UsedQuotaFiles = 1
u.UsedQuotaSize = 2
u.Filters.TLSUsername = dataprovider.TLSUsernameCN
u.Filters.Hooks.CheckPasswordDisabled = true
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
assert.Equal(t, 0, user.UsedQuotaFiles)
@ -991,6 +992,9 @@ func TestUpdateUser(t *testing.T) {
user.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword}
user.Filters.DeniedProtocols = []string{common.ProtocolWebDAV}
user.Filters.TLSUsername = dataprovider.TLSUsernameNone
user.Filters.Hooks.ExternalAuthDisabled = true
user.Filters.Hooks.PreLoginDisabled = true
user.Filters.Hooks.CheckPasswordDisabled = false
user.Filters.FileExtensions = append(user.Filters.FileExtensions, dataprovider.ExtensionsFilter{
Path: "/subdir",
AllowedExtensions: []string{".zip", ".rar"},
@ -1027,6 +1031,7 @@ func TestUpdateUser(t *testing.T) {
})
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
_, _, err = httpdtest.UpdateUser(user, http.StatusBadRequest, "invalid")
assert.NoError(t, err)
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "0")
@ -4852,6 +4857,7 @@ func TestWebUserAddMock(t *testing.T) {
form.Set("denied_patterns", "/dir1::*.zip\n/dir3::*.rar\n/dir2::*.mkv")
form.Set("additional_info", user.AdditionalInfo)
form.Set("description", user.Description)
form.Add("hooks", "external_auth_disabled")
b, contentType, _ := getMultipartFormData(form, "", "")
// test invalid url escape
req, _ = http.NewRequest(http.MethodPost, webUserPath+"?a=%2", &b)
@ -5011,6 +5017,9 @@ func TestWebUserAddMock(t *testing.T) {
assert.Equal(t, int64(1000), newUser.Filters.MaxUploadFileSize)
assert.Equal(t, user.AdditionalInfo, newUser.AdditionalInfo)
assert.Equal(t, user.Description, newUser.Description)
assert.True(t, newUser.Filters.Hooks.ExternalAuthDisabled)
assert.False(t, newUser.Filters.Hooks.PreLoginDisabled)
assert.False(t, newUser.Filters.Hooks.CheckPasswordDisabled)
assert.True(t, utils.IsStringInSlice(testPubKey, newUser.PublicKeys))
if val, ok := newUser.Permissions["/subdir"]; ok {
assert.True(t, utils.IsStringInSlice(dataprovider.PermListItems, val))
@ -5416,6 +5425,8 @@ func TestUserTemplateMock(t *testing.T) {
form.Set("allowed_extensions", "/dir1::.jpg,.png")
form.Set("denied_extensions", "/dir2::.zip")
form.Set("max_upload_file_size", "0")
form.Add("hooks", "external_auth_disabled")
form.Add("hooks", "check_password_disabled")
// test invalid s3_upload_part_size
form.Set("s3_upload_part_size", "a")
b, contentType, _ := getMultipartFormData(form, "", "")
@ -5487,6 +5498,12 @@ func TestUserTemplateMock(t *testing.T) {
err = user2.FsConfig.S3Config.AccessSecret.Decrypt()
require.NoError(t, err)
require.Equal(t, "password2", user2.FsConfig.S3Config.AccessSecret.GetPayload())
require.True(t, user1.Filters.Hooks.ExternalAuthDisabled)
require.True(t, user1.Filters.Hooks.CheckPasswordDisabled)
require.False(t, user1.Filters.Hooks.PreLoginDisabled)
require.True(t, user2.Filters.Hooks.ExternalAuthDisabled)
require.True(t, user2.Filters.Hooks.CheckPasswordDisabled)
require.False(t, user2.Filters.Hooks.PreLoginDisabled)
}
func TestFolderTemplateMock(t *testing.T) {
@ -5663,6 +5680,7 @@ func TestWebUserS3Mock(t *testing.T) {
form.Set("denied_extensions", "/dir2::.zip")
form.Set("max_upload_file_size", "0")
form.Set("description", user.Description)
form.Add("hooks", "pre_login_disabled")
// test invalid s3_upload_part_size
form.Set("s3_upload_part_size", "a")
b, contentType, _ := getMultipartFormData(form, "", "")
@ -5710,6 +5728,9 @@ func TestWebUserS3Mock(t *testing.T) {
assert.Empty(t, updateUser.FsConfig.S3Config.AccessSecret.GetKey())
assert.Empty(t, updateUser.FsConfig.S3Config.AccessSecret.GetAdditionalData())
assert.Equal(t, user.Description, updateUser.Description)
assert.True(t, updateUser.Filters.Hooks.PreLoginDisabled)
assert.False(t, updateUser.Filters.Hooks.ExternalAuthDisabled)
assert.False(t, updateUser.Filters.Hooks.CheckPasswordDisabled)
// now check that a redacted password is not saved
form.Set("s3_access_secret", redactedSecret)
b, contentType, _ = getMultipartFormData(form, "", "")

View file

@ -1421,6 +1421,22 @@ components:
description: 'list of, case insensitive, denied files extension. Denied file extensions are evaluated before the allowed ones'
example:
- .zip
HooksFilter:
type: object
properties:
external_auth_disabled:
type: boolean
example: false
description: If true, the external auth hook, if defined, will not be executed
pre_login_disabled:
type: boolean
example: false
description: If true, the pre-login hook, if defined, will not be executed
check_password_disabled:
type: boolean
example: false
description: If true, the check password hook, if defined, will not be executed
description: User specific hook overrides
UserFilters:
type: object
properties:
@ -1469,6 +1485,8 @@ components:
- None
- CommonName
description: 'defines the TLS certificate field to use as username. For FTP clients it must match the name provided using the "USER" command. For WebDAV, if no username is provided, the CN will be used as username. For WebDAV clients it must match the implicit or provided username. Ignored if mutual TLS is disabled'
hooks:
$ref: '#/components/schemas/HooksFilter'
description: Additional user restrictions
Secret:
type: object
@ -1615,6 +1633,9 @@ components:
description: Concurrent reads are safe to use and disabling them will degrade performance. Some servers automatically delete files once they are downloaded. Using concurrent reads is problematic with such servers.
buffer_size:
type: integer
minimum: 0
maximum: 16
example: 2
description: The size of the buffer (in MB) to use for transfers. By enabling buffering, the reads and writes, from/to the remote SFTP server, are split in multiple concurrent requests and this allows data to be transferred at a faster rate, over high latency networks, by overlapping round-trip times. With buffering enabled, resuming uploads is not supported and a file cannot be opened for both reading and writing at the same time. 0 means disabled.
FilesystemConfig:
type: object

View file

@ -662,6 +662,16 @@ func getFiltersFromUserPostFields(r *http.Request) dataprovider.UserFilters {
filters.FileExtensions = getFileExtensionsFromPostField(r.Form.Get("allowed_extensions"), r.Form.Get("denied_extensions"))
filters.FilePatterns = getFilePatternsFromPostField(r.Form.Get("allowed_patterns"), r.Form.Get("denied_patterns"))
filters.TLSUsername = dataprovider.TLSUsername(r.Form.Get("tls_username"))
hooks := r.Form["hooks"]
if utils.IsStringInSlice("external_auth_disabled", hooks) {
filters.Hooks.ExternalAuthDisabled = true
}
if utils.IsStringInSlice("pre_login_disabled", hooks) {
filters.Hooks.PreLoginDisabled = true
}
if utils.IsStringInSlice("check_password_disabled", hooks) {
filters.Hooks.CheckPasswordDisabled = true
}
return filters
}

View file

@ -1168,6 +1168,15 @@ func compareUserFilterSubStructs(expected *dataprovider.User, actual *dataprovid
return errors.New("denied protocols contents mismatch")
}
}
if expected.Filters.Hooks.ExternalAuthDisabled != actual.Filters.Hooks.ExternalAuthDisabled {
return errors.New("external_auth_disabled hook mismatch")
}
if expected.Filters.Hooks.PreLoginDisabled != actual.Filters.Hooks.PreLoginDisabled {
return errors.New("pre_login_disabled hook mismatch")
}
if expected.Filters.Hooks.CheckPasswordDisabled != actual.Filters.Hooks.CheckPasswordDisabled {
return errors.New("check_password_disabled hook mismatch")
}
return nil
}

View file

@ -2084,6 +2084,20 @@ func TestPreLoginScript(t *testing.T) {
if !assert.Error(t, err, "pre-login script returned a non json response, login must fail") {
client.Close()
}
// now disable the the hook
user.Filters.Hooks.PreLoginDisabled = true
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getSftpClient(u, usePubKey)
if assert.NoError(t, err) {
defer client.Close()
assert.NoError(t, checkBasicSFTP(client))
}
user.Filters.Hooks.PreLoginDisabled = false
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
user.Status = 0
err = os.WriteFile(preLoginPath, getPreLoginScriptContent(user, false), os.ModePerm)
assert.NoError(t, err)
@ -2091,6 +2105,7 @@ func TestPreLoginScript(t *testing.T) {
if !assert.Error(t, err, "pre-login script returned a disabled user, login must fail") {
client.Close()
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
@ -2230,6 +2245,22 @@ func TestCheckPwdHook(t *testing.T) {
client.Close()
}
// now disable the the hook
user.Filters.Hooks.CheckPasswordDisabled = true
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
err = checkBasicSFTP(client)
assert.NoError(t, err)
client.Close()
}
// enable the hook again
user.Filters.Hooks.CheckPasswordDisabled = false
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
err = os.WriteFile(checkPwdPath, getCheckPwdScriptsContents(1, ""), os.ModePerm)
assert.NoError(t, err)
user.Password = defaultPassword + "1"
@ -2323,6 +2354,23 @@ func TestLoginExternalAuthPwdAndPubKey(t *testing.T) {
assert.Equal(t, testFileSize, user.UsedQuotaSize)
assert.Equal(t, 1, user.UsedQuotaFiles)
u.Status = 0
err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, false, ""), os.ModePerm)
assert.NoError(t, err)
client, err = getSftpClient(u, usePubKey)
if !assert.Error(t, err) {
client.Close()
}
// now disable the the hook
user.Filters.Hooks.ExternalAuthDisabled = true
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client, err = getSftpClient(u, usePubKey)
if assert.NoError(t, err) {
defer client.Close()
assert.NoError(t, checkBasicSFTP(client))
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())

View file

@ -251,7 +251,7 @@
<label for="idSFTPUploadBufferSize" class="col-sm-2 col-form-label">Buffer size (MB)</label>
<div class="col-sm-3">
<input type="number" class="form-control" id="idSFTPBufferSize" name="sftp_buffer_size" placeholder=""
value="{{.SFTPConfig.BufferSize}}" min="0" max="64" aria-describedby="SFTPBufferHelpBlock">
value="{{.SFTPConfig.BufferSize}}" min="0" max="16" aria-describedby="SFTPBufferHelpBlock">
<small id="SFTPBufferHelpBlock" class="form-text text-muted">
A buffer size > 0 enables concurrent transfers
</small>

View file

@ -360,6 +360,23 @@
</div>
</div>
<div class="form-group row">
<label for="idHooks" class="col-sm-2 col-form-label">Hooks</label>
<div class="col-sm-10">
<select class="form-control" id="idHooks" name="hooks" multiple>
<option value="external_auth_disabled" {{if .User.Filters.Hooks.ExternalAuthDisabled}}selected{{end}}>
External auth disabled
</option>
<option value="pre_login_disabled" {{if .User.Filters.Hooks.PreLoginDisabled}}selected{{end}}>
Pre-login disabled
</option>
<option value="check_password_disabled" {{if .User.Filters.Hooks.CheckPasswordDisabled}}selected{{end}}>
Check password disabled
</option>
</select>
</div>
</div>
{{template "fshtml" .User.FsConfig}}
<div class="form-group row">

View file

@ -109,8 +109,8 @@ func (c *SFTPFsConfig) Validate() error {
if c.Username == "" {
return errors.New("username cannot be empty")
}
if c.BufferSize < 0 || c.BufferSize > 64 {
return errors.New("invalid buffer_size, valid range is 0-64")
if c.BufferSize < 0 || c.BufferSize > 16 {
return errors.New("invalid buffer_size, valid range is 0-16")
}
if err := c.validateCredentials(); err != nil {
return err
@ -262,16 +262,7 @@ func (fs *SFTPFs) Open(name string, offset int64) (File, *pipeat.PipeReaderAt, f
return nil, nil, nil, err
}
go func() {
var n int64
var err error
if fs.config.DisableCouncurrentReads {
n, err = fs.copy(w, f)
} else {
br := bufio.NewReaderSize(f, int(fs.config.BufferSize)*1024*1024)
// we don't use io.Copy since bufio.Reader implements io.ReadFrom and
// so it calls the sftp.File ReadFrom method without buffering
n, err = fs.copy(w, br)
}
n, err := io.Copy(w, f)
w.CloseWithError(err) //nolint:errcheck
f.Close()
fsLog(fs, logger.LevelDebug, "download completed, path: %#v size: %v, err: %v", name, n, err)