From 534b253c20e40707f677421a0bdc933f940445bb Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Mon, 1 Mar 2021 19:28:11 +0100 Subject: [PATCH] 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 --- dataprovider/dataprovider.go | 76 ++++- docs/dynamic-user-mod.md | 2 +- docs/full-configuration.md | 2 +- docs/post-login-hook.md | 2 +- go.mod | 17 +- go.sum | 37 ++- httpd/httpd.go | 2 +- httpd/internal_test.go | 2 +- httpd/schema/openapi.yaml | 2 +- httpd/server.go | 4 +- logger/request_logger.go | 2 +- metrics/metrics.go | 2 +- metrics/metrics_disabled.go | 2 +- telemetry/router.go | 4 +- telemetry/telemetry.go | 2 +- webdavd/internal_test.go | 58 +++- webdavd/server.go | 124 ++++--- webdavd/webdavd.go | 6 +- webdavd/webdavd_test.go | 621 ++++++++++++++++++++++++++++++++--- 19 files changed, 816 insertions(+), 151 deletions(-) diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index a409e207..ef9cd9d9 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -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 diff --git a/docs/dynamic-user-mod.md b/docs/dynamic-user-mod.md index 0ebd4c31..c6a73d09 100644 --- a/docs/dynamic-user-mod.md +++ b/docs/dynamic-user-mod.md @@ -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` diff --git a/docs/full-configuration.md b/docs/full-configuration.md index 2bd06bf2..d4d73b41 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -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`. diff --git a/docs/post-login-hook.md b/docs/post-login-hook.md index c943f765..acbfa1af 100644 --- a/docs/post-login-hook.md +++ b/docs/post-login-hook.md @@ -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` diff --git a/go.mod b/go.mod index 21e6b1a9..5ef9a682 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 16e5f2d1..1dea163a 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/httpd/httpd.go b/httpd/httpd.go index 125bdf9d..f57de0bd 100644 --- a/httpd/httpd.go +++ b/httpd/httpd.go @@ -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" diff --git a/httpd/internal_test.go b/httpd/internal_test.go index b11570df..0654f53a 100644 --- a/httpd/internal_test.go +++ b/httpd/internal_test.go @@ -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" diff --git a/httpd/schema/openapi.yaml b/httpd/schema/openapi.yaml index 20546f73..14a0a58f 100644 --- a/httpd/schema/openapi.yaml +++ b/httpd/schema/openapi.yaml @@ -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 diff --git a/httpd/server.go b/httpd/server.go index 4fa2030b..4092981d 100644 --- a/httpd/server.go +++ b/httpd/server.go @@ -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" diff --git a/logger/request_logger.go b/logger/request_logger.go index 69e1cef8..74e44665 100644 --- a/logger/request_logger.go +++ b/logger/request_logger.go @@ -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" diff --git a/metrics/metrics.go b/metrics/metrics.go index d135bafd..cf272a05 100644 --- a/metrics/metrics.go +++ b/metrics/metrics.go @@ -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" diff --git a/metrics/metrics_disabled.go b/metrics/metrics_disabled.go index 3efdd524..2ef77dc5 100644 --- a/metrics/metrics_disabled.go +++ b/metrics/metrics_disabled.go @@ -3,7 +3,7 @@ package metrics import ( - "github.com/go-chi/chi" + "github.com/go-chi/chi/v5" "github.com/drakkan/sftpgo/version" ) diff --git a/telemetry/router.go b/telemetry/router.go index 9712f301..d11d036b 100644 --- a/telemetry/router.go +++ b/telemetry/router.go @@ -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" diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go index fecb4792..cea2a497 100644 --- a/telemetry/telemetry.go +++ b/telemetry/telemetry.go @@ -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" diff --git a/webdavd/internal_test.go b/webdavd/internal_test.go index 679f1c58..d002d69f 100644 --- a/webdavd/internal_test.go +++ b/webdavd/internal_test.go @@ -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 +} diff --git a/webdavd/server.go b/webdavd/server.go index 16c792f7..197e6973 100644 --- a/webdavd/server.go +++ b/webdavd/server.go @@ -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) } diff --git a/webdavd/webdavd.go b/webdavd/webdavd.go index f69427ce..6f03cb81 100644 --- a/webdavd/webdavd.go +++ b/webdavd/webdavd.go @@ -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) diff --git a/webdavd/webdavd_test.go b/webdavd/webdavd_test.go index 06ac28c3..a6ba3972 100644 --- a/webdavd/webdavd_test.go +++ b/webdavd/webdavd_test.go @@ -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 }