From e1cfa255dffac9d17c81f62cbbc9e0fd90a2def6 Mon Sep 17 00:00:00 2001 From: Anton Davydov Date: Sat, 21 Dec 2024 10:58:07 +0300 Subject: [PATCH] feat: add CORS headers (#516) --- README.md | 26 ++++++++++++ pkg/config/config.go | 15 +++++++ pkg/config/config_test.go | 44 ++++++++++++++++++++ pkg/web/server.go | 8 ++++ testscript/testdata/http-cors.txtar | 64 +++++++++++++++++++++++++++++ 5 files changed, 157 insertions(+) create mode 100644 testscript/testdata/http-cors.txtar diff --git a/README.md b/README.md index cbefcd784..3f77385e3 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,32 @@ http: # Make sure to use https:// if you are using TLS. public_url: "http://localhost:23232" + # The cross-origin request security options + cors: + # The allowed cross-origin headers + allowed_headers: + - Accept + - Accept-Language + - Content-Language + - Origin + # - Content-Type + # - X-Requested-With + # - User-Agent + # - Authorization + # - Access-Control-Request-Method + + # The allowed cross-origin URLs + # allowed_origins: + # - * + + # The allowed cross-origin methods + allowed_methods: + - GET + - HEAD + - POST + # - PUT + # - OPTIONS + # The database configuration. db: # The database driver to use. diff --git a/pkg/config/config.go b/pkg/config/config.go index 05dd2c38e..581fbee9a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -61,6 +61,15 @@ type GitConfig struct { MaxConnections int `env:"MAX_CONNECTIONS" yaml:"max_connections"` } +// CORSConfig is the CORS configuration for the server. +type CORSConfig struct { + AllowedHeaders []string `env:"ALLOWED_HEADERS" yaml:"allowed_headers"` + + AllowedOrigins []string `env:"ALLOWED_ORIGINS" yaml:"allowed_origins"` + + AllowedMethods []string `env:"ALLOWED_METHODS" yaml:"allowed_methods"` +} + // HTTPConfig is the HTTP configuration for the server. type HTTPConfig struct { // Enabled toggles the HTTP server on/off @@ -77,6 +86,9 @@ type HTTPConfig struct { // PublicURL is the public URL of the HTTP server. PublicURL string `env:"PUBLIC_URL" yaml:"public_url"` + + // HTTP is the configuration for the HTTP server. + CORS CORSConfig `envPrefix:"CORS_" yaml:"cors"` } // StatsConfig is the configuration for the stats server. @@ -196,6 +208,9 @@ func (c *Config) Environ() []string { fmt.Sprintf("SOFT_SERVE_HTTP_TLS_KEY_PATH=%s", c.HTTP.TLSKeyPath), fmt.Sprintf("SOFT_SERVE_HTTP_TLS_CERT_PATH=%s", c.HTTP.TLSCertPath), fmt.Sprintf("SOFT_SERVE_HTTP_PUBLIC_URL=%s", c.HTTP.PublicURL), + fmt.Sprintf("SOFT_SERVE_HTTP_CORS_ALLOWED_HEADERS=%s", strings.Join(c.HTTP.CORS.AllowedHeaders, ",")), + fmt.Sprintf("SOFT_SERVE_HTTP_CORS_ALLOWED_ORIGINS=%s", strings.Join(c.HTTP.CORS.AllowedOrigins, ",")), + fmt.Sprintf("SOFT_SERVE_HTTP_CORS_ALLOWED_METHODS=%s", strings.Join(c.HTTP.CORS.AllowedMethods, ",")), fmt.Sprintf("SOFT_SERVE_STATS_ENABLED=%t", c.Stats.Enabled), fmt.Sprintf("SOFT_SERVE_STATS_LISTEN_ADDR=%s", c.Stats.ListenAddr), fmt.Sprintf("SOFT_SERVE_LOG_FORMAT=%s", c.Log.Format), diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 8b84ed822..b0653f282 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -79,3 +79,47 @@ func TestCustomConfigLocation(t *testing.T) { cfg = DefaultConfig() is.Equal(cfg.Name, "Soft Serve") } + +func TestParseMultipleHeaders(t *testing.T) { + is := is.New(t) + is.NoErr(os.Setenv("SOFT_SERVE_HTTP_CORS_ALLOWED_HEADERS", "Accept,Accept-Language,User-Agent")) + t.Cleanup(func() { + is.NoErr(os.Unsetenv("SOFT_SERVE_HTTP_CORS_ALLOWED_HEADERS")) + }) + cfg := DefaultConfig() + is.NoErr(cfg.ParseEnv()) + is.Equal(cfg.HTTP.CORS.AllowedHeaders, []string{ + "Accept", + "Accept-Language", + "User-Agent", + }) +} + +func TestParseMultipleOrigins(t *testing.T) { + is := is.New(t) + is.NoErr(os.Setenv("SOFT_SERVE_HTTP_CORS_ALLOWED_ORIGINS", "https://foo.example,https://foo.example2")) + t.Cleanup(func() { + is.NoErr(os.Unsetenv("SOFT_SERVE_HTTP_CORS_ALLOWED_ORIGINS")) + }) + cfg := DefaultConfig() + is.NoErr(cfg.ParseEnv()) + is.Equal(cfg.HTTP.CORS.AllowedOrigins, []string{ + "https://foo.example", + "https://foo.example2", + }) +} + +func TestParseMultipleMethods(t *testing.T) { + is := is.New(t) + is.NoErr(os.Setenv("SOFT_SERVE_HTTP_CORS_ALLOWED_METHODS", "GET,POST,PUT")) + t.Cleanup(func() { + is.NoErr(os.Unsetenv("SOFT_SERVE_HTTP_CORS_ALLOWED_METHODS")) + }) + cfg := DefaultConfig() + is.NoErr(cfg.ParseEnv()) + is.Equal(cfg.HTTP.CORS.AllowedMethods, []string{ + "GET", + "POST", + "PUT", + }) +} diff --git a/pkg/web/server.go b/pkg/web/server.go index 74a04f5b1..ab336e89a 100644 --- a/pkg/web/server.go +++ b/pkg/web/server.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/charmbracelet/log" + "github.com/charmbracelet/soft-serve/pkg/config" "github.com/gorilla/handlers" "github.com/gorilla/mux" ) @@ -26,5 +27,12 @@ func NewRouter(ctx context.Context) http.Handler { h = handlers.CompressHandler(h) h = handlers.RecoveryHandler()(h) + cfg := config.FromContext(ctx) + + h = handlers.CORS(handlers.AllowedHeaders(cfg.HTTP.CORS.AllowedHeaders), + handlers.AllowedOrigins(cfg.HTTP.CORS.AllowedOrigins), + handlers.AllowedMethods(cfg.HTTP.CORS.AllowedMethods), + )(h) + return h } diff --git a/testscript/testdata/http-cors.txtar b/testscript/testdata/http-cors.txtar new file mode 100644 index 000000000..c545ab7a1 --- /dev/null +++ b/testscript/testdata/http-cors.txtar @@ -0,0 +1,64 @@ +# vi: set ft=conf + +# FIXME: don't skip windows +[windows] skip 'curl makes github actions hang' + +# convert crlf to lf on windows +[windows] dos2unix http1.txt http2.txt http3.txt goget.txt gitclone.txt + +# start soft serve +exec soft serve & +# wait for SSH server to start +ensureserverrunning SSH_PORT + +# create user +soft user create user1 --key "$USER1_AUTHORIZED_KEY" + +# create access token +soft token create --expires-in '1h' 'repo2' +cp stdout tokenfile +envfile TOKEN=tokenfile +soft token create --expires-in '1ns' 'repo2' +cp stdout etokenfile +envfile ETOKEN=etokenfile +usoft token create 'repo2' +cp stdout utokenfile +envfile UTOKEN=utokenfile + +# push & create repo with some files, commits, tags... +mkdir ./repo2 +git -c init.defaultBranch=master -C repo2 init +mkfile ./repo2/README.md '# Project\nfoo' +mkfile ./repo2/foo.png 'foo' +mkfile ./repo2/bar.png 'bar' +git -C repo2 remote add origin http://$TOKEN@localhost:$HTTP_PORT/repo2 +git -C repo2 lfs install --local +git -C repo2 lfs track '*.png' +git -C repo2 add -A +git -C repo2 commit -m 'first' +git -C repo2 tag v0.1.0 +git -C repo2 push origin HEAD +git -C repo2 push origin HEAD --tags + +curl -v --request OPTIONS http://localhost:$HTTP_PORT/repo2.git/info/refs -H 'Origin: https://foo.example' -H 'Access-Control-Request-Method: GET' +stderr '.*Method Not Allowed.*' + +# stop the server +stopserver + +# allow cross-origin OPTIONS requests +env SOFT_SERVE_HTTP_CORS_ALLOWED_ORIGINS="https://foo.example" +env SOFT_SERVE_HTTP_CORS_ALLOWED_METHODS="GET,OPTIONS" +env SOFT_SERVE_HTTP_CORS_ALLOWED_HEADERS="Origin,Access-Control-Request-Method" + +# restart soft serve +exec soft serve & +# wait for SSH server to start +ensureserverrunning SSH_PORT + +curl -v --request OPTIONS http://localhost:$HTTP_PORT/repo2.git/info/refs -H 'Origin: https://foo.example' -H 'Access-Control-Request-Method: GET' +stderr '.*200 OK.*' + +# stop the server +[windows] stopserver +[windows] ! stderr .