-
Notifications
You must be signed in to change notification settings - Fork 12
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
fix: dynamic subdomain gateway detection #53
Changes from 3 commits
de4f2c2
1cfe4ca
e68b250
1fa7e2b
df38c11
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1 @@ | ||
/ipns/* /?helia-sw=/ipns/:splat 302 | ||
/ipfs/* /?helia-sw=/ipfs/:splat 302 | ||
/* /?helia-sw=/:splat 302 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,6 @@ import './app.css' | |
import App from './app.tsx' | ||
import { loadConfigFromLocalStorage } from './lib/config-db.ts' | ||
import { isPathOrSubdomainRequest } from './lib/path-or-subdomain.ts' | ||
import { BASE_URL } from './lib/webpack-constants.ts' | ||
import RedirectPage from './redirectPage.tsx' | ||
|
||
await loadConfigFromLocalStorage() | ||
|
@@ -15,13 +14,14 @@ const sw = await navigator.serviceWorker.register(new URL('sw.ts', import.meta.u | |
const root = ReactDOMClient.createRoot(container) | ||
|
||
// SW did not trigger for this request | ||
if (isPathOrSubdomainRequest(BASE_URL, window.location)) { | ||
if (isPathOrSubdomainRequest(window.location)) { | ||
// but the requested path is something it should, so show redirect and redirect to the same URL | ||
root.render( | ||
<RedirectPage /> | ||
) | ||
window.location.replace(window.location.href) | ||
} else { | ||
// TODO: add detection of DNSLink gateways (alowing use with Host: en.wikipedia-on-ipfs.org) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if |
||
// the requested path is not recognized as a path or subdomain request, so render the app UI | ||
if (window.location.pathname !== '/') { | ||
// pathname is not blank, but is invalid. redirect to the root | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,35 @@ | ||
import { CID } from 'multiformats/cid' | ||
|
||
/** | ||
* For dnslinks see https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header | ||
* DNSLink names include . which means they must be inlined into a single DNS label to provide unique origin and work with wildcard TLS certificates. | ||
*/ | ||
|
||
// DNS label can have up to 63 characters, consisting of alphanumeric | ||
// characters or hyphens -, but it must not start or end with a hyphen. | ||
const dnsLabelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/ | ||
|
||
/** | ||
* We can receive either IPNS Name string or DNSLink label string here. | ||
* IPNS Names do not have dots or dashes. | ||
*/ | ||
export function isValidDnsLabel (label: string): boolean { | ||
// If string is not a valid IPNS Name (CID) | ||
// then we assume it may be a valid DNSLabel. | ||
try { | ||
CID.parse(label) | ||
return false | ||
} catch { | ||
return dnsLabelRegex.test(label) | ||
} | ||
} | ||
|
||
/** | ||
* We can receive either a peerId string or dnsLink label string here. PeerId strings do not have dots or dashes. | ||
* Checks if label looks like inlined DNSLink. | ||
* (https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header) | ||
*/ | ||
export function isDnsLabel (label: string): boolean { | ||
return ['-', '.'].some((char) => label.includes(char)) | ||
export function isInlinedDnsLink (label: string): boolean { | ||
return isValidDnsLabel(label) && label.includes('-') && !label.includes('.') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ℹ️ to avoid false-positives added code to ensure an inlined DNSLink
|
||
} | ||
|
||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,13 @@ | ||
import { createVerifiedFetch, type ContentTypeParser } from '@helia/verified-fetch' | ||
import { fileTypeFromBuffer } from '@sgtpooki/file-type' | ||
import { dnsLinkLabelDecoder, isDnsLabel } from './dns-link-labels.ts' | ||
import type { Helia } from '@helia/interface' | ||
|
||
export interface HeliaFetchOptions { | ||
path: string | ||
helia: Helia | ||
signal?: AbortSignal | ||
headers?: Headers | ||
origin?: string | null | ||
id?: string | null | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ℹ️ had to rename, would be confusing for web devs ("origin" in web security context, including |
||
protocol?: string | null | ||
} | ||
|
||
|
@@ -116,29 +115,26 @@ function changeCssFontPath (path: string): string { | |
* heliaFetch should have zero awareness of whether it's being used inside a service worker or not. | ||
* | ||
* The `path` supplied should be either: | ||
* * /ipfs/CID | ||
* * /ipns/DNSLink | ||
* * /ipns/key | ||
* * /ipfs/CID (https://docs.ipfs.tech/concepts/content-addressing/) | ||
* * /ipns/DNSLink (https://dnslink.dev/) | ||
* * /ipns/IPNSName (https://specs.ipfs.tech/ipns/ipns-record/#ipns-name) | ||
Comment on lines
+118
to
+120
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 |
||
* | ||
* Things to do: | ||
* * TODO: implement as much of the gateway spec as possible. | ||
* * TODO: why we would be better than ipfs.io/other-gateway | ||
* * TODO: have error handling that renders 404/500/other if the request is bad. | ||
* | ||
*/ | ||
export async function heliaFetch ({ path, helia, signal, headers, origin, protocol }: HeliaFetchOptions): Promise<Response> { | ||
export async function heliaFetch ({ path, helia, signal, headers, id, protocol }: HeliaFetchOptions): Promise<Response> { | ||
const verifiedFetch = await createVerifiedFetch(helia, { | ||
contentTypeParser | ||
}) | ||
|
||
let verifiedFetchUrl: string | ||
if (origin != null && protocol != null) { | ||
if (protocol === 'ipns' && isDnsLabel(origin)) { | ||
verifiedFetchUrl = `${protocol}://${dnsLinkLabelDecoder(origin)}/${path}` | ||
} else { | ||
// likely a peerId instead of a dnsLink label | ||
verifiedFetchUrl = `${protocol}://${origin}${path}` | ||
} | ||
|
||
if (id != null && protocol != null) { | ||
verifiedFetchUrl = `${protocol}://${id}${path}` | ||
|
||
// eslint-disable-next-line no-console | ||
console.log('subdomain fetch for ', verifiedFetchUrl) | ||
} else { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💭 I did not clean this up to make the PR smaller and easier to review. My plan is to move majority of subdomain handling to |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,8 +2,8 @@ | |
// import { clientsClaim } from 'workbox-core' | ||
import mime from 'mime-types' | ||
import { getHelia } from './get-helia.ts' | ||
import { dnsLinkLabelDecoder, isInlinedDnsLink } from './lib/dns-link-labels.ts' | ||
import { heliaFetch } from './lib/heliaFetch.ts' | ||
import { BASE_URL } from './lib/webpack-constants.ts' | ||
import type { Helia } from '@helia/interface' | ||
|
||
declare let self: ServiceWorkerGlobalScope | ||
|
@@ -51,8 +51,8 @@ const fetchHandler = async ({ path, request }: FetchHandlerArg): Promise<Respons | |
// 5 minute timeout | ||
const abortController = AbortSignal.timeout(5 * 60 * 1000) | ||
try { | ||
const { origin, protocol } = getSubdomainParts(request) | ||
return await heliaFetch({ path, helia, signal: abortController, headers: request.headers, origin, protocol }) | ||
const { id, protocol } = getSubdomainParts(request) | ||
return await heliaFetch({ path, helia, signal: abortController, headers: request.headers, id, protocol }) | ||
} catch (err: unknown) { | ||
const errorMessages: string[] = [] | ||
if (isAggregateError(err)) { | ||
|
@@ -91,23 +91,34 @@ const isRootRequestForContent = (event: FetchEvent): boolean => { | |
return isRootRequest // && getCidFromUrl(event.request.url) != null | ||
} | ||
|
||
function getSubdomainParts (request: Request): { origin: string | null, protocol: string | null } { | ||
function getSubdomainParts (request: Request): { id: string | null, protocol: string | null } { | ||
const urlString = request.url | ||
const url = new URL(urlString) | ||
const subdomain = url.hostname.replace(`.${BASE_URL}`, '') | ||
const subdomainRegex = /^(?<origin>[^/]+)\.(?<protocol>ip[fn]s)?$/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ℹ️ this produced invalid results for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we redirect from subdomains like "docs.ipfs.tech" to "docs-ipfs-tech" ? |
||
const subdomainMatch = subdomain.match(subdomainRegex) | ||
const { origin, protocol } = subdomainMatch?.groups ?? { origin: null, protocol: null } | ||
const labels = new URL(urlString).hostname.split('.') | ||
let id: string | null = null; let protocol: string | null = null | ||
|
||
// DNS label inspection happens from from right to left | ||
// to work fine with edge cases like docs.ipfs.tech.ipns.foo.localhost | ||
for (let i = labels.length - 1; i >= 0; i--) { | ||
if (labels[i].startsWith('ipfs') || labels[i].startsWith('ipns')) { | ||
protocol = labels[i] | ||
id = labels.slice(0, i).join('.') | ||
if (protocol === 'ipns' && isInlinedDnsLink(id)) { | ||
// un-inline DNSLink names according to https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header | ||
id = dnsLinkLabelDecoder(id) | ||
} | ||
break | ||
} | ||
} | ||
|
||
return { origin, protocol } | ||
return { id, protocol } | ||
} | ||
|
||
function isSubdomainRequest (event: FetchEvent): boolean { | ||
const { origin, protocol } = getSubdomainParts(event.request) | ||
console.log('isSubdomainRequest.origin: ', origin) | ||
const { id, protocol } = getSubdomainParts(event.request) | ||
console.log('isSubdomainRequest.id: ', id) | ||
console.log('isSubdomainRequest.protocol: ', protocol) | ||
|
||
return origin != null && protocol != null | ||
return id != null && protocol != null | ||
} | ||
|
||
const isValidRequestForSW = (event: FetchEvent): boolean => | ||
|
@@ -151,8 +162,6 @@ self.addEventListener('fetch', event => { | |
const newUrlString = newParts.join('/') + '/' + destinationParts.slice(index).join('/') | ||
const newUrl = new URL(newUrlString) | ||
|
||
// const { origin, protocol } = getSubdomainParts(event) | ||
|
||
/** | ||
* respond with redirect to newUrl | ||
*/ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💭 I was not able to find where
?helia-sw
is handled – assumed it is not wired up yet, but I've updated_redirects
so it will work on web (subdomain or dnslink) gateway once we add it.@SgtPooki what was the plan around
?helia-sw-subdomain
and?helia-sw
? I want to add origin isolation enforcement in follow-up PR and most likely will have to touch this.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?helia-sw & ?helia-sw-subdomain are no longer required and should be able to be removed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
they were used to help with registering the service worker originally, but now, if the service worker is hit, we never render the jsx, and if the service worker isn't hit, we determine (in app.tsx) if we need to load the or not