f99cf71fc96dd34791b99b24ae6ba523c8394074

Author
fetsorn <fetsorn@gmail.com>
Committer
GitHub <noreply@github.com>
Date

Message

feat: add CORS headers (#516)

Diff

  1diff --git a/README.md b/README.md
  2index cbefcd78454a8727921dd3365f298e54026f3724..d5bd030a0f82435bd4a8529bacf57ddce76afeb2 100644
  3--- a/README.md
  4+++ b/README.md
  5@@ -158,6 +158,32 @@ ssh:
  6   # This is the address that will be used to clone repositories.
  7   public_url: "ssh://localhost:23231"
  8 
  9+  # The cross-origin request security options
 10+  cors:
 11+    # The allowed cross-origin headers
 12+    allowed_headers:
 13+      - Accept
 14+      - Accept-Language
 15+      - Content-Language
 16+      - Origin
 17+    #  - Content-Type
 18+    #  - X-Requested-With
 19+    #  - User-Agent
 20+    #  - Authorization
 21+    #  - Access-Control-Request-Method
 22+
 23+    # The allowed cross-origin URLs
 24+    # allowed_origins:
 25+    #    - *
 26+
 27+    # The allowed cross-origin methods
 28+    allowed_methods:
 29+       - GET
 30+       - HEAD
 31+       - POST
 32+    #   - PUT
 33+    #   - OPTIONS
 34+
 35   # The path to the SSH server's private key.
 36   key_path: "ssh/soft_serve_host"
 37 
 38diff --git a/pkg/config/config.go b/pkg/config/config.go
 39index 05dd2c38e3e3f70d6e7facfe526afd8fe0fc4215..8db84ebf2763ad9fe15f24948c3268de39738d73 100644
 40--- a/pkg/config/config.go
 41+++ b/pkg/config/config.go
 42@@ -61,6 +61,15 @@ type GitConfig struct {
 43 	MaxConnections int `env:"MAX_CONNECTIONS" yaml:"max_connections"`
 44 }
 45 
 46+// CORSConfig is the CORS configuration for the server.
 47+type CORSConfig struct {
 48+	AllowedHeaders []string `env:"ALLOWED_HEADERS" yaml:"allowed_headers"`
 49+
 50+	AllowedOrigins []string `env:"ALLOWED_ORIGINS" yaml:"allowed_origins"`
 51+
 52+	AllowedMethods []string `env:"ALLOWED_METHODS" yaml:"allowed_methods"`
 53+}
 54+
 55 // HTTPConfig is the HTTP configuration for the server.
 56 type HTTPConfig struct {
 57 	// Enabled toggles the HTTP server on/off
 58@@ -77,6 +86,9 @@ type HTTPConfig struct {
 59 
 60 	// PublicURL is the public URL of the HTTP server.
 61 	PublicURL string `env:"PUBLIC_URL" yaml:"public_url"`
 62+
 63+	// HTTP is the configuration for the HTTP server.
 64+	CORS CORSConfig `envPrefix:"CORS_" yaml:"cors"`
 65 }
 66 
 67 // StatsConfig is the configuration for the stats server.
 68@@ -196,6 +208,9 @@ func (c *Config) Environ() []string {
 69 		fmt.Sprintf("SOFT_SERVE_HTTP_TLS_KEY_PATH=%s", c.HTTP.TLSKeyPath),
 70 		fmt.Sprintf("SOFT_SERVE_HTTP_TLS_CERT_PATH=%s", c.HTTP.TLSCertPath),
 71 		fmt.Sprintf("SOFT_SERVE_HTTP_PUBLIC_URL=%s", c.HTTP.PublicURL),
 72+		fmt.Sprintf("SOFT_SERVE_HTTP_CORS_ALLOWED_HEADERS=%s", strings.Join(c.HTTP.CORS.AllowedHeaders, ",")),
 73+		fmt.Sprintf("SOFT_SERVE_HTTP_CORS_ALLOWED_ORIGINS=%s", strings.Join(c.HTTP.CORS.AllowedOrigins, ",")),
 74+		fmt.Sprintf("SOFT_SERVE_HTTP_CORS_ALLOWED_METHODS=%s", strings.Join(c.HTTP.CORS.AllowedMethods, ",")),
 75 		fmt.Sprintf("SOFT_SERVE_STATS_ENABLED=%t", c.Stats.Enabled),
 76 		fmt.Sprintf("SOFT_SERVE_STATS_LISTEN_ADDR=%s", c.Stats.ListenAddr),
 77 		fmt.Sprintf("SOFT_SERVE_LOG_FORMAT=%s", c.Log.Format),
 78diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
 79index 8b84ed8222ba39ca5ad4d1236f5c787b19b0ab00..b0653f28241239b14c225f76f247c154d67c82aa 100644
 80--- a/pkg/config/config_test.go
 81+++ b/pkg/config/config_test.go
 82@@ -79,3 +79,47 @@ func TestCustomConfigLocation(t *testing.T) {
 83 	cfg = DefaultConfig()
 84 	is.Equal(cfg.Name, "Soft Serve")
 85 }
 86+
 87+func TestParseMultipleHeaders(t *testing.T) {
 88+	is := is.New(t)
 89+	is.NoErr(os.Setenv("SOFT_SERVE_HTTP_CORS_ALLOWED_HEADERS", "Accept,Accept-Language,User-Agent"))
 90+	t.Cleanup(func() {
 91+		is.NoErr(os.Unsetenv("SOFT_SERVE_HTTP_CORS_ALLOWED_HEADERS"))
 92+	})
 93+	cfg := DefaultConfig()
 94+	is.NoErr(cfg.ParseEnv())
 95+	is.Equal(cfg.HTTP.CORS.AllowedHeaders, []string{
 96+		"Accept",
 97+		"Accept-Language",
 98+		"User-Agent",
 99+	})
100+}
101+
102+func TestParseMultipleOrigins(t *testing.T) {
103+	is := is.New(t)
104+	is.NoErr(os.Setenv("SOFT_SERVE_HTTP_CORS_ALLOWED_ORIGINS", "https://foo.example,https://foo.example2"))
105+	t.Cleanup(func() {
106+		is.NoErr(os.Unsetenv("SOFT_SERVE_HTTP_CORS_ALLOWED_ORIGINS"))
107+	})
108+	cfg := DefaultConfig()
109+	is.NoErr(cfg.ParseEnv())
110+	is.Equal(cfg.HTTP.CORS.AllowedOrigins, []string{
111+		"https://foo.example",
112+		"https://foo.example2",
113+	})
114+}
115+
116+func TestParseMultipleMethods(t *testing.T) {
117+	is := is.New(t)
118+	is.NoErr(os.Setenv("SOFT_SERVE_HTTP_CORS_ALLOWED_METHODS", "GET,POST,PUT"))
119+	t.Cleanup(func() {
120+		is.NoErr(os.Unsetenv("SOFT_SERVE_HTTP_CORS_ALLOWED_METHODS"))
121+	})
122+	cfg := DefaultConfig()
123+	is.NoErr(cfg.ParseEnv())
124+	is.Equal(cfg.HTTP.CORS.AllowedMethods, []string{
125+		"GET",
126+		"POST",
127+		"PUT",
128+	})
129+}
130diff --git a/pkg/web/server.go b/pkg/web/server.go
131index 74a04f5b176bee7d1710f023643436119b181c97..ab336e89a5f358be803812b93144a10722af4010 100644
132--- a/pkg/web/server.go
133+++ b/pkg/web/server.go
134@@ -5,6 +5,7 @@ import (
135 	"net/http"
136 
137 	"github.com/charmbracelet/log"
138+	"github.com/charmbracelet/soft-serve/pkg/config"
139 	"github.com/gorilla/handlers"
140 	"github.com/gorilla/mux"
141 )
142@@ -26,5 +27,12 @@ func NewRouter(ctx context.Context) http.Handler {
143 	h = handlers.CompressHandler(h)
144 	h = handlers.RecoveryHandler()(h)
145 
146+	cfg := config.FromContext(ctx)
147+
148+	h = handlers.CORS(handlers.AllowedHeaders(cfg.HTTP.CORS.AllowedHeaders),
149+		handlers.AllowedOrigins(cfg.HTTP.CORS.AllowedOrigins),
150+		handlers.AllowedMethods(cfg.HTTP.CORS.AllowedMethods),
151+	)(h)
152+
153 	return h
154 }
155diff --git a/testscript/testdata/http-cors.txtar b/testscript/testdata/http-cors.txtar
156new file mode 100644
157index 0000000000000000000000000000000000000000..c545ab7a1d908fde301410b90a651d8992aeb9e0
158--- /dev/null
159+++ b/testscript/testdata/http-cors.txtar
160@@ -0,0 +1,64 @@
161+# vi: set ft=conf
162+
163+# FIXME: don't skip windows
164+[windows] skip 'curl makes github actions hang'
165+
166+# convert crlf to lf on windows
167+[windows] dos2unix http1.txt http2.txt http3.txt goget.txt gitclone.txt
168+
169+# start soft serve
170+exec soft serve &
171+# wait for SSH server to start
172+ensureserverrunning SSH_PORT
173+
174+# create user
175+soft user create user1 --key "$USER1_AUTHORIZED_KEY"
176+
177+# create access token
178+soft token create --expires-in '1h' 'repo2'
179+cp stdout tokenfile
180+envfile TOKEN=tokenfile
181+soft token create --expires-in '1ns' 'repo2'
182+cp stdout etokenfile
183+envfile ETOKEN=etokenfile
184+usoft token create 'repo2'
185+cp stdout utokenfile
186+envfile UTOKEN=utokenfile
187+
188+# push & create repo with some files, commits, tags...
189+mkdir ./repo2
190+git -c init.defaultBranch=master -C repo2 init
191+mkfile ./repo2/README.md '# Project\nfoo'
192+mkfile ./repo2/foo.png 'foo'
193+mkfile ./repo2/bar.png 'bar'
194+git -C repo2 remote add origin http://$TOKEN@localhost:$HTTP_PORT/repo2
195+git -C repo2 lfs install --local
196+git -C repo2 lfs track '*.png'
197+git -C repo2 add -A
198+git -C repo2 commit -m 'first'
199+git -C repo2 tag v0.1.0
200+git -C repo2 push origin HEAD
201+git -C repo2 push origin HEAD --tags
202+
203+curl -v --request OPTIONS http://localhost:$HTTP_PORT/repo2.git/info/refs -H 'Origin: https://foo.example' -H 'Access-Control-Request-Method: GET'
204+stderr '.*Method Not Allowed.*'
205+
206+# stop the server
207+stopserver
208+
209+# allow cross-origin OPTIONS requests
210+env SOFT_SERVE_HTTP_CORS_ALLOWED_ORIGINS="https://foo.example"
211+env SOFT_SERVE_HTTP_CORS_ALLOWED_METHODS="GET,OPTIONS"
212+env SOFT_SERVE_HTTP_CORS_ALLOWED_HEADERS="Origin,Access-Control-Request-Method"
213+
214+# restart soft serve
215+exec soft serve &
216+# wait for SSH server to start
217+ensureserverrunning SSH_PORT
218+
219+curl -v --request OPTIONS http://localhost:$HTTP_PORT/repo2.git/info/refs -H 'Origin: https://foo.example' -H 'Access-Control-Request-Method: GET'
220+stderr '.*200 OK.*'
221+
222+# stop the server
223+[windows] stopserver
224+[windows] ! stderr .