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

feat: support ED25519 at subdomain gw with TLS #7441

Merged
merged 1 commit into from
Jul 10, 2020
Merged
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
137 changes: 105 additions & 32 deletions core/corehttp/hostname.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,15 @@ var defaultKnownGateways = map[string]config.GatewaySpec{
"dweb.link": subdomainGatewaySpec,
}

// Label's max length in DNS (https://tools.ietf.org/html/rfc1034#page-7)
const dnsLabelMaxLength int = 63

// HostnameOption rewrites an incoming request based on the Host header.
func HostnameOption() ServeOption {
return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
childMux := http.NewServeMux()

coreApi, err := coreapi.NewCoreAPI(n)
coreAPI, err := coreapi.NewCoreAPI(n)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -101,7 +104,12 @@ func HostnameOption() ServeOption {
if gw.UseSubdomains {
// Yes, redirect if applicable
// Example: dweb.link/ipfs/{cid} → {cid}.ipfs.dweb.link
if newURL, ok := toSubdomainURL(host, r.URL.Path, r); ok {
newURL, err := toSubdomainURL(host, r.URL.Path, r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if newURL != "" {
// Just to be sure single Origin can't be abused in
// web browsers that ignored the redirect for some
// reason, Clear-Site-Data header clears browsing
Expand Down Expand Up @@ -131,7 +139,7 @@ func HostnameOption() ServeOption {
// Not a whitelisted path

// Try DNSLink, if it was not explicitly disabled for the hostname
if !gw.NoDNSLink && isDNSLinkRequest(r.Context(), coreApi, host) {
if !gw.NoDNSLink && isDNSLinkRequest(r.Context(), coreAPI, host) {
// rewrite path and handle as DNSLink
r.URL.Path = "/ipns/" + stripPort(host) + r.URL.Path
childMux.ServeHTTP(w, r)
Expand All @@ -158,16 +166,44 @@ func HostnameOption() ServeOption {
return
}

// Do we need to fix multicodec in PeerID represented as CIDv1?
if isPeerIDNamespace(ns) {
keyCid, err := cid.Decode(rootID)
if err == nil && keyCid.Type() != cid.Libp2pKey {
if newURL, ok := toSubdomainURL(hostname, pathPrefix+r.URL.Path, r); ok {
// Redirect to CID fixed inside of toSubdomainURL()
// Check if rootID is a valid CID
if rootCID, err := cid.Decode(rootID); err == nil {
// Do we need to redirect root CID to a canonical DNS representation?
dnsCID, err := toDNSPrefix(rootID, rootCID)
lidel marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
lidel marked this conversation as resolved.
Show resolved Hide resolved
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if !strings.HasPrefix(r.Host, dnsCID) {
lidel marked this conversation as resolved.
Show resolved Hide resolved
dnsPrefix := "/" + ns + "/" + dnsCID
newURL, err := toSubdomainURL(hostname, dnsPrefix+r.URL.Path, r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if newURL != "" {
// Redirect to deterministic CID to ensure CID
// always gets the same Origin on the web
http.Redirect(w, r, newURL, http.StatusMovedPermanently)
return
}
}

// Do we need to fix multicodec in PeerID represented as CIDv1?
if isPeerIDNamespace(ns) {
if rootCID.Type() != cid.Libp2pKey {
newURL, err := toSubdomainURL(hostname, pathPrefix+r.URL.Path, r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if newURL != "" {
// Redirect to CID fixed inside of toSubdomainURL()
http.Redirect(w, r, newURL, http.StatusMovedPermanently)
return
}
}
}
}

// Rewrite the path to not use subdomains
Expand All @@ -183,7 +219,7 @@ func HostnameOption() ServeOption {
// 1. is wildcard DNSLink enabled (Gateway.NoDNSLink=false)?
// 2. does Host header include a fully qualified domain name (FQDN)?
// 3. does DNSLink record exist in DNS?
if !cfg.Gateway.NoDNSLink && isDNSLinkRequest(r.Context(), coreApi, host) {
if !cfg.Gateway.NoDNSLink && isDNSLinkRequest(r.Context(), coreAPI, host) {
// rewrite path and handle as DNSLink
r.URL.Path = "/ipns/" + stripPort(host) + r.URL.Path
childMux.ServeHTTP(w, r)
Expand Down Expand Up @@ -273,18 +309,38 @@ func isPeerIDNamespace(ns string) bool {
}
}

// Converts an identifier to DNS-safe representation that fits in 63 characters
func toDNSPrefix(rootID string, rootCID cid.Cid) (prefix string, err error) {
// Return as-is if things fit
if len(rootID) <= dnsLabelMaxLength {
return rootID, nil
}

// Convert to Base36 and see if that helped
rootID, err = cid.NewCidV1(rootCID.Type(), rootCID.Hash()).StringOfBase(mbase.Base36)
if err != nil {
return "", err
}
if len(rootID) <= dnsLabelMaxLength {
return rootID, nil
}

// Can't win with DNS at this point, return error
return "", fmt.Errorf("CID incompatible with DNS label length limit of 63: %s", rootID)
}

// Converts a hostname/path to a subdomain-based URL, if applicable.
func toSubdomainURL(hostname, path string, r *http.Request) (redirURL string, ok bool) {
func toSubdomainURL(hostname, path string, r *http.Request) (redirURL string, err error) {
var scheme, ns, rootID, rest string

query := r.URL.RawQuery
parts := strings.SplitN(path, "/", 4)
safeRedirectURL := func(in string) (out string, ok bool) {
safeRedirectURL := func(in string) (out string, err error) {
safeURI, err := url.ParseRequestURI(in)
if err != nil {
return "", false
return "", err
}
return safeURI.String(), true
return safeURI.String(), nil
}

// Support X-Forwarded-Proto if added by a reverse proxy
Expand All @@ -304,11 +360,11 @@ func toSubdomainURL(hostname, path string, r *http.Request) (redirURL string, ok
ns = parts[1]
rootID = parts[2]
default:
return "", false
return "", nil
}

if !isSubdomainNamespace(ns) {
return "", false
return "", nil
}

// add prefix if query is present
Expand All @@ -327,25 +383,42 @@ func toSubdomainURL(hostname, path string, r *http.Request) (redirURL string, ok
}

// If rootID is a CID, ensure it uses DNS-friendly text representation
if rootCid, err := cid.Decode(rootID); err == nil {
multicodec := rootCid.Type()

// PeerIDs represented as CIDv1 are expected to have libp2p-key
// multicodec (https://github.com/libp2p/specs/pull/209).
// We ease the transition by fixing multicodec on the fly:
// https://github.com/ipfs/go-ipfs/issues/5287#issuecomment-492163929
if isPeerIDNamespace(ns) && multicodec != cid.Libp2pKey {
multicodec = cid.Libp2pKey
if rootCID, err := cid.Decode(rootID); err == nil {
multicodec := rootCID.Type()
var base mbase.Encoding = mbase.Base32

// Normalizations specific to /ipns/{libp2p-key}
if isPeerIDNamespace(ns) {
// Using Base36 for /ipns/ for consistency
// Context: https://github.com/ipfs/go-ipfs/pull/7441#discussion_r452372828
base = mbase.Base36

// PeerIDs represented as CIDv1 are expected to have libp2p-key
// multicodec (https://github.com/libp2p/specs/pull/209).
// We ease the transition by fixing multicodec on the fly:
// https://github.com/ipfs/go-ipfs/issues/5287#issuecomment-492163929
if multicodec != cid.Libp2pKey {
multicodec = cid.Libp2pKey
}
}

// 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, err = cid.NewCidV1(multicodec, rootCid.Hash()).StringOfBase(mbase.Base32)
// Ensure CID text representation used in subdomain is compatible
// with the way DNS and URIs are implemented in user agents.
//
// 1. Switch to CIDv1 and enable case-insensitive Base encoding
// to avoid issues when user agent force-lowercases the hostname
// before making the request
// (https://github.com/ipfs/in-web-browsers/issues/89)
rootCID = cid.NewCidV1(multicodec, rootCID.Hash())
rootID, err = rootCID.StringOfBase(base)
if err != nil {
return "", err
}
// 2. Make sure CID fits in a DNS label, adjust encoding if needed
// (https://github.com/ipfs/go-ipfs/issues/7318)
rootID, err = toDNSPrefix(rootID, rootCID)
lidel marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
// should not error, but if it does, its clealy not possible to
// produce a subdomain URL
return "", false
return "", err
}
}

Expand Down
54 changes: 43 additions & 11 deletions core/corehttp/hostname_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package corehttp

import (
"errors"
"net/http/httptest"
"testing"

cid "github.com/ipfs/go-cid"
config "github.com/ipfs/go-ipfs-config"
)

Expand All @@ -15,23 +17,25 @@ func TestToSubdomainURL(t *testing.T) {
path string
// out:
url string
ok bool
err error
}{
// DNSLink
{"localhost", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost/", true},
{"localhost", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost/", nil},
// Hostname with port
{"localhost:8080", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost:8080/", true},
{"localhost:8080", "/ipns/dnslink.io", "http://dnslink.io.ipns.localhost:8080/", nil},
// CIDv0 → CIDv1base32
{"localhost", "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.localhost/", true},
{"localhost", "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.localhost/", nil},
// CIDv1 with long sha512
{"localhost", "/ipfs/bafkrgqe3ohjcjplc6n4f3fwunlj6upltggn7xqujbsvnvyw764srszz4u4rshq6ztos4chl4plgg4ffyyxnayrtdi5oc4xb2332g645433aeg", "", errors.New("CID incompatible with DNS label length limit of 63: kf1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5oj")},
// PeerID as CIDv1 needs to have libp2p-key multicodec
{"localhost", "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", "http://bafzbeieqhtl2l3mrszjnhv6hf2iloiitsx7mexiolcnywnbcrzkqxwslja.ipns.localhost/", true},
{"localhost", "/ipns/bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "http://bafzbeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm.ipns.localhost/", true},
// PeerID: ed25519+identity multihash
{"localhost", "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", "http://bafzaajaiaejcat4yhiwnr2qz73mtu6vrnj2krxlpfoa3wo2pllfi37quorgwh2jw.ipns.localhost/", true},
{"localhost", "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", "http://k2k4r8n0flx3ra0y5dr8fmyvwbzy3eiztmtq6th694k5a3rznayp3e4o.ipns.localhost/", nil},
{"localhost", "/ipns/bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "http://k2k4r8l9ja7hkzynavdqup76ou46tnvuaqegbd04a4o1mpbsey0meucb.ipns.localhost/", nil},
// PeerID: ed25519+identity multihash → CIDv1Base36
{"localhost", "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", "http://k51qzi5uqu5di608geewp3nqkg0bpujoasmka7ftkyxgcm3fh1aroup0gsdrna.ipns.localhost/", nil},
} {
url, ok := toSubdomainURL(test.hostname, test.path, r)
if ok != test.ok || url != test.url {
t.Errorf("(%s, %s) returned (%s, %t), expected (%s, %t)", test.hostname, test.path, url, ok, test.url, ok)
url, err := toSubdomainURL(test.hostname, test.path, r)
if url != test.url || !equalError(err, test.err) {
t.Errorf("(%s, %s) returned (%s, %v), expected (%s, %v)", test.hostname, test.path, url, err, test.url, test.err)
}
}
}
Expand Down Expand Up @@ -75,6 +79,30 @@ func TestPortStripping(t *testing.T) {

}

func TestDNSPrefix(t *testing.T) {
for _, test := range []struct {
in string
out string
err error
}{
// <= 63
{"QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", nil},
{"bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", nil},
// > 63
// PeerID: ed25519+identity multihash → CIDv1Base36
{"bafzaajaiaejca4syrpdu6gdx4wsdnokxkprgzxf4wrstuc34gxw5k5jrag2so5gk", "k51qzi5uqu5dj16qyiq0tajolkojyl9qdkr254920wxv7ghtuwcz593tp69z9m", nil},
// CIDv1 with long sha512 → error
{"bafkrgqe3ohjcjplc6n4f3fwunlj6upltggn7xqujbsvnvyw764srszz4u4rshq6ztos4chl4plgg4ffyyxnayrtdi5oc4xb2332g645433aeg", "", errors.New("CID incompatible with DNS label length limit of 63: kf1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5oj")},
} {
inCID, _ := cid.Decode(test.in)
out, err := toDNSPrefix(test.in, inCID)
if out != test.out || !equalError(err, test.err) {
t.Errorf("(%s): returned (%s, %v) expected (%s, %v)", test.in, out, err, test.out, test.err)
}
}

}

func TestKnownSubdomainDetails(t *testing.T) {
gwSpec := config.GatewaySpec{
UseSubdomains: true,
Expand Down Expand Up @@ -150,3 +178,7 @@ func TestKnownSubdomainDetails(t *testing.T) {
}

}

func equalError(a, b error) bool {
return (a == nil && b == nil) || (a != nil && b != nil && a.Error() == b.Error())
}
Loading