WebDAV: improve TLS certificate authentication

For each user you can now configure:

- TLS certificate auth
- TLS certificate auth and password
- Password auth

For TLS certificate auth, the certificate common name is used as
username
This commit is contained in:
Nicola Murino 2021-03-01 19:28:11 +01:00
parent 901cafc6da
commit 534b253c20
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
19 changed files with 816 additions and 151 deletions

View file

@ -570,6 +570,80 @@ func CheckAdminAndPass(username, password, ip string) (Admin, error) {
return provider.validateAdminAndPass(username, password, ip)
}
// CheckCachedUserCredentials checks the credentials for a cached user
func CheckCachedUserCredentials(user *CachedUser, password, loginMethod, protocol string, tlsCert *x509.Certificate) error {
if loginMethod != LoginMethodPassword {
_, err := checkUserAndTLSCertificate(&user.User, protocol, tlsCert)
if err != nil {
return err
}
if loginMethod == LoginMethodTLSCertificate {
if !user.User.IsLoginMethodAllowed(LoginMethodTLSCertificate, nil) {
return fmt.Errorf("Certificate login method is not allowed for user %#v", user.User.Username)
}
return nil
}
}
if err := checkLoginConditions(&user.User); err != nil {
return err
}
if password == "" {
return ErrInvalidCredentials
}
if user.Password != "" {
if password == user.Password {
return nil
}
} else {
if ok, _ := isPasswordOK(&user.User, password); ok {
return nil
}
}
return ErrInvalidCredentials
}
// CheckCompositeCredentials checks multiple credentials.
// WebDAV users can send both a password and a TLS certificate within the same request
func CheckCompositeCredentials(username, password, ip, loginMethod, protocol string, tlsCert *x509.Certificate) (User, string, error) {
if loginMethod == LoginMethodPassword {
user, err := CheckUserAndPass(username, password, ip, protocol)
return user, loginMethod, err
}
user, err := CheckUserBeforeTLSAuth(username, ip, protocol, tlsCert)
if err != nil {
return user, loginMethod, err
}
if !user.IsTLSUsernameVerificationEnabled() {
// for backward compatibility with 2.0.x we only check the password and change the login method here
// in future updates we have to return an error
user, err := CheckUserAndPass(username, password, ip, protocol)
return user, LoginMethodPassword, err
}
user, err = checkUserAndTLSCertificate(&user, protocol, tlsCert)
if err != nil {
return user, loginMethod, err
}
if loginMethod == LoginMethodTLSCertificate && !user.IsLoginMethodAllowed(LoginMethodTLSCertificate, nil) {
return user, loginMethod, fmt.Errorf("Certificate login method is not allowed for user %#v", user.Username)
}
if loginMethod == LoginMethodTLSCertificateAndPwd {
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&1 != 0) {
user, err = doExternalAuth(username, password, nil, "", ip, protocol, nil)
if err != nil {
return user, loginMethod, err
}
}
if config.PreLoginHook != "" {
user, err = executePreLoginHook(username, LoginMethodPassword, ip, protocol)
if err != nil {
return user, loginMethod, err
}
}
user, err = checkUserAndPass(&user, password, ip, protocol)
}
return user, loginMethod, err
}
// CheckUserBeforeTLSAuth checks if a user exits before trying mutual TLS
func CheckUserBeforeTLSAuth(username, ip, protocol string, tlsCert *x509.Certificate) (User, error) {
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&8 != 0) {
@ -1458,7 +1532,7 @@ func checkUserAndTLSCertificate(user *User, protocol string, tlsCert *x509.Certi
return *user, err
}
switch protocol {
case "FTP":
case "FTP", "DAV":
if user.Filters.TLSUsername == TLSUsernameCN {
if user.Username == tlsCert.Subject.CommonName {
return *user, nil

View file

@ -6,7 +6,7 @@ To enable dynamic user modification, you must set the absolute path of your prog
The external program can read the following environment variables to get info about the user trying to login:
- `SFTPGO_LOGIND_USER`, it contains the user trying to login serialized as JSON. A JSON serialized user id equal to zero means the user does not exist inside SFTPGo
- `SFTPGO_LOGIND_METHOD`, possible values are: `password`, `publickey` and `keyboard-interactive`
- `SFTPGO_LOGIND_METHOD`, possible values are: `password`, `publickey`, `keyboard-interactive`, `TLSCertificate`
- `SFTPGO_LOGIND_IP`, ip address of the user trying to login
- `SFTPGO_LOGIND_PROTOCOL`, possible values are `SSH`, `FTP`, `DAV`

View file

@ -132,7 +132,7 @@ The configuration file contains the following sections:
- `port`, integer. The port used for serving WebDAV requests. 0 means disabled. Default: 0.
- `address`, string. Leave blank to listen on all available network interfaces. Default: "".
- `enable_https`, boolean. Set to `true` and provide both a certificate and a key file to enable HTTPS connection for this binding. Default `false`.
- `client_auth_type`, integer. Set to `1` to require client certificate authentication in addition to basic authentication. You need to define at least a certificate authority for this to work. Default: 0.
- `client_auth_type`, integer. Set to `1` to require a client certificate and verify it. Set to `2` to request a client certificate during the TLS handshake and verify it if given, in this mode the client is allowed not to send a certificate. At least one certification authority must be defined in order to verify client certificates. If no certification authority is defined, this setting is ignored. Default: 0.
- `tls_cipher_suites`, list of strings. List of supported cipher suites for TLS version 1.2. If empty, a default list of secure cipher suites is used, with a preference order based on hardware performance. Note that TLS 1.3 ciphersuites are not configurable. The supported ciphersuites names are defined [here](https://github.com/golang/go/blob/master/src/crypto/tls/cipher_suites.go#L52). Any invalid name will be silently ignored. The order matters, the ciphers listed first will be the preferred ones. Default: empty.
- `bind_port`, integer. Deprecated, please use `bindings`.
- `bind_address`, string. Deprecated, please use `bindings`.

View file

@ -10,7 +10,7 @@ If the hook defines an external program it can reads the following environment v
- `SFTPGO_LOGIND_USER`, it contains the user serialized as JSON. The username is empty if the connection is closed for authentication timeout
- `SFTPGO_LOGIND_IP`
- `SFTPGO_LOGIND_METHOD`, possible values are `publickey`, `password`, `keyboard-interactive`, `publickey+password`, `publickey+keyboard-interactive` or `no_auth_tryed`
- `SFTPGO_LOGIND_METHOD`, possible values are `publickey`, `password`, `keyboard-interactive`, `publickey+password`, `publickey+keyboard-interactive`, `TLSCertificate`, `TLSCertificate+password` or `no_auth_tryed`
- `SFTPGO_LOGIND_STATUS`, 1 means login OK, 0 login KO
- `SFTPGO_LOGIND_PROTOCOL`, possible values are `SSH`, `FTP`, `DAV`

17
go.mod
View file

@ -1,6 +1,6 @@
module github.com/drakkan/sftpgo
go 1.15
go 1.16
require (
cloud.google.com/go v0.78.0 // indirect
@ -9,16 +9,17 @@ require (
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-20201228115903-cf543ebc1f7b
github.com/aws/aws-sdk-go v1.37.18
github.com/aws/aws-sdk-go v1.37.20
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 v1.5.3
github.com/go-chi/chi/v5 v5.0.0
github.com/go-chi/jwtauth v1.2.0
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/golang/snappy v0.0.3 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.2.0 // indirect
github.com/google/wire v0.5.0 // indirect
@ -26,11 +27,12 @@ require (
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.6.8
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
github.com/lestrrat-go/jwx v1.1.4-0.20210228091017-d69abec6f5b4
github.com/lib/pq v1.9.0
github.com/magiconair/properties v1.8.4 // indirect
github.com/mattn/go-sqlite3 v1.14.6
github.com/miekg/dns v1.1.39 // indirect
github.com/miekg/dns v1.1.40 // indirect
github.com/minio/sha256-simd v1.0.0
github.com/minio/sio v0.2.1
github.com/mitchellh/mapstructure v1.4.1 // indirect
@ -39,7 +41,7 @@ require (
github.com/pires/go-proxyproto v0.4.2
github.com/pkg/sftp v1.12.1-0.20210222152308-b8102da57e75
github.com/prometheus/client_golang v1.9.0
github.com/prometheus/common v0.17.0 // indirect
github.com/prometheus/common v0.18.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/rs/cors v1.7.1-0.20200626170627-8b4a00bd362b
github.com/rs/xid v1.2.1
@ -62,10 +64,11 @@ require (
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
golang.org/x/net v0.0.0-20210224082022-3d97a244fca7
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93 // indirect
golang.org/x/sys v0.0.0-20210223212115-eede4237b368
golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
google.golang.org/api v0.40.0
google.golang.org/genproto v0.0.0-20210224155714-063164c882e6 // indirect
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705 // indirect
google.golang.org/grpc v1.36.0 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect

37
go.sum
View file

@ -77,7 +77,6 @@ github.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpz
github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk=
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE=
github.com/Azure/go-autorest/autorest/validation v0.3.0/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E=
@ -117,8 +116,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.37.18 h1:SRdWLg+DqMFWX8HB3UvXyAoZpw9IDIUYnSTwgzOYbqg=
github.com/aws/aws-sdk-go v1.37.18/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.37.20 h1:CJCXpMYmBJrRH8YwoSE0oB9S3J5ax+62F14sYlDCztg=
github.com/aws/aws-sdk-go v1.37.20/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=
@ -211,9 +210,10 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-chi/chi v1.5.1 h1:kfTK3Cxd/dkMu/rKs5ZceWYp+t5CtiE7vmaTv3LjC6w=
github.com/go-chi/chi v1.5.1/go.mod h1:REp24E+25iKvxgeTfHmdUoL5x15kBiDBlnIl5bCwe2k=
github.com/go-chi/chi v1.5.3 h1:+DVDS9/D3MTbEu3WrrH3oz9oP6PlSPSNj8LLw3X17yU=
github.com/go-chi/chi v1.5.3/go.mod h1:Q8xfe6s3fjZyMr8ZTv5jL+vxhVaFyCq2s+RvSfzTD0E=
github.com/go-chi/chi/v5 v5.0.0 h1:DBPx88FjZJH3FsICfDAfIfnb7XxKIYVGG6lOPlhENAg=
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
github.com/go-chi/jwtauth v1.2.0 h1:Z116SPpevIABBYsv8ih/AHYBHmd4EufKSKsLUnWdrTM=
github.com/go-chi/jwtauth v1.2.0/go.mod h1:NTUpKoTQV6o25UwYE6w/VaLUu83hzrVKYTVo+lE6qDA=
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
@ -243,7 +243,6 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO
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-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
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=
@ -288,8 +287,9 @@ github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw=
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -455,8 +455,9 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lestrrat-go/backoff/v2 v2.0.7 h1:i2SeK33aOFJlUNJZzf2IpXRBvqBBnaGXfY5Xaop/GsE=
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/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=
@ -492,8 +493,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.39 h1:6dRfDGnHiXOMmTZkwWANy7bBXXlKls5Qu+pn+Ue0TLo=
github.com/miekg/dns v1.1.39/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/minio/sio v0.2.1 h1:NjzKiIMSMcHediVQR0AFVx2tp7Wxh9tKPfDI3kH7aHQ=
@ -549,7 +550,6 @@ github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh
github.com/otiai10/copy v1.5.0 h1:SoXDGnlTUZoqB/wSuj/Y5L6T5i6iN4YRAcMCd+JnLNU=
github.com/otiai10/copy v1.5.0/go.mod h1:XWfuS3CrI0R6IE0FbgHsEazaXO8G0LpMp9o8tos0x4E=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/curr v1.0.0 h1:TJIWdbX0B+kpNagQrjgq8bCMrbhiuX73M2XwgtDMoOI=
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.2 h1:VYWnrP5fXmz1MXvjuUvcBrXSjGE6xjON+axB/UrpO3E=
@ -601,8 +601,8 @@ github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
github.com/prometheus/common v0.17.0 h1:kDIZLI74SS+3tedSvEkykgBkD7txMxaJAPj8DtJUKYA=
github.com/prometheus/common v0.17.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
github.com/prometheus/common v0.18.0 h1:WCVKW7aL6LEe1uryfI9dnEc2ZqNB1Fn0ok930v0iL1Y=
github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
@ -842,8 +842,8 @@ golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210223212115-eede4237b368 h1:fDE3p0qf2V1co1vfj3/o87Ps8Hq6QTGNxJ5Xe7xSp80=
golang.org/x/sys v0.0.0-20210223212115-eede4237b368/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46 h1:V066+OYJ66oTjnhm4Yrn7SXIwSCiDQJxpBxmvqb1N1c=
golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
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=
@ -1013,8 +1013,8 @@ google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210203152818-3206188e46ba/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210224155714-063164c882e6 h1:bXUwz2WkXXrXgiLxww3vWmoSHLOGv4ipdPdTvKymcKw=
google.golang.org/genproto v0.0.0-20210224155714-063164c882e6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705 h1:PYBmACG+YEv8uQPW0r1kJj8tR+gkF0UWq7iFdUezwEw=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
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=
@ -1038,8 +1038,9 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
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 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8=
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/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

@ -14,7 +14,7 @@ import (
"sync"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/go-chi/jwtauth"
"github.com/drakkan/sftpgo/common"

View file

@ -20,7 +20,7 @@ import (
"testing"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/go-chi/jwtauth"
"github.com/lestrrat-go/jwx/jwt"
"github.com/rs/xid"

View file

@ -1378,7 +1378,7 @@ components:
enum:
- 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. Ignored if mutual TLS is disabled
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
description: Additional user restrictions
Secret:
type: object

View file

@ -9,8 +9,8 @@ import (
"net/http"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/jwtauth"
"github.com/go-chi/render"

View file

@ -5,7 +5,7 @@ import (
"net/http"
"time"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/chi/v5/middleware"
"github.com/rs/zerolog"
"github.com/drakkan/sftpgo/metrics"

View file

@ -4,7 +4,7 @@
package metrics
import (
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"

View file

@ -3,7 +3,7 @@
package metrics
import (
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/drakkan/sftpgo/version"
)

View file

@ -3,8 +3,8 @@ package telemetry
import (
"net/http"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/render"
"github.com/drakkan/sftpgo/common"

View file

@ -12,7 +12,7 @@ import (
"runtime"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/v5"
"github.com/drakkan/sftpgo/common"
"github.com/drakkan/sftpgo/logger"

View file

@ -398,7 +398,7 @@ func TestUserInvalidParams(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%v", u.Username), nil)
assert.NoError(t, err)
_, err = server.validateUser(u, req)
_, err = server.validateUser(u, req, dataprovider.LoginMethodPassword)
if assert.Error(t, err) {
assert.EqualError(t, err, fmt.Sprintf("cannot login user with invalid home dir: %#v", u.HomeDir))
}
@ -422,7 +422,7 @@ func TestUserInvalidParams(t *testing.T) {
VirtualPath: vdirPath2,
})
_, err = server.validateUser(u, req)
_, err = server.validateUser(u, req, dataprovider.LoginMethodPassword)
if assert.Error(t, err) {
assert.EqualError(t, err, "overlapping mapped folders are allowed only with quota tracking disabled")
}
@ -923,14 +923,15 @@ func TestBasicUsersCache(t *testing.T) {
ipAddr := "127.0.0.1"
_, _, _, err = server.authenticate(req, ipAddr) //nolint:dogsled
_, _, _, _, err = server.authenticate(req, ipAddr) //nolint:dogsled
assert.Error(t, err)
now := time.Now()
req.SetBasicAuth(username, password)
_, isCached, _, err := server.authenticate(req, ipAddr)
_, isCached, _, loginMethod, err := server.authenticate(req, ipAddr)
assert.NoError(t, err)
assert.False(t, isCached)
assert.Equal(t, dataprovider.LoginMethodPassword, loginMethod)
// now the user should be cached
var cachedUser *dataprovider.CachedUser
result, ok := dataprovider.GetCachedWebDAVUser(username)
@ -939,14 +940,14 @@ func TestBasicUsersCache(t *testing.T) {
assert.False(t, cachedUser.IsExpired())
assert.True(t, cachedUser.Expiration.After(now.Add(time.Duration(c.Cache.Users.ExpirationTime)*time.Minute)))
// authenticate must return the cached user now
authUser, isCached, _, err := server.authenticate(req, ipAddr)
authUser, isCached, _, _, err := server.authenticate(req, ipAddr)
assert.NoError(t, err)
assert.True(t, isCached)
assert.Equal(t, cachedUser.User, authUser)
}
// a wrong password must fail
req.SetBasicAuth(username, "wrong")
_, _, _, err = server.authenticate(req, ipAddr) //nolint:dogsled
_, _, _, _, err = server.authenticate(req, ipAddr) //nolint:dogsled
assert.EqualError(t, err, dataprovider.ErrInvalidCredentials.Error())
req.SetBasicAuth(username, password)
@ -959,9 +960,10 @@ func TestBasicUsersCache(t *testing.T) {
assert.True(t, cachedUser.IsExpired())
}
// now authenticate should get the user from the data provider and update the cache
_, isCached, _, err = server.authenticate(req, ipAddr)
_, isCached, _, loginMethod, err = server.authenticate(req, ipAddr)
assert.NoError(t, err)
assert.False(t, isCached)
assert.Equal(t, dataprovider.LoginMethodPassword, loginMethod)
result, ok = dataprovider.GetCachedWebDAVUser(username)
if assert.True(t, ok) {
cachedUser = result.(*dataprovider.CachedUser)
@ -973,9 +975,10 @@ func TestBasicUsersCache(t *testing.T) {
_, ok = dataprovider.GetCachedWebDAVUser(username)
assert.False(t, ok)
_, isCached, _, err = server.authenticate(req, ipAddr)
_, isCached, _, loginMethod, err = server.authenticate(req, ipAddr)
assert.NoError(t, err)
assert.False(t, isCached)
assert.Equal(t, dataprovider.LoginMethodPassword, loginMethod)
_, ok = dataprovider.GetCachedWebDAVUser(username)
assert.True(t, ok)
// cache is invalidated after user deletion
@ -1045,23 +1048,26 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%v", user1.Username), nil)
assert.NoError(t, err)
req.SetBasicAuth(user1.Username, password+"1")
_, isCached, _, err := server.authenticate(req, ipAddr)
_, isCached, _, loginMehod, err := server.authenticate(req, ipAddr)
assert.NoError(t, err)
assert.False(t, isCached)
assert.Equal(t, dataprovider.LoginMethodPassword, loginMehod)
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/%v", user2.Username), nil)
assert.NoError(t, err)
req.SetBasicAuth(user2.Username, password+"2")
_, isCached, _, err = server.authenticate(req, ipAddr)
_, isCached, _, loginMehod, err = server.authenticate(req, ipAddr)
assert.NoError(t, err)
assert.False(t, isCached)
assert.Equal(t, dataprovider.LoginMethodPassword, loginMehod)
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/%v", user3.Username), nil)
assert.NoError(t, err)
req.SetBasicAuth(user3.Username, password+"3")
_, isCached, _, err = server.authenticate(req, ipAddr)
_, isCached, _, loginMehod, err = server.authenticate(req, ipAddr)
assert.NoError(t, err)
assert.False(t, isCached)
assert.Equal(t, dataprovider.LoginMethodPassword, loginMehod)
// the first 3 users are now cached
_, ok := dataprovider.GetCachedWebDAVUser(user1.Username)
@ -1074,9 +1080,10 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/%v", user4.Username), nil)
assert.NoError(t, err)
req.SetBasicAuth(user4.Username, password+"4")
_, isCached, _, err = server.authenticate(req, ipAddr)
_, isCached, _, loginMehod, err = server.authenticate(req, ipAddr)
assert.NoError(t, err)
assert.False(t, isCached)
assert.Equal(t, dataprovider.LoginMethodPassword, loginMehod)
// user1, the first cached, should be removed now
_, ok = dataprovider.GetCachedWebDAVUser(user1.Username)
assert.False(t, ok)
@ -1091,9 +1098,10 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/%v", user1.Username), nil)
assert.NoError(t, err)
req.SetBasicAuth(user1.Username, password+"1")
_, isCached, _, err = server.authenticate(req, ipAddr)
_, isCached, _, loginMehod, err = server.authenticate(req, ipAddr)
assert.NoError(t, err)
assert.False(t, isCached)
assert.Equal(t, dataprovider.LoginMethodPassword, loginMehod)
_, ok = dataprovider.GetCachedWebDAVUser(user2.Username)
assert.False(t, ok)
_, ok = dataprovider.GetCachedWebDAVUser(user1.Username)
@ -1107,9 +1115,10 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/%v", user2.Username), nil)
assert.NoError(t, err)
req.SetBasicAuth(user2.Username, password+"2")
_, isCached, _, err = server.authenticate(req, ipAddr)
_, isCached, _, loginMehod, err = server.authenticate(req, ipAddr)
assert.NoError(t, err)
assert.False(t, isCached)
assert.Equal(t, dataprovider.LoginMethodPassword, loginMehod)
_, ok = dataprovider.GetCachedWebDAVUser(user3.Username)
assert.False(t, ok)
_, ok = dataprovider.GetCachedWebDAVUser(user1.Username)
@ -1123,9 +1132,10 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/%v", user3.Username), nil)
assert.NoError(t, err)
req.SetBasicAuth(user3.Username, password+"3")
_, isCached, _, err = server.authenticate(req, ipAddr)
_, isCached, _, loginMehod, err = server.authenticate(req, ipAddr)
assert.NoError(t, err)
assert.False(t, isCached)
assert.Equal(t, dataprovider.LoginMethodPassword, loginMehod)
_, ok = dataprovider.GetCachedWebDAVUser(user4.Username)
assert.False(t, ok)
_, ok = dataprovider.GetCachedWebDAVUser(user1.Username)
@ -1144,16 +1154,18 @@ func TestUsersCacheSizeAndExpiration(t *testing.T) {
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/%v", user4.Username), nil)
assert.NoError(t, err)
req.SetBasicAuth(user4.Username, password+"4")
_, isCached, _, err = server.authenticate(req, ipAddr)
_, isCached, _, loginMehod, err = server.authenticate(req, ipAddr)
assert.NoError(t, err)
assert.False(t, isCached)
assert.Equal(t, dataprovider.LoginMethodPassword, loginMehod)
req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("/%v", user1.Username), nil)
assert.NoError(t, err)
req.SetBasicAuth(user1.Username, password+"1")
_, isCached, _, err = server.authenticate(req, ipAddr)
_, isCached, _, loginMehod, err = server.authenticate(req, ipAddr)
assert.NoError(t, err)
assert.False(t, isCached)
assert.Equal(t, dataprovider.LoginMethodPassword, loginMehod)
_, ok = dataprovider.GetCachedWebDAVUser(user2.Username)
assert.False(t, ok)
_, ok = dataprovider.GetCachedWebDAVUser(user1.Username)
@ -1272,3 +1284,15 @@ func TestVerifyTLSConnection(t *testing.T) {
certMgr = oldCertMgr
}
func TestMisc(t *testing.T) {
oldCertMgr := certMgr
certMgr = nil
err := ReloadCertificateMgr()
assert.Nil(t, err)
val := getConfigPath("", ".")
assert.Empty(t, val)
certMgr = oldCertMgr
}

View file

@ -14,7 +14,7 @@ import (
"strings"
"time"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/chi/v5/middleware"
"github.com/rs/cors"
"github.com/rs/xid"
"golang.org/x/net/webdav"
@ -69,10 +69,15 @@ func (s *webDavServer) listenAndServe(compressor *middleware.Compressor) error {
}
logger.Debug(logSender, "", "configured TLS cipher suites for binding %#v: %v", s.binding.GetAddress(),
httpServer.TLSConfig.CipherSuites)
if s.binding.ClientAuthType == 1 {
if s.binding.isMutualTLSEnabled() {
httpServer.TLSConfig.ClientCAs = certMgr.GetRootCAs()
httpServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
httpServer.TLSConfig.VerifyConnection = s.verifyTLSConnection
switch s.binding.ClientAuthType {
case 1:
httpServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
case 2:
httpServer.TLSConfig.ClientAuth = tls.VerifyClientCertIfGiven
}
}
logger.Info(logSender, "", "starting HTTPS serving, binding: %v", s.binding.GetAddress())
return httpServer.ListenAndServeTLS("", "")
@ -92,6 +97,9 @@ func (s *webDavServer) verifyTLSConnection(state tls.ConnectionState) error {
clientCrtName = clientCrt.Subject.String()
}
if len(state.VerifiedChains) == 0 {
if s.binding.ClientAuthType == 2 {
return nil
}
logger.Warn(logSender, "", "TLS connection cannot be verified: unable to get verification chain")
return errors.New("TLS connection cannot be verified: unable to get verification chain")
}
@ -152,28 +160,28 @@ func (s *webDavServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Error(w, common.ErrConnectionDenied.Error(), http.StatusForbidden)
return
}
user, _, lockSystem, err := s.authenticate(r, ipAddr)
user, _, lockSystem, loginMethod, err := s.authenticate(r, ipAddr)
if err != nil {
w.Header().Set("WWW-Authenticate", "Basic realm=\"SFTPGo WebDAV\"")
http.Error(w, err401.Error(), http.StatusUnauthorized)
http.Error(w, fmt.Sprintf("Authentication error: %v", err), http.StatusUnauthorized)
return
}
connectionID, err := s.validateUser(&user, r)
connectionID, err := s.validateUser(&user, r, loginMethod)
if err != nil {
updateLoginMetrics(&user, ipAddr, err)
updateLoginMetrics(&user, ipAddr, loginMethod, err)
http.Error(w, err.Error(), http.StatusForbidden)
return
}
fs, err := user.GetFilesystem(connectionID)
if err != nil {
updateLoginMetrics(&user, ipAddr, err)
updateLoginMetrics(&user, ipAddr, loginMethod, err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
updateLoginMetrics(&user, ipAddr, err)
updateLoginMetrics(&user, ipAddr, loginMethod, err)
ctx := context.WithValue(r.Context(), requestIDKey, connectionID)
ctx = context.WithValue(ctx, requestStartKey, time.Now())
@ -202,12 +210,32 @@ func (s *webDavServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
handler.ServeHTTP(w, r.WithContext(ctx))
}
func (s *webDavServer) authenticate(r *http.Request, ip string) (dataprovider.User, bool, webdav.LockSystem, error) {
func (s *webDavServer) getCredentialsAndLoginMethod(r *http.Request) (string, string, string, *x509.Certificate, bool) {
var tlsCert *x509.Certificate
loginMethod := dataprovider.LoginMethodPassword
username, password, ok := r.BasicAuth()
if s.binding.isMutualTLSEnabled() && r.TLS != nil {
if len(r.TLS.PeerCertificates) > 0 {
tlsCert = r.TLS.PeerCertificates[0]
if ok {
loginMethod = dataprovider.LoginMethodTLSCertificateAndPwd
} else {
loginMethod = dataprovider.LoginMethodTLSCertificate
username = tlsCert.Subject.CommonName
password = ""
}
ok = true
}
}
return username, password, loginMethod, tlsCert, ok
}
func (s *webDavServer) authenticate(r *http.Request, ip string) (dataprovider.User, bool, webdav.LockSystem, string, error) {
var user dataprovider.User
var err error
username, password, ok := r.BasicAuth()
username, password, loginMethod, tlsCert, ok := s.getCredentialsAndLoginMethod(r)
if !ok {
return user, false, nil, err401
return user, false, nil, loginMethod, err401
}
result, ok := dataprovider.GetCachedWebDAVUser(username)
if ok {
@ -215,43 +243,47 @@ func (s *webDavServer) authenticate(r *http.Request, ip string) (dataprovider.Us
if cachedUser.IsExpired() {
dataprovider.RemoveCachedWebDAVUser(username)
} else {
if password != "" && cachedUser.Password == password {
return cachedUser.User, true, cachedUser.LockSystem, nil
if !cachedUser.User.IsTLSUsernameVerificationEnabled() {
// for backward compatibility with 2.0.x we only check the password
tlsCert = nil
loginMethod = dataprovider.LoginMethodPassword
}
updateLoginMetrics(&cachedUser.User, ip, dataprovider.ErrInvalidCredentials)
return user, false, nil, dataprovider.ErrInvalidCredentials
if err := dataprovider.CheckCachedUserCredentials(cachedUser, password, loginMethod, common.ProtocolWebDAV, tlsCert); err == nil {
return cachedUser.User, true, cachedUser.LockSystem, loginMethod, nil
}
updateLoginMetrics(&cachedUser.User, ip, loginMethod, dataprovider.ErrInvalidCredentials)
return user, false, nil, loginMethod, dataprovider.ErrInvalidCredentials
}
}
user, err = dataprovider.CheckUserAndPass(username, password, ip, common.ProtocolWebDAV)
user, loginMethod, err = dataprovider.CheckCompositeCredentials(username, password, ip, loginMethod,
common.ProtocolWebDAV, tlsCert)
if err != nil {
user.Username = username
updateLoginMetrics(&user, ip, err)
return user, false, nil, err
updateLoginMetrics(&user, ip, loginMethod, err)
return user, false, nil, loginMethod, err
}
lockSystem := webdav.NewMemLS()
if password != "" {
cachedUser := &dataprovider.CachedUser{
User: user,
Password: password,
LockSystem: lockSystem,
}
if s.config.Cache.Users.ExpirationTime > 0 {
cachedUser.Expiration = time.Now().Add(time.Duration(s.config.Cache.Users.ExpirationTime) * time.Minute)
}
dataprovider.CacheWebDAVUser(cachedUser, s.config.Cache.Users.MaxSize)
if user.FsConfig.Provider != dataprovider.SFTPFilesystemProvider {
// for sftp fs check root path does nothing so don't open a useless SFTP connection
tempFs, err := user.GetFilesystem("temp")
if err == nil {
tempFs.CheckRootPath(user.Username, user.UID, user.GID)
tempFs.Close()
}
cachedUser := &dataprovider.CachedUser{
User: user,
Password: password,
LockSystem: lockSystem,
}
if s.config.Cache.Users.ExpirationTime > 0 {
cachedUser.Expiration = time.Now().Add(time.Duration(s.config.Cache.Users.ExpirationTime) * time.Minute)
}
dataprovider.CacheWebDAVUser(cachedUser, s.config.Cache.Users.MaxSize)
if user.FsConfig.Provider != dataprovider.SFTPFilesystemProvider {
// for sftp fs check root path does nothing so don't open a useless SFTP connection
tempFs, err := user.GetFilesystem("temp")
if err == nil {
tempFs.CheckRootPath(user.Username, user.UID, user.GID)
tempFs.Close()
}
}
return user, false, lockSystem, nil
return user, false, lockSystem, loginMethod, nil
}
func (s *webDavServer) validateUser(user *dataprovider.User, r *http.Request) (string, error) {
func (s *webDavServer) validateUser(user *dataprovider.User, r *http.Request, loginMethod string) (string, error) {
connID := xid.New().String()
connectionID := fmt.Sprintf("%v_%v", common.ProtocolWebDAV, connID)
@ -264,9 +296,9 @@ func (s *webDavServer) validateUser(user *dataprovider.User, r *http.Request) (s
logger.Debug(logSender, connectionID, "cannot login user %#v, protocol DAV is not allowed", user.Username)
return connID, fmt.Errorf("Protocol DAV is not allowed for user %#v", user.Username)
}
if !user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, nil) {
logger.Debug(logSender, connectionID, "cannot login user %#v, password login method is not allowed", user.Username)
return connID, fmt.Errorf("Password login method is not allowed for user %#v", user.Username)
if !user.IsLoginMethodAllowed(loginMethod, nil) {
logger.Debug(logSender, connectionID, "cannot login user %#v, %v login method is not allowed", user.Username, loginMethod)
return connID, fmt.Errorf("Login method %v is not allowed for user %#v", loginMethod, user.Username)
}
if user.MaxSessions > 0 {
activeSessions := common.Connections.GetActiveSessions(user.Username)
@ -335,16 +367,16 @@ func checkRemoteAddress(r *http.Request) {
}
}
func updateLoginMetrics(user *dataprovider.User, ip string, err error) {
metrics.AddLoginAttempt(dataprovider.LoginMethodPassword)
func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err error) {
metrics.AddLoginAttempt(loginMethod)
if err != nil {
logger.ConnectionFailedLog(user.Username, ip, dataprovider.LoginMethodPassword, common.ProtocolWebDAV, err.Error())
logger.ConnectionFailedLog(user.Username, ip, loginMethod, common.ProtocolWebDAV, err.Error())
event := common.HostEventLoginFailed
if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
event = common.HostEventUserNotFound
}
common.AddDefenderEvent(ip, event)
}
metrics.AddLoginResult(dataprovider.LoginMethodPassword, err)
dataprovider.ExecutePostLoginHook(user, dataprovider.LoginMethodPassword, ip, common.ProtocolWebDAV, err)
metrics.AddLoginResult(loginMethod, err)
dataprovider.ExecutePostLoginHook(user, loginMethod, ip, common.ProtocolWebDAV, err)
}

View file

@ -5,7 +5,7 @@ import (
"fmt"
"path/filepath"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/chi/v5/middleware"
"github.com/drakkan/sftpgo/common"
"github.com/drakkan/sftpgo/logger"
@ -88,6 +88,10 @@ type Binding struct {
TLSCipherSuites []string `json:"tls_cipher_suites" mapstructure:"tls_cipher_suites"`
}
func (b *Binding) isMutualTLSEnabled() bool {
return b.ClientAuthType == 1 || b.ClientAuthType == 2
}
// GetAddress returns the binding address
func (b *Binding) GetAddress() string {
return fmt.Sprintf("%s:%d", b.Address, b.Port)

View file

@ -3,6 +3,7 @@ package webdavd_test
import (
"bytes"
"crypto/rand"
"crypto/tls"
"encoding/json"
"fmt"
"io"
@ -36,15 +37,17 @@ import (
)
const (
logSender = "webavdTesting"
webDavServerAddr = "127.0.0.1:9090"
webDavServerPort = 9090
sftpServerAddr = "127.0.0.1:9022"
defaultUsername = "test_user_dav"
defaultPassword = "test_password"
configDir = ".."
osWindows = "windows"
webDavCert = `-----BEGIN CERTIFICATE-----
logSender = "webavdTesting"
webDavServerAddr = "localhost:9090"
webDavTLSServerAddr = "localhost:9443"
webDavServerPort = 9090
webDavTLSServerPort = 9443
sftpServerAddr = "127.0.0.1:9022"
defaultUsername = "test_user_dav"
defaultPassword = "test_password"
configDir = ".."
osWindows = "windows"
webDavCert = `-----BEGIN CERTIFICATE-----
MIICHTCCAaKgAwIBAgIUHnqw7QnB1Bj9oUsNpdb+ZkFPOxMwCgYIKoZIzj0EAwIw
RTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGElu
dGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDAyMDQwOTUzMDRaFw0zMDAyMDEw
@ -67,8 +70,161 @@ UM2lmBLIXpGgBwYFK4EEACKhZANiAARCjRMqJ85rzMC998X5z761nJ+xL3bkmGVq
WvrJ51t5OxV0v25NsOgR82CANXUgvhVYs7vNFN+jxtb2aj6Xg+/2G/BNxkaFspIV
CzgWkxiz7XE4lgUwX44FCXZM3+JeUbI=
-----END EC PRIVATE KEY-----`
testFileName = "test_file_dav.dat"
testDLFileName = "test_download_dav.dat"
caCRT = `-----BEGIN CERTIFICATE-----
MIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0
QXV0aDAeFw0yMTAxMDIyMTIwNTVaFw0yMjA3MDIyMTMwNTJaMBMxETAPBgNVBAMT
CENlcnRBdXRoMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4Tiho5xW
AC15JRkMwfp3/TJwI2As7MY5dele5cmdr5bHAE+sRKqC+Ti88OJWCV5saoyax/1S
CjxJlQMZMl169P1QYJskKjdG2sdv6RLWLMgwSNRRjxp/Bw9dHdiEb9MjLgu28Jro
9peQkHcRHeMf5hM9WvlIJGrdzbC4hUehmqggcqgARainBkYjf0SwuWxHeu4nMqkp
Ak5tcSTLCjHfEFHZ9Te0TIPG5YkWocQKyeLgu4lvuU+DD2W2lym+YVUtRMGs1Env
k7p+N0DcGU26qfzZ2sF5ZXkqm7dBsGQB9pIxwc2Q8T1dCIyP9OQCKVILdc5aVFf1
cryQFHYzYNNZXFlIBims5VV5Mgfp8ESHQSue+v6n6ykecLEyKt1F1Y/MWY/nWUSI
8zdq83jdBAZVjo9MSthxVn57/06s/hQca65IpcTZV2gX0a+eRlAVqaRbAhL3LaZe
bYsW3WHKoUOftwemuep3nL51TzlXZVL7Oz/ClGaEOsnGG9KFO6jh+W768qC0zLQI
CdE7v2Zex98sZteHCg9fGJHIaYoF0aJG5P3WI5oZf2fy7UIYN9ADLFZiorCXAZEh
CSU6mDoRViZ4RGR9GZxbDZ9KYn7O8M/KCR72bkQg73TlMsk1zSXEw0MKLUjtsw6c
rZ0Jt8t3sRatHO3JrYHALMt9vZfyNCZp0IsCAwEAAaNFMEMwDgYDVR0PAQH/BAQD
AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFO1yCNAGr/zQTJIi8lw3
w5OiuBvMMA0GCSqGSIb3DQEBCwUAA4ICAQA6gCNuM7r8mnx674dm31GxBjQy5ZwB
7CxDzYEvL/oiZ3Tv3HlPfN2LAAsJUfGnghh9DOytenL2CTZWjl/emP5eijzmlP+9
zva5I6CIMCf/eDDVsRdO244t0o4uG7+At0IgSDM3bpVaVb4RHZNjEziYChsEYY8d
HK6iwuRSvFniV6yhR/Vj1Ymi9yZ5xclqseLXiQnUB0PkfIk23+7s42cXB16653fH
O/FsPyKBLiKJArizLYQc12aP3QOrYoYD9+fAzIIzew7A5C0aanZCGzkuFpO6TRlD
Tb7ry9Gf0DfPpCgxraH8tOcmnqp/ka3hjqo/SRnnTk0IFrmmLdarJvjD46rKwBo4
MjyAIR1mQ5j8GTlSFBmSgETOQ/EYvO3FPLmra1Fh7L+DvaVzTpqI9fG3TuyyY+Ri
Fby4ycTOGSZOe5Fh8lqkX5Y47mCUJ3zHzOA1vUJy2eTlMRGpu47Eb1++Vm6EzPUP
2EF5aD+zwcssh+atZvQbwxpgVqVcyLt91RSkKkmZQslh0rnlTb68yxvUnD3zw7So
o6TAf9UvwVMEvdLT9NnFd6hwi2jcNte/h538GJwXeBb8EkfpqLKpTKyicnOdkamZ
7E9zY8SHNRYMwB9coQ/W8NvufbCgkvOoLyMXk5edbXofXl3PhNGOlraWbghBnzf5
r3rwjFsQOoZotA==
-----END CERTIFICATE-----`
caCRL = `-----BEGIN X509 CRL-----
MIICpzCBkAIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhDZXJ0QXV0aBcN
MjEwMTAyMjEzNDA1WhcNMjMwMTAyMjEzNDA1WjAkMCICEQC+l04DbHWMyC3fG09k
VXf+Fw0yMTAxMDIyMTM0MDVaoCMwITAfBgNVHSMEGDAWgBTtcgjQBq/80EySIvJc
N8OTorgbzDANBgkqhkiG9w0BAQsFAAOCAgEAEJ7z+uNc8sqtxlOhSdTGDzX/xput
E857kFQkSlMnU2whQ8c+XpYrBLA5vIZJNSSwohTpM4+zVBX/bJpmu3wqqaArRO9/
YcW5mQk9Anvb4WjQW1cHmtNapMTzoC9AiYt/OWPfy+P6JCgCr4Hy6LgQyIRL6bM9
VYTalolOm1qa4Y5cIeT7iHq/91mfaqo8/6MYRjLl8DOTROpmw8OS9bCXkzGKdCat
AbAzwkQUSauyoCQ10rpX+Y64w9ng3g4Dr20aCqPf5osaqplEJ2HTK8ljDTidlslv
9anQj8ax3Su89vI8+hK+YbfVQwrThabgdSjQsn+veyx8GlP8WwHLAQ379KjZjWg+
OlOSwBeU1vTdP0QcB8X5C2gVujAyuQekbaV86xzIBOj7vZdfHZ6ee30TZ2FKiMyg
7/N2OqW0w77ChsjB4MSHJCfuTgIeg62GzuZXLM+Q2Z9LBdtm4Byg+sm/P52adOEg
gVb2Zf4KSvsAmA0PIBlu449/QXUFcMxzLFy7mwTeZj2B4Ln0Hm0szV9f9R8MwMtB
SyLYxVH+mgqaR6Jkk22Q/yYyLPaELfafX5gp/AIXG8n0zxfVaTvK3auSgb1Q6ZLS
5QH9dSIsmZHlPq7GoSXmKpMdjUL8eaky/IMteioyXgsBiATzl5L2dsw6MTX3MDF0
QbDK+MzhmbKfDxs=
-----END X509 CRL-----`
client1Crt = `-----BEGIN CERTIFICATE-----
MIIEITCCAgmgAwIBAgIRAIppZHoj1hM80D7WzTEKLuAwDQYJKoZIhvcNAQELBQAw
EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjEwMTAyMjEyMzEwWhcNMjIwNzAyMjEz
MDUxWjASMRAwDgYDVQQDEwdjbGllbnQxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAoKbYY9MdF2kF/nhBESIiZTdVYtA8XL9xrIZyDj9EnCiTxHiVbJtH
XVwszqSl5TRrotPmnmAQcX3r8OCk+z+RQZ0QQj257P3kG6q4rNnOcWCS5xEd20jP
yhQ3m+hMGfZsotNTQze1ochuQgLUN6IPyPxZkH22ia3jX4iu1eo/QxeLYHj1UHw4
3Cii9yE+j5kPUC21xmnrGKdUrB55NYLXHx6yTIqYR5znSOVB8oJi18/hwdZmH859
DHhm0Hx1HrS+jbjI3+CMorZJ3WUyNf+CkiVLD3xYutPbxzEpwiqkG/XYzLH0habT
cDcILo18n+o3jvem2KWBrDhyairjIDscwQIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC
A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBSJ5GIv
zIrE4ZSQt2+CGblKTDswizAfBgNVHSMEGDAWgBTtcgjQBq/80EySIvJcN8OTorgb
zDANBgkqhkiG9w0BAQsFAAOCAgEALh4f5GhvNYNou0Ab04iQBbLEdOu2RlbK1B5n
K9P/umYenBHMY/z6HT3+6tpcHsDuqE8UVdq3f3Gh4S2Gu9m8PRitT+cJ3gdo9Plm
3rD4ufn/s6rGg3ppydXcedm17492tbccUDWOBZw3IO/ASVq13WPgT0/Kev7cPq0k
sSdSNhVeXqx8Myc2/d+8GYyzbul2Kpfa7h9i24sK49E9ftnSmsIvngONo08eT1T0
3wAOyK2981LIsHaAWcneShKFLDB6LeXIT9oitOYhiykhFlBZ4M1GNlSNfhQ8IIQP
xbqMNXCLkW4/BtLhGEEcg0QVso6Kudl9rzgTfQknrdF7pHp6rS46wYUjoSyIY6dl
oLmnoAVJX36J3QPWelePI9e07X2wrTfiZWewwgw3KNRWjd6/zfPLe7GoqXnK1S2z
PT8qMfCaTwKTtUkzXuTFvQ8bAo2My/mS8FOcpkt2oQWeOsADHAUX7fz5BCoa2DL3
k/7Mh4gVT+JYZEoTwCFuYHgMWFWe98naqHi9lB4yR981p1QgXgxO7qBeipagKY1F
LlH1iwXUqZ3MZnkNA+4e1Fglsw3sa/rC+L98HnznJ/YbTfQbCP6aQ1qcOymrjMud
7MrFwqZjtd/SK4Qx1VpK6jGEAtPgWBTUS3p9ayg6lqjMBjsmySWfvRsDQbq6P5Ct
O/e3EH8=
-----END CERTIFICATE-----`
client1Key = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAoKbYY9MdF2kF/nhBESIiZTdVYtA8XL9xrIZyDj9EnCiTxHiV
bJtHXVwszqSl5TRrotPmnmAQcX3r8OCk+z+RQZ0QQj257P3kG6q4rNnOcWCS5xEd
20jPyhQ3m+hMGfZsotNTQze1ochuQgLUN6IPyPxZkH22ia3jX4iu1eo/QxeLYHj1
UHw43Cii9yE+j5kPUC21xmnrGKdUrB55NYLXHx6yTIqYR5znSOVB8oJi18/hwdZm
H859DHhm0Hx1HrS+jbjI3+CMorZJ3WUyNf+CkiVLD3xYutPbxzEpwiqkG/XYzLH0
habTcDcILo18n+o3jvem2KWBrDhyairjIDscwQIDAQABAoIBAEBSjVFqtbsp0byR
aXvyrtLX1Ng7h++at2jca85Ihq//jyqbHTje8zPuNAKI6eNbmb0YGr5OuEa4pD9N
ssDmMsKSoG/lRwwcm7h4InkSvBWpFShvMgUaohfHAHzsBYxfnh+TfULsi0y7c2n6
t/2OZcOTRkkUDIITnXYiw93ibHHv2Mv2bBDu35kGrcK+c2dN5IL5ZjTjMRpbJTe2
44RBJbdTxHBVSgoGBnugF+s2aEma6Ehsj70oyfoVpM6Aed5kGge0A5zA1JO7WCn9
Ay/DzlULRXHjJIoRWd2NKvx5n3FNppUc9vJh2plRHalRooZ2+MjSf8HmXlvG2Hpb
ScvmWgECgYEA1G+A/2KnxWsr/7uWIJ7ClcGCiNLdk17Pv3DZ3G4qUsU2ITftfIbb
tU0Q/b19na1IY8Pjy9ptP7t74/hF5kky97cf1FA8F+nMj/k4+wO8QDI8OJfzVzh9
PwielA5vbE+xmvis5Hdp8/od1Yrc/rPSy2TKtPFhvsqXjqoUmOAjDP8CgYEAwZjH
9dt1sc2lx/rMxihlWEzQ3JPswKW9/LJAmbRBoSWF9FGNjbX7uhWtXRKJkzb8ZAwa
88azluNo2oftbDD/+jw8b2cDgaJHlLAkSD4O1D1RthW7/LKD15qZ/oFsRb13NV85
ZNKtwslXGbfVNyGKUVFm7fVA8vBAOUey+LKDFj8CgYEAg8WWstOzVdYguMTXXuyb
ruEV42FJaDyLiSirOvxq7GTAKuLSQUg1yMRBIeQEo2X1XU0JZE3dLodRVhuO4EXP
g7Dn4X7Th9HSvgvNuIacowWGLWSz4Qp9RjhGhXhezUSx2nseY6le46PmFavJYYSR
4PBofMyt4PcyA6Cknh+KHmkCgYEAnTriG7ETE0a7v4DXUpB4TpCEiMCy5Xs2o8Z5
ZNva+W+qLVUWq+MDAIyechqeFSvxK6gRM69LJ96lx+XhU58wJiFJzAhT9rK/g+jS
bsHH9WOfu0xHkuHA5hgvvV2Le9B2wqgFyva4HJy82qxMxCu/VG/SMqyfBS9OWbb7
ibQhdq0CgYAl53LUWZsFSZIth1vux2LVOsI8C3X1oiXDGpnrdlQ+K7z57hq5EsRq
GC+INxwXbvKNqp5h0z2MvmKYPDlGVTgw8f8JjM7TkN17ERLcydhdRrMONUryZpo8
1xTob+8blyJgfxZUIAKbMbMbIiU0WAF0rfD/eJJwS4htOW/Hfv4TGA==
-----END RSA PRIVATE KEY-----`
// client 2 crt is revoked
client2Crt = `-----BEGIN CERTIFICATE-----
MIIEITCCAgmgAwIBAgIRAL6XTgNsdYzILd8bT2RVd/4wDQYJKoZIhvcNAQELBQAw
EzERMA8GA1UEAxMIQ2VydEF1dGgwHhcNMjEwMTAyMjEyMzIwWhcNMjIwNzAyMjEz
MDUxWjASMRAwDgYDVQQDEwdjbGllbnQyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEA6xjW5KQR3/OFQtV5M75WINqQ4AzXSu6DhSz/yumaaQZP/UxY+6hi
jcrFzGo9MMie/Sza8DhkXOFAl2BelUubrOeB2cl+/Gr8OCyRi2Gv6j3zCsuN/4jQ
tNaoez/IbkDvI3l/ZpzBtnuNY2RiemGgHuORXHRVf3qVlsw+npBIRW5rM2HkO/xG
oZjeBErWVu390Lyn+Gvk2TqQDnkutWnxUC60/zPlHhXZ4BwaFAekbSnjsSDB1YFM
s8HwW4oBryoxdj3/+/qLrBHt75IdLw3T7/V1UDJQM3EvSQOr12w4egpldhtsC871
nnBQZeY6qA5feffIwwg/6lJm70o6S6OX6wIDAQABo3EwbzAOBgNVHQ8BAf8EBAMC
A7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBTB84v5
t9HqhLhMODbn6oYkEQt3KzAfBgNVHSMEGDAWgBTtcgjQBq/80EySIvJcN8OTorgb
zDANBgkqhkiG9w0BAQsFAAOCAgEALGtBCve5k8tToL3oLuXp/oSik6ovIB/zq4I/
4zNMYPU31+ZWz6aahysgx1JL1yqTa3Qm8o2tu52MbnV10dM7CIw7c/cYa+c+OPcG
5LF97kp13X+r2axy+CmwM86b4ILaDGs2Qyai6VB6k7oFUve+av5o7aUrNFpqGCJz
HWdtHZSVA3JMATzy0TfWanwkzreqfdw7qH0yZ9bDURlBKAVWrqnCstva9jRuv+AI
eqxr/4Ro986TFjJdoAP3Vr16CPg7/B6GA/KmsBWJrpeJdPWq4i2gpLKvYZoy89qD
mUZf34RbzcCtV4NvV1DadGnt4us0nvLrvS5rL2+2uWD09kZYq9RbLkvgzF/cY0fz
i7I1bi5XQ+alWe0uAk5ZZL/D+GTRYUX1AWwCqwJxmHrMxcskMyO9pXvLyuSWRDLo
YNBrbX9nLcfJzVCp+X+9sntTHjs4l6Cw+fLepJIgtgqdCHtbhTiv68vSM6cgb4br
6n2xrXRKuioiWFOrTSRr+oalZh8dGJ/xvwY8IbWknZAvml9mf1VvfE7Ma5P777QM
fsbYVTq0Y3R/5hIWsC3HA5z6MIM8L1oRe/YyhP3CTmrCHkVKyDOosGXpGz+JVcyo
cfYkY5A3yFKB2HaCwZSfwFmRhxkrYWGEbHv3Cd9YkZs1J3hNhGFZyVMC9Uh0S85a
6zdDidU=
-----END CERTIFICATE-----`
client2Key = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA6xjW5KQR3/OFQtV5M75WINqQ4AzXSu6DhSz/yumaaQZP/UxY
+6hijcrFzGo9MMie/Sza8DhkXOFAl2BelUubrOeB2cl+/Gr8OCyRi2Gv6j3zCsuN
/4jQtNaoez/IbkDvI3l/ZpzBtnuNY2RiemGgHuORXHRVf3qVlsw+npBIRW5rM2Hk
O/xGoZjeBErWVu390Lyn+Gvk2TqQDnkutWnxUC60/zPlHhXZ4BwaFAekbSnjsSDB
1YFMs8HwW4oBryoxdj3/+/qLrBHt75IdLw3T7/V1UDJQM3EvSQOr12w4egpldhts
C871nnBQZeY6qA5feffIwwg/6lJm70o6S6OX6wIDAQABAoIBAFatstVb1KdQXsq0
cFpui8zTKOUiduJOrDkWzTygAmlEhYtrccdfXu7OWz0x0lvBLDVGK3a0I/TGrAzj
4BuFY+FM/egxTVt9in6fmA3et4BS1OAfCryzUdfK6RV//8L+t+zJZ/qKQzWnugpy
QYjDo8ifuMFwtvEoXizaIyBNLAhEp9hnrv+Tyi2O2gahPvCHsD48zkyZRCHYRstD
NH5cIrwz9/RJgPO1KI+QsJE7Nh7stR0sbr+5TPU4fnsL2mNhMUF2TJrwIPrc1yp+
YIUjdnh3SO88j4TQT3CIrWi8i4pOy6N0dcVn3gpCRGaqAKyS2ZYUj+yVtLO4KwxZ
SZ1lNvECgYEA78BrF7f4ETfWSLcBQ3qxfLs7ibB6IYo2x25685FhZjD+zLXM1AKb
FJHEXUm3mUYrFJK6AFEyOQnyGKBOLs3S6oTAswMPbTkkZeD1Y9O6uv0AHASLZnK6
pC6ub0eSRF5LUyTQ55Jj8D7QsjXJueO8v+G5ihWhNSN9tB2UA+8NBmkCgYEA+weq
cvoeMIEMBQHnNNLy35bwfqrceGyPIRBcUIvzQfY1vk7KW6DYOUzC7u+WUzy/hA52
DjXVVhua2eMQ9qqtOav7djcMc2W9RbLowxvno7K5qiCss013MeWk64TCWy+WMp5A
AVAtOliC3hMkIKqvR2poqn+IBTh1449agUJQqTMCgYEAu06IHGq1GraV6g9XpGF5
wqoAlMzUTdnOfDabRilBf/YtSr+J++ThRcuwLvXFw7CnPZZ4TIEjDJ7xjj3HdxeE
fYYjineMmNd40UNUU556F1ZLvJfsVKizmkuCKhwvcMx+asGrmA+tlmds4p3VMS50
KzDtpKzLWlmU/p/RINWlRmkCgYBy0pHTn7aZZx2xWKqCDg+L2EXPGqZX6wgZDpu7
OBifzlfM4ctL2CmvI/5yPmLbVgkgBWFYpKUdiujsyyEiQvWTUKhn7UwjqKDHtcsk
G6p7xS+JswJrzX4885bZJ9Oi1AR2yM3sC9l0O7I4lDbNPmWIXBLeEhGMmcPKv/Kc
91Ff4wKBgQCF3ur+Vt0PSU0ucrPVHjCe7tqazm0LJaWbPXL1Aw0pzdM2EcNcW/MA
w0kqpr7MgJ94qhXCBcVcfPuFN9fBOadM3UBj1B45Cz3pptoK+ScI8XKno6jvVK/p
xr5cb9VBRBtB9aOKVfuRhpatAfS2Pzm2Htae9lFn7slGPUmu2hkjDw==
-----END RSA PRIVATE KEY-----`
testFileName = "test_file_dav.dat"
testDLFileName = "test_download_dav.dat"
tlsClient1Username = "client1"
tlsClient2Username = "client2"
)
var (
@ -81,6 +237,8 @@ var (
logFilePath string
certPath string
keyPath string
caCrtPath string
caCRLPath string
)
func TestMain(m *testing.M) {
@ -108,6 +266,8 @@ func TestMain(m *testing.M) {
certPath = filepath.Join(os.TempDir(), "test_dav.crt")
keyPath = filepath.Join(os.TempDir(), "test_dav.key")
caCrtPath = filepath.Join(os.TempDir(), "test_dav_ca.crt")
caCRLPath = filepath.Join(os.TempDir(), "test_dav_crl.crt")
err = os.WriteFile(certPath, []byte(webDavCert), os.ModePerm)
if err != nil {
logger.ErrorToConsole("error writing WebDAV certificate: %v", err)
@ -118,6 +278,16 @@ func TestMain(m *testing.M) {
logger.ErrorToConsole("error writing WebDAV private key: %v", err)
os.Exit(1)
}
err = os.WriteFile(caCrtPath, []byte(caCRT), os.ModePerm)
if err != nil {
logger.ErrorToConsole("error writing WebDAV CA crt: %v", err)
os.Exit(1)
}
err = os.WriteFile(caCRLPath, []byte(caCRL), os.ModePerm)
if err != nil {
logger.ErrorToConsole("error writing WebDAV CRL: %v", err)
os.Exit(1)
}
err = common.Initialize(commonConf)
if err != nil {
@ -155,10 +325,19 @@ func TestMain(m *testing.M) {
sftpdConf.HostKeys = []string{hostKeyPath}
webDavConf := config.GetWebDAVDConfig()
webDavConf.CertificateFile = certPath
webDavConf.CertificateKeyFile = keyPath
webDavConf.CACertificates = []string{caCrtPath}
webDavConf.CARevocationLists = []string{caCRLPath}
webDavConf.Bindings = []webdavd.Binding{
{
Port: webDavServerPort,
},
{
Port: webDavTLSServerPort,
EnableHTTPS: true,
ClientAuthType: 2,
},
}
webDavConf.Cors = webdavd.Cors{
Enabled: true,
@ -209,6 +388,7 @@ func TestMain(m *testing.M) {
}()
waitTCPListening(webDavConf.Bindings[0].GetAddress())
waitTCPListening(webDavConf.Bindings[1].GetAddress())
waitTCPListening(httpdConf.Bindings[0].GetAddress())
waitTCPListening(sftpdConf.Bindings[0].GetAddress())
webdavd.ReloadCertificateMgr() //nolint:errcheck
@ -220,6 +400,8 @@ func TestMain(m *testing.M) {
os.Remove(postConnectPath)
os.Remove(certPath)
os.Remove(keyPath)
os.Remove(caCrtPath)
os.Remove(caCRLPath)
os.Remove(hostKeyPath)
os.Remove(hostKeyPath + ".pub")
os.Exit(exitCode)
@ -281,6 +463,13 @@ func TestInitialization(t *testing.T) {
cfg.CARevocationLists = nil
err = cfg.Initialize(configDir)
assert.Error(t, err)
cfg.CertificateFile = certPath
cfg.CertificateKeyFile = keyPath
cfg.CACertificates = []string{caCrtPath}
cfg.CARevocationLists = []string{caCRLPath}
err = cfg.Initialize(configDir)
assert.Error(t, err)
}
func TestBasicHandling(t *testing.T) {
@ -293,7 +482,7 @@ func TestBasicHandling(t *testing.T) {
sftpUser, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
for _, user := range []dataprovider.User{localUser, sftpUser} {
client := getWebDavClient(user)
client := getWebDavClient(user, true, nil)
assert.NoError(t, checkBasicFunc(client))
testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := int64(65535)
@ -378,7 +567,7 @@ func TestBasicHandlingCryptFs(t *testing.T) {
u.QuotaSize = 6553600
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
client := getWebDavClient(user, false, nil)
assert.NoError(t, checkBasicFunc(client))
testFilePath := filepath.Join(homeBasePath, testFileName)
@ -459,7 +648,7 @@ func TestPropPatch(t *testing.T) {
for _, u := range []dataprovider.User{getTestUser(), getTestUserWithCryptFs(), sftpUser} {
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
client := getWebDavClient(user, true, nil)
assert.NoError(t, checkBasicFunc(client), sftpUser.Username)
testFilePath := filepath.Join(homeBasePath, testFileName)
@ -500,10 +689,10 @@ func TestLoginInvalidPwd(t *testing.T) {
u := getTestUser()
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
client := getWebDavClient(user, false, nil)
assert.NoError(t, checkBasicFunc(client))
user.Password = "wrong"
client = getWebDavClient(user)
client = getWebDavClient(user, false, nil)
assert.Error(t, checkBasicFunc(client))
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
@ -511,7 +700,7 @@ func TestLoginInvalidPwd(t *testing.T) {
func TestLoginNonExistentUser(t *testing.T) {
user := getTestUser()
client := getWebDavClient(user)
client := getWebDavClient(user, true, nil)
assert.Error(t, checkBasicFunc(client))
}
@ -527,17 +716,17 @@ func TestDefender(t *testing.T) {
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
client := getWebDavClient(user, true, nil)
assert.NoError(t, checkBasicFunc(client))
for i := 0; i < 3; i++ {
user.Password = "wrong_pwd"
client = getWebDavClient(user)
client = getWebDavClient(user, false, nil)
assert.Error(t, checkBasicFunc(client))
}
user.Password = defaultPassword
client = getWebDavClient(user)
client = getWebDavClient(user, true, nil)
err = checkBasicFunc(client)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "403")
@ -568,10 +757,10 @@ func TestLoginExternalAuth(t *testing.T) {
providerConf.ExternalAuthScope = 0
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
client := getWebDavClient(u)
client := getWebDavClient(u, false, nil)
assert.NoError(t, checkBasicFunc(client))
u.Username = defaultUsername + "1"
client = getWebDavClient(u)
client = getWebDavClient(u, false, nil)
assert.Error(t, checkBasicFunc(client))
user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)
assert.NoError(t, err)
@ -608,20 +797,20 @@ func TestPreLoginHook(t *testing.T) {
assert.NoError(t, err)
_, _, err = httpdtest.GetUserByUsername(defaultUsername, http.StatusNotFound)
assert.NoError(t, err)
client := getWebDavClient(u)
client := getWebDavClient(u, true, nil)
assert.NoError(t, checkBasicFunc(client))
user, _, err := httpdtest.GetUserByUsername(defaultUsername, http.StatusOK)
assert.NoError(t, err)
// test login with an existing user
client = getWebDavClient(user)
client = getWebDavClient(user, true, nil)
assert.NoError(t, checkBasicFunc(client))
err = os.WriteFile(preLoginPath, getPreLoginScriptContent(user, true), os.ModePerm)
assert.NoError(t, err)
// update the user to remove it from the cache
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client = getWebDavClient(user)
client = getWebDavClient(user, true, nil)
assert.Error(t, checkBasicFunc(client))
// update the user to remove it from the cache
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
@ -629,7 +818,7 @@ func TestPreLoginHook(t *testing.T) {
user.Status = 0
err = os.WriteFile(preLoginPath, getPreLoginScriptContent(user, false), os.ModePerm)
assert.NoError(t, err)
client = getWebDavClient(user)
client = getWebDavClient(user, true, nil)
assert.Error(t, checkBasicFunc(client))
_, err = httpdtest.RemoveUser(user, http.StatusOK)
@ -658,7 +847,7 @@ func TestPostConnectHook(t *testing.T) {
assert.NoError(t, err)
err = os.WriteFile(postConnectPath, getPostConnectScriptContent(0), os.ModePerm)
assert.NoError(t, err)
client := getWebDavClient(user)
client := getWebDavClient(user, false, nil)
assert.NoError(t, checkBasicFunc(client))
err = os.WriteFile(postConnectPath, getPostConnectScriptContent(1), os.ModePerm)
assert.NoError(t, err)
@ -684,7 +873,7 @@ func TestMaxConnections(t *testing.T) {
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
client := getWebDavClient(user, true, nil)
assert.NoError(t, checkBasicFunc(client))
// now add a fake connection
fs := vfs.NewOsFs("id", os.TempDir(), nil)
@ -708,7 +897,7 @@ func TestMaxSessions(t *testing.T) {
u.MaxSessions = 1
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
client := getWebDavClient(user, false, nil)
assert.NoError(t, checkBasicFunc(client))
// now add a fake connection
fs := vfs.NewOsFs("id", os.TempDir(), nil)
@ -731,7 +920,7 @@ func TestLoginWithIPilters(t *testing.T) {
u.Filters.AllowedIP = []string{"172.19.0.0/16"}
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
client := getWebDavClient(user, true, nil)
assert.Error(t, checkBasicFunc(client))
_, err = httpdtest.RemoveUser(user, http.StatusOK)
@ -765,7 +954,7 @@ func TestDownloadErrors(t *testing.T) {
}
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
client := getWebDavClient(user, false, nil)
testFilePath1 := filepath.Join(user.HomeDir, subDir1, "file.zipp")
testFilePath2 := filepath.Join(user.HomeDir, subDir2, "file.zipp")
testFilePath3 := filepath.Join(user.HomeDir, subDir2, "file.jpg")
@ -815,7 +1004,7 @@ func TestUploadErrors(t *testing.T) {
}
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
client := getWebDavClient(user, true, nil)
testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := user.QuotaSize
err = createTestFile(testFilePath, testFileSize)
@ -859,13 +1048,13 @@ func TestDeniedLoginMethod(t *testing.T) {
u.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword}
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
client := getWebDavClient(user, true, nil)
assert.Error(t, checkBasicFunc(client))
user.Filters.DeniedLoginMethods = []string{dataprovider.SSHLoginMethodPublicKey, dataprovider.SSHLoginMethodKeyAndKeyboardInt}
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client = getWebDavClient(user)
client = getWebDavClient(user, true, nil)
assert.NoError(t, checkBasicFunc(client))
_, err = httpdtest.RemoveUser(user, http.StatusOK)
@ -879,13 +1068,13 @@ func TestDeniedProtocols(t *testing.T) {
u.Filters.DeniedProtocols = []string{common.ProtocolWebDAV}
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
client := getWebDavClient(user, false, nil)
assert.Error(t, checkBasicFunc(client))
user.Filters.DeniedProtocols = []string{common.ProtocolSSH, common.ProtocolFTP}
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client = getWebDavClient(user)
client = getWebDavClient(user, false, nil)
assert.NoError(t, checkBasicFunc(client))
_, err = httpdtest.RemoveUser(user, http.StatusOK)
@ -918,7 +1107,7 @@ func TestQuotaLimits(t *testing.T) {
testFilePath2 := filepath.Join(homeBasePath, testFileName2)
err = createTestFile(testFilePath2, testFileSize2)
assert.NoError(t, err)
client := getWebDavClient(user)
client := getWebDavClient(user, true, nil)
// test quota files
err = uploadFile(testFilePath, testFileName+".quota", testFileSize, client)
assert.NoError(t, err)
@ -1001,7 +1190,7 @@ func TestUploadMaxSize(t *testing.T) {
testFilePath1 := filepath.Join(homeBasePath, testFileName1)
err = createTestFile(testFilePath1, testFileSize1)
assert.NoError(t, err)
client := getWebDavClient(user)
client := getWebDavClient(user, false, nil)
err = uploadFile(testFilePath1, testFileName1, testFileSize1, client)
assert.Error(t, err)
err = uploadFile(testFilePath, testFileName, testFileSize, client)
@ -1048,7 +1237,7 @@ func TestClientClose(t *testing.T) {
testFilePath := filepath.Join(homeBasePath, testFileName)
err = createTestFile(testFilePath, testFileSize)
assert.NoError(t, err)
client := getWebDavClient(user)
client := getWebDavClient(user, true, nil)
assert.NoError(t, checkBasicFunc(client))
var wg sync.WaitGroup
@ -1149,7 +1338,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
assert.NoFileExists(t, credentialsFile)
client := getWebDavClient(user)
client := getWebDavClient(user, false, nil)
err = client.Connect()
assert.NoError(t, err)
@ -1183,7 +1372,7 @@ func TestLoginInvalidFs(t *testing.T) {
err = os.Remove(credentialsFile)
assert.NoError(t, err)
client := getWebDavClient(user)
client := getWebDavClient(user, true, nil)
assert.Error(t, checkBasicFunc(client))
_, err = httpdtest.RemoveUser(user, http.StatusOK)
@ -1208,7 +1397,7 @@ func TestBytesRangeRequests(t *testing.T) {
fileContent := []byte("test file contents")
err = os.WriteFile(testFilePath, fileContent, os.ModePerm)
assert.NoError(t, err)
client := getWebDavClient(user)
client := getWebDavClient(user, true, nil)
err = uploadFile(testFilePath, testFileName, int64(len(fileContent)), client)
assert.NoError(t, err)
remotePath := fmt.Sprintf("http://%v/%v", webDavServerAddr, testFileName)
@ -1290,7 +1479,7 @@ func TestGETAsPROPFIND(t *testing.T) {
resp.Body.Close()
}
}
client := getWebDavClient(user)
client := getWebDavClient(user, false, nil)
err = client.MkdirAll(path.Join(subDir1, "sub", "sub1"), os.ModePerm)
assert.NoError(t, err)
subPath := fmt.Sprintf("http://%v/%v", webDavServerAddr, subDir1)
@ -1341,7 +1530,7 @@ func TestStat(t *testing.T) {
u.Permissions["/subdir"] = []string{dataprovider.PermUpload, dataprovider.PermListItems, dataprovider.PermDownload}
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
client := getWebDavClient(user, true, nil)
subDir := "subdir"
testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := int64(65535)
@ -1387,7 +1576,7 @@ func TestUploadOverwriteVfolder(t *testing.T) {
assert.NoError(t, err)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
client := getWebDavClient(user)
client := getWebDavClient(user, false, nil)
files, err := client.ReadDir(".")
assert.NoError(t, err)
vdirFound := false
@ -1442,7 +1631,7 @@ func TestMiscCommands(t *testing.T) {
assert.NoError(t, err)
for _, user := range []dataprovider.User{localUser, sftpUser} {
dir := "testDir"
client := getWebDavClient(user)
client := getWebDavClient(user, true, nil)
err = client.MkdirAll(path.Join(dir, "sub1", "sub2"), os.ModePerm)
assert.NoError(t, err)
testFilePath := filepath.Join(homeBasePath, testFileName)
@ -1504,6 +1693,319 @@ func TestMiscCommands(t *testing.T) {
assert.NoError(t, err)
}
func TestClientCertificateAuthRevokedCert(t *testing.T) {
u := getTestUser()
u.Username = tlsClient2Username
u.Filters.TLSUsername = dataprovider.TLSUsernameCN
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
tlsConfig := &tls.Config{
ServerName: "localhost",
InsecureSkipVerify: true, // use this for tests only
MinVersion: tls.VersionTLS12,
}
tlsCert, err := tls.X509KeyPair([]byte(client2Crt), []byte(client2Key))
assert.NoError(t, err)
tlsConfig.Certificates = append(tlsConfig.Certificates, tlsCert)
client := getWebDavClient(user, true, tlsConfig)
err = checkBasicFunc(client)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "bad certificate")
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestClientCertificateAuth(t *testing.T) {
u := getTestUser()
u.Username = tlsClient1Username
u.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword, dataprovider.LoginMethodTLSCertificateAndPwd}
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
tlsConfig := &tls.Config{
ServerName: "localhost",
InsecureSkipVerify: true, // use this for tests only
MinVersion: tls.VersionTLS12,
}
tlsCert, err := tls.X509KeyPair([]byte(client1Crt), []byte(client1Key))
assert.NoError(t, err)
tlsConfig.Certificates = append(tlsConfig.Certificates, tlsCert)
// TLS username is not enabled, mutual TLS should fail
resp, err := getTLSHTTPClient(tlsConfig).Get(fmt.Sprintf("https://%v/", webDavTLSServerAddr))
if assert.NoError(t, err) {
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode, string(body))
}
user.Filters.TLSUsername = dataprovider.TLSUsernameCN
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client := getWebDavClient(user, true, tlsConfig)
err = checkBasicFunc(client)
assert.NoError(t, err)
user.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword, dataprovider.LoginMethodTLSCertificate}
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client = getWebDavClient(user, true, tlsConfig)
err = checkBasicFunc(client)
assert.NoError(t, err)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestWrongClientCertificate(t *testing.T) {
u := getTestUser()
u.Username = tlsClient2Username
u.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodTLSCertificateAndPwd}
u.Filters.TLSUsername = dataprovider.TLSUsernameCN
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
tlsConfig := &tls.Config{
ServerName: "localhost",
InsecureSkipVerify: true, // use this for tests only
MinVersion: tls.VersionTLS12,
}
tlsCert, err := tls.X509KeyPair([]byte(client1Crt), []byte(client1Key))
assert.NoError(t, err)
tlsConfig.Certificates = append(tlsConfig.Certificates, tlsCert)
// the certificate common name is client1 and it does not exists
resp, err := getTLSHTTPClient(tlsConfig).Get(fmt.Sprintf("https://%v/", webDavTLSServerAddr))
if assert.NoError(t, err) {
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode, string(body))
}
user.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword, dataprovider.LoginMethodTLSCertificate}
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
// now create client1
u = getTestUser()
u.Username = tlsClient1Username
u.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodPassword, dataprovider.LoginMethodTLSCertificate}
u.Filters.TLSUsername = dataprovider.TLSUsernameCN
user1, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
resp, err = getTLSHTTPClient(tlsConfig).Get(fmt.Sprintf("https://%v:%v@%v/", tlsClient2Username, defaultPassword,
webDavTLSServerAddr))
if assert.NoError(t, err) {
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode, string(body))
assert.Contains(t, string(body), "CN \"client1\" does not match username \"client2\"")
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
_, err = httpdtest.RemoveUser(user1, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user1.GetHomeDir())
assert.NoError(t, err)
}
func TestClientCertificateAuthCachedUser(t *testing.T) {
u := getTestUser()
u.Username = tlsClient1Username
u.Filters.TLSUsername = dataprovider.TLSUsernameCN
u.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodTLSCertificateAndPwd}
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
tlsConfig := &tls.Config{
ServerName: "localhost",
InsecureSkipVerify: true, // use this for tests only
MinVersion: tls.VersionTLS12,
}
tlsCert, err := tls.X509KeyPair([]byte(client1Crt), []byte(client1Key))
assert.NoError(t, err)
tlsConfig.Certificates = append(tlsConfig.Certificates, tlsCert)
client := getWebDavClient(user, true, tlsConfig)
err = checkBasicFunc(client)
assert.NoError(t, err)
// the user is now cached without a password, try a simple password login with and without TLS
client = getWebDavClient(user, true, nil)
err = checkBasicFunc(client)
assert.NoError(t, err)
client = getWebDavClient(user, false, nil)
err = checkBasicFunc(client)
assert.NoError(t, err)
// and now with a wrong password
user.Password = "wrong"
client = getWebDavClient(user, false, nil)
err = checkBasicFunc(client)
assert.Error(t, err)
// allow cert+password only
user.Password = ""
user.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodTLSCertificate}
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client = getWebDavClient(user, true, tlsConfig)
err = checkBasicFunc(client)
assert.NoError(t, err)
// the user is now cached
client = getWebDavClient(user, true, tlsConfig)
err = checkBasicFunc(client)
assert.NoError(t, err)
// password auth should work too
client = getWebDavClient(user, false, nil)
err = checkBasicFunc(client)
assert.NoError(t, err)
client = getWebDavClient(user, true, nil)
err = checkBasicFunc(client)
assert.NoError(t, err)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestExternatAuthWithClientCert(t *testing.T) {
if runtime.GOOS == osWindows {
t.Skip("this test is not available on Windows")
}
u := getTestUser()
u.Username = tlsClient1Username
u.Filters.TLSUsername = dataprovider.TLSUsernameCN
u.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodTLSCertificate, dataprovider.LoginMethodPassword}
err := dataprovider.Close()
assert.NoError(t, err)
err = config.LoadConfig(configDir, "")
assert.NoError(t, err)
providerConf := config.GetProviderConf()
err = os.WriteFile(extAuthPath, getExtAuthScriptContent(u, false, ""), os.ModePerm)
assert.NoError(t, err)
providerConf.ExternalAuthHook = extAuthPath
providerConf.ExternalAuthScope = 0
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
tlsConfig := &tls.Config{
ServerName: "localhost",
InsecureSkipVerify: true, // use this for tests only
MinVersion: tls.VersionTLS12,
}
tlsCert, err := tls.X509KeyPair([]byte(client1Crt), []byte(client1Key))
assert.NoError(t, err)
tlsConfig.Certificates = append(tlsConfig.Certificates, tlsCert)
client := getWebDavClient(u, true, tlsConfig)
assert.NoError(t, checkBasicFunc(client))
resp, err := getTLSHTTPClient(tlsConfig).Get(fmt.Sprintf("https://%v:%v@%v/", tlsClient2Username, defaultPassword,
webDavTLSServerAddr))
if assert.NoError(t, err) {
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode, string(body))
assert.Contains(t, string(body), "invalid credentials")
}
user, _, err := httpdtest.GetUserByUsername(tlsClient1Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, tlsClient1Username, user.Username)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
err = dataprovider.Close()
assert.NoError(t, err)
err = config.LoadConfig(configDir, "")
assert.NoError(t, err)
providerConf = config.GetProviderConf()
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
err = os.Remove(extAuthPath)
assert.NoError(t, err)
}
func TestPreLoginHookWithClientCert(t *testing.T) {
if runtime.GOOS == osWindows {
t.Skip("this test is not available on Windows")
}
u := getTestUser()
u.Username = tlsClient1Username
u.Filters.TLSUsername = dataprovider.TLSUsernameCN
u.Filters.DeniedLoginMethods = []string{dataprovider.LoginMethodTLSCertificate, dataprovider.LoginMethodPassword}
err := dataprovider.Close()
assert.NoError(t, err)
err = config.LoadConfig(configDir, "")
assert.NoError(t, err)
providerConf := config.GetProviderConf()
err = os.WriteFile(preLoginPath, getPreLoginScriptContent(u, false), os.ModePerm)
assert.NoError(t, err)
providerConf.PreLoginHook = preLoginPath
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
_, _, err = httpdtest.GetUserByUsername(tlsClient1Username, http.StatusNotFound)
assert.NoError(t, err)
tlsConfig := &tls.Config{
ServerName: "localhost",
InsecureSkipVerify: true, // use this for tests only
MinVersion: tls.VersionTLS12,
}
tlsCert, err := tls.X509KeyPair([]byte(client1Crt), []byte(client1Key))
assert.NoError(t, err)
tlsConfig.Certificates = append(tlsConfig.Certificates, tlsCert)
client := getWebDavClient(u, true, tlsConfig)
assert.NoError(t, checkBasicFunc(client))
user, _, err := httpdtest.GetUserByUsername(tlsClient1Username, http.StatusOK)
assert.NoError(t, err)
// test login with an existing user
client = getWebDavClient(user, true, tlsConfig)
assert.NoError(t, checkBasicFunc(client))
err = os.WriteFile(preLoginPath, getPreLoginScriptContent(user, true), os.ModePerm)
assert.NoError(t, err)
// update the user to remove it from the cache
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
client = getWebDavClient(user, true, tlsConfig)
assert.Error(t, checkBasicFunc(client))
// update the user to remove it from the cache
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)
client = getWebDavClient(user, true, tlsConfig)
assert.Error(t, checkBasicFunc(client))
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
err = dataprovider.Close()
assert.NoError(t, err)
err = config.LoadConfig(configDir, "")
assert.NoError(t, err)
providerConf = config.GetProviderConf()
err = dataprovider.Initialize(providerConf, configDir, true)
assert.NoError(t, err)
err = os.Remove(preLoginPath)
assert.NoError(t, err)
}
func checkBasicFunc(client *gowebdav.Client) error {
err := client.Connect()
if err != nil {
@ -1557,14 +2059,39 @@ func downloadFile(remoteSourcePath string, localDestPath string, expectedSize in
return nil
}
func getWebDavClient(user dataprovider.User) *gowebdav.Client {
func getTLSHTTPClient(tlsConfig *tls.Config) *http.Client {
customTransport := http.DefaultTransport.(*http.Transport).Clone()
customTransport.TLSClientConfig = tlsConfig
return &http.Client{
Timeout: 5 * time.Second,
Transport: customTransport,
}
}
func getWebDavClient(user dataprovider.User, useTLS bool, tlsConfig *tls.Config) *gowebdav.Client {
rootPath := fmt.Sprintf("http://%v/", webDavServerAddr)
if useTLS {
rootPath = fmt.Sprintf("https://%v/", webDavTLSServerAddr)
if tlsConfig == nil {
tlsConfig = &tls.Config{
ServerName: "localhost",
InsecureSkipVerify: true, // use this for tests only
MinVersion: tls.VersionTLS12,
}
}
}
pwd := defaultPassword
if user.Password != "" {
pwd = user.Password
}
client := gowebdav.NewClient(rootPath, user.Username, pwd)
client.SetTimeout(5 * time.Second)
if tlsConfig != nil {
customTransport := http.DefaultTransport.(*http.Transport).Clone()
customTransport.TLSClientConfig = tlsConfig
client.SetTransport(customTransport)
}
return client
}