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: redirect to dweb link when ipns #25

Merged
merged 1 commit into from
Apr 13, 2022
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
1 change: 1 addition & 0 deletions packages/edge-gateway/src/constants.js
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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'
)
})