Skip to content
Open
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
5 changes: 4 additions & 1 deletion add-on/src/lib/ipfs-client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ export async function destroyIpfsClient (browser) {
log('destroy ipfs client')
if (!client) return
try {
await client.destroy(browser)
// Only destroy if client has a destroy method (not SW Gateway)
if (client.destroy) {
await client.destroy(browser)
}
await _reloadIpfsClientDependents(browser) // sync (API stopped working)
} finally {
client = null
Expand Down
15 changes: 15 additions & 0 deletions add-on/src/lib/ipfs-companion.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { guiURLString, migrateOptions, optionDefaults, safeURL, storeMissingOpti
import { cleanupRules, getExtraInfoSpec } from './redirect-handler/blockOrObserve.js'
import createRuntimeChecks from './runtime-checks.js'
import { initState, offlinePeerCount } from './state.js'
import { redirectToSwGateway, isFeatureDisabledForSwGateway } from './ipfs-request-sw-gateway.js'

// this won't work in webworker context. Needs to be enabled manually
// https://github.com/debug-js/debug/issues/916
Expand Down Expand Up @@ -169,6 +170,12 @@ export default async function init (inQuickImport = false) {
}

function onBeforeRequest (request) {
if (state && (state.isServiceWorkerGateway || state.ipfsNodeType === 'service_worker_gateway')) {
const swRedirect = redirectToSwGateway(state, request)
if (swRedirect) {
return swRedirect
}
}
return modifyRequest.onBeforeRequest(request)
}

Expand Down Expand Up @@ -648,6 +655,14 @@ export default async function init (inQuickImport = false) {
case 'ipfsNodeType':
shouldRestartIpfsClient = true
state[key] = change.newValue
state.isServiceWorkerGateway = (change.newValue === 'service_worker_gateway')
break
case 'serviceWorkerGatewayUrl':
state[key] = change.newValue
if (change.newValue) {
state.swGwURL = safeURL(change.newValue)
state.swGwURLString = state.swGwURL?.toString()
}
break
case 'ipfsNodeConfig':
shouldRestartIpfsClient = true
Expand Down
72 changes: 72 additions & 0 deletions add-on/src/lib/ipfs-request-sw-gateway.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use strict'
/* eslint-env browser, webextensions */

import debug from 'debug'
const log = debug('ipfs-companion:sw-gateway')

/**
* Handles Service Worker Gateway redirects
* @param {import('../types/companion.js').CompanionState} state
* @param {browser.WebRequest.OnBeforeRequestDetailsType | {url: string, type?: string}} request
* @returns {Object|undefined}
*/
export function redirectToSwGateway(state, request) {
// Only redirect if SW Gateway is active
if (!state.isServiceWorkerGateway || !state.active) {
return
}

if (!request || !request.url) {
return
}

try {
const url = new URL(request.url)

// Skip if already redirected to SW gateway
if (url.hostname.includes('inbrowser.link') ||
url.hostname.includes('inbrowser.dev')) {
return
}

// Check for IPFS/IPNS paths
const match = url.pathname.match(/^\/(ipfs|ipns)\/([^\/]+)(\/.*)?$/)
if (!match) return

const [, protocol, cid, path = ''] = match
const gatewayUrl = state.serviceWorkerGatewayUrl || 'https://inbrowser.link'
const gwUrl = new URL(gatewayUrl)

// Use PATH format instead of subdomain to preserve CID case
// https://inbrowser.link/ipfs/QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco/wiki/
const redirectUrl = `${gwUrl.protocol}//${gwUrl.hostname}/${protocol}/${cid}${path}${url.search}${url.hash}`

log(`Redirecting to SW Gateway: ${request.url} → ${redirectUrl}`)
return { redirectUrl }
} catch (error) {
log.error('Error in SW Gateway redirect:', error)
return
}
}

/**
* Check if feature should be disabled for SW Gateway
* @param {string} feature
* @param {import('../types/companion.js').CompanionState} state
* @returns {boolean}
*/
export function isFeatureDisabledForSwGateway(feature, state) {
if (!state.isServiceWorkerGateway) return false

const disabledFeatures = [
'quickImport',
'ipfsProxy',
'pinning',
'mfsSupport',
'ipfsNodeInfo',
'swarmPeers',
'webui'
]

return disabledFeatures.includes(feature)
}
39 changes: 38 additions & 1 deletion add-on/src/lib/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import isFQDN from 'is-fqdn'
import { isIPv4, isIPv6 } from 'is-ip'
import { POSSIBLE_NODE_TYPES } from './state.js'
export const SERVICE_WORKER_GATEWAY_NODE = 'service_worker_gateway'

/**
* @type {Readonly<import('../types/companion.js').CompanionOptions>}
Expand Down Expand Up @@ -35,7 +36,9 @@ export const optionDefaults = Object.freeze({
importDir: '/ipfs-companion-imports/%Y-%M-%D_%h%m%s/',
useLatestWebUI: false,
dismissedUpdate: null,
openViaWebUI: true
openViaWebUI: true,
serviceWorkerGatewayUrl: 'https://inbrowser.link',
serviceWorkerGatewayFallbackUrl: 'https://inbrowser.dev'
})

function buildDefaultIpfsNodeConfig () {
Expand Down Expand Up @@ -98,6 +101,18 @@ export function guiURLString (url, opts) {
return safeURL(url, opts).toString().replace(/\/$/, '')
}

export function normalizeGatewayURL (value) {
try {
const u = new URL(value)
if (u.protocol !== 'https:' && (u.hostname.endsWith('inbrowser.link') || u.hostname.endsWith('inbrowser.dev'))) {
u.protocol = 'https:'
}
return guiURLString(u)
} catch {
// fall back to prod default if invalid
return optionDefaults.serviceWorkerGatewayUrl
}
}
// ensure value is a valid URL.hostname (FQDN || ipv4 || ipv6 WITH brackets)
export function isHostname (x) {
if (isFQDN(x) || isIPv4(x)) {
Expand Down Expand Up @@ -228,5 +243,27 @@ export async function migrateOptions (storage, debug) {
}
}

// v3.x: normalize/introduce Service Worker Gateway options
{
// Map any historical synonyms to the canonical value
const { ipfsNodeType } = await storage.get(['ipfsNodeType'])
if (ipfsNodeType === 'service-worker-gateway' || ipfsNodeType === 'sw-gateway') {
await storage.set({ ipfsNodeType: SERVICE_WORKER_GATEWAY_NODE })
}

// Ensureing SW gateway URLs exist and are normalized (drop trailing slash, enforce https on known hosts)
const { serviceWorkerGatewayUrl, serviceWorkerGatewayFallbackUrl } =
await storage.get(['serviceWorkerGatewayUrl', 'serviceWorkerGatewayFallbackUrl'])

const desiredPrimary = serviceWorkerGatewayUrl || optionDefaults.serviceWorkerGatewayUrl
const desiredFallback = serviceWorkerGatewayFallbackUrl || optionDefaults.serviceWorkerGatewayFallbackUrl

const normalized = {
serviceWorkerGatewayUrl: normalizeGatewayURL(desiredPrimary),
serviceWorkerGatewayFallbackUrl: normalizeGatewayURL(desiredFallback)
}
await storage.set(normalized)
}

// TODO: refactor this, so migrations only run once (like https://github.com/sindresorhus/electron-store#migrations)
}
15 changes: 14 additions & 1 deletion add-on/src/lib/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import { isHostname, safeURL } from './options.js'

export const offlinePeerCount = -1
export const POSSIBLE_NODE_TYPES = ['external']
export const SERVICE_WORKER_GATEWAY_NODE = 'service_worker_gateway'
export const POSSIBLE_NODE_TYPES = ['external',SERVICE_WORKER_GATEWAY_NODE]

/**
*
Expand Down Expand Up @@ -36,6 +37,18 @@ export function initState (options, overrides) {
state.gwURLString = state.gwURL?.toString()
delete state.customGatewayUrl
state.dnslinkPolicy = String(options.dnslinkPolicy) === 'false' ? false : options.dnslinkPolicy
state.isServiceWorkerGateway =
options.ipfsNodeType === SERVICE_WORKER_GATEWAY_NODE

// Normalize and expose current/fallback SW gateway endpoints (keep originals too)
if (options.serviceWorkerGatewayUrl) {
state.swGwURL = safeURL(options.serviceWorkerGatewayUrl)
state.swGwURLString = state.swGwURL?.toString()
}
if (options.serviceWorkerGatewayFallbackUrl) {
state.swGwFallbackURL = safeURL(options.serviceWorkerGatewayFallbackUrl)
state.swGwFallbackURLString = state.swGwFallbackURL?.toString()
}

// attach helper functions
state.activeIntegrations = (url) => {
Expand Down
71 changes: 70 additions & 1 deletion add-on/src/options/forms/gateways-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,73 @@ import { POSSIBLE_NODE_TYPES } from '../../lib/state.js'
// https://github.com/ipfs-shipyard/ipfs-companion/issues/648
const secureContextUrl = /^https:\/\/|^http:\/\/localhost|^http:\/\/127.0.0.1|^http:\/\/\[::1\]/

// Add this function before the export default
function renderServiceWorkerGatewayOptions({
ipfsNodeType,
serviceWorkerGatewayUrl,
onOptionChange
}) {
const isSwGateway = ipfsNodeType === 'service_worker_gateway'
const onSwGatewayUrlChange = onOptionChange('serviceWorkerGatewayUrl', guiURLString)

if (!isSwGateway) return null

return html`
<div class="flex-row-ns pb0-ns">
<label for="serviceWorkerGatewayUrl">
<dl>
<dt>Service Worker Gateway URL</dt>
<dd>
The Service Worker Gateway endpoint for trustless content retrieval.
<div class="mt2">
<button
type="button"
class="button-reset pv1 ph2 ba b--gray hover-bg-light-gray"
onclick=${() => {
const el = document.getElementById('serviceWorkerGatewayUrl')
el.value = 'https://inbrowser.link'
el.dispatchEvent(new Event('change', { bubbles: true }))
}}>
Use Production
</button>

<button
type="button"
class="ml2 button-reset pv1 ph2 ba b--gray hover-bg-light-gray"
onclick=${() => {
const el = document.getElementById('serviceWorkerGatewayUrl')
el.value = 'https://inbrowser.dev'
el.dispatchEvent(new Event('change', { bubbles: true }))
}}>
Use Staging
</button>
</div>
</dd>
</dl>
</label>
<input
class="bg-white navy self-center-ns"
id="serviceWorkerGatewayUrl"
type="url"
inputmode="url"
required
pattern="https?://.+"
spellcheck="false"
title="Service Worker Gateway URL"
onchange=${onSwGatewayUrlChange}
value=${serviceWorkerGatewayUrl || 'https://inbrowser.link'} />
</div>
<div class="pa3 bg-washed-yellow navy br2 f6">
<strong>ℹ️ Service Worker Gateway Mode:</strong>
<ul class="mt2 mb0 pl3">
<li>Provides trustless, browser-based IPFS content fetching</li>
<li>Upload and pinning features are disabled in this mode</li>
<li>No local IPFS node required</li>
</ul>
</div>
`
}

export default function gatewaysForm ({
ipfsNodeType,
customGatewayUrl,
Expand All @@ -21,7 +88,8 @@ export default function gatewaysForm ({
enabledOn,
publicGatewayUrl,
publicSubdomainGatewayUrl,
onOptionChange
onOptionChange,
serviceWorkerGatewayUrl
}) {
const onCustomGatewayUrlChange = onOptionChange('customGatewayUrl', (url) => guiURLString(url, { useLocalhostName: useSubdomains }))
const onUseCustomGatewayChange = onOptionChange('useCustomGateway')
Expand All @@ -38,6 +106,7 @@ export default function gatewaysForm ({
<form>
<fieldset class="mb3 pa1 pa4-ns pa3 bg-snow-muted charcoal">
<h2 class="ttu tracked f6 fw4 teal mt0-ns mb3-ns mb1 mt2 ">${browser.i18n.getMessage('option_header_gateways')}</h2>
${renderServiceWorkerGatewayOptions({ ipfsNodeType, serviceWorkerGatewayUrl, onOptionChange })}
<div class="flex-row-ns pb0-ns">
<label for="publicGatewayUrl">
<dl>
Expand Down
5 changes: 5 additions & 0 deletions add-on/src/options/forms/ipfs-node-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export default function ipfsNodeForm ({ ipfsNodeType, onOptionChange }) {
selected=${ipfsNodeType === 'external'}>
${browser.i18n.getMessage('option_ipfsNodeType_external')}
</option>
<option
value='service_worker_gateway'
selected=${ipfsNodeType === 'service_worker_gateway'}>
Service Worker Gateway (Trustless)
</option>
</select>
</div>
</fieldset>
Expand Down
14 changes: 11 additions & 3 deletions add-on/src/types/companion.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@

export interface CompanionOptions {
active: boolean
ipfsNodeType: string
ipfsNodeConfig: string
publicGatewayUrl: string
publicSubdomainGatewayUrl: string
Expand All @@ -27,7 +26,10 @@ export interface CompanionOptions {
importDir: string
useLatestWebUI: boolean
dismissedUpdate: null | string
openViaWebUI: boolean
openViaWebUI: boolean,
ipfsNodeType: 'external' | 'service_worker_gateway'
serviceWorkerGatewayUrl: string
serviceWorkerGatewayFallbackUrl: string
}

export interface CompanionState extends Omit<CompanionOptions, 'publicGatewayUrl' | 'publicSubdomainGatewayUrl' | 'useCustomGateway' | 'ipfsApiUrl' | 'customGatewayUrl'> {
Expand All @@ -43,7 +45,13 @@ export interface CompanionState extends Omit<CompanionOptions, 'publicGatewayUrl
gwURLString: string
activeIntegrations: (url: string) => boolean
localGwAvailable: boolean
webuiRootUrl: string
webuiRootUrl: string | null
nodeActive: boolean
isServiceWorkerGateway?: boolean
swGwURL?: URL
swGwURLString?: string
swGwFallbackURL?: URL
swGwFallbackURLString?: string
}

interface SwitchToggleArguments {
Expand Down
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"@istanbuljs/esm-loader-hook": "0.2.0",
"@types/chai": "4.3.20",
"@types/debug": "4.1.12",
"@types/is-fqdn": "^2.0.0",
"@types/mocha": "10.0.10",
"@types/selenium-webdriver": "4.1.28",
"@types/webextension-polyfill": "0.10.7",
Expand Down