diff --git a/gateway/core/corehttp/gateway.go b/gateway/core/corehttp/gateway.go index 2d300183a..775961bf0 100644 --- a/gateway/core/corehttp/gateway.go +++ b/gateway/core/corehttp/gateway.go @@ -1,18 +1,20 @@ package corehttp import ( + "context" "fmt" "net" "net/http" "sort" + coreiface "github.com/ipfs/interface-go-ipfs-core" + options "github.com/ipfs/interface-go-ipfs-core/options" + path "github.com/ipfs/interface-go-ipfs-core/path" version "github.com/ipfs/kubo" core "github.com/ipfs/kubo/core" coreapi "github.com/ipfs/kubo/core/coreapi" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" - - options "github.com/ipfs/interface-go-ipfs-core/options" id "github.com/libp2p/go-libp2p/p2p/protocol/identify" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) type GatewayConfig struct { @@ -22,6 +24,21 @@ type GatewayConfig struct { FastDirIndexThreshold int } +// NodeAPI defines the minimal set of API services required by a gateway handler +type NodeAPI interface { + // Unixfs returns an implementation of Unixfs API + Unixfs() coreiface.UnixfsAPI + + // Block returns an implementation of Block API + Block() coreiface.BlockAPI + + // Dag returns an implementation of Dag API + Dag() coreiface.APIDagService + + // ResolvePath resolves the path using Unixfs resolver + ResolvePath(context.Context, path.Path) (path.Resolved, error) +} + // A helper function to clean up a set of headers: // 1. Canonicalizes. // 2. Deduplicates. @@ -59,49 +76,19 @@ func GatewayOption(writable bool, paths ...string) ServeOption { headers[http.CanonicalHeaderKey(h)] = v } - // Hard-coded headers. - const ACAHeadersName = "Access-Control-Allow-Headers" - const ACEHeadersName = "Access-Control-Expose-Headers" - const ACAOriginName = "Access-Control-Allow-Origin" - const ACAMethodsName = "Access-Control-Allow-Methods" + AddAccessControlHeaders(headers) - if _, ok := headers[ACAOriginName]; !ok { - // Default to *all* - headers[ACAOriginName] = []string{"*"} - } - if _, ok := headers[ACAMethodsName]; !ok { - // Default to GET - headers[ACAMethodsName] = []string{http.MethodGet} + offlineApi, err := api.WithOptions(options.Api.Offline(true)) + if err != nil { + return nil, err } - headers[ACAHeadersName] = cleanHeaderSet( - append([]string{ - "Content-Type", - "User-Agent", - "Range", - "X-Requested-With", - }, headers[ACAHeadersName]...)) - - headers[ACEHeadersName] = cleanHeaderSet( - append([]string{ - "Content-Length", - "Content-Range", - "X-Chunked-Output", - "X-Stream-Output", - "X-Ipfs-Path", - "X-Ipfs-Roots", - }, headers[ACEHeadersName]...)) - - var gateway http.Handler - gateway, err = newGatewayHandler(GatewayConfig{ + gateway := NewGatewayHandler(GatewayConfig{ Headers: headers, Writable: writable, PathPrefixes: cfg.Gateway.PathPrefixes, FastDirIndexThreshold: int(cfg.Gateway.FastDirIndexThreshold.WithDefault(100)), - }, api) - if err != nil { - return nil, err - } + }, api, offlineApi) gateway = otelhttp.NewHandler(gateway, "Gateway.Request") @@ -112,6 +99,50 @@ func GatewayOption(writable bool, paths ...string) ServeOption { } } +// AddAccessControlHeaders adds default headers used for controlling +// cross-origin requests. This function adds several values to the +// Access-Control-Allow-Headers and Access-Control-Expose-Headers entries. +// If the Access-Control-Allow-Origin entry is missing a value of '*' is +// added, indicating that browsers should allow requesting code from any +// origin to access the resource. +// If the Access-Control-Allow-Methods entry is missing a value of 'GET' is +// added, indicating that browsers may use the GET method when issuing cross +// origin requests. +func AddAccessControlHeaders(headers map[string][]string) { + // Hard-coded headers. + const ACAHeadersName = "Access-Control-Allow-Headers" + const ACEHeadersName = "Access-Control-Expose-Headers" + const ACAOriginName = "Access-Control-Allow-Origin" + const ACAMethodsName = "Access-Control-Allow-Methods" + + if _, ok := headers[ACAOriginName]; !ok { + // Default to *all* + headers[ACAOriginName] = []string{"*"} + } + if _, ok := headers[ACAMethodsName]; !ok { + // Default to GET + headers[ACAMethodsName] = []string{http.MethodGet} + } + + headers[ACAHeadersName] = cleanHeaderSet( + append([]string{ + "Content-Type", + "User-Agent", + "Range", + "X-Requested-With", + }, headers[ACAHeadersName]...)) + + headers[ACEHeadersName] = cleanHeaderSet( + append([]string{ + "Content-Length", + "Content-Range", + "X-Chunked-Output", + "X-Stream-Output", + "X-Ipfs-Path", + "X-Ipfs-Roots", + }, headers[ACEHeadersName]...)) +} + func VersionOption() ServeOption { return func(_ *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) { mux.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) { diff --git a/gateway/core/corehttp/gateway_handler.go b/gateway/core/corehttp/gateway_handler.go index 2a7ee155f..a6dbdcb20 100644 --- a/gateway/core/corehttp/gateway_handler.go +++ b/gateway/core/corehttp/gateway_handler.go @@ -25,7 +25,6 @@ import ( path "github.com/ipfs/go-path" "github.com/ipfs/go-path/resolver" coreiface "github.com/ipfs/interface-go-ipfs-core" - options "github.com/ipfs/interface-go-ipfs-core/options" ipath "github.com/ipfs/interface-go-ipfs-core/path" routing "github.com/libp2p/go-libp2p-core/routing" prometheus "github.com/prometheus/client_golang/prometheus" @@ -69,8 +68,8 @@ type redirectTemplateData struct { // (it serves requests like GET /ipfs/QmVRzPKPzNtSrEzBFm2UZfxmPAgnaLke4DMcerbsGGSaFe/link) type gatewayHandler struct { config GatewayConfig - api coreiface.CoreAPI - offlineApi coreiface.CoreAPI + api NodeAPI + offlineApi NodeAPI // generic metrics firstContentBlockGetMetric *prometheus.HistogramVec @@ -214,11 +213,13 @@ func newGatewayHistogramMetric(name string, help string) *prometheus.HistogramVe return histogramMetric } -func newGatewayHandler(c GatewayConfig, api coreiface.CoreAPI) (*gatewayHandler, error) { - offlineApi, err := api.WithOptions(options.Api.Offline(true)) - if err != nil { - return nil, err - } +// NewGatewayHandler returns an http.Handler that can act as a gateway to IPFS content +// offlineApi is a version of the API that should not make network requests for missing data +func NewGatewayHandler(c GatewayConfig, api NodeAPI, offlineApi NodeAPI) http.Handler { + return newGatewayHandler(c, api, offlineApi) +} + +func newGatewayHandler(c GatewayConfig, api NodeAPI, offlineApi NodeAPI) *gatewayHandler { i := &gatewayHandler{ config: c, api: api, @@ -263,7 +264,7 @@ func newGatewayHandler(c GatewayConfig, api coreiface.CoreAPI) (*gatewayHandler, "The time to receive the first UnixFS node on a GET from the gateway.", ), } - return i, nil + return i } func parseIpfsPath(p string) (cid.Cid, string, error) {