From da817101bfb84ee3edda9871f39d36ff13dfa5f9 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 13 Apr 2022 16:12:33 +0200 Subject: [PATCH] feat: redirect to dweb link when ipns (#25) --- packages/edge-gateway/src/constants.js | 1 + packages/edge-gateway/src/env.js | 4 ++ packages/edge-gateway/src/gateway.js | 8 +++ packages/edge-gateway/src/index.js | 3 ++ packages/edge-gateway/src/ipfs.js | 2 +- packages/edge-gateway/src/ipns.js | 42 +++++++++++++++ packages/edge-gateway/test/ipns.spec.js | 71 +++++++++++++++++++++++++ 7 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 packages/edge-gateway/src/ipns.js create mode 100644 packages/edge-gateway/test/ipns.spec.js diff --git a/packages/edge-gateway/src/constants.js b/packages/edge-gateway/src/constants.js index d73a82f..be704e4 100644 --- a/packages/edge-gateway/src/constants.js +++ b/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' diff --git a/packages/edge-gateway/src/env.js b/packages/edge-gateway/src/env.js index fa12584..1469424 100644 --- a/packages/edge-gateway/src/env.js +++ b/packages/edge-gateway/src/env.js @@ -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} ipfsGateways * @property {DurableObjectNamespace} gatewayMetricsDurable * @property {DurableObjectNamespace} summaryMetricsDurable @@ -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') diff --git a/packages/edge-gateway/src/gateway.js b/packages/edge-gateway/src/gateway.js index f74d107..ae5d940 100644 --- a/packages/edge-gateway/src/gateway.js +++ b/packages/edge-gateway/src/gateway.js @@ -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) diff --git a/packages/edge-gateway/src/index.js b/packages/edge-gateway/src/index.js index 13dedfd..82ec704 100644 --- a/packages/edge-gateway/src/index.js +++ b/packages/edge-gateway/src/index.js @@ -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' @@ -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)) diff --git a/packages/edge-gateway/src/ipfs.js b/packages/edge-gateway/src/ipfs.js index 203420a..9d92b77 100644 --- a/packages/edge-gateway/src/ipfs.js +++ b/packages/edge-gateway/src/ipfs.js @@ -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) diff --git a/packages/edge-gateway/src/ipns.js b/packages/edge-gateway/src/ipns.js new file mode 100644 index 0000000..21ddcd6 --- /dev/null +++ b/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 +} diff --git a/packages/edge-gateway/test/ipns.spec.js b/packages/edge-gateway/test/ipns.spec.js new file mode 100644 index 0000000..0ad2e3c --- /dev/null +++ b/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' + ) +})