Skip to content

Commit

Permalink
test: /api in subdomains
Browse files Browse the repository at this point in the history
License: MIT
Signed-off-by: Marcin Rataj <[email protected]>
  • Loading branch information
lidel committed Mar 3, 2020
1 parent 2ebe0a8 commit defb1fd
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 26 deletions.
54 changes: 37 additions & 17 deletions core/corehttp/hostname.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ import (
)

var pathGatewaySpec = config.GatewaySpec{
Paths: []string{ipfsPathPrefix, ipnsPathPrefix, "/api", "/p2p", "/version"},
Paths: []string{"/ipfs/", "/ipns/", "/api/", "/p2p/", "/version"},
UseSubdomains: false,
}

var subdomainGatewaySpec = config.GatewaySpec{
Paths: []string{ipfsPathPrefix, ipnsPathPrefix},
Paths: []string{"/ipfs/", "/ipns/", "/api/", "/p2p/"},
UseSubdomains: true,
}

Expand All @@ -38,7 +38,7 @@ var defaultKnownGateways = map[string]config.GatewaySpec{

// Find content identifier, protocol, and remaining hostname (host+optional port)
// of a subdomain gateway (eg. *.ipfs.foo.bar.co.uk)
var subdomainGatewayRegex = regexp.MustCompile("^(.+).(ipfs|ipns|ipld|p2p).([^/?#&]+)$")
var subdomainGatewayRegex = regexp.MustCompile("^(.+).(ipfs|ipns|ipld|p2p|api).([^/?#&]+)$")

// HostnameOption rewrites an incoming request based on the Host header.
func HostnameOption() ServeOption {
Expand Down Expand Up @@ -112,7 +112,7 @@ func HostnameOption() ServeOption {
if gw.UseSubdomains {
// Yes, redirect if applicable (pretty much everything except `/api`).
// Example: dweb.link/ipfs/{cid} → {cid}.ipfs.dweb.link
if newURL, ok := toSubdomainURL(r.Host, r.URL.Path); ok {
if newURL, ok := toSubdomainURL(r.Host, r.URL.Path, r.URL.RawQuery); ok {
http.Redirect(w, r, newURL, http.StatusMovedPermanently)
return
}
Expand All @@ -130,24 +130,24 @@ func HostnameOption() ServeOption {

// HTTP Host check: is this one of our subdomain-based "known gateways"?
// Example: {cid}.ipfs.localhost, {cid}.ipfs.dweb.link
if hostname, ns, rootId, ok := parseSubdomains(r.Host); ok {
if hostname, ns, rootID, ok := parseSubdomains(r.Host); ok {
// Looks like we're using subdomains.

// Again, is this a known gateway that supports subdomains?
if gw, ok := isKnownGateway(hostname); ok {

// Assemble original path prefix.
pathPrefix := "/" + ns + "/" + rootId
pathPrefix := "/" + ns + "/" + rootID

// Does this gateway _handle_ this path?
if gw.UseSubdomains && hasPrefix(pathPrefix, gw.Paths...) {

// Do we need to fix multicodec in CID?
if ns == "ipns" {
keyCid, err := cid.Decode(rootId)
keyCid, err := cid.Decode(rootID)
if err == nil && keyCid.Type() != cid.Libp2pKey {

if newURL, ok := toSubdomainURL(hostname, pathPrefix+r.URL.Path); ok {
if newURL, ok := toSubdomainURL(hostname, pathPrefix+r.URL.Path, r.URL.RawQuery); ok {
// Redirect to CID fixed inside of toSubdomainURL()
http.Redirect(w, r, newURL, http.StatusMovedPermanently)
return
Expand Down Expand Up @@ -198,7 +198,7 @@ func HostnameOption() ServeOption {

func isSubdomainNamespace(ns string) bool {
switch ns {
case "ipfs", "ipns", "p2p", "ipld":
case "ipfs", "ipns", "p2p", "ipld", "api":
return true
default:
return false
Expand All @@ -207,7 +207,7 @@ func isSubdomainNamespace(ns string) bool {

// Parses Host header of a subdomain-based URL and returns it's components
// Note: hostname is host + optional port
func parseSubdomains(hostHeader string) (hostname, ns, rootId string, ok bool) {
func parseSubdomains(hostHeader string) (hostname, ns, rootID string, ok bool) {
parts := subdomainGatewayRegex.FindStringSubmatch(hostHeader)
if len(parts) < 4 || !isSubdomainNamespace(parts[2]) {
return "", "", "", false
Expand All @@ -216,17 +216,17 @@ func parseSubdomains(hostHeader string) (hostname, ns, rootId string, ok bool) {
}

// Converts a hostname/path to a subdomain-based URL, if applicable.
func toSubdomainURL(hostname, path string) (url string, ok bool) {
func toSubdomainURL(hostname, path string, query string) (url string, ok bool) {
parts := strings.SplitN(path, "/", 4)

var ns, rootId, rest string
var ns, rootID, rest string
switch len(parts) {
case 4:
rest = parts[3]
fallthrough
case 3:
ns = parts[1]
rootId = parts[2]
rootID = parts[2]
default:
return "", false
}
Expand All @@ -235,7 +235,26 @@ func toSubdomainURL(hostname, path string) (url string, ok bool) {
return "", false
}

if rootCid, err := cid.Decode(rootId); err == nil {
// add prefix if query is present
if query != "" {
query = "?" + query
}

if ns == "api" || ns == "p2p" {
// API and P2P proxy use the same paths on subdomains:
// api.hostname/api/.. and p2p.hostname/p2p/..
return fmt.Sprintf(
"http://%s.%s/%s/%s/%s%s",
ns,
hostname,
ns,
rootID,
rest,
query,
), true
}

if rootCid, err := cid.Decode(rootID); err == nil {
multicodec := rootCid.Type()

// CIDs in IPNS are expected to have libp2p-key multicodec.
Expand All @@ -248,15 +267,16 @@ func toSubdomainURL(hostname, path string) (url string, ok bool) {
// if object turns out to be a valid CID,
// ensure text representation used in subdomain is CIDv1 in Base32
// https://github.com/ipfs/in-web-browsers/issues/89
rootId = cid.NewCidV1(multicodec, rootCid.Hash()).String()
rootID = cid.NewCidV1(multicodec, rootCid.Hash()).String()
}

return fmt.Sprintf(
"http://%s.%s.%s/%s",
rootId,
"http://%s.%s.%s/%s%s",
rootID,
ns,
hostname,
rest,
query,
), true
}

Expand Down
48 changes: 39 additions & 9 deletions test/sharness/t0114-gateway-subdomains.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@

test_description="Test subdomain support on the HTTP gateway"


. lib/test-lib.sh

## ============================================================================
## Helpers specific to subdomain tests
## ============================================================================

# Helper that tests gateway response over direct HTTP
# and in all supported HTTP proxy modes
test_localhost_gateway_response_should_contain() {
Expand Down Expand Up @@ -41,6 +46,9 @@ test_hostname_gateway_response_should_contain() {
test_should_contain \"$4\" response
"
}
## ============================================================================
## Start IPFS Node and prepare test CIDs
## ============================================================================

test_init_ipfs
test_launch_ipfs_daemon --offline
Expand All @@ -53,6 +61,13 @@ test_expect_success "Add test text file" '
CIDv0to1=$(echo "$CIDv0" | ipfs cid base32)
'

test_expect_success "Add the test directory" '
mkdir -p testdirlisting/subdir1/subdir2 &&
echo "hello" > testdirlisting/hello &&
echo "subdir2-bar" > testdirlisting/subdir1/subdir2/bar &&
DIR_CID=$(ipfs add -Qr --cid-version 1 testdirlisting)
'

test_expect_success "Publish test text file to IPNS" '
PEERID=$(ipfs id --format="<id>")
IPNS_IDv0=$(echo "$PEERID" | ipfs cid format -v 0)
Expand All @@ -64,6 +79,7 @@ test_expect_success "Publish test text file to IPNS" '
printf "/ipfs/%s\n" "$CIDv1" > expected2 &&
test_cmp expected2 output
'

#test_kill_ipfs_daemon
#test_launch_ipfs_daemon

Expand Down Expand Up @@ -108,6 +124,13 @@ test_localhost_gateway_response_should_contain \
"http://localhost:$GWAY_PORT/ipns/en.wikipedia-on-ipfs.org/wiki" \
"Location: http://en.wikipedia-on-ipfs.org.ipns.localhost:$GWAY_PORT/wiki"

# /api/ → api.localhost/api

test_localhost_gateway_response_should_contain \
"Request for localhost/api redirect to api.localhost" \
"http://localhost:$GWAY_PORT/api/v0/refs?arg=${DIR_CID}&r=true" \
"Location: http://api.localhost:$GWAY_PORT/api/v0/refs?arg=${DIR_CID}&r=true"

## ============================================================================
## Test subdomain-based requests to a local gateway with default config
## (origin per content root at http://*.localhost)
Expand All @@ -127,13 +150,6 @@ test_localhost_gateway_response_should_contain \

# {CID}.ipfs.localhost/sub/dir (Directory Listing)

test_expect_success "Add the test directory" '
mkdir -p testdirlisting/subdir1/subdir2 &&
echo "hello" > testdirlisting/hello &&
echo "subdir2-bar" > testdirlisting/subdir1/subdir2/bar &&
DIR_CID=$(ipfs add -Qr --cid-version 1 testdirlisting)
'

test_expect_success "Valid file and subdirectory paths in directory listing at {cid}.ipfs.localhost" '
curl -s "http://${DIR_CID}.ipfs.localhost:$GWAY_PORT" > list_response &&
test_should_contain "<a href=\"/hello\">hello</a>" list_response &&
Expand Down Expand Up @@ -184,14 +200,21 @@ test_localhost_gateway_response_should_contain \
# test_cmp docs_cid_expected dnslink_response
#'

# api.localhost/api

# Note: use DIR_CID so refs -r returns some CIDs for child nodes
test_localhost_gateway_response_should_contain \
"Request for api.localhost returns API response" \
"http://api.localhost:$GWAY_PORT/api/v0/refs?arg=$DIR_CID&r=true" \
"Ref"

## ============================================================================
## Test subdomain-based requests with a custom hostname config
## (origin per content root at http://*.example.com)
## ============================================================================

# set explicit subdomain gateway config for the hostname
ipfs config --json Gateway.PublicGateways '{"example.com": { "UseSubdomains": true, "Paths": ["/ipfs", "/ipns"] }}'
ipfs config --json Gateway.PublicGateways '{"example.com": { "UseSubdomains": true, "Paths": ["/ipfs", "/ipns", "/api"] }}'
# restart daemon to apply config changes
test_kill_ipfs_daemon
test_launch_ipfs_daemon --offline
Expand Down Expand Up @@ -281,7 +304,14 @@ test_hostname_gateway_response_should_contain \
"http://127.0.0.1:$GWAY_PORT" \
"Location: http://${IPNS_IDv1}.ipns.example.com/"

# TODO: api.example.com/v0/id
# api.example.com
# ============================================================================

test_hostname_gateway_response_should_contain \
"Request for api.example.com/api/v0/refs returns expected payload when /api is on Paths whitelist" \
"api.example.com" \
"http://127.0.0.1:$GWAY_PORT/api/v0/refs?arg=${DIR_CID}&r=true" \
"Ref"
#
# DNSLink requests (could be moved to separate test file)
#
Expand Down

0 comments on commit defb1fd

Please sign in to comment.