mirror of
https://github.com/ipfs/kubo.git
synced 2026-02-21 18:37:45 +08:00
Some checks are pending
CodeQL / codeql (push) Waiting to run
Docker Check / lint (push) Waiting to run
Docker Check / build (push) Waiting to run
Gateway Conformance / gateway-conformance (push) Waiting to run
Gateway Conformance / gateway-conformance-libp2p-experiment (push) Waiting to run
Go Build / go-build (push) Waiting to run
Go Check / go-check (push) Waiting to run
Go Lint / go-lint (push) Waiting to run
Go Test / go-test (push) Waiting to run
Interop / interop-prep (push) Waiting to run
Interop / helia-interop (push) Blocked by required conditions
Interop / ipfs-webui (push) Blocked by required conditions
Sharness / sharness-test (push) Waiting to run
Spell Check / spellcheck (push) Waiting to run
adds Gateway.MaxRangeRequestFileSize configuration to protect against CDN bugs where range requests over certain sizes return entire files instead of requested byte ranges, causing unexpected bandwidth costs. - default: 0 (no limit) - returns 501 Not Implemented for oversized range requests - protects against CDNs like Cloudflare that ignore range requests over 5GiB also introduces OptionalBytes type to reduce code duplication when handling byte-size configuration values, replacing manual string parsing with humanize.ParseBytes. migrates existing byte-size configs to use this new type. Fixes: https://github.com/ipfs/boxo/issues/856
305 lines
9.9 KiB
Go
305 lines
9.9 KiB
Go
package corehttp
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/ipfs/boxo/blockservice"
|
|
"github.com/ipfs/boxo/exchange/offline"
|
|
"github.com/ipfs/boxo/files"
|
|
"github.com/ipfs/boxo/gateway"
|
|
"github.com/ipfs/boxo/namesys"
|
|
"github.com/ipfs/boxo/path"
|
|
offlineroute "github.com/ipfs/boxo/routing/offline"
|
|
"github.com/ipfs/go-cid"
|
|
version "github.com/ipfs/kubo"
|
|
"github.com/ipfs/kubo/config"
|
|
"github.com/ipfs/kubo/core"
|
|
iface "github.com/ipfs/kubo/core/coreiface"
|
|
"github.com/ipfs/kubo/core/node"
|
|
"github.com/libp2p/go-libp2p/core/routing"
|
|
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
|
)
|
|
|
|
func GatewayOption(paths ...string) ServeOption {
|
|
return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
|
|
config, headers, err := getGatewayConfig(n)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
backend, err := newGatewayBackend(n)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
handler := gateway.NewHandler(config, backend)
|
|
handler = gateway.NewHeaders(headers).ApplyCors().Wrap(handler)
|
|
handler = otelhttp.NewHandler(handler, "Gateway")
|
|
|
|
for _, p := range paths {
|
|
mux.Handle(p+"/", handler)
|
|
}
|
|
|
|
return mux, nil
|
|
}
|
|
}
|
|
|
|
func HostnameOption() ServeOption {
|
|
return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
|
|
config, headers, err := getGatewayConfig(n)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
backend, err := newGatewayBackend(n)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
childMux := http.NewServeMux()
|
|
|
|
var handler http.Handler
|
|
handler = gateway.NewHostnameHandler(config, backend, childMux)
|
|
handler = gateway.NewHeaders(headers).ApplyCors().Wrap(handler)
|
|
handler = otelhttp.NewHandler(handler, "HostnameGateway")
|
|
|
|
mux.Handle("/", handler)
|
|
return childMux, nil
|
|
}
|
|
}
|
|
|
|
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) {
|
|
fmt.Fprintf(w, "Commit: %s\n", version.CurrentCommit)
|
|
fmt.Fprintf(w, "Client Version: %s\n", version.GetUserAgentVersion())
|
|
})
|
|
return mux, nil
|
|
}
|
|
}
|
|
|
|
func Libp2pGatewayOption() ServeOption {
|
|
return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
|
|
bserv := blockservice.New(n.Blocks.Blockstore(), offline.Exchange(n.Blocks.Blockstore()))
|
|
|
|
backend, err := gateway.NewBlocksBackend(bserv,
|
|
// GatewayOverLibp2p only returns things that are in local blockstore
|
|
// (same as Gateway.NoFetch=true), we have to pass offline path resolver
|
|
gateway.WithResolver(n.OfflineUnixFSPathResolver),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get gateway configuration from the node's config
|
|
cfg, err := n.Repo.Config()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
gwConfig := gateway.Config{
|
|
// Keep these constraints for security
|
|
DeserializedResponses: false, // Trustless-only
|
|
NoDNSLink: true, // No DNS resolution
|
|
DisableHTMLErrors: true, // Plain text errors only
|
|
PublicGateways: nil,
|
|
Menu: nil,
|
|
// Apply timeout and concurrency limits from user config
|
|
RetrievalTimeout: cfg.Gateway.RetrievalTimeout.WithDefault(config.DefaultRetrievalTimeout),
|
|
MaxConcurrentRequests: int(cfg.Gateway.MaxConcurrentRequests.WithDefault(int64(config.DefaultMaxConcurrentRequests))),
|
|
MaxRangeRequestFileSize: int64(cfg.Gateway.MaxRangeRequestFileSize.WithDefault(uint64(config.DefaultMaxRangeRequestFileSize))),
|
|
DiagnosticServiceURL: "", // Not used since DisableHTMLErrors=true
|
|
}
|
|
|
|
handler := gateway.NewHandler(gwConfig, &offlineGatewayErrWrapper{gwimpl: backend})
|
|
handler = otelhttp.NewHandler(handler, "Libp2p-Gateway")
|
|
|
|
mux.Handle("/ipfs/", handler)
|
|
|
|
return mux, nil
|
|
}
|
|
}
|
|
|
|
func newGatewayBackend(n *core.IpfsNode) (gateway.IPFSBackend, error) {
|
|
cfg, err := n.Repo.Config()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bserv := n.Blocks
|
|
var vsRouting routing.ValueStore = n.Routing
|
|
nsys := n.Namesys
|
|
pathResolver := n.UnixFSPathResolver
|
|
|
|
if cfg.Gateway.NoFetch {
|
|
bserv = blockservice.New(bserv.Blockstore(), offline.Exchange(bserv.Blockstore()))
|
|
|
|
cs := cfg.Ipns.ResolveCacheSize
|
|
if cs == 0 {
|
|
cs = node.DefaultIpnsCacheSize
|
|
}
|
|
if cs < 0 {
|
|
return nil, fmt.Errorf("cannot specify negative resolve cache size")
|
|
}
|
|
|
|
nsOptions := []namesys.Option{
|
|
namesys.WithDatastore(n.Repo.Datastore()),
|
|
namesys.WithDNSResolver(n.DNSResolver),
|
|
namesys.WithCache(cs),
|
|
namesys.WithMaxCacheTTL(cfg.Ipns.MaxCacheTTL.WithDefault(config.DefaultIpnsMaxCacheTTL)),
|
|
}
|
|
|
|
vsRouting = offlineroute.NewOfflineRouter(n.Repo.Datastore(), n.RecordValidator)
|
|
nsys, err = namesys.NewNameSystem(vsRouting, nsOptions...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error constructing namesys: %w", err)
|
|
}
|
|
|
|
// Gateway.NoFetch=true requires offline path resolver
|
|
// to avoid fetching missing blocks during path traversal
|
|
pathResolver = n.OfflineUnixFSPathResolver
|
|
}
|
|
|
|
backend, err := gateway.NewBlocksBackend(bserv,
|
|
gateway.WithValueStore(vsRouting),
|
|
gateway.WithNameSystem(nsys),
|
|
gateway.WithResolver(pathResolver),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &offlineGatewayErrWrapper{gwimpl: backend}, nil
|
|
}
|
|
|
|
type offlineGatewayErrWrapper struct {
|
|
gwimpl gateway.IPFSBackend
|
|
}
|
|
|
|
func offlineErrWrap(err error) error {
|
|
if errors.Is(err, iface.ErrOffline) {
|
|
return fmt.Errorf("%s : %w", err.Error(), gateway.ErrServiceUnavailable)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (o *offlineGatewayErrWrapper) Get(ctx context.Context, path path.ImmutablePath, ranges ...gateway.ByteRange) (gateway.ContentPathMetadata, *gateway.GetResponse, error) {
|
|
md, n, err := o.gwimpl.Get(ctx, path, ranges...)
|
|
err = offlineErrWrap(err)
|
|
return md, n, err
|
|
}
|
|
|
|
func (o *offlineGatewayErrWrapper) GetAll(ctx context.Context, path path.ImmutablePath) (gateway.ContentPathMetadata, files.Node, error) {
|
|
md, n, err := o.gwimpl.GetAll(ctx, path)
|
|
err = offlineErrWrap(err)
|
|
return md, n, err
|
|
}
|
|
|
|
func (o *offlineGatewayErrWrapper) GetBlock(ctx context.Context, path path.ImmutablePath) (gateway.ContentPathMetadata, files.File, error) {
|
|
md, n, err := o.gwimpl.GetBlock(ctx, path)
|
|
err = offlineErrWrap(err)
|
|
return md, n, err
|
|
}
|
|
|
|
func (o *offlineGatewayErrWrapper) Head(ctx context.Context, path path.ImmutablePath) (gateway.ContentPathMetadata, *gateway.HeadResponse, error) {
|
|
md, n, err := o.gwimpl.Head(ctx, path)
|
|
err = offlineErrWrap(err)
|
|
return md, n, err
|
|
}
|
|
|
|
func (o *offlineGatewayErrWrapper) ResolvePath(ctx context.Context, path path.ImmutablePath) (gateway.ContentPathMetadata, error) {
|
|
md, err := o.gwimpl.ResolvePath(ctx, path)
|
|
err = offlineErrWrap(err)
|
|
return md, err
|
|
}
|
|
|
|
func (o *offlineGatewayErrWrapper) GetCAR(ctx context.Context, path path.ImmutablePath, params gateway.CarParams) (gateway.ContentPathMetadata, io.ReadCloser, error) {
|
|
md, data, err := o.gwimpl.GetCAR(ctx, path, params)
|
|
err = offlineErrWrap(err)
|
|
return md, data, err
|
|
}
|
|
|
|
func (o *offlineGatewayErrWrapper) IsCached(ctx context.Context, path path.Path) bool {
|
|
return o.gwimpl.IsCached(ctx, path)
|
|
}
|
|
|
|
func (o *offlineGatewayErrWrapper) GetIPNSRecord(ctx context.Context, c cid.Cid) ([]byte, error) {
|
|
rec, err := o.gwimpl.GetIPNSRecord(ctx, c)
|
|
err = offlineErrWrap(err)
|
|
return rec, err
|
|
}
|
|
|
|
func (o *offlineGatewayErrWrapper) ResolveMutable(ctx context.Context, path path.Path) (path.ImmutablePath, time.Duration, time.Time, error) {
|
|
imPath, ttl, lastMod, err := o.gwimpl.ResolveMutable(ctx, path)
|
|
err = offlineErrWrap(err)
|
|
return imPath, ttl, lastMod, err
|
|
}
|
|
|
|
func (o *offlineGatewayErrWrapper) GetDNSLinkRecord(ctx context.Context, s string) (path.Path, error) {
|
|
p, err := o.gwimpl.GetDNSLinkRecord(ctx, s)
|
|
err = offlineErrWrap(err)
|
|
return p, err
|
|
}
|
|
|
|
var _ gateway.IPFSBackend = (*offlineGatewayErrWrapper)(nil)
|
|
|
|
var defaultPaths = []string{"/ipfs/", "/ipns/", "/p2p/"}
|
|
|
|
var subdomainGatewaySpec = &gateway.PublicGateway{
|
|
Paths: defaultPaths,
|
|
UseSubdomains: true,
|
|
}
|
|
|
|
var defaultKnownGateways = map[string]*gateway.PublicGateway{
|
|
"localhost": subdomainGatewaySpec,
|
|
}
|
|
|
|
func getGatewayConfig(n *core.IpfsNode) (gateway.Config, map[string][]string, error) {
|
|
cfg, err := n.Repo.Config()
|
|
if err != nil {
|
|
return gateway.Config{}, nil, err
|
|
}
|
|
|
|
// Initialize gateway configuration, with empty PublicGateways, handled after.
|
|
gwCfg := gateway.Config{
|
|
DeserializedResponses: cfg.Gateway.DeserializedResponses.WithDefault(config.DefaultDeserializedResponses),
|
|
DisableHTMLErrors: cfg.Gateway.DisableHTMLErrors.WithDefault(config.DefaultDisableHTMLErrors),
|
|
NoDNSLink: cfg.Gateway.NoDNSLink,
|
|
PublicGateways: map[string]*gateway.PublicGateway{},
|
|
RetrievalTimeout: cfg.Gateway.RetrievalTimeout.WithDefault(config.DefaultRetrievalTimeout),
|
|
MaxConcurrentRequests: int(cfg.Gateway.MaxConcurrentRequests.WithDefault(int64(config.DefaultMaxConcurrentRequests))),
|
|
MaxRangeRequestFileSize: int64(cfg.Gateway.MaxRangeRequestFileSize.WithDefault(uint64(config.DefaultMaxRangeRequestFileSize))),
|
|
DiagnosticServiceURL: cfg.Gateway.DiagnosticServiceURL.WithDefault(config.DefaultDiagnosticServiceURL),
|
|
}
|
|
|
|
// Add default implicit known gateways, such as subdomain gateway on localhost.
|
|
for hostname, gw := range defaultKnownGateways {
|
|
gwCfg.PublicGateways[hostname] = gw
|
|
}
|
|
|
|
// Apply values from cfg.Gateway.PublicGateways if they exist.
|
|
for hostname, gw := range cfg.Gateway.PublicGateways {
|
|
if gw == nil {
|
|
// Remove any implicit defaults, if present. This is useful when one
|
|
// wants to disable subdomain gateway on localhost, etc.
|
|
delete(gwCfg.PublicGateways, hostname)
|
|
continue
|
|
}
|
|
|
|
gwCfg.PublicGateways[hostname] = &gateway.PublicGateway{
|
|
Paths: gw.Paths,
|
|
NoDNSLink: gw.NoDNSLink,
|
|
UseSubdomains: gw.UseSubdomains,
|
|
InlineDNSLink: gw.InlineDNSLink.WithDefault(config.DefaultInlineDNSLink),
|
|
DeserializedResponses: gw.DeserializedResponses.WithDefault(gwCfg.DeserializedResponses),
|
|
}
|
|
}
|
|
|
|
return gwCfg, cfg.Gateway.HTTPHeaders, nil
|
|
}
|