Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gateway: make API commands configurable #5565

Closed
wants to merge 3 commits into from
Closed
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: 1 addition & 1 deletion cmd/ipfs/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,7 @@ func serveHTTPGateway(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, e
corehttp.GatewayOption(writable, "/ipfs", "/ipns"),
corehttp.VersionOption(),
corehttp.CheckVersionOption(),
corehttp.CommandsROOption(*cctx),
corehttp.GatewayCommandsOption(*cctx, cfg.Gateway.APICommands),
}

if len(cfg.Gateway.RootRedirect) > 0 {
Expand Down
48 changes: 24 additions & 24 deletions core/commands/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,31 @@ func collectPaths(prefix string, cmd *cmds.Command, out map[string]struct{}) {
}

func TestROCommands(t *testing.T) {
list := []string{
"/block",
"/block/get",
"/block/stat",
"/cat",
"/commands",
"/dag",
"/dag/get",
"/dag/resolve",
"/dns",
"/get",
"/ls",
"/name",
"/name/resolve",
"/object",
"/object/data",
"/object/get",
"/object/links",
"/object/stat",
"/refs",
"/resolve",
"/version",
whitelist := []string{
"ls",
"cat",
"dag", // only included for the check afterwards
"dag/resolve",
"dag/get",
}

list := func() []string {
out := make([]string, len(whitelist))

for i, line := range whitelist {
out[i] = "/" + line
}

return out
}()

root, err := RootSubset(whitelist)
if err != nil {
t.Errorf("RootSubset error: %s", err)
}

cmdSet := make(map[string]struct{})
collectPaths("", RootRO, cmdSet)
collectPaths("", root, cmdSet)

for _, path := range list {
if _, ok := cmdSet[path]; !ok {
Expand All @@ -58,14 +57,15 @@ func TestROCommands(t *testing.T) {
for _, path := range list {
path = path[1:] // remove leading slash
split := strings.Split(path, "/")
sub, err := RootRO.Get(split)
sub, err := root.Get(split)
if err != nil {
t.Errorf("error getting subcommand %q: %v", path, err)
} else if sub == nil {
t.Errorf("subcommand %q is nil even though there was no error", path)
}
}
}

func TestCommands(t *testing.T) {
list := []string{
"/add",
Expand Down
91 changes: 37 additions & 54 deletions core/commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package commands

import (
"errors"
"fmt"
"io"
"strings"

Expand Down Expand Up @@ -147,64 +148,46 @@ var rootSubcommands = map[string]*cmds.Command{
"cid": CidCmd,
}

// RootRO is the readonly version of Root
var RootRO = &cmds.Command{}

var CommandsDaemonROCmd = CommandsCmd(RootRO)

var RefsROCmd = &oldcmds.Command{}

var rootROSubcommands = map[string]*cmds.Command{
"commands": CommandsDaemonROCmd,
"cat": CatCmd,
"block": &cmds.Command{
Subcommands: map[string]*cmds.Command{
"stat": blockStatCmd,
"get": blockGetCmd,
},
},
"get": GetCmd,
"dns": lgc.NewCommand(DNSCmd),
"ls": lgc.NewCommand(LsCmd),
"name": &cmds.Command{
Subcommands: map[string]*cmds.Command{
"resolve": name.IpnsCmd,
},
},
"object": lgc.NewCommand(&oldcmds.Command{
Subcommands: map[string]*oldcmds.Command{
"data": ocmd.ObjectDataCmd,
"links": ocmd.ObjectLinksCmd,
"get": ocmd.ObjectGetCmd,
"stat": ocmd.ObjectStatCmd,
},
}),
"dag": lgc.NewCommand(&oldcmds.Command{
Subcommands: map[string]*oldcmds.Command{
"get": dag.DagGetCmd,
"resolve": dag.DagResolveCmd,
},
}),
"resolve": ResolveCmd,
"version": lgc.NewCommand(VersionCmd),
}

func init() {
Root.ProcessHelp()
*RootRO = *Root

// sanitize readonly refs command
*RefsROCmd = *RefsCmd
RefsROCmd.Subcommands = map[string]*oldcmds.Command{}

// this was in the big map definition above before,
// but if we leave it there lgc.NewCommand will be executed
// before the value is updated (:/sanitize readonly refs command/)
rootROSubcommands["refs"] = lgc.NewCommand(RefsROCmd)

Root.Subcommands = rootSubcommands
}

RootRO.Subcommands = rootROSubcommands
func RootSubset(allowed []string) (*cmds.Command, error) {
subset := new(cmds.Command)
*subset = *Root
subset.Subcommands = map[string]*cmds.Command{}

commands := false
for _, path := range allowed {
if path == "commands" {
commands = true
continue
}

pathelems := strings.Split(path, "/")
in := Root
out := subset
for _, elem := range pathelems {
nextIn, ok := in.Subcommands[elem]
if !ok {
return nil, fmt.Errorf("unknown command: %s", path)
}

nextOut := new(cmds.Command)
*nextOut = *nextIn
nextOut.Subcommands = map[string]*cmds.Command{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, so you unconditionally create a new subcommand mal. That confuses me. Because if you have e.g. both dag/get and dag/resolve in allowed, you would overwrite dag and delete dag/get when adding dag/resolve.
Checking the tests now to see why this passes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the tests and now they fail. Could you fix this?


out.Subcommands[elem] = nextOut
out = nextOut
in = nextIn
}
}

if commands {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't you do this inside the loop (at line 64, where you currently set commands to true)?

subset.Subcommands["commands"] = CommandsCmd(subset)
}
return subset, nil
}

type MessageOutput struct {
Expand Down
1 change: 0 additions & 1 deletion core/commands/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,4 @@ func TestCommandTree(t *testing.T) {
}
}
printErrors(Root.DebugValidate())
printErrors(RootRO.DebugValidate())
}
33 changes: 31 additions & 2 deletions core/corehttp/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,26 @@ cli arguments:
// APIPath is the path at which the API is mounted.
const APIPath = "/api/v0"

var GatewayAPICommands = []string{
"commands",
"cat",
"get",
"ls",
"resolve",
"name/resolve",
"dag/get",
"dag/resolve",
"object/data",
"object/links",
"object/get",
"object/stat",
"refs",
"block/stat",
"block/get",
"dns",
"version",
}

var defaultLocalhostOrigins = []string{
"http://127.0.0.1:<port>",
"https://127.0.0.1:<port>",
Expand Down Expand Up @@ -143,8 +163,17 @@ func CommandsOption(cctx oldcmds.Context) ServeOption {

// CommandsROOption constructs a ServerOption for hooking the read-only commands
// into the HTTP server.
func CommandsROOption(cctx oldcmds.Context) ServeOption {
return commandsOption(cctx, corecommands.RootRO)
func GatewayCommandsOption(cctx oldcmds.Context, allowed []string) ServeOption {
if len(allowed) == 0 {
allowed = append(allowed, GatewayAPICommands...)
}
root, err := corecommands.RootSubset(allowed)
if err != nil {
return func(n *core.IpfsNode, l net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
return nil, err
}
}
return commandsOption(cctx, root)
}

// CheckVersionOption returns a ServeOption that checks whether the client ipfs version matches. Does nothing when the user agent string does not contain `/go-ipfs/`
Expand Down
22 changes: 22 additions & 0 deletions core/corehttp/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

version "github.com/ipfs/go-ipfs"
core "github.com/ipfs/go-ipfs/core"
commands "github.com/ipfs/go-ipfs/core/commands"
coreunix "github.com/ipfs/go-ipfs/core/coreunix"
namesys "github.com/ipfs/go-ipfs/namesys"
nsopts "github.com/ipfs/go-ipfs/namesys/opts"
Expand Down Expand Up @@ -597,3 +598,24 @@ func TestVersion(t *testing.T) {
t.Fatalf("response doesn't contain protocol version:\n%s", s)
}
}

func TestCommands(t *testing.T) {
printErrors := func(errs map[string][]error) {
if errs == nil {
return
}
t.Error("In Root command tree:")
for cmd, err := range errs {
t.Errorf(" In X command %s:", cmd)
for _, e := range err {
t.Errorf(" %s", e)
}
}
}

gwroot, err := commands.RootSubset(GatewayAPICommands)
if err != nil {
t.Errorf("RootSubset error: %s", err)
}
printErrors(gwroot.DebugValidate())
}
93 changes: 0 additions & 93 deletions test/sharness/t0110-gateway.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ test_init_ipfs
test_launch_ipfs_daemon

port=$GWAY_PORT
apiport=$API_PORT

# TODO check both 5001 and 5002.
# 5001 should have a readable gateway (part of the API)
Expand Down Expand Up @@ -80,47 +79,6 @@ test_expect_success "GET invalid path errors" '
test_must_fail curl -sf "http://127.0.0.1:$port/12345"
'

test_expect_success "GET /webui returns code expected" '
test_curl_resp_http_code "http://127.0.0.1:$apiport/webui" "HTTP/1.1 302 Found" "HTTP/1.1 301 Moved Permanently"
'

test_expect_success "GET /webui/ returns code expected" '
test_curl_resp_http_code "http://127.0.0.1:$apiport/webui/" "HTTP/1.1 302 Found" "HTTP/1.1 301 Moved Permanently"
'

test_expect_success "GET /logs returns logs" '
test_expect_code 28 curl http://127.0.0.1:$apiport/logs -m1 > log_out
'

test_expect_success "log output looks good" '
grep "log API client connected" log_out
'

test_expect_success "GET /api/v0/version succeeds" '
curl -v "http://127.0.0.1:$apiport/api/v0/version" 2> version_out
'

test_expect_success "output only has one transfer encoding header" '
grep "Transfer-Encoding: chunked" version_out | wc -l | xargs echo > tecount_out &&
echo "1" > tecount_exp &&
test_cmp tecount_out tecount_exp
'

curl_pprofmutex() {
curl -f -X POST "http://127.0.0.1:$apiport/debug/pprof-mutex/?fraction=$1"
}

test_expect_success "set mutex fraction for pprof (negative so it doesn't enable)" '
curl_pprofmutex -1
'

test_expect_success "test failure conditions of mutex pprof endpoint" '
test_must_fail curl_pprofmutex &&
test_must_fail curl_pprofmutex that_is_string &&
test_must_fail curl -f -X GET "http://127.0.0.1:$apiport/debug/pprof-mutex/?fraction=-1"
'


test_expect_success "setup index hash" '
mkdir index &&
echo "<p></p>" > index/index.html &&
Expand All @@ -141,57 +99,6 @@ test_expect_success "HEAD 'index.html' has no content" '
[ ! -s output ]
'

# test ipfs readonly api

test_curl_gateway_api() {
curl -sfo actual "http://127.0.0.1:$port/api/v0/$1"
}

test_expect_success "get IPFS directory file through readonly API succeeds" '
test_curl_gateway_api "cat?arg=$HASH2/test"
'

test_expect_success "get IPFS directory file through readonly API output looks good" '
test_cmp dir/test actual
'

test_expect_success "refs IPFS directory file through readonly API succeeds" '
test_curl_gateway_api "refs?arg=$HASH2/test"
'

for cmd in add \
block/put \
bootstrap \
config \
dht \
diag \
id \
mount \
name/publish \
object/put \
object/new \
object/patch \
pin \
ping \
repo \
stats \
swarm \
file \
update \
bitswap
do
test_expect_success "test gateway api is sanitized: $cmd" '
test_curl_resp_http_code "http://127.0.0.1:$port/api/v0/$cmd" "HTTP/1.1 404 Not Found"
'
done

# This one is different. `local` will be interpreted as a path if the command isn't defined.
test_expect_success "test gateway api is sanitized: refs/local" '
echo "Error: invalid '"'ipfs ref'"' path" > refs_local_expected &&
! ipfs --api /ip4/127.0.0.1/tcp/$port refs local > refs_local_actual 2>&1 &&
test_cmp refs_local_expected refs_local_actual
'

test_expect_success "create raw-leaves node" '
echo "This is RAW!" > rfile &&
echo "This is RAW!" | ipfs add --raw-leaves -q > rhash
Expand Down
Loading