Skip to content

Commit

Permalink
feat: wildcard support for public gateways
Browse files Browse the repository at this point in the history
Add support for one or more wildcards in the hostname definition
of a public gateway. This is useful for example to support easily
multiples environment.

Wildcarded hostname are set in the config as for example "*.domain.tld".
  • Loading branch information
MichaelMure committed Jul 3, 2020
1 parent 31ca7ce commit f4a3878
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 24 deletions.
92 changes: 71 additions & 21 deletions core/corehttp/hostname.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net"
"net/http"
"net/url"
"regexp"
"strings"

cid "github.com/ipfs/go-cid"
Expand Down Expand Up @@ -55,22 +56,8 @@ func HostnameOption() ServeOption {
if err != nil {
return nil, err
}
knownGateways := make(
map[string]config.GatewaySpec,
len(defaultKnownGateways)+len(cfg.Gateway.PublicGateways),
)
for hostname, gw := range defaultKnownGateways {
knownGateways[hostname] = gw
}
for hostname, gw := range cfg.Gateway.PublicGateways {
if gw == nil {
// Allows the user to remove gateways but _also_
// allows us to continuously update the list.
delete(knownGateways, hostname)
} else {
knownGateways[hostname] = *gw
}
}

knownGateways := prepareKnownGateways(cfg.Gateway.PublicGateways)

mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Unfortunately, many (well, ipfs.io) gateways use
Expand Down Expand Up @@ -190,22 +177,85 @@ func HostnameOption() ServeOption {
}
}

type gatewayHosts struct {
exact map[string]config.GatewaySpec
wildcard []wildcardHost
}

type wildcardHost struct {
re *regexp.Regexp
spec config.GatewaySpec
}

func prepareKnownGateways(publicGateways map[string]*config.GatewaySpec) gatewayHosts {
var hosts gatewayHosts

if len(publicGateways) == 0 {
hosts.exact = make(
map[string]config.GatewaySpec,
len(defaultKnownGateways),
)
for hostname, gw := range defaultKnownGateways {
hosts.exact[hostname] = gw
}
return hosts
}

hosts.exact = make(map[string]config.GatewaySpec, len(publicGateways))

for hostname, gw := range publicGateways {
if gw == nil {
continue
}
if strings.Contains(hostname, "*") {
// from *.domain.tld, construct a regexp that match any direct subdomain
// of .domain.tld.
//
// Regexp will be in the form of ^[^.]+\.domain.tld(?::\d+)?$

escaped := strings.ReplaceAll(hostname, ".", `\.`)
regexed := strings.ReplaceAll(escaped, "*", "[^.]+")

re, err := regexp.Compile(fmt.Sprintf(`^%s(?::\d+)?$`, regexed))
if err != nil {
log.Warn("invalid wildcard gateway hostname \"%s\"", hostname)
}

hosts.wildcard = append(hosts.wildcard, wildcardHost{re: re, spec: *gw})
} else {
hosts.exact[hostname] = *gw
}
}

return hosts
}

// isKnownHostname checks Gateway.PublicGateways and returns matching
// GatewaySpec with gracefull fallback to version without port
func isKnownHostname(hostname string, knownGateways map[string]config.GatewaySpec) (gw config.GatewaySpec, ok bool) {
func isKnownHostname(hostname string, knownGateways gatewayHosts) (gw config.GatewaySpec, ok bool) {
// Try hostname (host+optional port - value from Host header as-is)
if gw, ok := knownGateways[hostname]; ok {
if gw, ok := knownGateways.exact[hostname]; ok {
return gw, ok
}
// Also test without port
if gw, ok = knownGateways.exact[stripPort(hostname)]; ok {
return gw, ok
}
// Fallback to hostname without port
gw, ok = knownGateways[stripPort(hostname)]

// Wildcard support. Test both with and without port.
for _, host := range knownGateways.wildcard {
if host.re.MatchString(hostname) {
return host.spec, true
}
}

return gw, ok
}

// Parses Host header and looks for a known subdomain gateway host.
// If found, returns GatewaySpec and subdomain components.
// Note: hostname is host + optional port
func knownSubdomainDetails(hostname string, knownGateways map[string]config.GatewaySpec) (gw config.GatewaySpec, knownHostname, ns, rootID string, ok bool) {
func knownSubdomainDetails(hostname string, knownGateways gatewayHosts) (gw config.GatewaySpec, knownHostname, ns, rootID string, ok bool) {
labels := strings.Split(hostname, ".")
// Look for FQDN of a known gateway hostname.
// Example: given "dist.ipfs.io.ipns.dweb.link":
Expand Down
16 changes: 13 additions & 3 deletions core/corehttp/hostname_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func TestToSubdomainURL(t *testing.T) {
{"localhost", "/ipns/bafybeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm", "http://bafzbeickencdqw37dpz3ha36ewrh4undfjt2do52chtcky4rxkj447qhdm.ipns.localhost/", true},
// PeerID: ed25519+identity multihash
{"localhost", "/ipns/12D3KooWFB51PRY9BxcXSH6khFXw1BZeszeLDy7C8GciskqCTZn5", "http://bafzaajaiaejcat4yhiwnr2qz73mtu6vrnj2krxlpfoa3wo2pllfi37quorgwh2jw.ipns.localhost/", true},
{"sub.localhost", "/ipfs/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", "http://bafybeif7a7gdklt6hodwdrmwmxnhksctcuav6lfxlcyfz4khzl3qfmvcgu.ipfs.sub.localhost/", true},
} {
url, ok := toSubdomainURL(test.hostname, test.path, r)
if ok != test.ok || url != test.url {
Expand Down Expand Up @@ -76,14 +77,16 @@ func TestPortStripping(t *testing.T) {
}

func TestKnownSubdomainDetails(t *testing.T) {
gwSpec := config.GatewaySpec{
gwSpec := &config.GatewaySpec{
UseSubdomains: true,
}
knownGateways := map[string]config.GatewaySpec{
knownGateways := prepareKnownGateways(map[string]*config.GatewaySpec{
"localhost": gwSpec,
"dweb.link": gwSpec,
"dweb.ipfs.pvt.k12.ma.us": gwSpec, // note the sneaky ".ipfs." ;-)
}
"*.wildcard1.tld": gwSpec,
"*.*.wildcard2.tld": gwSpec,
})

for _, test := range []struct {
// in:
Expand Down Expand Up @@ -130,6 +133,13 @@ func TestKnownSubdomainDetails(t *testing.T) {
// other namespaces
{"api.localhost", "", "", "", false},
{"peerid.p2p.localhost", "localhost", "p2p", "peerid", true},
// wildcards
{"wildcard1.tld", "", "", "", false},
{".wildcard1.tld", "", "", "", false},
{"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.wildcard1.tld", "", "", "", false},
{"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.sub.wildcard1.tld", "sub.wildcard1.tld", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
{"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.sub1.sub2.wildcard1.tld", "", "", "", false},
{"bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am.ipfs.sub1.sub2.wildcard2.tld", "sub1.sub2.wildcard2.tld", "ipfs", "bafkreicysg23kiwv34eg2d7qweipxwosdo2py4ldv42nbauguluen5v6am", true},
} {
gw, hostname, ns, rootID, ok := knownSubdomainDetails(test.hostHeader, knownGateways)
if ok != test.ok {
Expand Down
6 changes: 6 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,12 @@ Default: `[]`

`PublicGateways` is a dictionary for defining gateway behavior on specified hostnames.

Hostnames can optionally be defined with one or more wildcards.

Examples:
- `*.example.com` will match requests to `http://foo.example.com/ipfs/*` or `http://{cid}.ipfs.bar.example.com/*`.
- `foo-*.example.com` will match requests to `http://foo-bar.example.com/ipfs/*` or `http://{cid}.ipfs.foo-xyz.example.com/*`.

#### `Gateway.PublicGateways: Paths`

Array of paths that should be exposed on the hostname.
Expand Down

0 comments on commit f4a3878

Please sign in to comment.