diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..6313b56c
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text=auto eol=lf
diff --git a/package.json b/package.json
index 93ce0604..57022382 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,7 @@
"serve": "webpack serve --mode=development",
"serve:prod": "webpack serve --mode=production",
"start": "npm run serve",
- "test:node": "webpack --env test && npx mocha dist/tests.js",
+ "test:node": "webpack --env test && npx mocha test-build/tests.js",
"postinstall": "patch-package"
},
"browser": "./dist/src/index.js",
diff --git a/public/_redirects b/public/_redirects
index 5a3a60a0..3b2133ec 100644
--- a/public/_redirects
+++ b/public/_redirects
@@ -1,2 +1 @@
-/ipns/* /?helia-sw=/ipns/:splat 302
-/ipfs/* /?helia-sw=/ipfs/:splat 302
+/* /?helia-sw=/:splat 302
diff --git a/src/index.tsx b/src/index.tsx
index 5ad27ed2..9c730db5 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -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(
)
window.location.replace(window.location.href)
} else {
+ // TODO: add detection of DNSLink gateways (alowing use with Host: en.wikipedia-on-ipfs.org)
// 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
diff --git a/src/lib/dns-link-labels.ts b/src/lib/dns-link-labels.ts
index 04aabe40..1e65edcb 100644
--- a/src/lib/dns-link-labels.ts
+++ b/src/lib/dns-link-labels.ts
@@ -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('.')
}
/**
diff --git a/src/lib/heliaFetch.ts b/src/lib/heliaFetch.ts
index 62fa39de..4f4e14f6 100644
--- a/src/lib/heliaFetch.ts
+++ b/src/lib/heliaFetch.ts
@@ -1,6 +1,5 @@
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 {
@@ -8,7 +7,7 @@ export interface HeliaFetchOptions {
helia: Helia
signal?: AbortSignal
headers?: Headers
- origin?: string | null
+ id?: string | null
protocol?: string | null
}
@@ -116,9 +115,9 @@ 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)
*
* Things to do:
* * TODO: implement as much of the gateway spec as possible.
@@ -126,19 +125,16 @@ function changeCssFontPath (path: string): string {
* * 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 {
+export async function heliaFetch ({ path, helia, signal, headers, id, protocol }: HeliaFetchOptions): Promise {
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 {
diff --git a/src/lib/path-or-subdomain.ts b/src/lib/path-or-subdomain.ts
index 9f7ac86a..8b868d61 100644
--- a/src/lib/path-or-subdomain.ts
+++ b/src/lib/path-or-subdomain.ts
@@ -1,8 +1,9 @@
-const subdomainRegex = /^(?[^/]+)\.(?ip[fn]s)?$/
+// TODO: dry, this is same regex code as in getSubdomainParts
+const subdomainRegex = /^(?[^/]+)\.(?ip[fn]s)\.[^/]+$/
const pathRegex = /^\/(?ip[fn]s)\/(?.*)$/
-export const isPathOrSubdomainRequest = (baseUrl: string, location: Pick): boolean => {
- const subdomain = location.hostname.replace(`.${baseUrl}`, '')
+export const isPathOrSubdomainRequest = (location: Pick): boolean => {
+ const subdomain = location.hostname
const subdomainMatch = subdomain.match(subdomainRegex)
const pathMatch = location.pathname.match(pathRegex)
diff --git a/src/lib/webpack-constants.ts b/src/lib/webpack-constants.ts
deleted file mode 100644
index 77c23819..00000000
--- a/src/lib/webpack-constants.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/* eslint-disable @typescript-eslint/no-non-null-assertion */
-
-/**
- * You can change the BASE_URL when deploying this app to a different domain.
- */
-const BASE_URL = process.env.BASE_URL!
-
-export {
- BASE_URL
-}
diff --git a/src/sw.ts b/src/sw.ts
index 4947cbf0..bd5a126a 100644
--- a/src/sw.ts
+++ b/src/sw.ts
@@ -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 {
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 = /^(?[^/]+)\.(?ip[fn]s)?$/
- 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
*/
diff --git a/tests/path-or-subdomain.spec.ts b/tests/path-or-subdomain.spec.ts
index 83fa105c..0c26a94a 100644
--- a/tests/path-or-subdomain.spec.ts
+++ b/tests/path-or-subdomain.spec.ts
@@ -4,33 +4,44 @@ import { isPathOrSubdomainRequest } from '../src/lib/path-or-subdomain.ts'
describe('isPathOrSubdomainRequest', () => {
it('returns true for path-based request', () => {
- expect(isPathOrSubdomainRequest('example.com', {
+ expect(isPathOrSubdomainRequest({
hostname: 'example.com',
pathname: '/ipfs/bafyFoo'
})).to.equal(true)
- expect(isPathOrSubdomainRequest('example.com', {
+ expect(isPathOrSubdomainRequest({
hostname: 'example.com',
pathname: '/ipns/specs.ipfs.tech'
})).to.equal(true)
})
it('returns true for subdomain request', () => {
- expect(isPathOrSubdomainRequest('example.com', {
+ expect(isPathOrSubdomainRequest({
hostname: 'bafyFoo.ipfs.example.com',
pathname: '/'
})).to.equal(true)
- expect(isPathOrSubdomainRequest('example.com', {
+ expect(isPathOrSubdomainRequest({
+ hostname: 'docs.ipfs.tech.ipns.example.com',
+ pathname: '/'
+ })).to.equal(true)
+ })
+
+ it('returns true for inlined dnslink subdomain request', () => {
+ expect(isPathOrSubdomainRequest({
+ hostname: 'bafyFoo.ipfs.example.com',
+ pathname: '/'
+ })).to.equal(true)
+ expect(isPathOrSubdomainRequest({
hostname: 'specs-ipfs-tech.ipns.example.com',
pathname: '/'
})).to.equal(true)
})
it('returns false for non-path and non-subdomain request', () => {
- expect(isPathOrSubdomainRequest('example.com', {
+ expect(isPathOrSubdomainRequest({
hostname: 'example.com',
pathname: '/foo/bar'
})).to.equal(false)
- expect(isPathOrSubdomainRequest('example.com', {
+ expect(isPathOrSubdomainRequest({
hostname: 'foo.bar.example.com',
pathname: '/'
})).to.equal(false)
diff --git a/webpack.config.js b/webpack.config.js
index c1c65a14..c3a10fac 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -95,7 +95,7 @@ const dev = {
// Only update what has changed on hot reload
hot: true,
port: 3000,
- allowedHosts: [process.env.BASE_URL ?? 'helia-sw-gateway.localhost', 'localhost']
+ allowedHosts: ['helia-sw-gateway.localhost', 'localhost']
},
plugins: [