httpd service: add CORS support

This commit is contained in:
Nicola Murino 2021-11-13 23:14:50 +01:00
parent ee5c5e033d
commit 0833b4698e
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
10 changed files with 78 additions and 18 deletions

View file

@ -195,7 +195,7 @@ func Init() {
CertificateKeyFile: "", CertificateKeyFile: "",
CACertificates: []string{}, CACertificates: []string{},
CARevocationLists: []string{}, CARevocationLists: []string{},
Cors: webdavd.Cors{ Cors: webdavd.CorsConfig{
Enabled: false, Enabled: false,
AllowedOrigins: []string{}, AllowedOrigins: []string{},
AllowedMethods: []string{}, AllowedMethods: []string{},
@ -280,6 +280,15 @@ func Init() {
CARevocationLists: nil, CARevocationLists: nil,
SigningPassphrase: "", SigningPassphrase: "",
MaxUploadFileSize: 1048576000, MaxUploadFileSize: 1048576000,
Cors: httpd.CorsConfig{
Enabled: false,
AllowedOrigins: []string{},
AllowedMethods: []string{},
AllowedHeaders: []string{},
ExposedHeaders: []string{},
AllowCredentials: false,
MaxAge: 0,
},
}, },
HTTPConfig: httpclient.Config{ HTTPConfig: httpclient.Config{
Timeout: 20, Timeout: 20,
@ -1217,6 +1226,13 @@ func setViperDefaults() {
viper.SetDefault("httpd.ca_revocation_lists", globalConf.HTTPDConfig.CARevocationLists) viper.SetDefault("httpd.ca_revocation_lists", globalConf.HTTPDConfig.CARevocationLists)
viper.SetDefault("httpd.signing_passphrase", globalConf.HTTPDConfig.SigningPassphrase) viper.SetDefault("httpd.signing_passphrase", globalConf.HTTPDConfig.SigningPassphrase)
viper.SetDefault("httpd.max_upload_file_size", globalConf.HTTPDConfig.MaxUploadFileSize) 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.timeout", globalConf.HTTPConfig.Timeout)
viper.SetDefault("http.retry_wait_min", globalConf.HTTPConfig.RetryWaitMin) viper.SetDefault("http.retry_wait_min", globalConf.HTTPConfig.RetryWaitMin)
viper.SetDefault("http.retry_wait_max", globalConf.HTTPConfig.RetryWaitMax) viper.SetDefault("http.retry_wait_max", globalConf.HTTPConfig.RetryWaitMax)

View file

@ -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. - `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. - `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. - `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) - **"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_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" - `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"

4
go.mod
View file

@ -8,7 +8,7 @@ require (
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
github.com/alexedwards/argon2id v0.0.0-20210511081203-7d35d68092b8 github.com/alexedwards/argon2id v0.0.0-20210511081203-7d35d68092b8
github.com/aws/aws-sdk-go v1.42.4 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/eikenb/pipeat v0.0.0-20210603033007-44fc3ffce52b
github.com/fclairamb/ftpserverlib v0.16.0 github.com/fclairamb/ftpserverlib v0.16.0
github.com/fclairamb/go-log v0.1.0 github.com/fclairamb/go-log v0.1.0
@ -25,7 +25,7 @@ require (
github.com/hashicorp/go-retryablehttp v0.7.0 github.com/hashicorp/go-retryablehttp v0.7.0
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126 github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
github.com/klauspost/compress v1.13.6 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/lib/pq v1.10.4
github.com/lithammer/shortuuid/v3 v3.0.7 github.com/lithammer/shortuuid/v3 v3.0.7
github.com/mattn/go-sqlite3 v1.14.9 github.com/mattn/go-sqlite3 v1.14.9

8
go.sum
View file

@ -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 h1:zH8ljVhhq7yC0MIeUL/IviMtY8hx2mK8cN9wEYb8ggw=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 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/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.3 h1:2881elKwTMrAWuSP2N/4PtU6XyqoyI55Fv3TSTD+Efo=
github.com/cockroachdb/cockroach-go/v2 v2.2.1/go.mod h1:u3MiKYGupPPjkn3ozknpMUpxPaNLTFWAya419/zv6eI= 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/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-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= 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 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A=
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= 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.6/go.mod h1:tJuGuAI3LC71IicTx82Mz1n3w9woAs2bYJZpkjJQ5aU=
github.com/lestrrat-go/jwx v1.2.10 h1:rz6Ywm3wCRWsy2lyRZ7uHzE4E09m7X9eINaoAEVXCKw= github.com/lestrrat-go/jwx v1.2.11 h1:e9BS5NQ003hxXogNsgf5fEWf01ZJvj4Aj1qy7Dykqm8=
github.com/lestrrat-go/jwx v1.2.10/go.mod h1:25DcLbNWArPA/Ew5CcBmewl32cJKxOk5cbepBsIJFzw= 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 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 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= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=

View file

@ -319,6 +319,17 @@ type ServicesStatus struct {
MFA mfa.ServiceStatus `json:"mfa"` 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 // Conf httpd daemon configuration
type Conf struct { type Conf struct {
// Addresses and ports to bind to // 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. // MaxUploadFileSize Defines the maximum request body size, in bytes, for Web Client/API HTTP upload requests.
// 0 means no limit // 0 means no limit
MaxUploadFileSize int64 `json:"max_upload_file_size" mapstructure:"max_upload_file_size"` MaxUploadFileSize int64 `json:"max_upload_file_size" mapstructure:"max_upload_file_size"`
// CORS configuration
Cors CorsConfig `json:"cors" mapstructure:"cors"`
} }
type apiResponse struct { type apiResponse struct {
@ -456,7 +469,7 @@ func (c *Conf) Initialize(configDir string) error {
} }
go func(b Binding) { go func(b Binding) {
server := newHttpdServer(b, staticFilesPath, c.SigningPassphrase) server := newHttpdServer(b, staticFilesPath, c.SigningPassphrase, c.Cors)
exitChannel <- server.listenAndServe() exitChannel <- server.listenAndServe()
}(binding) }(binding)
@ -600,7 +613,7 @@ func GetHTTPRouter() http.Handler {
EnableWebAdmin: true, EnableWebAdmin: true,
EnableWebClient: true, EnableWebClient: true,
} }
server := newHttpdServer(b, "../static", "") server := newHttpdServer(b, "../static", "", CorsConfig{})
server.initializeRouter() server.initializeRouter()
return server.router return server.router
} }

View file

@ -1406,7 +1406,7 @@ func TestProxyHeaders(t *testing.T) {
} }
err = b.parseAllowedProxy() err = b.parseAllowedProxy()
assert.NoError(t, err) assert.NoError(t, err)
server := newHttpdServer(b, "", "") server := newHttpdServer(b, "", "", CorsConfig{Enabled: true})
server.initializeRouter() server.initializeRouter()
testServer := httptest.NewServer(server.router) testServer := httptest.NewServer(server.router)
defer testServer.Close() defer testServer.Close()
@ -1492,7 +1492,7 @@ func TestRecoverer(t *testing.T) {
EnableWebAdmin: true, EnableWebAdmin: true,
EnableWebClient: false, EnableWebClient: false,
} }
server := newHttpdServer(b, "../static", "") server := newHttpdServer(b, "../static", "", CorsConfig{})
server.initializeRouter() server.initializeRouter()
server.router.Get(recoveryPath, func(w http.ResponseWriter, r *http.Request) { server.router.Get(recoveryPath, func(w http.ResponseWriter, r *http.Request) {
panic("panic") panic("panic")
@ -1607,7 +1607,7 @@ func TestWebAdminRedirect(t *testing.T) {
EnableWebAdmin: true, EnableWebAdmin: true,
EnableWebClient: false, EnableWebClient: false,
} }
server := newHttpdServer(b, "../static", "") server := newHttpdServer(b, "../static", "", CorsConfig{})
server.initializeRouter() server.initializeRouter()
testServer := httptest.NewServer(server.router) testServer := httptest.NewServer(server.router)
defer testServer.Close() defer testServer.Close()

View file

@ -16,6 +16,7 @@ import (
"github.com/go-chi/jwtauth/v5" "github.com/go-chi/jwtauth/v5"
"github.com/go-chi/render" "github.com/go-chi/render"
"github.com/lestrrat-go/jwx/jwa" "github.com/lestrrat-go/jwx/jwa"
"github.com/rs/cors"
"github.com/rs/xid" "github.com/rs/xid"
"github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/common"
@ -41,15 +42,17 @@ type httpdServer struct {
router *chi.Mux router *chi.Mux
tokenAuth *jwtauth.JWTAuth tokenAuth *jwtauth.JWTAuth
signingPassphrase string signingPassphrase string
cors CorsConfig
} }
func newHttpdServer(b Binding, staticFilesPath, signingPassphrase string) *httpdServer { func newHttpdServer(b Binding, staticFilesPath, signingPassphrase string, cors CorsConfig) *httpdServer {
return &httpdServer{ return &httpdServer{
binding: b, binding: b,
staticFilesPath: staticFilesPath, staticFilesPath: staticFilesPath,
enableWebAdmin: b.EnableWebAdmin, enableWebAdmin: b.EnableWebAdmin,
enableWebClient: b.EnableWebClient, enableWebClient: b.EnableWebClient,
signingPassphrase: signingPassphrase, signingPassphrase: signingPassphrase,
cors: cors,
} }
} }
@ -943,6 +946,17 @@ func (s *httpdServer) initializeRouter() {
s.router.Use(s.checkConnection) s.router.Use(s.checkConnection)
s.router.Use(logger.NewStructuredLogger(logger.GetLogger())) s.router.Use(logger.NewStructuredLogger(logger.GetLogger()))
s.router.Use(recoverer) 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.GetHead)
s.router.Use(middleware.StripSlashes) s.router.Use(middleware.StripSlashes)

View file

@ -221,7 +221,16 @@
"ca_certificates": [], "ca_certificates": [],
"ca_revocation_lists": [], "ca_revocation_lists": [],
"signing_passphrase": "", "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": { "telemetry": {
"bind_port": 10000, "bind_port": 10000,

View file

@ -37,8 +37,8 @@ type ServiceStatus struct {
Bindings []Binding `json:"bindings"` Bindings []Binding `json:"bindings"`
} }
// Cors configuration // CorsConfig defines the CORS configuration
type Cors struct { type CorsConfig struct {
AllowedOrigins []string `json:"allowed_origins" mapstructure:"allowed_origins"` AllowedOrigins []string `json:"allowed_origins" mapstructure:"allowed_origins"`
AllowedMethods []string `json:"allowed_methods" mapstructure:"allowed_methods"` AllowedMethods []string `json:"allowed_methods" mapstructure:"allowed_methods"`
AllowedHeaders []string `json:"allowed_headers" mapstructure:"allowed_headers"` AllowedHeaders []string `json:"allowed_headers" mapstructure:"allowed_headers"`
@ -135,7 +135,7 @@ type Configuration struct {
// if a client certificate has been revoked // if a client certificate has been revoked
CARevocationLists []string `json:"ca_revocation_lists" mapstructure:"ca_revocation_lists"` CARevocationLists []string `json:"ca_revocation_lists" mapstructure:"ca_revocation_lists"`
// CORS configuration // CORS configuration
Cors Cors `json:"cors" mapstructure:"cors"` Cors CorsConfig `json:"cors" mapstructure:"cors"`
// Cache configuration // Cache configuration
Cache Cache `json:"cache" mapstructure:"cache"` Cache Cache `json:"cache" mapstructure:"cache"`
} }

View file

@ -347,7 +347,7 @@ func TestMain(m *testing.M) {
ClientAuthType: 2, ClientAuthType: 2,
}, },
} }
webDavConf.Cors = webdavd.Cors{ webDavConf.Cors = webdavd.CorsConfig{
Enabled: true, Enabled: true,
AllowedOrigins: []string{"*"}, AllowedOrigins: []string{"*"},
AllowedMethods: []string{ AllowedMethods: []string{