Skip to content

Commit

Permalink
Merge commits from ipfs/kubo/prepare-gateway-extraction
Browse files Browse the repository at this point in the history
  • Loading branch information
hacdias committed Jan 27, 2023
2 parents 38b7ae8 + 35c75a7 commit 8e32707
Show file tree
Hide file tree
Showing 41 changed files with 6,741 additions and 0 deletions.
193 changes: 193 additions & 0 deletions gateway/core/corehttp/commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package corehttp

import (
"errors"
"fmt"
"net"
"net/http"
"os"
"strconv"
"strings"

version "github.com/ipfs/kubo"
oldcmds "github.com/ipfs/kubo/commands"
"github.com/ipfs/kubo/core"
corecommands "github.com/ipfs/kubo/core/commands"

cmds "github.com/ipfs/go-ipfs-cmds"
cmdsHttp "github.com/ipfs/go-ipfs-cmds/http"
path "github.com/ipfs/go-path"
config "github.com/ipfs/kubo/config"
)

var (
errAPIVersionMismatch = errors.New("api version mismatch")
)

const originEnvKey = "API_ORIGIN"
const originEnvKeyDeprecate = `You are using the ` + originEnvKey + `ENV Variable.
This functionality is deprecated, and will be removed in future versions.
Instead, try either adding headers to the config, or passing them via
cli arguments:
ipfs config API.HTTPHeaders --json '{"Access-Control-Allow-Origin": ["*"]}'
ipfs daemon
`

// APIPath is the path at which the API is mounted.
const APIPath = "/api/v0"

var defaultLocalhostOrigins = []string{
"http://127.0.0.1:<port>",
"https://127.0.0.1:<port>",
"http://[::1]:<port>",
"https://[::1]:<port>",
"http://localhost:<port>",
"https://localhost:<port>",
}

var companionBrowserExtensionOrigins = []string{
"chrome-extension://nibjojkomfdiaoajekhjakgkdhaomnch", // ipfs-companion
"chrome-extension://hjoieblefckbooibpepigmacodalfndh", // ipfs-companion-beta
}

func addCORSFromEnv(c *cmdsHttp.ServerConfig) {
origin := os.Getenv(originEnvKey)
if origin != "" {
log.Warn(originEnvKeyDeprecate)
c.AppendAllowedOrigins(origin)
}
}

func addHeadersFromConfig(c *cmdsHttp.ServerConfig, nc *config.Config) {
log.Info("Using API.HTTPHeaders:", nc.API.HTTPHeaders)

if acao := nc.API.HTTPHeaders[cmdsHttp.ACAOrigin]; acao != nil {
c.SetAllowedOrigins(acao...)
}
if acam := nc.API.HTTPHeaders[cmdsHttp.ACAMethods]; acam != nil {
c.SetAllowedMethods(acam...)
}
for _, v := range nc.API.HTTPHeaders[cmdsHttp.ACACredentials] {
c.SetAllowCredentials(strings.ToLower(v) == "true")
}

c.Headers = make(map[string][]string, len(nc.API.HTTPHeaders)+1)

// Copy these because the config is shared and this function is called
// in multiple places concurrently. Updating these in-place *is* racy.
for h, v := range nc.API.HTTPHeaders {
h = http.CanonicalHeaderKey(h)
switch h {
case cmdsHttp.ACAOrigin, cmdsHttp.ACAMethods, cmdsHttp.ACACredentials:
// these are handled by the CORs library.
default:
c.Headers[h] = v
}
}
c.Headers["Server"] = []string{"kubo/" + version.CurrentVersionNumber}
}

func addCORSDefaults(c *cmdsHttp.ServerConfig) {
// always safelist certain origins
c.AppendAllowedOrigins(defaultLocalhostOrigins...)
c.AppendAllowedOrigins(companionBrowserExtensionOrigins...)

// by default, use GET, PUT, POST
if len(c.AllowedMethods()) == 0 {
c.SetAllowedMethods(http.MethodGet, http.MethodPost, http.MethodPut)
}
}

func patchCORSVars(c *cmdsHttp.ServerConfig, addr net.Addr) {

// we have to grab the port from an addr, which may be an ip6 addr.
// TODO: this should take multiaddrs and derive port from there.
port := ""
if tcpaddr, ok := addr.(*net.TCPAddr); ok {
port = strconv.Itoa(tcpaddr.Port)
} else if udpaddr, ok := addr.(*net.UDPAddr); ok {
port = strconv.Itoa(udpaddr.Port)
}

// we're listening on tcp/udp with ports. ("udp!?" you say? yeah... it happens...)
oldOrigins := c.AllowedOrigins()
newOrigins := make([]string, len(oldOrigins))
for i, o := range oldOrigins {
// TODO: allow replacing <host>. tricky, ip4 and ip6 and hostnames...
if port != "" {
o = strings.Replace(o, "<port>", port, -1)
}
newOrigins[i] = o
}
c.SetAllowedOrigins(newOrigins...)
}

func commandsOption(cctx oldcmds.Context, command *cmds.Command, allowGet bool) ServeOption {
return func(n *core.IpfsNode, l net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {

cfg := cmdsHttp.NewServerConfig()
cfg.AllowGet = allowGet
corsAllowedMethods := []string{http.MethodPost}
if allowGet {
corsAllowedMethods = append(corsAllowedMethods, http.MethodGet)
}

cfg.SetAllowedMethods(corsAllowedMethods...)
cfg.APIPath = APIPath
rcfg, err := n.Repo.Config()
if err != nil {
return nil, err
}

addHeadersFromConfig(cfg, rcfg)
addCORSFromEnv(cfg)
addCORSDefaults(cfg)
patchCORSVars(cfg, l.Addr())

cmdHandler := cmdsHttp.NewHandler(&cctx, command, cfg)
mux.Handle(APIPath+"/", cmdHandler)
return mux, nil
}
}

// CommandsOption constructs a ServerOption for hooking the commands into the
// HTTP server. It will NOT allow GET requests.
func CommandsOption(cctx oldcmds.Context) ServeOption {
return commandsOption(cctx, corecommands.Root, false)
}

// CommandsROOption constructs a ServerOption for hooking the read-only commands
// into the HTTP server. It will allow GET requests.
func CommandsROOption(cctx oldcmds.Context) ServeOption {
return commandsOption(cctx, corecommands.RootRO, true)
}

// CheckVersionOption returns a ServeOption that checks whether the client ipfs version matches. Does nothing when the user agent string does not contain `/kubo/` or `/go-ipfs/`
func CheckVersionOption() ServeOption {
daemonVersion := version.ApiVersion

return ServeOption(func(n *core.IpfsNode, l net.Listener, parent *http.ServeMux) (*http.ServeMux, error) {
mux := http.NewServeMux()
parent.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, APIPath) {
cmdqry := r.URL.Path[len(APIPath):]
pth := path.SplitList(cmdqry)

// backwards compatibility to previous version check
if len(pth) >= 2 && pth[1] != "version" {
clientVersion := r.UserAgent()
// skips check if client is not kubo (go-ipfs)
if (strings.Contains(clientVersion, "/go-ipfs/") || strings.Contains(clientVersion, "/kubo/")) && daemonVersion != clientVersion {
http.Error(w, fmt.Sprintf("%s (%s != %s)", errAPIVersionMismatch, daemonVersion, clientVersion), http.StatusBadRequest)
return
}
}
}

mux.ServeHTTP(w, r)
})

return mux, nil
})
}
141 changes: 141 additions & 0 deletions gateway/core/corehttp/corehttp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
Package corehttp provides utilities for the webui, gateways, and other
high-level HTTP interfaces to IPFS.
*/
package corehttp

import (
"context"
"fmt"
"net"
"net/http"
"time"

logging "github.com/ipfs/go-log"
core "github.com/ipfs/kubo/core"
"github.com/jbenet/goprocess"
periodicproc "github.com/jbenet/goprocess/periodic"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
)

var log = logging.Logger("core/server")

// shutdownTimeout is the timeout after which we'll stop waiting for hung
// commands to return on shutdown.
const shutdownTimeout = 30 * time.Second

// ServeOption registers any HTTP handlers it provides on the given mux.
// It returns the mux to expose to future options, which may be a new mux if it
// is interested in mediating requests to future options, or the same mux
// initially passed in if not.
type ServeOption func(*core.IpfsNode, net.Listener, *http.ServeMux) (*http.ServeMux, error)

// makeHandler turns a list of ServeOptions into a http.Handler that implements
// all of the given options, in order.
func makeHandler(n *core.IpfsNode, l net.Listener, options ...ServeOption) (http.Handler, error) {
topMux := http.NewServeMux()
mux := topMux
for _, option := range options {
var err error
mux, err = option(n, l, mux)
if err != nil {
return nil, err
}
}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// ServeMux does not support requests with CONNECT method,
// so we need to handle them separately
// https://golang.org/src/net/http/request.go#L111
if r.Method == http.MethodConnect {
w.WriteHeader(http.StatusOK)
return
}
topMux.ServeHTTP(w, r)
})
return handler, nil
}

// ListenAndServe runs an HTTP server listening at |listeningMultiAddr| with
// the given serve options. The address must be provided in multiaddr format.
//
// TODO intelligently parse address strings in other formats so long as they
// unambiguously map to a valid multiaddr. e.g. for convenience, ":8080" should
// map to "/ip4/0.0.0.0/tcp/8080".
func ListenAndServe(n *core.IpfsNode, listeningMultiAddr string, options ...ServeOption) error {
addr, err := ma.NewMultiaddr(listeningMultiAddr)
if err != nil {
return err
}

list, err := manet.Listen(addr)
if err != nil {
return err
}

// we might have listened to /tcp/0 - let's see what we are listing on
addr = list.Multiaddr()
fmt.Printf("API server listening on %s\n", addr)

return Serve(n, manet.NetListener(list), options...)
}

// Serve accepts incoming HTTP connections on the listener and pass them
// to ServeOption handlers.
func Serve(node *core.IpfsNode, lis net.Listener, options ...ServeOption) error {
// make sure we close this no matter what.
defer lis.Close()

handler, err := makeHandler(node, lis, options...)
if err != nil {
return err
}

addr, err := manet.FromNetAddr(lis.Addr())
if err != nil {
return err
}

select {
case <-node.Process.Closing():
return fmt.Errorf("failed to start server, process closing")
default:
}

server := &http.Server{
Handler: handler,
}

var serverError error
serverProc := node.Process.Go(func(p goprocess.Process) {
serverError = server.Serve(lis)
})

// wait for server to exit.
select {
case <-serverProc.Closed():
// if node being closed before server exits, close server
case <-node.Process.Closing():
log.Infof("server at %s terminating...", addr)

warnProc := periodicproc.Tick(5*time.Second, func(_ goprocess.Process) {
log.Infof("waiting for server at %s to terminate...", addr)
})

// This timeout shouldn't be necessary if all of our commands
// are obeying their contexts but we should have *some* timeout.
ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
err := server.Shutdown(ctx)

// Should have already closed but we still need to wait for it
// to set the error.
<-serverProc.Closed()
serverError = err

warnProc.Close()
}

log.Infof("server at %s terminated", addr)
return serverError
}
Loading

0 comments on commit 8e32707

Please sign in to comment.