Skip to content

Commit

Permalink
feat: add CORS headers (#516)
Browse files Browse the repository at this point in the history
  • Loading branch information
fetsorn committed Dec 21, 2024
1 parent ec03324 commit e1cfa25
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 0 deletions.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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),
Expand Down
44 changes: 44 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
})
}
8 changes: 8 additions & 0 deletions pkg/web/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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
}
64 changes: 64 additions & 0 deletions testscript/testdata/http-cors.txtar
Original file line number Diff line number Diff line change
@@ -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 .

0 comments on commit e1cfa25

Please sign in to comment.