Skip to content

Commit 43e6494

Browse files
committed
wip: subdomain gateway via HTTP proxy
This adds support for subdomain gateways introduced in go-ipfs v0.5.0, specifically, one running at *.localhost subdomains They key challenge was to ensure *.localhost DNS names resolve to 127.0.0.1 on all platforms. We do that by setting up HTTP Gateway port of local go-ipfs to act as HTTP Proxy. This removes DNS lookup step from the browser, and go-ipfs ships with implicit support for subdomain gateway when request comes with "Host: <cid>.ipfs.localhost:8080" or similar. We register HTTP proxy using Firefox and Chromium-specific APIs, but the end result is the same. When enables, default gateway uses 'localhost' hostname (subdomain gateway) instead of '127.0.0.1' (path gateway) and every path-pased request gets redirected to subdomain by go-ipfs itself, which decreases complexity on browser extension side. This is work in progress. Note: this requires uodates to is-ipfs which were not published yet.
1 parent 4bf9d09 commit 43e6494

17 files changed

+522
-155
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ build
77
npm-debug.log
88
yarn-error.log
99
crowdin.yml
10+
.connect-deps*
1011
.*~
1112
add-on/dist
1213
add-on/webui/

add-on/_locales/en/messages.json

+8
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,14 @@
279279
"message": "Redirect requests for IPFS resources to the Custom gateway",
280280
"description": "An option description on the Preferences screen (option_useCustomGateway_description)"
281281
},
282+
"option_useSubdomainProxy_title": {
283+
"message": "Use Subdomain Proxy",
284+
"description": "An option title on the Preferences screen (option_useSubdomainProxy_title)"
285+
},
286+
"option_useSubdomainProxy_description": {
287+
"message": "Use Custom Gateway as HTTP Proxy to enable Origin isolation per content root at *.ipfs.localhost",
288+
"description": "An option description on the Preferences screen (option_useSubdomainProxy_description)"
289+
},
282290
"option_dnslinkRedirect_title": {
283291
"message": "Load websites from Custom Gateway",
284292
"description": "An option title on the Preferences screen (option_dnslinkRedirect_title)"

add-on/_locales/nl/messages.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@
268268
"description": "An option description on the Preferences screen (option_customGatewayUrl_description)"
269269
},
270270
"option_customGatewayUrl_warning": {
271-
"message": "IPFS content will be blocked from loading on HTTPS websites unless your gateway URL starts with “http://127.0.0.1”, “http://[::1]” or “https://”",
271+
"message": "IPFS content will be blocked from loading on HTTPS websites unless your gateway URL starts with “http://localhost”, “http://127.0.0.1”, “http://[::1]” or “https://”",
272272
"description": "A warning on the Preferences screen, displayed when URL does not belong to Secure Context (option_customGatewayUrl_warning)"
273273
},
274274
"option_useCustomGateway_title": {

add-on/manifest.common.json

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"unlimitedStorage",
2121
"contextMenus",
2222
"clipboardWrite",
23+
"proxy",
2324
"webNavigation",
2425
"webRequest",
2526
"webRequestBlocking"

add-on/src/lib/http-proxy.js

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
'use strict'
2+
/* eslint-env browser, webextensions */
3+
4+
const browser = require('webextension-polyfill')
5+
const { safeURL } = require('./options')
6+
7+
const debug = require('debug')
8+
const log = debug('ipfs-companion:http-proxy')
9+
log.error = debug('ipfs-companion:http-proxy:error')
10+
11+
// Preface:
12+
//
13+
// When go-ipfs runs on localhost, it exposes two types of gateway:
14+
// 127.0.0.1:8080 - old school path gateway
15+
// localhost:8080 - subdomain gateway supporting Origins like $cid.ipfs.localhost
16+
// More: https://docs-beta.ipfs.io/how-to/address-ipfs-on-web/#subdomain-gateway
17+
//
18+
// In a web browser contexts we care about Origin per content root (CID)
19+
// because entire web security model uses it as a basis for sandboxing and
20+
// access controls:
21+
// https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy
22+
23+
// registerSubdomainProxy is necessary wourkaround for supporting subdomains
24+
// under 'localhost' (*.ipfs.localhost) because some operating systems do not
25+
// resolve them to local IP and return NX error not found instead
26+
async function registerSubdomainProxy (getState, runtime) {
27+
const { useSubdomainProxy: enable, gwURLString } = getState()
28+
29+
// HTTP Proxy feature is exposed on the gateway port
30+
// Just ensure we use localhost IP to remove any dependency on DNS
31+
const proxy = safeURL(gwURLString, { useLocalhostName: false })
32+
33+
// Firefox uses own APIs for selective proxying
34+
if (runtime.isFirefox) {
35+
return registerSubdomainProxyFirefox(enable, proxy.hostname, proxy.port)
36+
}
37+
38+
// at this point we asume Chromium
39+
return registerSubdomainProxyChromium(enable, proxy.host)
40+
}
41+
42+
// storing listener for later
43+
var onRequestProxyListener
44+
45+
// registerSubdomainProxyFirefox sets proxy using API available in Firefox
46+
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/proxy/onRequest
47+
async function registerSubdomainProxyFirefox (enable, host, port) {
48+
const { onRequest } = browser.proxy
49+
50+
// always remove the old listener (host and port could change)
51+
const oldListener = onRequestProxyListener
52+
if (oldListener && onRequest.hasListener(oldListener)) {
53+
onRequest.removeListener(oldListener)
54+
}
55+
56+
if (enable) {
57+
// create new listener with the latest host:port
58+
onRequestProxyListener = (request) => ({ type: 'http', host, port })
59+
60+
// register the listener
61+
onRequest.addListener(onRequestProxyListener, {
62+
urls: ['http://*.localhost/*'],
63+
incognito: false
64+
})
65+
log(`enabled ${host}:${port} as HTTP proxy for *.localhost`)
66+
return
67+
}
68+
69+
// at this point we effectively disabled proxy
70+
log('disabled HTTP proxy for *.localhost')
71+
}
72+
73+
// registerSubdomainProxyChromium sets proxy using API available in Chromium
74+
// https://developer.chrome.com/extensions/proxy
75+
async function registerSubdomainProxyChromium (enable, proxyHost) {
76+
const get = async (opts) => new Promise(resolve => chrome.proxy.settings.get(opts, resolve))
77+
const set = async (opts) => new Promise(resolve => chrome.proxy.settings.set(opts, resolve))
78+
const clear = async (opts) => new Promise(resolve => chrome.proxy.settings.clear(opts, resolve))
79+
const scope = 'regular_only'
80+
81+
// read current proxy settings
82+
const settings = await get({ incognito: false })
83+
84+
// set or update, if enabled
85+
if (enable) {
86+
// PAC script enables selective routing to PROXY at host+port
87+
// here, PROXY is the same as HTTP API endpoint
88+
const pacConfig = {
89+
mode: 'pac_script',
90+
pacScript: {
91+
data: 'function FindProxyForURL(url, host) {\n' +
92+
" if (shExpMatch(host, '*.localhost'))\n" +
93+
` return 'PROXY ${proxyHost}';\n` +
94+
" return 'DIRECT';\n" +
95+
'}'
96+
}
97+
}
98+
set({ value: pacConfig, scope })
99+
log(`enabled ${proxyHost} as HTTP proxy for *.localhost`)
100+
// log('updated chrome.proxy.settings', await get({ incognito: false }))
101+
return
102+
}
103+
104+
// else: remove any existing proxy settings
105+
if (settings && settings.levelOfControl === 'controlled_by_this_extension') {
106+
// remove any proxy settings ipfs-companion set up before
107+
await clear({ scope })
108+
log('disabled HTTP proxy for *.localhost')
109+
}
110+
}
111+
112+
module.exports.registerSubdomainProxy = registerSubdomainProxy

add-on/src/lib/ipfs-companion.js

+26-9
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ log.error = debug('ipfs-companion:main:error')
88
const browser = require('webextension-polyfill')
99
const toMultiaddr = require('uri-to-multiaddr')
1010
const pMemoize = require('p-memoize')
11-
const { optionDefaults, storeMissingOptions, migrateOptions } = require('./options')
11+
const { optionDefaults, storeMissingOptions, migrateOptions, guiURLString } = require('./options')
1212
const { initState, offlinePeerCount } = require('./state')
13-
const { createIpfsPathValidator } = require('./ipfs-path')
13+
const { createIpfsPathValidator, sameGateway } = require('./ipfs-path')
1414
const createDnslinkResolver = require('./dnslink')
1515
const { createRequestModifier } = require('./ipfs-request')
1616
const { initIpfsClient, destroyIpfsClient } = require('./ipfs-client')
@@ -22,6 +22,7 @@ const createInspector = require('./inspector')
2222
const { createRuntimeChecks } = require('./runtime-checks')
2323
const { createContextMenus, findValueForContext, contextMenuCopyAddressAtPublicGw, contextMenuCopyRawCid, contextMenuCopyCanonicalAddress, contextMenuViewOnGateway } = require('./context-menus')
2424
const createIpfsProxy = require('./ipfs-proxy')
25+
const { registerSubdomainProxy } = require('./http-proxy')
2526
const { showPendingLandingPages } = require('./on-installed')
2627

2728
// init happens on addon load in background/background.js
@@ -83,6 +84,7 @@ module.exports = async function init () {
8384
ipfsProxyContentScript = await registerIpfsProxyContentScript()
8485
log('register all listeners')
8586
registerListeners()
87+
await registerSubdomainProxy(getState, runtime)
8688
await setApiStatusUpdateInterval(options.ipfsApiPollMs)
8789
log('init done')
8890
await showPendingLandingPages()
@@ -353,7 +355,7 @@ module.exports = async function init () {
353355
// Chrome does not permit for both pageAction and browserAction to be enabled at the same time
354356
// https://github.com/ipfs-shipyard/ipfs-companion/issues/398
355357
if (runtime.isFirefox && ipfsPathValidator.isIpfsPageActionsContext(url)) {
356-
if (url.startsWith(state.gwURLString) || url.startsWith(state.apiURLString)) {
358+
if (sameGateway(url, state.gwURL) || sameGateway(url, state.apiURL)) {
357359
await browser.pageAction.setIcon({ tabId: tabId, path: '/icons/ipfs-logo-on.svg' })
358360
await browser.pageAction.setTitle({ tabId: tabId, title: browser.i18n.getMessage('pageAction_titleIpfsAtCustomGateway') })
359361
} else {
@@ -619,7 +621,8 @@ module.exports = async function init () {
619621
case 'customGatewayUrl':
620622
state.gwURL = new URL(change.newValue)
621623
state.gwURLString = state.gwURL.toString()
622-
state.webuiRootUrl = `${state.gwURLString}ipfs/${state.webuiCid}/`
624+
// TODO: for now we load webui from API port, should we remove this?
625+
// state.webuiRootUrl = `${state.gwURLString}ipfs/${state.webuiCid}/`
623626
break
624627
case 'publicGatewayUrl':
625628
state.pubGwURL = new URL(change.newValue)
@@ -632,8 +635,26 @@ module.exports = async function init () {
632635
case 'useCustomGateway':
633636
state.redirect = change.newValue
634637
break
638+
case 'useSubdomainProxy':
639+
state[key] = change.newValue
640+
// More work is needed, as this key decides how requests are routed
641+
// to the gateway:
642+
await browser.storage.local.set({
643+
// We need to update the hostname in customGatewayUrl:
644+
// 127.0.0.1 - path gateway
645+
// localhost - subdomain gateway
646+
customGatewayUrl: guiURLString(
647+
state.gwURLString, {
648+
useLocalhostName: state.useSubdomainProxy
649+
}
650+
)
651+
})
652+
// Finally, update proxy settings based on the state
653+
await registerSubdomainProxy(getState, runtime)
654+
break
635655
case 'ipfsProxy':
636656
state[key] = change.newValue
657+
// This is window.ipfs proxy, requires update of the content script:
637658
ipfsProxyContentScript = await registerIpfsProxyContentScript()
638659
break
639660
case 'dnslinkPolicy':
@@ -642,16 +663,12 @@ module.exports = async function init () {
642663
await browser.storage.local.set({ detectIpfsPathHeader: true })
643664
}
644665
break
645-
case 'recoverFailedHttpRequests':
646-
state[key] = change.newValue
647-
break
648666
case 'logNamespaces':
649667
shouldReloadExtension = true
650668
state[key] = localStorage.debug = change.newValue
651669
break
670+
case 'recoverFailedHttpRequests':
652671
case 'importDir':
653-
state[key] = change.newValue
654-
break
655672
case 'linkify':
656673
case 'catchUnhandledProtocols':
657674
case 'displayNotifications':

0 commit comments

Comments
 (0)