Skip to content

Commit ea8799b

Browse files
authored
feat: add CORS headers (#654)
1 parent 15e9e25 commit ea8799b

6 files changed

Lines changed: 210 additions & 0 deletions

File tree

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,34 @@ http:
204204
# Make sure to use https:// if you are using TLS.
205205
public_url: "http://localhost:23232"
206206

207+
# The cross-origin request security options
208+
cors:
209+
# The allowed cross-origin headers
210+
allowed_headers:
211+
- "Accept"
212+
- "Accept-Language"
213+
- "Content-Language"
214+
- "Content-Type"
215+
- "Origin"
216+
- "X-Requested-With"
217+
- "User-Agent"
218+
- "Authorization"
219+
- "Access-Control-Request-Method"
220+
- "Access-Control-Allow-Origin"
221+
222+
# The allowed cross-origin URLs
223+
allowed_origins:
224+
- "http://localhost:23232" # always allowed
225+
# - "https://example.com"
226+
227+
# The allowed cross-origin methods
228+
allowed_methods:
229+
- "GET"
230+
- "HEAD"
231+
- "POST"
232+
- "PUT"
233+
- "OPTIONS"
234+
207235
# The database configuration.
208236
db:
209237
# The database driver to use.

pkg/config/config.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ type GitConfig struct {
6161
MaxConnections int `env:"MAX_CONNECTIONS" yaml:"max_connections"`
6262
}
6363

64+
// CORSConfig is the CORS configuration for the server.
65+
type CORSConfig struct {
66+
AllowedHeaders []string `env:"ALLOWED_HEADERS" yaml:"allowed_headers"`
67+
68+
AllowedOrigins []string `env:"ALLOWED_ORIGINS" yaml:"allowed_origins"`
69+
70+
AllowedMethods []string `env:"ALLOWED_METHODS" yaml:"allowed_methods"`
71+
}
72+
6473
// HTTPConfig is the HTTP configuration for the server.
6574
type HTTPConfig struct {
6675
// Enabled toggles the HTTP server on/off
@@ -77,6 +86,9 @@ type HTTPConfig struct {
7786

7887
// PublicURL is the public URL of the HTTP server.
7988
PublicURL string `env:"PUBLIC_URL" yaml:"public_url"`
89+
90+
// CORS is the cross-origin configuration for the HTTP server.
91+
CORS CORSConfig `envPrefix:"CORS_" yaml:"cors"`
8092
}
8193

8294
// StatsConfig is the configuration for the stats server.
@@ -196,6 +208,9 @@ func (c *Config) Environ() []string {
196208
fmt.Sprintf("SOFT_SERVE_HTTP_TLS_KEY_PATH=%s", c.HTTP.TLSKeyPath),
197209
fmt.Sprintf("SOFT_SERVE_HTTP_TLS_CERT_PATH=%s", c.HTTP.TLSCertPath),
198210
fmt.Sprintf("SOFT_SERVE_HTTP_PUBLIC_URL=%s", c.HTTP.PublicURL),
211+
fmt.Sprintf("SOFT_SERVE_HTTP_CORS_ALLOWED_HEADERS=%s", strings.Join(c.HTTP.CORS.AllowedHeaders, ",")),
212+
fmt.Sprintf("SOFT_SERVE_HTTP_CORS_ALLOWED_ORIGINS=%s", strings.Join(c.HTTP.CORS.AllowedOrigins, ",")),
213+
fmt.Sprintf("SOFT_SERVE_HTTP_CORS_ALLOWED_METHODS=%s", strings.Join(c.HTTP.CORS.AllowedMethods, ",")),
199214
fmt.Sprintf("SOFT_SERVE_STATS_ENABLED=%t", c.Stats.Enabled),
200215
fmt.Sprintf("SOFT_SERVE_STATS_LISTEN_ADDR=%s", c.Stats.ListenAddr),
201216
fmt.Sprintf("SOFT_SERVE_LOG_FORMAT=%s", c.Log.Format),
@@ -355,6 +370,11 @@ func DefaultConfig() *Config {
355370
Enabled: true,
356371
ListenAddr: ":23232",
357372
PublicURL: "http://localhost:23232",
373+
CORS: CORSConfig{
374+
AllowedHeaders: []string{"Accept", "Accept-Language", "Content-Language", "Content-Type", "Origin", "X-Requested-With", "User-Agent", "Authorization", "Access-Control-Request-Method", "Access-Control-Allow-Origin"},
375+
AllowedMethods: []string{"GET", "HEAD", "POST", "PUT", "OPTIONS"},
376+
AllowedOrigins: []string{"http://localhost:23232"},
377+
},
358378
},
359379
Stats: StatsConfig{
360380
Enabled: true,
@@ -423,6 +443,8 @@ func (c *Config) Validate() error {
423443

424444
c.InitialAdminKeys = pks
425445

446+
c.HTTP.CORS.AllowedOrigins = append([]string{c.HTTP.PublicURL}, c.HTTP.CORS.AllowedOrigins...)
447+
426448
return nil
427449
}
428450

pkg/config/config_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,48 @@ func TestCustomConfigLocation(t *testing.T) {
7979
cfg = DefaultConfig()
8080
is.Equal(cfg.Name, "Soft Serve")
8181
}
82+
83+
func TestParseMultipleHeaders(t *testing.T) {
84+
is := is.New(t)
85+
is.NoErr(os.Setenv("SOFT_SERVE_HTTP_CORS_ALLOWED_HEADERS", "Accept,Accept-Language,User-Agent"))
86+
t.Cleanup(func() {
87+
is.NoErr(os.Unsetenv("SOFT_SERVE_HTTP_CORS_ALLOWED_HEADERS"))
88+
})
89+
cfg := DefaultConfig()
90+
is.NoErr(cfg.ParseEnv())
91+
is.Equal(cfg.HTTP.CORS.AllowedHeaders, []string{
92+
"Accept",
93+
"Accept-Language",
94+
"User-Agent",
95+
})
96+
}
97+
98+
func TestParseMultipleOrigins(t *testing.T) {
99+
is := is.New(t)
100+
is.NoErr(os.Setenv("SOFT_SERVE_HTTP_CORS_ALLOWED_ORIGINS", "http://example.com,https://example.com"))
101+
t.Cleanup(func() {
102+
is.NoErr(os.Unsetenv("SOFT_SERVE_HTTP_CORS_ALLOWED_ORIGINS"))
103+
})
104+
cfg := DefaultConfig()
105+
is.NoErr(cfg.ParseEnv())
106+
is.Equal(cfg.HTTP.CORS.AllowedOrigins, []string{
107+
"http://localhost:23232",
108+
"http://example.com",
109+
"https://example.com",
110+
})
111+
}
112+
113+
func TestParseMultipleMethods(t *testing.T) {
114+
is := is.New(t)
115+
is.NoErr(os.Setenv("SOFT_SERVE_HTTP_CORS_ALLOWED_METHODS", "GET,POST,PUT"))
116+
t.Cleanup(func() {
117+
is.NoErr(os.Unsetenv("SOFT_SERVE_HTTP_CORS_ALLOWED_METHODS"))
118+
})
119+
cfg := DefaultConfig()
120+
is.NoErr(cfg.ParseEnv())
121+
is.Equal(cfg.HTTP.CORS.AllowedMethods, []string{
122+
"GET",
123+
"POST",
124+
"PUT",
125+
})
126+
}

pkg/config/file.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,32 @@ http:
8989
# Make sure to use https:// if you are using TLS.
9090
public_url: "{{ .HTTP.PublicURL }}"
9191
92+
# The cross-origin request security options
93+
cors:
94+
# The allowed cross-origin headers
95+
allowed_headers:
96+
- "Accept"
97+
- "Accept-Language"
98+
- "Content-Language"
99+
- "Content-Type"
100+
- "Origin"
101+
- "X-Requested-With"
102+
- "User-Agent"
103+
- "Authorization"
104+
- "Access-Control-Request-Method"
105+
- "Access-Control-Allow-Origin"
106+
# The allowed cross-origin URLs
107+
allowed_origins:
108+
- "{{ .HTTP.PublicURL }}" # always allowed
109+
# - "https://example.com"
110+
# The allowed cross-origin methods
111+
allowed_methods:
112+
- "GET"
113+
- "HEAD"
114+
- "POST"
115+
- "PUT"
116+
- "OPTIONS"
117+
92118
# The stats server configuration.
93119
stats:
94120
# Enable the stats server.

pkg/web/server.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"net/http"
66

77
"github.com/charmbracelet/log/v2"
8+
"github.com/charmbracelet/soft-serve/pkg/config"
89
"github.com/gorilla/handlers"
910
"github.com/gorilla/mux"
1011
)
@@ -29,5 +30,12 @@ func NewRouter(ctx context.Context) http.Handler {
2930
h = handlers.CompressHandler(h)
3031
h = handlers.RecoveryHandler()(h)
3132

33+
cfg := config.FromContext(ctx)
34+
35+
h = handlers.CORS(handlers.AllowedHeaders(cfg.HTTP.CORS.AllowedHeaders),
36+
handlers.AllowedOrigins(cfg.HTTP.CORS.AllowedOrigins),
37+
handlers.AllowedMethods(cfg.HTTP.CORS.AllowedMethods),
38+
)(h)
39+
3240
return h
3341
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# vi: set ft=conf
2+
3+
# FIXME: don't skip windows
4+
[windows] skip 'curl makes github actions hang'
5+
6+
# convert crlf to lf on windows
7+
[windows] dos2unix http1.txt http2.txt http3.txt goget.txt gitclone.txt
8+
9+
# start soft serve
10+
exec soft serve &
11+
# wait for SSH server to start
12+
ensureserverrunning SSH_PORT
13+
14+
# create user
15+
soft user create user1 --key "$USER1_AUTHORIZED_KEY"
16+
17+
# create access token
18+
soft token create --expires-in '1h' 'repo2'
19+
cp stdout tokenfile
20+
envfile TOKEN=tokenfile
21+
soft token create --expires-in '1ns' 'repo2'
22+
cp stdout etokenfile
23+
envfile ETOKEN=etokenfile
24+
usoft token create 'repo2'
25+
cp stdout utokenfile
26+
envfile UTOKEN=utokenfile
27+
28+
# push & create repo with some files, commits, tags...
29+
mkdir ./repo2
30+
git -c init.defaultBranch=master -C repo2 init
31+
mkfile ./repo2/README.md '# Project\nfoo'
32+
mkfile ./repo2/foo.png 'foo'
33+
mkfile ./repo2/bar.png 'bar'
34+
git -C repo2 remote add origin http://$TOKEN@localhost:$HTTP_PORT/repo2
35+
git -C repo2 lfs install --local
36+
git -C repo2 lfs track '*.png'
37+
git -C repo2 add -A
38+
git -C repo2 commit -m 'first'
39+
git -C repo2 tag v0.1.0
40+
git -C repo2 push origin HEAD
41+
git -C repo2 push origin HEAD --tags
42+
43+
-- test 1 --
44+
# default public url is always allowed
45+
curl -v --request OPTIONS http://localhost:$HTTP_PORT/repo2/git-upload-pack -H 'Origin: http://localhost:23232' -H 'Access-Control-Request-Method: POST'
46+
stderr '.*200 OK.*'
47+
48+
# stop the server
49+
stopserver
50+
51+
-- test 2 --
52+
# by default the server does not allow example.com, so the response does not have the "Access-Control-Allow-Origin" header and cors will fail.
53+
54+
# restart soft serve
55+
exec soft serve &
56+
# wait for SSH server to start
57+
ensureserverrunning SSH_PORT
58+
59+
curl -v --request OPTIONS http://localhost:$HTTP_PORT/repo2/git-upload-pack -H 'Origin: https://example.com' -H 'Access-Control-Request-Method: POST'
60+
! stderr '.*Access-Control-Allow-Origin.*'
61+
62+
# stop the server
63+
stopserver
64+
65+
-- test 3 --
66+
# allow cross-origin OPTIONS requests for example.com
67+
env SOFT_SERVE_HTTP_CORS_ALLOWED_ORIGINS="https://example.com"
68+
env SOFT_SERVE_HTTP_CORS_ALLOWED_METHODS="GET,OPTIONS"
69+
env SOFT_SERVE_HTTP_CORS_ALLOWED_HEADERS="Origin,Access-Control-Request-Method"
70+
71+
# restart soft serve
72+
exec soft serve &
73+
# wait for SSH server to start
74+
ensureserverrunning SSH_PORT
75+
76+
curl -v --request OPTIONS http://localhost:$HTTP_PORT/repo2.git/info/refs -H 'Origin: https://example.com' -H 'Access-Control-Request-Method: GET'
77+
stderr '.*200 OK.*'
78+
79+
# stop the server
80+
[windows] stopserver
81+
[windows] ! stderr .

0 commit comments

Comments
 (0)