1
0
mirror of https://github.com/ipfs/kubo.git synced 2026-03-13 09:53:17 +08:00

refactor(gateway): simplify landing page implementation

- pass headers into LandingPageOption instead of reading config internally,
  the caller in daemon.go already has cfg in scope
- revert getGatewayConfig to original 3-return signature, only
  HostnameOption needs RootRedirect so it reads it directly from
  n.Repo.Config() instead of threading it through all callers
- remove Flush/Hijack from landingResponseWriter, the wrapper only
  intercepts "/" on loopback so streaming and websockets never apply,
  Unwrap is sufficient for http.ResponseController delegation
- use net.ParseIP + IsLoopback() for loopback detection instead of a
  hardcoded switch, covers addresses like 127.0.0.2 and ::ffff:127.0.0.1
- clone header slices in serveLandingPage with slices.Clone to avoid
  sharing backing arrays with the config map
This commit is contained in:
Marcin Rataj
2026-02-06 19:43:51 +01:00
parent 8272935876
commit 6bbcf3a867
5 changed files with 26 additions and 42 deletions

View File

@@ -881,7 +881,7 @@ func serveHTTPApi(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, error
if len(cfg.Gateway.RootRedirect) > 0 {
opts = append(opts, corehttp.RedirectOption("", cfg.Gateway.RootRedirect))
} else {
opts = append(opts, corehttp.LandingPageOption())
opts = append(opts, corehttp.LandingPageOption(cfg.Gateway.HTTPHeaders))
}
node, err := cctx.ConstructNode()

View File

@@ -28,7 +28,7 @@ import (
func GatewayOption(paths ...string) ServeOption {
return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
config, headers, _, err := getGatewayConfig(n)
config, headers, err := getGatewayConfig(n)
if err != nil {
return nil, err
}
@@ -56,7 +56,12 @@ func GatewayOption(paths ...string) ServeOption {
// will show a landing page instead.
func HostnameOption() ServeOption {
return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
cfg, headers, rootRedirect, err := getGatewayConfig(n)
gwCfg, headers, err := getGatewayConfig(n)
if err != nil {
return nil, err
}
nodeCfg, err := n.Repo.Config()
if err != nil {
return nil, err
}
@@ -69,13 +74,13 @@ func HostnameOption() ServeOption {
childMux := http.NewServeMux()
var handler http.Handler
handler = gateway.NewHostnameHandler(cfg, backend, childMux)
handler = gateway.NewHostnameHandler(gwCfg, backend, childMux)
handler = gateway.NewHeaders(headers).ApplyCors().Wrap(handler)
// When RootRedirect is not configured, wrap with landing page fallback.
// This intercepts 404 responses for "/" on loopback addresses (like localhost)
// and serves a kubo-specific landing page instead.
if rootRedirect == "" {
if nodeCfg.Gateway.RootRedirect == "" {
handler = withLandingPageFallback(handler, headers)
}
@@ -272,11 +277,10 @@ var defaultKnownGateways = map[string]*gateway.PublicGateway{
"localhost": subdomainGatewaySpec,
}
// getGatewayConfig returns gateway configuration, HTTP headers, and root redirect URL.
func getGatewayConfig(n *core.IpfsNode) (gateway.Config, map[string][]string, string, error) {
func getGatewayConfig(n *core.IpfsNode) (gateway.Config, map[string][]string, error) {
cfg, err := n.Repo.Config()
if err != nil {
return gateway.Config{}, nil, "", err
return gateway.Config{}, nil, err
}
// Initialize gateway configuration, with empty PublicGateways, handled after.
@@ -316,5 +320,5 @@ func getGatewayConfig(n *core.IpfsNode) (gateway.Config, map[string][]string, st
}
}
return gwCfg, cfg.Gateway.HTTPHeaders, cfg.Gateway.RootRedirect, nil
return gwCfg, cfg.Gateway.HTTPHeaders, nil
}

View File

@@ -206,7 +206,7 @@ func TestDeserializedResponsesInheritance(t *testing.T) {
n, err := core.NewNode(context.Background(), &core.BuildCfg{Repo: r})
assert.NoError(t, err)
gwCfg, _, _, err := getGatewayConfig(n)
gwCfg, _, err := getGatewayConfig(n)
assert.NoError(t, err)
assert.Contains(t, gwCfg.PublicGateways, "example.com")

View File

@@ -1,12 +1,12 @@
package corehttp
import (
"bufio"
_ "embed"
"net"
"net/http"
"slices"
core "github.com/ipfs/kubo/core"
"github.com/ipfs/kubo/core"
)
//go:embed assets/landing.html
@@ -17,13 +17,8 @@ var landingPageHTML []byte
// This helps third-party gateway operators by clearly indicating that the
// gateway software is working but needs configuration, and provides guidance
// for abuse reporting.
func LandingPageOption() ServeOption {
return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
cfg, err := n.Repo.Config()
if err != nil {
return nil, err
}
headers := cfg.Gateway.HTTPHeaders
func LandingPageOption(headers map[string][]string) ServeOption {
return func(_ *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
@@ -38,7 +33,7 @@ func LandingPageOption() ServeOption {
// serveLandingPage writes the landing page HTML with appropriate headers.
func serveLandingPage(w http.ResponseWriter, headers map[string][]string) {
for k, v := range headers {
w.Header()[http.CanonicalHeaderKey(k)] = v
w.Header()[http.CanonicalHeaderKey(k)] = slices.Clone(v)
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write(landingPageHTML)
@@ -72,12 +67,11 @@ func withLandingPageFallback(next http.Handler, headers map[string][]string) htt
if h, _, err := net.SplitHostPort(r.Host); err == nil {
host = h
}
switch host {
case "localhost", "127.0.0.1", "::1", "[::1]":
// Continue to intercept
default:
next.ServeHTTP(w, r)
return
if host != "localhost" {
if ip := net.ParseIP(host); ip == nil || !ip.IsLoopback() {
next.ServeHTTP(w, r)
return
}
}
// Wrap ResponseWriter to intercept 404 responses
@@ -93,6 +87,7 @@ func withLandingPageFallback(next http.Handler, headers map[string][]string) htt
// landingResponseWriter wraps http.ResponseWriter to intercept 404 responses.
// It suppresses the 404 status and body so we can serve a landing page instead.
// Unwrap allows http.ResponseController to reach the underlying writer.
type landingResponseWriter struct {
http.ResponseWriter
wroteHeader bool
@@ -121,21 +116,6 @@ func (w *landingResponseWriter) Write(b []byte) (int, error) {
return w.ResponseWriter.Write(b)
}
// Flush implements http.Flusher for streaming responses.
func (w *landingResponseWriter) Flush() {
if f, ok := w.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
// Hijack implements http.Hijacker for websocket support.
func (w *landingResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if h, ok := w.ResponseWriter.(http.Hijacker); ok {
return h.Hijack()
}
return nil, nil, http.ErrNotSupported
}
// Unwrap returns the underlying ResponseWriter for http.ResponseController.
func (w *landingResponseWriter) Unwrap() http.ResponseWriter {
return w.ResponseWriter

View File

@@ -24,7 +24,7 @@ import (
func RoutingOption() ServeOption {
return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
_, headers, _, err := getGatewayConfig(n)
_, headers, err := getGatewayConfig(n)
if err != nil {
return nil, err
}