Skip to content

Commit 3d57bce

Browse files
authored
gateway: extract CORS to headers middleware
1 parent 4c3a1f2 commit 3d57bce

File tree

11 files changed

+133
-127
lines changed

11 files changed

+133
-127
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@ The following emojis are used to highlight certain changes:
1818

1919
- `blockservice` now has `ContextWithSession` and `EmbedSessionInContext` functions, which allows to embed a session in a context. Future calls to `BlockGetter.GetBlock`, `BlockGetter.GetBlocks` and `NewSession` will use the session in the context.
2020
- `blockservice.NewWritethrough` deprecated function has been removed, instead you can do `blockservice.New(..., ..., WriteThrough())` like previously.
21+
- `gateway`: a new header configuration middleware has been added to replace the existing header configuration, which can be used more generically.
2122

2223
### Changed
2324

2425
### Removed
2526

27+
- 🛠 `gateway`: the header configuration `Config.Headers` and `AddAccessControlHeaders` has been replaced by the new middleware provided by `NewHeaders`.
28+
2629
### Security
2730

2831
## [v0.17.0]

examples/gateway/common/handler.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@ import (
1212

1313
func NewHandler(gwAPI gateway.IPFSBackend) http.Handler {
1414
conf := gateway.Config{
15-
// Initialize the headers. For this example, we do not add any special headers,
16-
// only the required ones via gateway.AddAccessControlHeaders.
17-
Headers: map[string][]string{},
18-
1915
// If you set DNSLink to point at the CID from CAR, you can load it!
2016
NoDNSLink: false,
2117

@@ -58,9 +54,6 @@ func NewHandler(gwAPI gateway.IPFSBackend) http.Handler {
5854
},
5955
}
6056

61-
// Add required access control headers to the configuration.
62-
gateway.AddAccessControlHeaders(conf.Headers)
63-
6457
// Creates a mux to serve the gateway paths. This is not strictly necessary
6558
// and gwHandler could be used directly. However, on the next step we also want
6659
// to add prometheus metrics, hence needing the mux.
@@ -86,6 +79,10 @@ func NewHandler(gwAPI gateway.IPFSBackend) http.Handler {
8679
// http.ServeMux which does not support CONNECT by default.
8780
handler = withConnect(handler)
8881

82+
// Add headers middleware that applies any headers we define to all requests
83+
// as well as a default CORS configuration.
84+
handler = gateway.NewHeaders(nil).ApplyCors().Wrap(handler)
85+
8986
// Finally, wrap with the otelhttp handler. This will allow the tracing system
9087
// to work and for correct propagation of tracing headers. This step is optional
9188
// and only required if you want to use tracing. Note that OTel must be correctly

gateway/README.md

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,7 @@ This example shows how you can start your own gateway, assuming you have an `IPF
1414
implementation.
1515

1616
```go
17-
// Initialize your headers and apply the default headers.
18-
headers := map[string][]string{}
19-
gateway.AddAccessControlHeaders(headers)
20-
21-
conf := gateway.Config{
22-
Headers: headers,
23-
}
17+
conf := gateway.Config{}
2418

2519
// Initialize an IPFSBackend interface for both an online and offline versions.
2620
// The offline version should not make any network request for missing content.
@@ -29,9 +23,11 @@ ipfsBackend := ...
2923
// Create http mux and setup path gateway handler.
3024
mux := http.NewServeMux()
3125
handler := gateway.NewHandler(conf, ipfsBackend)
26+
handler = gateway.NewHeaders(nil).ApplyCors().Wrap(handler)
3227
mux.Handle("/ipfs/", handler)
3328
mux.Handle("/ipns/", handler)
3429

30+
3531
// Start the server on :8080 and voilá! You have a basic IPFS gateway running
3632
// in http://localhost:8080.
3733
_ = http.ListenAndServe(":8080", mux)

gateway/errors_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func TestWebError(t *testing.T) {
4343
t.Parallel()
4444

4545
// Create a handler to be able to test `webError`.
46-
config := &Config{Headers: map[string][]string{}}
46+
config := &Config{}
4747

4848
t.Run("429 Too Many Requests", func(t *testing.T) {
4949
t.Parallel()
@@ -113,7 +113,7 @@ func TestWebError(t *testing.T) {
113113
t.Run("Error is sent as plain text when 'Accept' header contains 'text/html' and config.DisableHTMLErrors is true", func(t *testing.T) {
114114
t.Parallel()
115115

116-
config := &Config{Headers: map[string][]string{}, DisableHTMLErrors: true}
116+
config := &Config{DisableHTMLErrors: true}
117117
w := httptest.NewRecorder()
118118
r := httptest.NewRequest(http.MethodGet, "/blah", nil)
119119
r.Header.Set("Accept", "something/else, text/html")

gateway/gateway.go

Lines changed: 0 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import (
55
"errors"
66
"fmt"
77
"io"
8-
"net/http"
9-
"sort"
108
"strconv"
119
"strings"
1210
"time"
@@ -20,11 +18,6 @@ import (
2018

2119
// Config is the configuration used when creating a new gateway handler.
2220
type Config struct {
23-
// Headers is a map containing all the headers that should be sent by default
24-
// in all requests. You can define custom headers, as well as add the recommended
25-
// headers via AddAccessControlHeaders.
26-
Headers map[string][]string
27-
2821
// DeserializedResponses configures this gateway to support returning data
2922
// in deserialized format. By default, the gateway will only support
3023
// trustless, verifiable [application/vnd.ipld.raw] and
@@ -394,79 +387,6 @@ type WithContextHint interface {
394387
WrapContextForRequest(context.Context) context.Context
395388
}
396389

397-
// cleanHeaderSet is an helper function that cleans a set of headers by
398-
// (1) canonicalizing, (2) de-duplicating and (3) sorting.
399-
func cleanHeaderSet(headers []string) []string {
400-
// Deduplicate and canonicalize.
401-
m := make(map[string]struct{}, len(headers))
402-
for _, h := range headers {
403-
m[http.CanonicalHeaderKey(h)] = struct{}{}
404-
}
405-
result := make([]string, 0, len(m))
406-
for k := range m {
407-
result = append(result, k)
408-
}
409-
410-
// Sort
411-
sort.Strings(result)
412-
return result
413-
}
414-
415-
// AddAccessControlHeaders ensures safe default HTTP headers are used for
416-
// controlling cross-origin requests. This function adds several values to the
417-
// [Access-Control-Allow-Headers] and [Access-Control-Expose-Headers] entries
418-
// to be exposed on GET and OPTIONS responses, including [CORS Preflight].
419-
//
420-
// If the Access-Control-Allow-Origin entry is missing, a default value of '*' is
421-
// added, indicating that browsers should allow requesting code from any
422-
// origin to access the resource.
423-
//
424-
// If the Access-Control-Allow-Methods entry is missing a value, 'GET, HEAD,
425-
// OPTIONS' is added, indicating that browsers may use them when issuing cross
426-
// origin requests.
427-
//
428-
// [Access-Control-Allow-Headers]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
429-
// [Access-Control-Expose-Headers]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers
430-
// [CORS Preflight]: https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request
431-
func AddAccessControlHeaders(headers map[string][]string) {
432-
// Hard-coded headers.
433-
const ACAHeadersName = "Access-Control-Allow-Headers"
434-
const ACEHeadersName = "Access-Control-Expose-Headers"
435-
const ACAOriginName = "Access-Control-Allow-Origin"
436-
const ACAMethodsName = "Access-Control-Allow-Methods"
437-
438-
if _, ok := headers[ACAOriginName]; !ok {
439-
// Default to *all*
440-
headers[ACAOriginName] = []string{"*"}
441-
}
442-
if _, ok := headers[ACAMethodsName]; !ok {
443-
// Default to GET, HEAD, OPTIONS
444-
headers[ACAMethodsName] = []string{
445-
http.MethodGet,
446-
http.MethodHead,
447-
http.MethodOptions,
448-
}
449-
}
450-
451-
headers[ACAHeadersName] = cleanHeaderSet(
452-
append([]string{
453-
"Content-Type",
454-
"User-Agent",
455-
"Range",
456-
"X-Requested-With",
457-
}, headers[ACAHeadersName]...))
458-
459-
headers[ACEHeadersName] = cleanHeaderSet(
460-
append([]string{
461-
"Content-Length",
462-
"Content-Range",
463-
"X-Chunked-Output",
464-
"X-Stream-Output",
465-
"X-Ipfs-Path",
466-
"X-Ipfs-Roots",
467-
}, headers[ACEHeadersName]...))
468-
}
469-
470390
// RequestContextKey is a type representing a [context.Context] value key.
471391
type RequestContextKey string
472392

gateway/gateway_test.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -352,8 +352,7 @@ func TestHeaders(t *testing.T) {
352352
headers := map[string][]string{}
353353
headers[headerACAO] = []string{expectedACAO}
354354

355-
ts := newTestServerWithConfig(t, backend, Config{
356-
Headers: headers,
355+
ts := newTestServerWithConfigAndHeaders(t, backend, Config{
357356
PublicGateways: map[string]*PublicGateway{
358357
"subgw.example.com": {
359358
Paths: []string{"/ipfs", "/ipns"},
@@ -362,7 +361,7 @@ func TestHeaders(t *testing.T) {
362361
},
363362
},
364363
DeserializedResponses: true,
365-
})
364+
}, headers)
366365
t.Logf("test server url: %s", ts.URL)
367366

368367
testCORSPreflightRequest := func(t *testing.T, path, hostHeader string, requestOriginHeader string, code int) {
@@ -532,7 +531,6 @@ func TestRedirects(t *testing.T) {
532531
backend.namesys["/ipns/example.com"] = newMockNamesysItem(path.FromCid(root), 0)
533532

534533
ts := newTestServerWithConfig(t, backend, Config{
535-
Headers: map[string][]string{},
536534
NoDNSLink: false,
537535
PublicGateways: map[string]*PublicGateway{
538536
"example.com": {
@@ -590,7 +588,6 @@ func TestDeserializedResponses(t *testing.T) {
590588
backend, root := newMockBackend(t, "fixtures.car")
591589

592590
ts := newTestServerWithConfig(t, backend, Config{
593-
Headers: map[string][]string{},
594591
NoDNSLink: false,
595592
PublicGateways: map[string]*PublicGateway{
596593
"trustless.com": {
@@ -670,7 +667,6 @@ func TestDeserializedResponses(t *testing.T) {
670667
backend.namesys["/ipns/trusted.com"] = newMockNamesysItem(path.FromCid(root), 0)
671668

672669
ts := newTestServerWithConfig(t, backend, Config{
673-
Headers: map[string][]string{},
674670
NoDNSLink: false,
675671
PublicGateways: map[string]*PublicGateway{
676672
"trustless.com": {

gateway/handler.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,6 @@ func (i *handler) optionsHandler(w http.ResponseWriter, r *http.Request) {
179179
// OPTIONS is a noop request that is used by the browsers to check if server accepts
180180
// cross-site XMLHttpRequest, which is indicated by the presence of CORS headers:
181181
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Preflighted_requests
182-
addCustomHeaders(w, i.config.Headers) // return all custom headers (including CORS ones, if set)
183182
}
184183

185184
// addAllowHeader sets Allow header with supported HTTP methods
@@ -264,7 +263,6 @@ func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) {
264263
trace.SpanFromContext(r.Context()).SetAttributes(attribute.String("ResponseFormat", responseFormat))
265264
i.requestTypeMetric.WithLabelValues(contentPath.Namespace(), responseFormat).Inc()
266265

267-
addCustomHeaders(w, i.config.Headers) // ok, _now_ write user's headers.
268266
w.Header().Set("X-Ipfs-Path", contentPath.String())
269267

270268
// Fail fast if unsupported request type was sent to a Trustless Gateway.
@@ -340,12 +338,6 @@ func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) {
340338
}
341339
}
342340

343-
func addCustomHeaders(w http.ResponseWriter, headers map[string][]string) {
344-
for k, v := range headers {
345-
w.Header()[http.CanonicalHeaderKey(k)] = v
346-
}
347-
}
348-
349341
// isDeserializedResponsePossible returns true if deserialized responses
350342
// are allowed on the specified hostname, or globally. Host-specific rules
351343
// override global config.

gateway/handler_codec_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ func TestDagJsonCborPreview(t *testing.T) {
1616
backend, root := newMockBackend(t, "fixtures.car")
1717

1818
ts := newTestServerWithConfig(t, backend, Config{
19-
Headers: map[string][]string{},
2019
NoDNSLink: false,
2120
PublicGateways: map[string]*PublicGateway{
2221
"example.com": {

gateway/headers.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package gateway
2+
3+
import (
4+
"net/http"
5+
"sort"
6+
)
7+
8+
// Headers is an HTTP middleware that sets the configured headers in all requests.
9+
type Headers struct {
10+
headers map[string][]string
11+
}
12+
13+
// NewHeaders creates a new [Headers] middleware that applies the given headers
14+
// to all requests. If you call [Headers.ApplyCors], the default CORS configuration
15+
// will also be applied, if any of the CORS headers is missing.
16+
func NewHeaders(headers map[string][]string) *Headers {
17+
h := &Headers{
18+
headers: map[string][]string{},
19+
}
20+
21+
for k, v := range headers {
22+
h.headers[http.CanonicalHeaderKey(k)] = v
23+
}
24+
25+
return h
26+
}
27+
28+
// ApplyCors applies safe default HTTP headers for controlling cross-origin
29+
// requests. This function adds several values to the [Access-Control-Allow-Headers]
30+
// and [Access-Control-Expose-Headers] entries to be exposed on GET and OPTIONS
31+
// responses, including [CORS Preflight].
32+
//
33+
// If the Access-Control-Allow-Origin entry is missing, a default value of '*' is
34+
// added, indicating that browsers should allow requesting code from any
35+
// origin to access the resource.
36+
//
37+
// If the Access-Control-Allow-Methods entry is missing a value, 'GET, HEAD,
38+
// OPTIONS' is added, indicating that browsers may use them when issuing cross
39+
// origin requests.
40+
//
41+
// [Access-Control-Allow-Headers]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
42+
// [Access-Control-Expose-Headers]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers
43+
// [CORS Preflight]: https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request
44+
func (h *Headers) ApplyCors() *Headers {
45+
// Hard-coded headers.
46+
const ACAHeadersName = "Access-Control-Allow-Headers"
47+
const ACEHeadersName = "Access-Control-Expose-Headers"
48+
const ACAOriginName = "Access-Control-Allow-Origin"
49+
const ACAMethodsName = "Access-Control-Allow-Methods"
50+
51+
if _, ok := h.headers[ACAOriginName]; !ok {
52+
// Default to *all*
53+
h.headers[ACAOriginName] = []string{"*"}
54+
}
55+
if _, ok := h.headers[ACAMethodsName]; !ok {
56+
// Default to GET, HEAD, OPTIONS
57+
h.headers[ACAMethodsName] = []string{
58+
http.MethodGet,
59+
http.MethodHead,
60+
http.MethodOptions,
61+
}
62+
}
63+
64+
h.headers[ACAHeadersName] = cleanHeaderSet(
65+
append([]string{
66+
"Content-Type",
67+
"User-Agent",
68+
"Range",
69+
"X-Requested-With",
70+
}, h.headers[ACAHeadersName]...))
71+
72+
h.headers[ACEHeadersName] = cleanHeaderSet(
73+
append([]string{
74+
"Content-Length",
75+
"Content-Range",
76+
"X-Chunked-Output",
77+
"X-Stream-Output",
78+
"X-Ipfs-Path",
79+
"X-Ipfs-Roots",
80+
}, h.headers[ACEHeadersName]...))
81+
82+
return h
83+
}
84+
85+
// Wrap wraps the given [http.Handler] with the headers middleware.
86+
func (h *Headers) Wrap(next http.Handler) http.Handler {
87+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
88+
for k, v := range h.headers {
89+
w.Header()[k] = v
90+
}
91+
92+
next.ServeHTTP(w, r)
93+
})
94+
}
95+
96+
// cleanHeaderSet is an helper function that cleans a set of headers by
97+
// (1) canonicalizing, (2) de-duplicating and (3) sorting.
98+
func cleanHeaderSet(headers []string) []string {
99+
// Deduplicate and canonicalize.
100+
m := make(map[string]struct{}, len(headers))
101+
for _, h := range headers {
102+
m[http.CanonicalHeaderKey(h)] = struct{}{}
103+
}
104+
result := make([]string, 0, len(m))
105+
for k := range m {
106+
result = append(result, k)
107+
}
108+
109+
// Sort
110+
sort.Strings(result)
111+
return result
112+
}

0 commit comments

Comments
 (0)