Skip to content

Commit

Permalink
feat: redirect to dweb link when ipns (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
vasco-santos committed Apr 13, 2022
1 parent df21a37 commit da81710
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 1 deletion.
1 change: 1 addition & 0 deletions packages/edge-gateway/src/constants.js
@@ -1,4 +1,5 @@
export const CF_CACHE_MAX_OBJECT_SIZE = 512 * Math.pow(1024, 2) // 512MB to bytes
export const DNS_LABEL_MAX_LENGTH = 63 // Label's max length in DNS (https://tools.ietf.org/html/rfc1034#page-7)
export const METRICS_CACHE_MAX_AGE = 10 * 60 // in seconds (10 minutes)
export const CIDS_TRACKER_ID = 'cids'
export const SUMMARY_METRICS_ID = 'summary-metrics'
Expand Down
4 changes: 4 additions & 0 deletions packages/edge-gateway/src/env.js
Expand Up @@ -22,6 +22,8 @@ import { Logging } from './logs.js'
* @property {KVNamespace} DENYLIST
*
* @typedef {Object} EnvTransformed
* @property {string} IPFS_GATEWAY_HOSTNAME
* @property {string} IPNS_GATEWAY_HOSTNAME
* @property {Array<string>} ipfsGateways
* @property {DurableObjectNamespace} gatewayMetricsDurable
* @property {DurableObjectNamespace} summaryMetricsDurable
Expand Down Expand Up @@ -49,6 +51,8 @@ export function envAll(request, env, ctx) {
env.gatewayRateLimitsDurable = env.GATEWAYRATELIMITS
env.gatewayRedirectCounter = env.GATEWAYREDIRECTCOUNTER
env.REQUEST_TIMEOUT = env.REQUEST_TIMEOUT || 20000
env.IPFS_GATEWAY_HOSTNAME = env.GATEWAY_HOSTNAME
env.IPNS_GATEWAY_HOSTNAME = env.GATEWAY_HOSTNAME.replace('ipfs', 'ipns')

env.log = new Logging(request, env, ctx)
env.log.time('request')
Expand Down
8 changes: 8 additions & 0 deletions packages/edge-gateway/src/gateway.js
Expand Up @@ -38,6 +38,14 @@ import {
* @param {import('./index').Ctx} ctx
*/
export async function gatewayGet(request, env, ctx) {
// Redirect if ipns
if (request.url.includes(env.IPNS_GATEWAY_HOSTNAME)) {
return Response.redirect(
request.url.replace(env.IPNS_GATEWAY_HOSTNAME, 'ipns.dweb.link'),
302
)
}

const startTs = Date.now()
const reqUrl = new URL(request.url)
const cid = getCidFromSubdomainUrl(reqUrl)
Expand Down
3 changes: 3 additions & 0 deletions packages/edge-gateway/src/index.js
Expand Up @@ -3,6 +3,7 @@
import { Router } from 'itty-router'

import { ipfsGet } from './ipfs.js'
import { ipnsGet } from './ipns.js'
import { gatewayGet } from './gateway.js'
import { metricsGet } from './metrics.js'

Expand All @@ -26,6 +27,8 @@ router
.get('/ipfs/:cid/*', withCorsHeaders(ipfsGet))
.head('/ipfs/:cid', withCorsHeaders(ipfsGet))
.head('/ipfs/:cid/*', withCorsHeaders(ipfsGet))
.get('/ipns/:name', withCorsHeaders(ipnsGet))
.get('/ipns/:name/*', withCorsHeaders(ipnsGet))
.get('*', withCorsHeaders(gatewayGet))
.head('*', withCorsHeaders(gatewayGet))

Expand Down
2 changes: 1 addition & 1 deletion packages/edge-gateway/src/ipfs.js
Expand Up @@ -24,7 +24,7 @@ export async function ipfsGet(request, env) {
throw new InvalidUrlError(`invalid CID: ${cid}: ${err.message}`)
}
const url = new URL(
`https://${nCid}.${env.GATEWAY_HOSTNAME}${redirectPath}${redirectQueryString}`
`https://${nCid}.${env.IPFS_GATEWAY_HOSTNAME}${redirectPath}${redirectQueryString}`
)

return Response.redirect(url, 302)
Expand Down
42 changes: 42 additions & 0 deletions packages/edge-gateway/src/ipns.js
@@ -0,0 +1,42 @@
import { DNS_LABEL_MAX_LENGTH } from './constants.js'
import { InvalidUrlError } from './errors.js'

/**
* Handle IPNS path request
*
* @param {Request} request
* @param {import('./env').Env} env
*/
export async function ipnsGet(request, env) {
const name = request.params.name
const reqUrl = new URL(request.url)
const reqQueryString = reqUrl.searchParams.toString()

// Get pathname to query from URL pathname avoiding potential name appear in the domain
const redirectPath = reqUrl.pathname.split(name).slice(1).join(name)
const redirectQueryString = reqQueryString ? `?${reqQueryString}` : ''
const dnsLabel = toDNSLinkDNSLabel(name)

const url = new URL(
`https://${dnsLabel}.${env.IPNS_GATEWAY_HOSTNAME}${redirectPath}${redirectQueryString}`
)

return Response.redirect(url, 302)
}

/**
* Converts a FQDN to DNS-safe representation that fits in 63 characters.
* Example: my.v-long.example.com → my-v--long-example-com
* @param {string} fqdn
*/
function toDNSLinkDNSLabel(fqdn) {
const dnsLabel = fqdn.replaceAll('-', '--').replaceAll('.', '-')

if (dnsLabel.length > DNS_LABEL_MAX_LENGTH) {
throw new InvalidUrlError(
`invalid FQDN: ${fqdn}: longer than max length: ${DNS_LABEL_MAX_LENGTH}`
)
}

return dnsLabel
}
71 changes: 71 additions & 0 deletions packages/edge-gateway/test/ipns.spec.js
@@ -0,0 +1,71 @@
import test from 'ava'
import { createErrorHtmlContent } from '../src/errors.js'

import { getMiniflare } from './utils.js'

test.beforeEach((t) => {
// Create a new Miniflare environment for each test
t.context = {
mf: getMiniflare(),
}
})

test('Fails when invalid name with IPNS canonical resolution', async (t) => {
const { mf } = t.context

const response = await mf.dispatchFetch(
'https://localhost:8787/ipns/en.super-long-name-on-ipfs-exceeding-limit-from-ietf-rfc1034.org'
)
t.is(response.status, 400)

const textResponse = await response.text()
t.is(
textResponse,
createErrorHtmlContent(
400,
'invalid FQDN: en.super-long-name-on-ipfs-exceeding-limit-from-ietf-rfc1034.org: longer than max length: 63'
)
)
})

test('should redirect to subdomain with IPNS canonical resolution', async (t) => {
const { mf } = t.context

const response = await mf.dispatchFetch(
'https://localhost:8787/ipns/en.wikipedia-on-ipfs.org'
)
await response.waitUntil()
t.is(response.status, 302)
t.is(
response.headers.get('location'),
'https://en-wikipedia--on--ipfs-org.ipns.localhost:8787/'
)
})

test('should redirect to subdomain with IPNS canonical resolution keeping path and query params', async (t) => {
const { mf } = t.context

const response = await mf.dispatchFetch(
'https://localhost:8787/ipns/en.wikipedia-on-ipfs.org/Energy?key=value'
)
await response.waitUntil()
t.is(response.status, 302)
t.is(
response.headers.get('location'),
'https://en-wikipedia--on--ipfs-org.ipns.localhost:8787/Energy?key=value'
)
})

test('should redirect to dweb.link with IPNS subdomain resolution', async (t) => {
const { mf } = t.context

const response = await mf.dispatchFetch(
'https://en-wikipedia--on--ipfs-org.ipns.localhost:8787/Energy?key=value'
)
await response.waitUntil()
t.is(response.status, 302)
t.is(
response.headers.get('location'),
'https://en-wikipedia--on--ipfs-org.ipns.dweb.link/Energy?key=value'
)
})

0 comments on commit da81710

Please sign in to comment.