diff --git a/package-lock.json b/package-lock.json index ae2ab1af..77f590f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,18 +10,11 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@helia/block-brokers": "^2.0.1", - "@helia/http": "^1.0.0", - "@helia/interface": "^4.0.0", - "@helia/ipns": "^6.0.0", - "@helia/routers": "^1.0.0", + "@helia/ipns": "^6.0.1", "@helia/verified-fetch": "^1.1.0", "@libp2p/logger": "^4.0.6", "@sgtpooki/file-type": "^1.0.1", - "blockstore-idb": "^1.1.8", - "datastore-idb": "^2.1.8", "debug": "^4.3.4", - "mime-types": "^2.1.35", "multiformats": "^11.0.2", "react": "^18.2.0", "react-dom": "^18.2.0" @@ -6563,23 +6556,6 @@ "version": "13.0.1", "license": "Apache-2.0 OR MIT" }, - "node_modules/blockstore-idb": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/blockstore-idb/-/blockstore-idb-1.1.8.tgz", - "integrity": "sha512-pW3aXuexGBhTo02Fs7pmYnwKpp2AGBLs1VWGY+IzHOF43S4Ue2HPn8N39OcJrGpqweIjofqZfWuwgnEDnelbXA==", - "dependencies": { - "blockstore-core": "^4.0.0", - "idb": "^8.0.0", - "interface-blockstore": "^5.0.0", - "interface-store": "^5.0.0", - "multiformats": "^13.0.1" - } - }, - "node_modules/blockstore-idb/node_modules/multiformats": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.1.0.tgz", - "integrity": "sha512-HzdtdBwxsIkzpeXzhQ5mAhhuxcHbjEHH+JQoxt7hG/2HGFjjwyolLo7hbaexcnhoEuV4e0TNJ8kkpMjiEYY4VQ==" - }, "node_modules/blork": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/blork/-/blork-9.3.0.tgz", @@ -8337,26 +8313,6 @@ "it-all": "^3.0.0" } }, - "node_modules/datastore-idb": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/datastore-idb/-/datastore-idb-2.1.8.tgz", - "integrity": "sha512-L3k4kRLlP8YuUUuZsNxmRA9lu8RasEekgE8uLzivNOksWr/v/+ExxVvjZutOHDVlky5/spwJ5MKvB8GHfq7Fgw==", - "dependencies": { - "datastore-core": "^9.0.0", - "idb": "^8.0.0", - "interface-datastore": "^8.0.0", - "it-filter": "^3.0.4", - "it-sort": "^3.0.4" - } - }, - "node_modules/datastore-idb/node_modules/it-sort": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/it-sort/-/it-sort-3.0.4.tgz", - "integrity": "sha512-tvnC93JZZWjX4UxALy0asow0dzXabkoaRbrPJKClTKhNCqw4gzHr+H5axf1gohcthedRRkqd/ae+wl7WqoxFhw==", - "dependencies": { - "it-all": "^3.0.0" - } - }, "node_modules/date-fns": { "version": "1.30.1", "dev": true, @@ -12462,11 +12418,6 @@ "postcss": "^8.1.0" } }, - "node_modules/idb": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.0.tgz", - "integrity": "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw==" - }, "node_modules/ieee754": { "version": "1.2.1", "funding": [ @@ -15579,6 +15530,7 @@ }, "node_modules/mime-db": { "version": "1.52.0", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -15586,6 +15538,7 @@ }, "node_modules/mime-types": { "version": "2.1.35", + "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" diff --git a/package.json b/package.json index 7155c4db..70f830b5 100644 --- a/package.json +++ b/package.json @@ -32,18 +32,11 @@ } }, "dependencies": { - "@helia/block-brokers": "^2.0.1", - "@helia/http": "^1.0.0", - "@helia/interface": "^4.0.0", - "@helia/ipns": "^6.0.0", - "@helia/routers": "^1.0.0", + "@helia/ipns": "^6.0.1", "@helia/verified-fetch": "^1.1.0", "@libp2p/logger": "^4.0.6", "@sgtpooki/file-type": "^1.0.1", - "blockstore-idb": "^1.1.8", - "datastore-idb": "^2.1.8", "debug": "^4.3.4", - "mime-types": "^2.1.35", "multiformats": "^11.0.2", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/src/components/config.tsx b/src/components/config.tsx index 9bfcfa8f..9775863c 100644 --- a/src/components/config.tsx +++ b/src/components/config.tsx @@ -3,7 +3,8 @@ import { ConfigContext } from '../context/config-context.tsx' import { HeliaServiceWorkerCommsChannel } from '../lib/channel.ts' import { getConfig, loadConfigFromLocalStorage } from '../lib/config-db.ts' import { LOCAL_STORAGE_KEYS } from '../lib/local-storage.ts' -import { Collapsible } from './collapsible' +import { trace } from '../lib/logger.ts' +import { Collapsible } from './collapsible.tsx' import LocalStorageInput from './local-storage-input.tsx' import { LocalStorageToggle } from './local-storage-toggle' import { ServiceWorkerReadyButton } from './sw-ready-button.tsx' @@ -45,15 +46,16 @@ 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() - + trace('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 */ window.parent?.postMessage({ source: 'helia-sw-config-iframe', target: 'PARENT', action: 'RELOAD_CONFIG', config }, { targetOrigin }) + trace('config-page: RELOAD_CONFIG sent to parent window') }, []) useEffect(() => { @@ -66,9 +68,11 @@ export default (): JSX.Element | null => { const saveConfig = useCallback(async () => { try { await loadConfigFromLocalStorage() + trace('config-page: sending RELOAD_CONFIG to service worker') // update the BASE_URL service worker - // TODO: use channel.messageAndWaitForResponse to ensure that the config is loaded before proceeding. - channel.postMessage({ target: 'SW', action: 'RELOAD_CONFIG' }) + await channel.messageAndWaitForResponse('SW', { target: 'SW', action: 'RELOAD_CONFIG' }) + // base_domain service worker is updated + trace('config-page: RELOAD_CONFIG_SUCCESS for %s', window.location.origin) // update the ..BASE_URL service worker await postFromIframeToParentSw() setConfigExpanded(false) diff --git a/src/get-helia.ts b/src/get-helia.ts index 6568b771..a4cfa9bf 100644 --- a/src/get-helia.ts +++ b/src/get-helia.ts @@ -1,30 +1,20 @@ -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 { dnsJsonOverHttps } from '@helia/ipns/dns-resolvers' +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 { contentTypeParser } from './lib/content-type-parser.ts' +import { log } from './lib/logger.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() + log(`config-debug: got config for sw location ${self.location.origin}`, config) - 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 ?? ['https://trustless-gateway.link'], + routers: config.routers ?? ['https://delegated-ipfs.dev'], + dnsResolvers: ['https://delegated-ipfs.dev/dns-query'].map(dnsJsonOverHttps) + }, { + contentTypeParser }) - return helia + return verifiedFetch } diff --git a/src/lib/common.ts b/src/lib/common.ts index 61c338db..0c3486cc 100644 --- a/src/lib/common.ts +++ b/src/lib/common.ts @@ -7,4 +7,5 @@ export enum COLORS { export enum ChannelActions { RELOAD_CONFIG = 'RELOAD_CONFIG', + RELOAD_CONFIG_SUCCESS = 'RELOAD_CONFIG_SUCCESS' } diff --git a/src/lib/config-db.ts b/src/lib/config-db.ts index 49708aec..9c526fdd 100644 --- a/src/lib/config-db.ts +++ b/src/lib/config-db.ts @@ -54,8 +54,8 @@ export async function loadConfigFromLocalStorage (): Promise { if (typeof globalThis.localStorage !== 'undefined') { const db = await openDatabase() const localStorage = globalThis.localStorage - const localStorageGatewaysString = localStorage.getItem(LOCAL_STORAGE_KEYS.config.gateways) ?? '[]' - const localStorageRoutersString = localStorage.getItem(LOCAL_STORAGE_KEYS.config.routers) ?? '[]' + const localStorageGatewaysString = localStorage.getItem(LOCAL_STORAGE_KEYS.config.gateways) ?? '["https://trustless-gateway.link"]' + const localStorageRoutersString = localStorage.getItem(LOCAL_STORAGE_KEYS.config.routers) ?? '["https://delegated-ipfs.dev"]' const autoReload = localStorage.getItem(LOCAL_STORAGE_KEYS.config.autoReload) === 'true' const debug = localStorage.getItem(LOCAL_STORAGE_KEYS.config.debug) ?? '' const gateways = JSON.parse(localStorageGatewaysString) @@ -71,8 +71,9 @@ export async function loadConfigFromLocalStorage (): Promise { } export async function setConfig (config: ConfigDb): Promise { - log('config-debug: setting config', config) - debugLib.enable(config.debug ?? '') + debugLib.enable(config.debug ?? '') // set debug level first. + log('config-debug: setting config %O for domain %s', config, window.location.origin) + const db = await openDatabase() await setInDatabase(db, 'gateways', config.gateways) await setInDatabase(db, 'routers', config.routers) @@ -84,8 +85,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..ea8db9d5 --- /dev/null +++ b/src/lib/content-type-parser.ts @@ -0,0 +1,47 @@ +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 deleted file mode 100644 index 24135a61..00000000 --- a/src/lib/heliaFetch.ts +++ /dev/null @@ -1,167 +0,0 @@ -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' - -export interface HeliaFetchOptions { - path: string - helia: Helia - 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 */ -const cssPathRegex = /(?.*\.css)(?.*\.(ttf|otf|woff|woff2){1})$/ - -/** - * Maps relative paths to font-faces from css files to the correct path from the root. - * - * e.g. in a css file (like specs.ipfs.tech's /ipns/specs.ipfs.tech/css/ipseity.min.css), you will find lines like: - * ``` - * @font-face { - * font-family: 'Plex'; - * font-style: normal; - * font-weight: 100; - * src: local('IBM Plex Sans'), - * local('IBM-Plex-Sans'), - * url('/fonts/IBMPlexSans-Thin.ttf') format('opentype'); - * } - * ``` - * which results in a request to `/ipns/specs.ipfs.tech/css/ipseity.min.css/fonts/IBMPlexSans-Thin.ttf`. Instead, - * we want to request `/ipns/specs.ipfs.tech/fonts/IBMPlexSans-Thin.ttf`. - * - * /ipns/blog.libp2p.io/assets/css/0.styles.4520169f.css/fonts/Montserrat-Medium.d8478173.woff - */ -function changeCssFontPath (path: string): string { - const match = path.match(cssPathRegex) - if (match == null) { - trace(`changeCssFontPath: No match for ${path}`) - return path - } - const { cssPath, fontPath } = match.groups as { cssPath?: string, fontPath?: string } - if (cssPath == null || fontPath == null) { - trace(`changeCssFontPath: No groups for ${path}`, match.groups) - return path - } - - trace(`changeCssFontPath: Changing font path from ${path} to ${fontPath}`) - return fontPath -} - -/** - * Test files: - * bafkreienxxjqg3jomg5b75k7547dgf7qlbd3qpxy2kbg537ck3rol4mcve - text - https://bafkreienxxjqg3jomg5b75k7547dgf7qlbd3qpxy2kbg537ck3rol4mcve.ipfs.w3s.link/?filename=test.txt - * bafkreicafxt3zr4cshf7qteztjzl62ouxqrofu647e44wt7s2iaqjn7bra - image/jpeg - http://127.0.0.1:8080/ipfs/bafkreicafxt3zr4cshf7qteztjzl62ouxqrofu647e44wt7s2iaqjn7bra?filename=bafkreicafxt3zr4cshf7qteztjzl62ouxqrofu647e44wt7s2iaqjn7bra - * broken - QmY7fzZEpgDUqZ7BEePSS5JxxezDj3Zy36EEpWSmKmv5mo - image/jpeg - http://127.0.0.1:8080/ipfs/QmY7fzZEpgDUqZ7BEePSS5JxxezDj3Zy36EEpWSmKmv5mo?filename=QmY7fzZEpgDUqZ7BEePSS5JxxezDj3Zy36EEpWSmKmv5mo - * web3_storageLogo.svg - bafkreif4ufrfpfcmqn5ltjvmeisgv4k7ykqz2mjygpngtwt4bijxosidqa - image/svg+xml - https://bafkreif4ufrfpfcmqn5ltjvmeisgv4k7ykqz2mjygpngtwt4bijxosidqa.ipfs.dweb.link/?filename=Web3.Storage-logo.svg - * broken - bafybeiekildl23opzqcsufewlbadhbabs6pyqg35tzpfavgtjyhchyikxa - video/quicktime - https://bafybeiekildl23opzqcsufewlbadhbabs6pyqg35tzpfavgtjyhchyikxa.ipfs.dweb.link - * stock_skateboarder.webm - bafkreiezuss4xkt5gu256vjccx7vocoksxk77vwmdrpwoumfbbxcy2zowq - video/webm (147.78 KiB) - https://bafkreiezuss4xkt5gu256vjccx7vocoksxk77vwmdrpwoumfbbxcy2zowq.ipfs.dweb.link - * bafybeierkpfmf4vhtdiujgahfptiyriykoetxf3rmd3vcxsdemincpjoyu - video/mp4 (2.80 MiB) - https://bafybeierkpfmf4vhtdiujgahfptiyriykoetxf3rmd3vcxsdemincpjoyu.ipfs.dweb.link - * bugbunny.mov - bafybeidsp6fva53dexzjycntiucts57ftecajcn5omzfgjx57pqfy3kwbq - video/mp4 (2.80 MiB) - https://bafybeieekpb73vggby3m35mnpre3pngdcdnnu47u25ehsz4r3xbmqum6nu.ipfs.w3s.link/ipfs/bafybeidsp6fva53dexzjycntiucts57ftecajcn5omzfgjx57pqfy3kwbq?filename=BugBunny.mov - * ipfs.tech website - QmeUdoMyahuQUPHS2odrZEL6yk2HnNfBJ147BeLXsZuqLJ - text/html - https://QmeUdoMyahuQUPHS2odrZEL6yk2HnNfBJ147BeLXsZuqLJ.ipfs.w3s.link - */ - -/** - * 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 (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. - * * 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, 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, { - 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 - onProgress: (e) => { - trace(`${e.type}: `, e.detail) - } - }) -} 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..4970a7ac 100644 --- a/src/redirectPage.tsx +++ b/src/redirectPage.tsx @@ -4,13 +4,13 @@ import { ServiceWorkerContext } from './context/service-worker-context.tsx' import { HeliaServiceWorkerCommsChannel } from './lib/channel.ts' import { setConfig, type ConfigDb } from './lib/config-db.ts' import { getSubdomainParts } from './lib/get-subdomain-parts' -import { error } from './lib/logger.ts' +import { error, trace } from './lib/logger.ts' 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 (