Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/ipfs/kubo/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,8 @@ 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())
}

node, err := cctx.ConstructNode()
Expand Down
94 changes: 94 additions & 0 deletions core/corehttp/assets/landing.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex">
<meta name="description" content="Default landing page for Kubo, an IPFS node implementation.">
<meta property="og:title" content="Welcome to Kubo IPFS Node!">
<meta property="og:description" content="Default landing page for Kubo, an IPFS node implementation.">
<meta property="og:type" content="website">
<title>Welcome to Kubo IPFS Node!</title>
<style>
html { color-scheme: light dark; }
body {
max-width: 40em;
margin: 2em auto;
padding: 0 1em;
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.6;
}
h1 {
border-bottom: 2px solid;
padding-bottom: 0.3em;
}
.note {
background: rgba(128, 128, 128, 0.1);
border-left: 4px solid rgba(128, 128, 128, 0.5);
padding: 0.5em 1em;
margin: 1.5em 0;
}
a { color: #0969da; }
@media (prefers-color-scheme: dark) {
a { color: #58a6ff; }
}
ul { padding-left: 1.5em; }
li { margin: 0.3em 0; }
code {
background: rgba(128, 128, 128, 0.15);
padding: 0.1em 0.3em;
border-radius: 3px;
font-size: 0.9em;
}
</style>
</head>
<body>
<h1>Welcome to Kubo!</h1>

<p>If you see this page, the <a href="https://github.com/ipfs/kubo" target="_blank" rel="noopener noreferrer">Kubo IPFS node</a> has been successfully installed and is working.</p>

<p>For configuration options, please refer to the <a href="https://github.com/ipfs/kubo/blob/master/docs/config.md" target="_blank" rel="noopener noreferrer">documentation</a>.</p>

<div class="note">
<strong>Note to operators:</strong> This is the default landing page.
Set <code>Gateway.RootRedirect</code> in the <a href="https://github.com/ipfs/kubo/blob/master/docs/config.md#gatewayrootredirect" target="_blank" rel="noopener noreferrer">configuration</a> to redirect to your own content.
</div>

<h2>Resources</h2>
<ul>
<li><a href="https://github.com/ipfs/kubo" target="_blank" rel="noopener noreferrer">Kubo on GitHub</a></li>
<li><a href="https://github.com/ipfs/kubo/blob/master/docs/config.md" target="_blank" rel="noopener noreferrer">Kubo Configuration Reference</a></li>
<li><a href="https://docs.ipfs.tech" target="_blank" rel="noopener noreferrer">IPFS Documentation</a></li>
<li><a href="https://docs.ipfs.tech/concepts/glossary/#gateway" target="_blank" rel="noopener noreferrer">IPFS Gateway Documentation</a></li>
<li><a href="https://specs.ipfs.tech/http-gateways/" target="_blank" rel="noopener noreferrer">IPFS HTTP Gateway Specifications</a></li>
</ul>

<div id="abuse-section">
<h2>Abuse Reports</h2>
<p>
This gateway is operated by a third party. To report abuse, contact the operator or owner of
<span id="gateway-host"></span>.
</p>
</div>
<script>
(function() {
var hostname = window.location.hostname;
var section = document.getElementById('abuse-section');
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {
section.style.display = 'none';
return;
}
var host = document.getElementById('gateway-host');
var link = document.createElement('a');
link.href = 'https://whois.domaintools.com/' + hostname;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.textContent = hostname;
host.appendChild(link);
})();
</script>
<noscript>
<p>To report abuse, look up the domain owner using a WHOIS service.</p>
</noscript>
</body>
</html>
25 changes: 19 additions & 6 deletions core/corehttp/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -50,9 +50,13 @@ func GatewayOption(paths ...string) ServeOption {
}
}

// HostnameOption returns a ServeOption that wraps the gateway with hostname-based
// routing (subdomain gateways, DNSLink). When Gateway.RootRedirect is not configured,
// requests to "/" that would return 404 (e.g., on known gateways like localhost)
// will show a landing page instead.
func HostnameOption() ServeOption {
return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
config, headers, err := getGatewayConfig(n)
cfg, headers, rootRedirect, err := getGatewayConfig(n)
if err != nil {
return nil, err
}
Expand All @@ -65,8 +69,16 @@ func HostnameOption() ServeOption {
childMux := http.NewServeMux()

var handler http.Handler
handler = gateway.NewHostnameHandler(config, backend, childMux)
handler = gateway.NewHostnameHandler(cfg, 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 == "" {
handler = withLandingPageFallback(handler, headers)
}

handler = otelhttp.NewHandler(handler, "HostnameGateway")

mux.Handle("/", handler)
Expand Down Expand Up @@ -259,10 +271,11 @@ var defaultKnownGateways = map[string]*gateway.PublicGateway{
"localhost": subdomainGatewaySpec,
}

func getGatewayConfig(n *core.IpfsNode) (gateway.Config, map[string][]string, error) {
// getGatewayConfig returns gateway configuration, HTTP headers, and root redirect URL.
func getGatewayConfig(n *core.IpfsNode) (gateway.Config, map[string][]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.
Expand Down Expand Up @@ -300,5 +313,5 @@ func getGatewayConfig(n *core.IpfsNode) (gateway.Config, map[string][]string, er
}
}

return gwCfg, cfg.Gateway.HTTPHeaders, nil
return gwCfg, cfg.Gateway.HTTPHeaders, cfg.Gateway.RootRedirect, nil
}
2 changes: 1 addition & 1 deletion core/corehttp/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
142 changes: 142 additions & 0 deletions core/corehttp/landing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package corehttp

import (
"bufio"
_ "embed"
"net"
"net/http"

core "github.com/ipfs/kubo/core"
)

//go:embed assets/landing.html
var landingPageHTML []byte

// LandingPageOption returns a ServeOption that serves a default landing page
// for the gateway root ("/") when Gateway.RootRedirect is not configured.
// 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
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
serveLandingPage(w, headers)
}))
return mux, nil
}
}

// 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().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write(landingPageHTML)
}

// withLandingPageFallback wraps an http.Handler to intercept 404 responses for
// the root path "/" on loopback addresses and serve a landing page instead.
//
// This is needed because boxo's HostnameHandler returns 404 for bare gateway
// hostnames (like "localhost") that don't have content configured. Without this
// fallback, users would see a confusing 404 instead of a helpful landing page.
//
// The middleware only intercepts requests to loopback addresses (127.0.0.1,
// localhost, ::1) because these cannot have DNSLink configured, so any 404 on
// "/" is guaranteed to be "no content configured" rather than "content not
// found". This avoids false positives where a real 404 (e.g., from DNSLink
// pointing to missing content) would incorrectly show the landing page.
func withLandingPageFallback(next http.Handler, headers map[string][]string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Only intercept requests to exactly "/"
if r.URL.Path != "/" {
next.ServeHTTP(w, r)
return
}

// Only intercept for loopback addresses. These cannot have DNSLink
// configured, so any 404 is genuinely "no content configured".
// For other hosts, pass through to avoid intercepting real 404s
// from DNSLink or other content resolution.
host := r.Host
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
}

// Wrap ResponseWriter to intercept 404 responses
lw := &landingResponseWriter{ResponseWriter: w}
next.ServeHTTP(lw, r)

// If 404 was suppressed, serve the landing page
if lw.suppressed404 {
serveLandingPage(w, headers)
}
})
}

// landingResponseWriter wraps http.ResponseWriter to intercept 404 responses.
// It suppresses the 404 status and body so we can serve a landing page instead.
type landingResponseWriter struct {
http.ResponseWriter
wroteHeader bool
suppressed404 bool
}

func (w *landingResponseWriter) WriteHeader(code int) {
if w.wroteHeader {
return
}
w.wroteHeader = true
if code == http.StatusNotFound {
w.suppressed404 = true
return // Suppress 404 - we'll serve landing page instead
}
w.ResponseWriter.WriteHeader(code)
}

func (w *landingResponseWriter) Write(b []byte) (int, error) {
if !w.wroteHeader {
w.WriteHeader(http.StatusOK)
}
if w.suppressed404 {
return len(b), nil // Discard 404 body
}
return w.ResponseWriter.Write(b)

Check warning

Code scanning / CodeQL

Information exposure through a stack trace Medium

HTTP response depends on
stack trace information
and may be exposed to an external user.

Copilot Autofix

AI 9 days ago

To fix the issue, we must ensure that stack trace information (the contents of buf in profile/goroutines.go) is not sent to the user via an HTTP response. When writing such diagnostics, the correct action is to log the stack trace on the server (for administrator/developer analysis) and, for the client, to send a generic error message instead.

Specifically:

  • In profile/goroutines.go, if WriteAllGoroutineStacks is used as a handler to write goroutine stacks to a user-facing HTTP response, it should instead:
    • Write a simple message to the response, such as "An unexpected error occurred".
    • Log the stack trace server-side using Go's log package (or an equivalent).
  • If there are places in core/corehttp/landing.go where stack trace information flows into an HTTP response (specifically via the Write method), this must be intercepted and only a generic error or status be sent.

As the data flow is traced from profile/goroutines.go:WriteAllGoroutineStacks, the fix is to log the stack trace and only send a generic message to the writer (ideally, the HTTP response writer).

Required changes:

  • In profile/goroutines.go, update WriteAllGoroutineStacks so that instead of writing buf to the io.Writer, it logs the stack trace server-side and writes a generic message to the writer.
  • Add an import for "log" if not present.

Suggested changeset 1
profile/goroutines.go
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/profile/goroutines.go b/profile/goroutines.go
--- a/profile/goroutines.go
+++ b/profile/goroutines.go
@@ -3,6 +3,7 @@
 import (
 	"io"
 	"runtime"
+	"log"
 )
 
 // WriteAllGoroutineStacks writes a stack trace to the given writer.
@@ -22,6 +23,9 @@
 		// }
 		buf = make([]byte, 2*len(buf))
 	}
-	_, err := w.Write(buf)
+	// Log stack trace on server for diagnostics
+	log.Printf("Goroutine stack trace:\n%s", string(buf))
+	// Write a generic message to the writer instead of stack trace
+	_, err := w.Write([]byte("An unexpected internal error occurred. Please contact support."))
 	return err
 }
EOF
@@ -3,6 +3,7 @@
import (
"io"
"runtime"
"log"
)

// WriteAllGoroutineStacks writes a stack trace to the given writer.
@@ -22,6 +23,9 @@
// }
buf = make([]byte, 2*len(buf))
}
_, err := w.Write(buf)
// Log stack trace on server for diagnostics
log.Printf("Goroutine stack trace:\n%s", string(buf))
// Write a generic message to the writer instead of stack trace
_, err := w.Write([]byte("An unexpected internal error occurred. Please contact support."))
return err
}
Copilot is powered by AI and may make mistakes. Always verify output.
}

// 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
}
2 changes: 1 addition & 1 deletion core/corehttp/routing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
9 changes: 8 additions & 1 deletion docs/changelogs/v0.40.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ This release was brought to you by the [Shipyard](https://ipshipyard.com/) team.
- [Overview](#overview)
- [🔦 Highlights](#-highlights)
- [Routing V1 HTTP API now exposed by default](#routing-v1-http-api-now-exposed-by-default)
- [Track total size when adding pins](#track-total-size-when-adding-pins]
- [Track total size when adding pins](#track-total-size-when-adding-pins)
- [Friendlier default landing page](#friendlier-default-landing-page)
- [📝 Changelog](#-changelog)
- [👨‍👩‍👧‍👦 Contributors](#-contributors)

Expand All @@ -32,6 +33,12 @@ Example output:
Fetched/Processed 336 nodes (83 MB)
```

#### Friendlier default landing page

Visiting the gateway root `/` now displays a landing page instead of returning a 404 error. The page confirms Kubo is running and provides links to documentation and configuration resources.

Gateway operators can customize this by setting [`Gateway.RootRedirect`](https://github.com/ipfs/kubo/blob/master/docs/config.md#gatewayrootredirect) to redirect visitors to their own documentation page.

### 📝 Changelog

### 👨‍👩‍👧‍👦 Contributors
6 changes: 5 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -1218,7 +1218,11 @@ Type: `object[string -> array[string]]`

A URL to redirect requests for `/` to.

Default: `""`
When not set, a default landing page is displayed instead. The landing page
indicates that the gateway software is working and provides links to
documentation and resources.

Default: `""` (landing page)

Type: `string` (url)

Expand Down
Loading
Loading