From 0833b4698e2bf98b8faad9119d51548b679c3249 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Sat, 13 Nov 2021 23:14:50 +0100 Subject: [PATCH] httpd service: add CORS support --- config/config.go | 18 +++++++++++++++++- docs/full-configuration.md | 8 ++++++++ go.mod | 4 ++-- go.sum | 8 ++++---- httpd/httpd.go | 17 +++++++++++++++-- httpd/internal_test.go | 6 +++--- httpd/server.go | 16 +++++++++++++++- sftpgo.json | 11 ++++++++++- webdavd/webdavd.go | 6 +++--- webdavd/webdavd_test.go | 2 +- 10 files changed, 78 insertions(+), 18 deletions(-) diff --git a/config/config.go b/config/config.go index 11c0af15..53805904 100644 --- a/config/config.go +++ b/config/config.go @@ -195,7 +195,7 @@ func Init() { CertificateKeyFile: "", CACertificates: []string{}, CARevocationLists: []string{}, - Cors: webdavd.Cors{ + Cors: webdavd.CorsConfig{ Enabled: false, AllowedOrigins: []string{}, AllowedMethods: []string{}, @@ -280,6 +280,15 @@ func Init() { CARevocationLists: nil, SigningPassphrase: "", MaxUploadFileSize: 1048576000, + Cors: httpd.CorsConfig{ + Enabled: false, + AllowedOrigins: []string{}, + AllowedMethods: []string{}, + AllowedHeaders: []string{}, + ExposedHeaders: []string{}, + AllowCredentials: false, + MaxAge: 0, + }, }, HTTPConfig: httpclient.Config{ Timeout: 20, @@ -1217,6 +1226,13 @@ func setViperDefaults() { viper.SetDefault("httpd.ca_revocation_lists", globalConf.HTTPDConfig.CARevocationLists) viper.SetDefault("httpd.signing_passphrase", globalConf.HTTPDConfig.SigningPassphrase) viper.SetDefault("httpd.max_upload_file_size", globalConf.HTTPDConfig.MaxUploadFileSize) + viper.SetDefault("httpd.cors.enabled", globalConf.HTTPDConfig.Cors.Enabled) + viper.SetDefault("httpd.cors.allowed_origins", globalConf.HTTPDConfig.Cors.AllowedOrigins) + viper.SetDefault("httpd.cors.allowed_methods", globalConf.HTTPDConfig.Cors.AllowedMethods) + viper.SetDefault("httpd.cors.allowed_headers", globalConf.HTTPDConfig.Cors.AllowedHeaders) + viper.SetDefault("httpd.cors.exposed_headers", globalConf.HTTPDConfig.Cors.ExposedHeaders) + viper.SetDefault("httpd.cors.allow_credentials", globalConf.HTTPDConfig.Cors.AllowCredentials) + viper.SetDefault("httpd.cors.max_age", globalConf.HTTPDConfig.Cors.MaxAge) viper.SetDefault("http.timeout", globalConf.HTTPConfig.Timeout) viper.SetDefault("http.retry_wait_min", globalConf.HTTPConfig.RetryWaitMin) viper.SetDefault("http.retry_wait_max", globalConf.HTTPConfig.RetryWaitMax) diff --git a/docs/full-configuration.md b/docs/full-configuration.md index 1fba6d55..733a2227 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -232,6 +232,14 @@ The configuration file contains the following sections: - `ca_revocation_lists`, list of strings. Set a revocation lists, one for each root CA, to be used to check if a client certificate has been revoked. The revocation lists can be reloaded on demand sending a `SIGHUP` signal on Unix based systems and a `paramchange` request to the running service on Windows. - `signing_passphrase`, string. Passphrase to use to derive the signing key for JWT and CSRF tokens. If empty a random signing key will be generated each time SFTPGo starts. If you set a signing passphrase you should consider rotating it periodically for added security. - `max_upload_file_size`, integer. Defines the maximum request body size, in bytes, for Web Client/API HTTP upload requests. 0 means no limit. Default: 1048576000. + - `cors` struct containing CORS configuration. SFTPGo uses [Go CORS handler](https://github.com/rs/cors), please refer to upstream documentation for fields meaning and their default values. + - `enabled`, boolean, set to true to enable CORS. + - `allowed_origins`, list of strings. + - `allowed_methods`, list of strings. + - `allowed_headers`, list of strings. + - `exposed_headers`, list of strings. + - `allow_credentials` boolean. + - `max_age`, integer. - **"telemetry"**, the configuration for the telemetry server, more details [below](#telemetry-server) - `bind_port`, integer. The port used for serving HTTP requests. Set to 0 to disable HTTP server. Default: 10000 - `bind_address`, string. Leave blank to listen on all available network interfaces. On \*NIX you can specify an absolute path to listen on a Unix-domain socket. Default: "127.0.0.1" diff --git a/go.mod b/go.mod index 7e2d8a91..63cb76f3 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 github.com/alexedwards/argon2id v0.0.0-20210511081203-7d35d68092b8 github.com/aws/aws-sdk-go v1.42.4 - github.com/cockroachdb/cockroach-go/v2 v2.2.1 + github.com/cockroachdb/cockroach-go/v2 v2.2.3 github.com/eikenb/pipeat v0.0.0-20210603033007-44fc3ffce52b github.com/fclairamb/ftpserverlib v0.16.0 github.com/fclairamb/go-log v0.1.0 @@ -25,7 +25,7 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.0 github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126 github.com/klauspost/compress v1.13.6 - github.com/lestrrat-go/jwx v1.2.10 + github.com/lestrrat-go/jwx v1.2.11 github.com/lib/pq v1.10.4 github.com/lithammer/shortuuid/v3 v3.0.7 github.com/mattn/go-sqlite3 v1.14.9 diff --git a/go.sum b/go.sum index f37dd0aa..bf7b61f3 100644 --- a/go.sum +++ b/go.sum @@ -192,8 +192,8 @@ github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1 h1:zH8ljVhhq7yC0MIeUL/IviMtY8hx2mK8cN9wEYb8ggw= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/cockroachdb/cockroach-go/v2 v2.2.1 h1:nZte1DDdL9iu8IV0YPmX8l9Lg2+HRJ3CMvkT3iG52rc= -github.com/cockroachdb/cockroach-go/v2 v2.2.1/go.mod h1:u3MiKYGupPPjkn3ozknpMUpxPaNLTFWAya419/zv6eI= +github.com/cockroachdb/cockroach-go/v2 v2.2.3 h1:2881elKwTMrAWuSP2N/4PtU6XyqoyI55Fv3TSTD+Efo= +github.com/cockroachdb/cockroach-go/v2 v2.2.3/go.mod h1:u3MiKYGupPPjkn3ozknpMUpxPaNLTFWAya419/zv6eI= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -566,8 +566,8 @@ github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++ github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A= github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= github.com/lestrrat-go/jwx v1.2.6/go.mod h1:tJuGuAI3LC71IicTx82Mz1n3w9woAs2bYJZpkjJQ5aU= -github.com/lestrrat-go/jwx v1.2.10 h1:rz6Ywm3wCRWsy2lyRZ7uHzE4E09m7X9eINaoAEVXCKw= -github.com/lestrrat-go/jwx v1.2.10/go.mod h1:25DcLbNWArPA/Ew5CcBmewl32cJKxOk5cbepBsIJFzw= +github.com/lestrrat-go/jwx v1.2.11 h1:e9BS5NQ003hxXogNsgf5fEWf01ZJvj4Aj1qy7Dykqm8= +github.com/lestrrat-go/jwx v1.2.11/go.mod h1:25DcLbNWArPA/Ew5CcBmewl32cJKxOk5cbepBsIJFzw= github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= diff --git a/httpd/httpd.go b/httpd/httpd.go index 6465363b..a066a6f4 100644 --- a/httpd/httpd.go +++ b/httpd/httpd.go @@ -319,6 +319,17 @@ type ServicesStatus struct { MFA mfa.ServiceStatus `json:"mfa"` } +// CorsConfig defines the CORS configuration +type CorsConfig struct { + AllowedOrigins []string `json:"allowed_origins" mapstructure:"allowed_origins"` + AllowedMethods []string `json:"allowed_methods" mapstructure:"allowed_methods"` + AllowedHeaders []string `json:"allowed_headers" mapstructure:"allowed_headers"` + ExposedHeaders []string `json:"exposed_headers" mapstructure:"exposed_headers"` + AllowCredentials bool `json:"allow_credentials" mapstructure:"allow_credentials"` + Enabled bool `json:"enabled" mapstructure:"enabled"` + MaxAge int `json:"max_age" mapstructure:"max_age"` +} + // Conf httpd daemon configuration type Conf struct { // Addresses and ports to bind to @@ -351,6 +362,8 @@ type Conf struct { // MaxUploadFileSize Defines the maximum request body size, in bytes, for Web Client/API HTTP upload requests. // 0 means no limit MaxUploadFileSize int64 `json:"max_upload_file_size" mapstructure:"max_upload_file_size"` + // CORS configuration + Cors CorsConfig `json:"cors" mapstructure:"cors"` } type apiResponse struct { @@ -456,7 +469,7 @@ func (c *Conf) Initialize(configDir string) error { } go func(b Binding) { - server := newHttpdServer(b, staticFilesPath, c.SigningPassphrase) + server := newHttpdServer(b, staticFilesPath, c.SigningPassphrase, c.Cors) exitChannel <- server.listenAndServe() }(binding) @@ -600,7 +613,7 @@ func GetHTTPRouter() http.Handler { EnableWebAdmin: true, EnableWebClient: true, } - server := newHttpdServer(b, "../static", "") + server := newHttpdServer(b, "../static", "", CorsConfig{}) server.initializeRouter() return server.router } diff --git a/httpd/internal_test.go b/httpd/internal_test.go index ddc8dcf8..aa2ed6f5 100644 --- a/httpd/internal_test.go +++ b/httpd/internal_test.go @@ -1406,7 +1406,7 @@ func TestProxyHeaders(t *testing.T) { } err = b.parseAllowedProxy() assert.NoError(t, err) - server := newHttpdServer(b, "", "") + server := newHttpdServer(b, "", "", CorsConfig{Enabled: true}) server.initializeRouter() testServer := httptest.NewServer(server.router) defer testServer.Close() @@ -1492,7 +1492,7 @@ func TestRecoverer(t *testing.T) { EnableWebAdmin: true, EnableWebClient: false, } - server := newHttpdServer(b, "../static", "") + server := newHttpdServer(b, "../static", "", CorsConfig{}) server.initializeRouter() server.router.Get(recoveryPath, func(w http.ResponseWriter, r *http.Request) { panic("panic") @@ -1607,7 +1607,7 @@ func TestWebAdminRedirect(t *testing.T) { EnableWebAdmin: true, EnableWebClient: false, } - server := newHttpdServer(b, "../static", "") + server := newHttpdServer(b, "../static", "", CorsConfig{}) server.initializeRouter() testServer := httptest.NewServer(server.router) defer testServer.Close() diff --git a/httpd/server.go b/httpd/server.go index a90ac685..333a104f 100644 --- a/httpd/server.go +++ b/httpd/server.go @@ -16,6 +16,7 @@ import ( "github.com/go-chi/jwtauth/v5" "github.com/go-chi/render" "github.com/lestrrat-go/jwx/jwa" + "github.com/rs/cors" "github.com/rs/xid" "github.com/drakkan/sftpgo/v2/common" @@ -41,15 +42,17 @@ type httpdServer struct { router *chi.Mux tokenAuth *jwtauth.JWTAuth signingPassphrase string + cors CorsConfig } -func newHttpdServer(b Binding, staticFilesPath, signingPassphrase string) *httpdServer { +func newHttpdServer(b Binding, staticFilesPath, signingPassphrase string, cors CorsConfig) *httpdServer { return &httpdServer{ binding: b, staticFilesPath: staticFilesPath, enableWebAdmin: b.EnableWebAdmin, enableWebClient: b.EnableWebClient, signingPassphrase: signingPassphrase, + cors: cors, } } @@ -943,6 +946,17 @@ func (s *httpdServer) initializeRouter() { s.router.Use(s.checkConnection) s.router.Use(logger.NewStructuredLogger(logger.GetLogger())) s.router.Use(recoverer) + if s.cors.Enabled { + c := cors.New(cors.Options{ + AllowedOrigins: s.cors.AllowedOrigins, + AllowedMethods: s.cors.AllowedMethods, + AllowedHeaders: s.cors.AllowedHeaders, + ExposedHeaders: s.cors.ExposedHeaders, + MaxAge: s.cors.MaxAge, + AllowCredentials: s.cors.AllowCredentials, + }) + s.router.Use(c.Handler) + } s.router.Use(middleware.GetHead) s.router.Use(middleware.StripSlashes) diff --git a/sftpgo.json b/sftpgo.json index cc4edb31..6e292b7f 100644 --- a/sftpgo.json +++ b/sftpgo.json @@ -221,7 +221,16 @@ "ca_certificates": [], "ca_revocation_lists": [], "signing_passphrase": "", - "max_upload_file_size": 1048576000 + "max_upload_file_size": 1048576000, + "cors": { + "enabled": false, + "allowed_origins": [], + "allowed_methods": [], + "allowed_headers": [], + "exposed_headers": [], + "allow_credentials": false, + "max_age": 0 + } }, "telemetry": { "bind_port": 10000, diff --git a/webdavd/webdavd.go b/webdavd/webdavd.go index 50262c18..06af903e 100644 --- a/webdavd/webdavd.go +++ b/webdavd/webdavd.go @@ -37,8 +37,8 @@ type ServiceStatus struct { Bindings []Binding `json:"bindings"` } -// Cors configuration -type Cors struct { +// CorsConfig defines the CORS configuration +type CorsConfig struct { AllowedOrigins []string `json:"allowed_origins" mapstructure:"allowed_origins"` AllowedMethods []string `json:"allowed_methods" mapstructure:"allowed_methods"` AllowedHeaders []string `json:"allowed_headers" mapstructure:"allowed_headers"` @@ -135,7 +135,7 @@ type Configuration struct { // if a client certificate has been revoked CARevocationLists []string `json:"ca_revocation_lists" mapstructure:"ca_revocation_lists"` // CORS configuration - Cors Cors `json:"cors" mapstructure:"cors"` + Cors CorsConfig `json:"cors" mapstructure:"cors"` // Cache configuration Cache Cache `json:"cache" mapstructure:"cache"` } diff --git a/webdavd/webdavd_test.go b/webdavd/webdavd_test.go index b8d5fa1b..9c48e05f 100644 --- a/webdavd/webdavd_test.go +++ b/webdavd/webdavd_test.go @@ -347,7 +347,7 @@ func TestMain(m *testing.M) { ClientAuthType: 2, }, } - webDavConf.Cors = webdavd.Cors{ + webDavConf.Cors = webdavd.CorsConfig{ Enabled: true, AllowedOrigins: []string{"*"}, AllowedMethods: []string{