Detailed changes
@@ -158,6 +158,32 @@ ssh:
# This is the address that will be used to clone repositories.
public_url: "ssh://localhost:23231"
+ # 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 path to the SSH server's private key.
key_path: "ssh/soft_serve_host"
@@ -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),
@@ -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",
+ })
+}
@@ -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
}
@@ -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 .