Skip to content

feat: probe for local gateway and add to config #403

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

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
10 changes: 8 additions & 2 deletions src/components/local-storage-toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ interface LocalStorageToggleProps {
className?: string
onClick?: React.MouseEventHandler<HTMLDivElement>
resetKey: number
disabled?: boolean
handleChange?(toggleValue: boolean): void
}

export const LocalStorageToggle: React.FC<LocalStorageToggleProps> = ({
Expand All @@ -22,6 +24,8 @@ export const LocalStorageToggle: React.FC<LocalStorageToggleProps> = ({
defaultValue,
resetKey,
localStorageKey,
disabled,
handleChange,
...props
}) => {
const [isChecked, setIsChecked] = useState(() => {
Expand All @@ -32,10 +36,11 @@ export const LocalStorageToggle: React.FC<LocalStorageToggleProps> = ({
setIsChecked(getLocalStorageValue(localStorageKey, defaultValue))
}, [resetKey])

const handleChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const handleChangeInner = (event: React.ChangeEvent<HTMLInputElement>): void => {
const newValue = event.target.checked
setIsChecked(newValue)
localStorage.setItem(localStorageKey, String(newValue))
handleChange?.(newValue)
}

return (
Expand All @@ -45,9 +50,10 @@ export const LocalStorageToggle: React.FC<LocalStorageToggleProps> = ({

<input
type="checkbox"
disabled={disabled}
id={localStorageKey}
checked={isChecked}
onChange={handleChange}
onChange={handleChangeInner}
className="dn"
/>
<label
Expand Down
36 changes: 36 additions & 0 deletions src/hooks/use-local-gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useEffect, useState } from 'react'
import { hasLocalGateway } from '../lib/local-gateway.js'
import type { ComponentLogger } from '@libp2p/logger'

export interface UseLocalGatewayProps {
logger: ComponentLogger
}
export interface UseLocalGatewayReturn {
available: boolean
checked: boolean
}

export function useLocalGateway ({ logger }: UseLocalGatewayProps): UseLocalGatewayReturn {
const log = logger.forComponent('use-local-gateway')
const [localGatewayAvailable, setLocalGatewayAvailable] = useState<boolean | null>(null)

useEffect(() => {
void (async () => {
try {
if (await hasLocalGateway()) {
setLocalGatewayAvailable(true)
} else {
setLocalGatewayAvailable(false)
}
} catch (e) {
log.error('failed to probe for local gateway', e)
setLocalGatewayAvailable(false)
}
})()
}, [])

return {
available: localGatewayAvailable === true,
checked: localGatewayAvailable !== null
}
}
27 changes: 27 additions & 0 deletions src/lib/local-gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { uiLogger } from './logger.js'

export const localGwUrl = 'http://127.0.0.1:8080'
// bafkqacdjobthgllto4fa is the inline CID for the string 'ipfs-sw'
const localGwTestUrl = 'http://127.0.0.1:8080/ipfs/bafkqacdjobthgllto4fa'

const log = uiLogger.forComponent('local-gateway-prober')

export async function hasLocalGateway (): Promise<boolean> {
try {
log(`probing for local trustless gateway at ${localGwTestUrl}`)
const resp = await fetch(localGwTestUrl, {
cache: 'no-store',
method: 'HEAD'
})
if (!resp.ok) {
return false
}
log(`found local trustless gateway at ${localGwTestUrl}`)
return true
} catch (e: unknown) {
log.error('failed to probe trustless gateway', e)
const currentLocation = location.href.replace(location.pathname, '').replace(location.search, '').replace(location.hash, '').replace(/\/$/, '')
log(`You may want to enable CORS if you do have a local gateway running:\nipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["${currentLocation}", "http://127.0.0.1:5001", "https://webui.ipfs.io"]'`)
return false
}
}
1 change: 1 addition & 0 deletions src/lib/local-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const LOCAL_STORAGE_KEYS = {
enableWss: getLocalStorageKey('config', 'enableWss'),
enableWebTransport: getLocalStorageKey('config', 'enableWebTransport'),
enableRecursiveGateways: getLocalStorageKey('config', 'enableRecursiveGateways'),
useLocalGateway: getLocalStorageKey('config', 'useLocalGateway'),
enableGatewayProviders: getLocalStorageKey('config', 'enableGatewayProviders'),
dnsJsonResolvers: getLocalStorageKey('config', 'dnsJsonResolvers'),
debug: getLocalStorageKey('config', 'debug')
Expand Down
39 changes: 38 additions & 1 deletion src/pages/config.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import Header from '../components/Header.jsx'
import { Collapsible } from '../components/collapsible.jsx'
import { InputSection } from '../components/input-section.jsx'
Expand All @@ -8,8 +8,10 @@ import { ServiceWorkerReadyButton } from '../components/sw-ready-button.jsx'
import { ConfigProvider } from '../context/config-context.jsx'
import { RouteContext } from '../context/router-context.jsx'
import { ServiceWorkerProvider } from '../context/service-worker-context.jsx'
import { useLocalGateway } from '../hooks/use-local-gateway.js'
import { HeliaServiceWorkerCommsChannel } from '../lib/channel.js'
import { defaultDnsJsonResolvers, defaultEnableGatewayProviders, defaultEnableRecursiveGateways, defaultEnableWebTransport, defaultEnableWss, defaultGateways, defaultRouters, getConfig, localStorageToIdb, resetConfig } from '../lib/config-db.js'
import { localGwUrl } from '../lib/local-gateway.js'
import { LOCAL_STORAGE_KEYS, convertDnsResolverInputToObject, convertDnsResolverObjectToInput, convertUrlArrayToInput, convertUrlInputToArray } from '../lib/local-storage.js'
import { getUiComponentLogger, uiLogger } from '../lib/logger.js'
import './default-page-styles.css'
Expand Down Expand Up @@ -85,6 +87,7 @@ function ConfigPage (): React.JSX.Element | null {
const { gotoPage } = React.useContext(RouteContext)
const [error, setError] = useState<Error | null>(null)
const [resetKey, setResetKey] = useState(0)
const { checked: localGatewayChecked, available: localGatewayAvailable } = useLocalGateway({ logger: uiComponentLogger })

const isLoadedInIframe = window.self !== window.top

Expand Down Expand Up @@ -138,6 +141,30 @@ function ConfigPage (): React.JSX.Element | null {
setResetKey((prev) => prev + 1)
}, [])

const localGatewayDescription = useMemo(() => {
if (localGatewayAvailable) {
return `A local gateway has been detected at ${localGwUrl}. Enable the local gateway?`
}
if (!localGatewayAvailable && localGatewayChecked) {
return `No local gateway detected at ${localGwUrl}. This toggle has been disabled.`
}
return 'Checking for local gateway...'
}, [localGatewayAvailable, localGatewayChecked])

const handleLocalGatewayChange = useCallback((toggleValue: boolean) => {
// get current value of the gateways list
const currentGateways = localStorage.getItem(LOCAL_STORAGE_KEYS.config.gateways)
const isAlreadyInList = currentGateways?.includes(localGwUrl) === true
// need to add to gateways list if true, and remove it if it's false and it's in the list
if (toggleValue && !isAlreadyInList) {
// add to the list
localStorage.setItem(LOCAL_STORAGE_KEYS.config.gateways, `${localGwUrl}\n${currentGateways}`)
} else if (!toggleValue && isAlreadyInList) {
// remove from the list
localStorage.setItem(LOCAL_STORAGE_KEYS.config.gateways, currentGateways?.replace(`${localGwUrl}\n`, '') ?? '')
}
}, [])

const newlineToJsonArray = (value: string): string => JSON.stringify(convertUrlInputToArray(value))
const jsonToNewlineString = (value: string): string => convertUrlArrayToInput(JSON.parse(value))

Expand Down Expand Up @@ -192,6 +219,16 @@ function ConfigPage (): React.JSX.Element | null {
localStorageKey={LOCAL_STORAGE_KEYS.config.enableRecursiveGateways}
resetKey={resetKey}
/>
<LocalStorageToggle
className="e2e-config-page-input"
label="Use local trustless gateway"
description={localGatewayDescription}
defaultValue={false}
disabled={!localGatewayChecked || !localGatewayAvailable}
localStorageKey={LOCAL_STORAGE_KEYS.config.useLocalGateway}
resetKey={resetKey}
handleChange={handleLocalGatewayChange}
/>
<LocalStorageInput
className="e2e-config-page-input e2e-config-page-input-gateways"
description="A newline delimited list of recursive trustless gateway URLs."
Expand Down
3 changes: 3 additions & 0 deletions src/sw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,9 @@ async function requestRouting (event: FetchEvent, url: URL): Promise<boolean> {
} else if (!isValidRequestForSW(event)) {
log.trace('not a valid request for helia-sw, ignoring ', event.request.url)
return false
} else if (url.href.includes('127.0.0.1:8080/ipfs/bafkqacdjobthgllto4fa')) {
log('Passing through local gateway test url', event.request.url)
return false
} else if (url.href.includes('bafkqaaa.ipfs')) {
/**
* `bafkqaaa` is an empty inline CID, so this response *is* valid, and prevents additional network calls.
Expand Down
Loading