From e2bc5581ae5a9cdc3e376b74646cb75be937dbfa Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Wed, 6 Mar 2024 14:53:06 +0100 Subject: [PATCH 01/10] refactor: remove unused helia andu se verified fetch --- src/get-helia.ts | 35 ++++----- src/lib/config-db.ts | 4 +- src/lib/content-type-parser.ts | 48 +++++++++++++ src/lib/heliaFetch.ts | 126 ++++++++++----------------------- src/sw.ts | 24 +++---- 5 files changed, 113 insertions(+), 124 deletions(-) create mode 100644 src/lib/content-type-parser.ts diff --git a/src/get-helia.ts b/src/get-helia.ts index 6568b771..e7a46e42 100644 --- a/src/get-helia.ts +++ b/src/get-helia.ts @@ -1,30 +1,21 @@ -import { trustlessGateway } from '@helia/block-brokers' -import { createHeliaHTTP } from '@helia/http' -import { delegatedHTTPRouting } from '@helia/routers' -import { IDBBlockstore } from 'blockstore-idb' -import { IDBDatastore } from 'datastore-idb' + +import { createVerifiedFetch, type VerifiedFetch } from '@helia/verified-fetch' import { getConfig } from './lib/config-db.ts' import { trace } from './lib/logger.ts' -import type { Helia } from '@helia/interface' +import { dnsJsonOverHttps } from '@helia/ipns/dns-resolvers' +import { contentTypeParser } from './lib/content-type-parser.ts' -export async function getHelia (): Promise { +export async function getVerifiedFetch (): Promise { const config = await getConfig() trace(`config-debug: got config for sw location ${self.location.origin}`, config) - const blockstore = new IDBBlockstore('./helia-sw/blockstore') - const datastore = new IDBDatastore('./helia-sw/datastore') - await blockstore.open() - await datastore.open() - const helia = await createHeliaHTTP({ - blockstore, - datastore, - blockBrokers: [ - trustlessGateway({ - gateways: [...config.gateways, 'https://trustless-gateway.link'] - }) - ], - routers: [...config.routers, 'https://delegated-ipfs.dev'].map(rUrl => delegatedHTTPRouting(rUrl)) + const verifiedFetch = await createVerifiedFetch({ + gateways: config.gateways, + routers: config.routers, + dnsResolvers: ['https://delegated-ipfs.dev/dns-query'].map(dnsJsonOverHttps) + }, { + contentTypeParser }) - return helia -} + return verifiedFetch +} \ No newline at end of file diff --git a/src/lib/config-db.ts b/src/lib/config-db.ts index 49708aec..aa7c3858 100644 --- a/src/lib/config-db.ts +++ b/src/lib/config-db.ts @@ -84,8 +84,8 @@ export async function setConfig (config: ConfigDb): Promise { export async function getConfig (): Promise { const db = await openDatabase() - const gateways = await getFromDatabase(db, 'gateways') ?? [] - const routers = await getFromDatabase(db, 'routers') ?? [] + const gateways = await getFromDatabase(db, 'gateways') ?? ['https://trustless-gateway.link'] + const routers = await getFromDatabase(db, 'routers') ?? ['https://delegated-ipfs.dev'] const autoReload = await getFromDatabase(db, 'autoReload') ?? false const debug = await getFromDatabase(db, 'debug') ?? '' debugLib.enable(debug) diff --git a/src/lib/content-type-parser.ts b/src/lib/content-type-parser.ts new file mode 100644 index 00000000..c4123054 --- /dev/null +++ b/src/lib/content-type-parser.ts @@ -0,0 +1,48 @@ +import { type ContentTypeParser } from '@helia/verified-fetch'; +import { fileTypeFromBuffer } from '@sgtpooki/file-type'; + +// default from verified-fetch is application/octect-stream, which forces a download. This is not what we want for MANY file types. +export const defaultMimeType = 'text/html' + + +export const contentTypeParser: ContentTypeParser = async (bytes, fileName) => { + const detectedType = (await fileTypeFromBuffer(bytes))?.mime; + if (detectedType != null) { + return detectedType; + } + if (fileName == null) { + // no other way to determine file-type. + return defaultMimeType; + } + + // no need to include file-types listed at https://github.com/SgtPooki/file-type#supported-file-types + switch (fileName.split('.').pop()) { + case 'css': + return 'text/css'; + case 'html': + return 'text/html'; + case 'js': + return 'application/javascript'; + case 'json': + return 'application/json'; + case 'txt': + return 'text/plain'; + case 'woff2': + return 'font/woff2'; + // see bottom of https://github.com/SgtPooki/file-type#supported-file-types + case 'svg': + return 'image/svg+xml'; + case 'csv': + return 'text/csv'; + case 'doc': + return 'application/msword'; + case 'xls': + return 'application/vnd.ms-excel'; + case 'ppt': + return 'application/vnd.ms-powerpoint'; + case 'msi': + return 'application/x-msdownload'; + default: + return defaultMimeType; + } +}; diff --git a/src/lib/heliaFetch.ts b/src/lib/heliaFetch.ts index 24135a61..ba2366c8 100644 --- a/src/lib/heliaFetch.ts +++ b/src/lib/heliaFetch.ts @@ -1,61 +1,11 @@ -import { dnsJsonOverHttps } from '@helia/ipns/dns-resolvers' -import { createVerifiedFetch, type ContentTypeParser } from '@helia/verified-fetch' -import { fileTypeFromBuffer } from '@sgtpooki/file-type' -import { getConfig } from './config-db.ts' -import { trace } from './logger.ts' -import type { Helia } from '@helia/interface' +import { type VerifiedFetch } from '@helia/verified-fetch' +import { trace, log } from './logger.ts' export interface HeliaFetchOptions { - path: string - helia: Helia + verifiedFetch: VerifiedFetch + verifiedFetchUrl: string signal?: AbortSignal headers?: Headers - id?: string | null - protocol?: string | null -} - -// default from verified-fetch is application/octect-stream, which forces a download. This is not what we want for MANY file types. -const defaultMimeType = 'text/html' -const contentTypeParser: ContentTypeParser = async (bytes, fileName) => { - const detectedType = (await fileTypeFromBuffer(bytes))?.mime - if (detectedType != null) { - return detectedType - } - if (fileName == null) { - // no other way to determine file-type. - return defaultMimeType - } - - // no need to include file-types listed at https://github.com/SgtPooki/file-type#supported-file-types - switch (fileName.split('.').pop()) { - case 'css': - return 'text/css' - case 'html': - return 'text/html' - case 'js': - return 'application/javascript' - case 'json': - return 'application/json' - case 'txt': - return 'text/plain' - case 'woff2': - return 'font/woff2' - // see bottom of https://github.com/SgtPooki/file-type#supported-file-types - case 'svg': - return 'image/svg+xml' - case 'csv': - return 'text/csv' - case 'doc': - return 'application/msword' - case 'xls': - return 'application/vnd.ms-excel' - case 'ppt': - return 'application/vnd.ms-powerpoint' - case 'msi': - return 'application/x-msdownload' - default: - return defaultMimeType - } } // Check for **/*.css/fonts/**/*.ttf urls */ @@ -123,40 +73,9 @@ 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, id, protocol }: HeliaFetchOptions): Promise { - const config = await getConfig() - const verifiedFetch = await createVerifiedFetch({ - gateways: [...config.gateways, 'https://trustless-gateway.link'], - routers: [...config.routers, 'https://delegated-ipfs.dev'], - dnsResolvers: ['https://delegated-ipfs.dev/dns-query'].map(dnsJsonOverHttps) - }, { - contentTypeParser - }) - - let verifiedFetchUrl: string - - if (id != null && protocol != null) { - verifiedFetchUrl = `${protocol}://${id}${path}` - } else { - const pathParts = path.split('/') - - let pathPartIndex = 0 - let namespaceString = pathParts[pathPartIndex++] - if (namespaceString === '') { - // we have a prefixed '/' in the path, use the new index instead - namespaceString = pathParts[pathPartIndex++] - } - if (namespaceString !== 'ipfs' && namespaceString !== 'ipns') { - throw new Error(`only /ipfs or /ipns namespaces supported got ${namespaceString}`) - } - const pathRootString = pathParts[pathPartIndex++] - const contentPath = pathParts.slice(pathPartIndex++).join('/') - verifiedFetchUrl = `${namespaceString}://${pathRootString}/${changeCssFontPath(contentPath)}` - } - - // eslint-disable-next-line no-console - console.log('verifiedFetch for ', verifiedFetchUrl) - return verifiedFetch(verifiedFetchUrl, { +export async function heliaFetch ({ verifiedFetch, verifiedFetchUrl, signal, headers }: HeliaFetchOptions): Promise { + log('verifiedFetch for ', verifiedFetchUrl) + const response = await verifiedFetch(verifiedFetchUrl, { signal, headers, // TODO redirect: 'manual', // enable when http urls are supported by verified-fetch: https://github.com/ipfs-shipyard/helia-service-worker-gateway/issues/62#issuecomment-1977661456 @@ -164,4 +83,35 @@ export async function heliaFetch ({ path, helia, signal, headers, id, protocol } trace(`${e.type}: `, e.detail) } }) + + return response +} + + +export interface GetVerifiedFetchUrlOptions { + protocol?: string | null + id?: string | null + path: string +} + + +export function getVerifiedFetchUrl({ protocol, id, path }: GetVerifiedFetchUrlOptions): string { + if (id != null && protocol != null) { + return `${protocol}://${id}${path}` + } + + const pathParts = path.split('/') + + let pathPartIndex = 0 + let namespaceString = pathParts[pathPartIndex++] + if (namespaceString === '') { + // we have a prefixed '/' in the path, use the new index instead + namespaceString = pathParts[pathPartIndex++] + } + if (namespaceString !== 'ipfs' && namespaceString !== 'ipns') { + throw new Error(`only /ipfs or /ipns namespaces supported got ${namespaceString}`) + } + const pathRootString = pathParts[pathPartIndex++] + const contentPath = pathParts.slice(pathPartIndex++).join('/') + return `${namespaceString}://${pathRootString}/${changeCssFontPath(contentPath)}` } diff --git a/src/sw.ts b/src/sw.ts index 8334aaf8..1c019c6b 100644 --- a/src/sw.ts +++ b/src/sw.ts @@ -1,16 +1,18 @@ import mime from 'mime-types' -import { getHelia } from './get-helia.ts' +import { getVerifiedFetch } from './get-helia.ts' import { HeliaServiceWorkerCommsChannel, type ChannelMessage } from './lib/channel.ts' import { getSubdomainParts } from './lib/get-subdomain-parts.ts' -import { heliaFetch } from './lib/heliaFetch.ts' +import { getVerifiedFetchUrl, heliaFetch } from './lib/heliaFetch.ts' import { error, log, trace } from './lib/logger.ts' import { findOriginIsolationRedirect } from './lib/path-or-subdomain.ts' -import type { Helia } from '@helia/interface' +import type { VerifiedFetch } from '@helia/verified-fetch' declare let self: ServiceWorkerGlobalScope -let helia: Helia -self.addEventListener('install', () => { +let verifiedFetch: VerifiedFetch + +self.addEventListener('install', (event) => { + // 👇 When a new version of the SW is installed, activate immediately void self.skipWaiting() }) @@ -21,8 +23,8 @@ self.addEventListener('activate', () => { const { action } = message.data switch (action) { case 'RELOAD_CONFIG': - void getHelia().then((newHelia) => { - helia = newHelia + void getVerifiedFetch().then((newVerifiedFetch) => { + verifiedFetch = newVerifiedFetch }) break default: @@ -63,10 +65,6 @@ const fetchHandler = async ({ path, request }: FetchHandlerArg): Promise Date: Wed, 6 Mar 2024 15:03:27 +0100 Subject: [PATCH 02/10] chore: remove empty line --- src/get-helia.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/get-helia.ts b/src/get-helia.ts index e7a46e42..e85fd6c1 100644 --- a/src/get-helia.ts +++ b/src/get-helia.ts @@ -1,4 +1,3 @@ - import { createVerifiedFetch, type VerifiedFetch } from '@helia/verified-fetch' import { getConfig } from './lib/config-db.ts' import { trace } from './lib/logger.ts' From 989e68f94f3ce876e51d7ab3a7c5b99a606fc431 Mon Sep 17 00:00:00 2001 From: Daniel N <2color@users.noreply.github.com> Date: Wed, 6 Mar 2024 16:53:30 +0100 Subject: [PATCH 03/10] fix: load config iframe using a hash fragment fixes #88 --- src/components/config.tsx | 4 ++-- src/lib/is-config-page.ts | 2 +- src/redirectPage.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/config.tsx b/src/components/config.tsx index 9bfcfa8f..800bea11 100644 --- a/src/components/config.tsx +++ b/src/components/config.tsx @@ -45,9 +45,9 @@ export default (): JSX.Element | null => { } // we get the iframe origin from a query parameter called 'origin', if this is loaded in an iframe // TODO: why we need this origin here? where is targetOrigin used? - const targetOrigin = decodeURIComponent(window.location.search.split('origin=')[1]) + const targetOrigin = decodeURIComponent(window.location.hash.split('@origin=')[1]) const config = await getConfig() - + console.log(`config-page: postMessage config to origin `, config, origin) /** * The reload page in the parent window is listening for this message, and then it passes a RELOAD_CONFIG message to the service worker */ diff --git a/src/lib/is-config-page.ts b/src/lib/is-config-page.ts index 2f31ab6e..a152e98d 100644 --- a/src/lib/is-config-page.ts +++ b/src/lib/is-config-page.ts @@ -1,5 +1,5 @@ export function isConfigPage (): boolean { const isConfigPathname = window.location.pathname === '/config' - const isConfigHashPath = window.location.hash === '#/config' // needed for _redirects and IPFS hosted sw gateways + const isConfigHashPath = window.location.hash.startsWith('#/config') // needed for _redirects and IPFS hosted sw gateways return isConfigPathname || isConfigHashPath } diff --git a/src/redirectPage.tsx b/src/redirectPage.tsx index 77f9f63a..dd8adc40 100644 --- a/src/redirectPage.tsx +++ b/src/redirectPage.tsx @@ -10,7 +10,7 @@ const ConfigIframe = (): JSX.Element => { const { parentDomain } = getSubdomainParts(window.location.href) const portString = window.location.port === '' ? '' : `:${window.location.port}` - const iframeSrc = `${window.location.protocol}//${parentDomain}${portString}/config?origin=${encodeURIComponent(window.location.origin)}` + const iframeSrc = `${window.location.protocol}//${parentDomain}${portString}#/config@origin=${encodeURIComponent(window.location.origin)}` return (