Skip to content

Commit

Permalink
feat(gw): improved dnslink inliner
Browse files Browse the repository at this point in the history
faster implementation + exported funcs for reuse
outside of boxo/gateway
  • Loading branch information
lidel authored and hacdias committed Oct 20, 2023
1 parent a7e134e commit 40fb162
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 8 deletions.
47 changes: 42 additions & 5 deletions gateway/hostname.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ func NewHostnameHandler(c Config, backend IPFSBackend, next http.Handler) http.H
// We un-inline and check for DNSLink presence on domain with '.'
// first to minimize the amount of DNS lookups:
// my-v--long-example-com → my.v-long.example.com
dnslinkFQDN := toDNSLinkFQDN(rootID)
dnslinkFQDN := UninlineDNSLink(rootID)

// Does _dnslink.my.v-long.example.com exist?
if hasDNSLinkRecord(r.Context(), backend, dnslinkFQDN) {
Expand Down Expand Up @@ -323,22 +323,59 @@ func isHTTPSRequest(r *http.Request) bool {

// Converts a FQDN to DNS-safe representation that fits in 63 characters:
// my.v-long.example.com → my-v--long-example-com
func toDNSLinkDNSLabel(fqdn string) (dnsLabel string, err error) {
// InlineDNSLink implements specification from https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header
func InlineDNSLink(fqdn string) (dnsLabel string, err error) {
/* What follows is an optimized version this three-liner:
dnsLabel = strings.ReplaceAll(fqdn, "-", "--")
dnsLabel = strings.ReplaceAll(dnsLabel, ".", "-")
if len(dnsLabel) > dnsLabelMaxLength {
return "", fmt.Errorf("DNSLink representation incompatible with DNS label length limit of 63: %s", dnsLabel)
}
return dnsLabel, nil
*/
result := make([]byte, 0, len(fqdn))
for i := 0; i < len(fqdn); i++ {
char := fqdn[i]
if char == '-' {
result = append(result, '-', '-')
} else if char == '.' {
result = append(result, '-')
} else {
result = append(result, char)
}
}
if len(result) > dnsLabelMaxLength {
return "", fmt.Errorf("inlined DNSLink incompatible with DNS label length limit of 63: %q", result)
}
return string(result), nil
}

// Converts a DNS-safe representation of DNSLink FQDN to real FQDN:
// my-v--long-example-com → my.v-long.example.com
func toDNSLinkFQDN(dnsLabel string) (fqdn string) {
// UninlineDNSLink implements specification from https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header
func UninlineDNSLink(dnsLabel string) (fqdn string) {
/* What follows is an optimized version this three-liner:
fqdn = strings.ReplaceAll(dnsLabel, "--", "@") // @ placeholder is unused in DNS labels
fqdn = strings.ReplaceAll(fqdn, "-", ".")
fqdn = strings.ReplaceAll(fqdn, "@", "-")
return fqdn
*/
result := make([]byte, 0, len(dnsLabel))
for i := 0; i < len(dnsLabel); i++ {
if dnsLabel[i] == '-' {
if i+1 < len(dnsLabel) && dnsLabel[i+1] == '-' {
// Handle '--' by appending a single '-'
result = append(result, '-')
i++
} else {
// Handle single '-' by appending '.'
result = append(result, '.')
}
} else {
result = append(result, dnsLabel[i])
}
}
return string(result)
}

// Converts a hostname/path to a subdomain-based URL, if applicable.
Expand Down Expand Up @@ -419,7 +456,7 @@ func toSubdomainURL(hostname, path string, r *http.Request, inlineDNSLink bool,
// e.g. when ipfs-companion extension passes value from subdomain gateway
// for further normalization: https://github.com/ipfs/ipfs-companion/issues/1278#issuecomment-1724550623
if ns == "ipns" && !strings.Contains(rootID, ".") && strings.Contains(rootID, "-") {
dnsLinkFqdn := toDNSLinkFQDN(rootID) // my-v--long-example-com → my.v-long.example.com
dnsLinkFqdn := UninlineDNSLink(rootID) // my-v--long-example-com → my.v-long.example.com
if hasDNSLinkRecord(r.Context(), backend, dnsLinkFqdn) {
// update path prefix to use real FQDN with DNSLink
rootID = dnsLinkFqdn
Expand All @@ -442,7 +479,7 @@ func toSubdomainURL(hostname, path string, r *http.Request, inlineDNSLink bool,
// https://my-v--long-example-com.ipns.dweb.link
if hasDNSLinkRecord(r.Context(), backend, rootID) {
// my.v-long.example.com → my-v--long-example-com
dnsLabel, err := toDNSLinkDNSLabel(rootID)
dnsLabel, err := InlineDNSLink(rootID)
if err != nil {
return "", err
}
Expand Down
65 changes: 62 additions & 3 deletions gateway/hostname_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/ipfs/boxo/path"
Expand Down Expand Up @@ -82,10 +83,13 @@ func TestToDNSLinkDNSLabel(t *testing.T) {
err error
}{
{"dnslink.long-name.example.com", "dnslink-long--name-example-com", nil},
{"dnslink.too-long.f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o.example.com", "", errors.New("DNSLink representation incompatible with DNS label length limit of 63: dnslink-too--long-f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o-example-com")},
{"singlelabel", "singlelabel", nil},
{"example.com", "example-com", nil},
{"en.wikipedia-on-ipfs.org", "en-wikipedia--on--ipfs-org", nil},
{"dnslink.too-long.f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o.example.com", "", errors.New(`inlined DNSLink incompatible with DNS label length limit of 63: "dnslink-too--long-f1siqrebi3vir8sab33hu5vcy008djegvay6atmz91ojesyjs8lx350b7y7i1nvyw2haytfukfyu2f2x4tocdrfa0zgij6p4zpl4u5o-example-com"`)},
} {
t.Run(test.in, func(t *testing.T) {
out, err := toDNSLinkDNSLabel(test.in)
out, err := InlineDNSLink(test.in)
require.Equal(t, test.out, out)
require.Equal(t, test.err, err)
})
Expand All @@ -99,11 +103,14 @@ func TestToDNSLinkFQDN(t *testing.T) {
out string
}{
{"singlelabel", "singlelabel"},
{"no--tld", "no-tld"},
{"example.com", "example.com"},
{"docs-ipfs-tech", "docs.ipfs.tech"},
{"en-wikipedia--on--ipfs-org", "en.wikipedia-on-ipfs.org"},
{"dnslink-long--name-example-com", "dnslink.long-name.example.com"},
} {
t.Run(test.in, func(t *testing.T) {
out := toDNSLinkFQDN(test.in)
out := UninlineDNSLink(test.in)
require.Equal(t, test.out, out)
})
}
Expand Down Expand Up @@ -305,3 +312,55 @@ func TestKnownSubdomainDetails(t *testing.T) {
})
}
}

const testInlinedDNSLinkA = "example-com"
const testInlinedDNSLinkB = "docs-ipfs-tech"
const testInlinedDNSLinkC = "en-wikipedia--on--ipfs-org"
const testDNSLinkA = "example.com"
const testDNSLinkB = "docs.ipfs.tech"
const testDNSLinkC = "en.wikipedia-on-ipfs.org"

func inlineDNSLinkSimple(fqdn string) (dnsLabel string, err error) {
dnsLabel = strings.ReplaceAll(fqdn, "-", "--")
dnsLabel = strings.ReplaceAll(dnsLabel, ".", "-")
if len(dnsLabel) > dnsLabelMaxLength {
return "", fmt.Errorf("inlined DNSLink incompatible with DNS label length limit of 63: %q", dnsLabel)
}
return dnsLabel, nil
}
func uninlineDNSLinkSimple(dnsLabel string) (fqdn string) {
fqdn = strings.ReplaceAll(dnsLabel, "--", "@") // @ placeholder is unused in DNS labels
fqdn = strings.ReplaceAll(fqdn, "-", ".")
fqdn = strings.ReplaceAll(fqdn, "@", "-")
return fqdn
}

func BenchmarkUninlineDNSLinkSimple(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = uninlineDNSLinkSimple(testInlinedDNSLinkA)
_ = uninlineDNSLinkSimple(testInlinedDNSLinkB)
_ = uninlineDNSLinkSimple(testInlinedDNSLinkC)
}
}
func BenchmarkUninlineDNSLink(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = UninlineDNSLink(testInlinedDNSLinkA)
_ = UninlineDNSLink(testInlinedDNSLinkB)
_ = UninlineDNSLink(testInlinedDNSLinkC)
}
}

func BenchmarkInlineDNSLinkSimple(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = inlineDNSLinkSimple(testDNSLinkA)
_, _ = inlineDNSLinkSimple(testDNSLinkB)
_, _ = inlineDNSLinkSimple(testDNSLinkC)
}
}
func BenchmarkInlineDNSLink(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = InlineDNSLink(testDNSLinkA)
_, _ = InlineDNSLink(testDNSLinkB)
_, _ = InlineDNSLink(testDNSLinkC)
}
}

0 comments on commit 40fb162

Please sign in to comment.