Skip to content

Commit

Permalink
wip: subdomain gateway via HTTP proxy
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
lidel committed Mar 23, 2020
1 parent 4bf9d09 commit 43e6494
Show file tree
Hide file tree
Showing 17 changed files with 522 additions and 155 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ build
npm-debug.log
yarn-error.log
crowdin.yml
.connect-deps*
.*~
add-on/dist
add-on/webui/
Expand Down
8 changes: 8 additions & 0 deletions add-on/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,14 @@
"message": "Redirect requests for IPFS resources to the Custom gateway",
"description": "An option description on the Preferences screen (option_useCustomGateway_description)"
},
"option_useSubdomainProxy_title": {
"message": "Use Subdomain Proxy",
"description": "An option title on the Preferences screen (option_useSubdomainProxy_title)"
},
"option_useSubdomainProxy_description": {
"message": "Use Custom Gateway as HTTP Proxy to enable Origin isolation per content root at *.ipfs.localhost",
"description": "An option description on the Preferences screen (option_useSubdomainProxy_description)"
},
"option_dnslinkRedirect_title": {
"message": "Load websites from Custom Gateway",
"description": "An option title on the Preferences screen (option_dnslinkRedirect_title)"
Expand Down
2 changes: 1 addition & 1 deletion add-on/_locales/nl/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@
"description": "An option description on the Preferences screen (option_customGatewayUrl_description)"
},
"option_customGatewayUrl_warning": {
"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://”",
"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://”",
"description": "A warning on the Preferences screen, displayed when URL does not belong to Secure Context (option_customGatewayUrl_warning)"
},
"option_useCustomGateway_title": {
Expand Down
1 change: 1 addition & 0 deletions add-on/manifest.common.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"unlimitedStorage",
"contextMenus",
"clipboardWrite",
"proxy",
"webNavigation",
"webRequest",
"webRequestBlocking"
Expand Down
112 changes: 112 additions & 0 deletions add-on/src/lib/http-proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
'use strict'
/* eslint-env browser, webextensions */

const browser = require('webextension-polyfill')
const { safeURL } = require('./options')

const debug = require('debug')
const log = debug('ipfs-companion:http-proxy')
log.error = debug('ipfs-companion:http-proxy:error')

// Preface:
//
// When go-ipfs runs on localhost, it exposes two types of gateway:
// 127.0.0.1:8080 - old school path gateway
// localhost:8080 - subdomain gateway supporting Origins like $cid.ipfs.localhost
// More: https://docs-beta.ipfs.io/how-to/address-ipfs-on-web/#subdomain-gateway
//
// In a web browser contexts we care about Origin per content root (CID)
// because entire web security model uses it as a basis for sandboxing and
// access controls:
// https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy

// registerSubdomainProxy is necessary wourkaround for supporting subdomains
// under 'localhost' (*.ipfs.localhost) because some operating systems do not
// resolve them to local IP and return NX error not found instead
async function registerSubdomainProxy (getState, runtime) {
const { useSubdomainProxy: enable, gwURLString } = getState()

// HTTP Proxy feature is exposed on the gateway port
// Just ensure we use localhost IP to remove any dependency on DNS
const proxy = safeURL(gwURLString, { useLocalhostName: false })

// Firefox uses own APIs for selective proxying
if (runtime.isFirefox) {
return registerSubdomainProxyFirefox(enable, proxy.hostname, proxy.port)
}

// at this point we asume Chromium
return registerSubdomainProxyChromium(enable, proxy.host)
}

// storing listener for later
var onRequestProxyListener

// registerSubdomainProxyFirefox sets proxy using API available in Firefox
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/proxy/onRequest
async function registerSubdomainProxyFirefox (enable, host, port) {
const { onRequest } = browser.proxy

// always remove the old listener (host and port could change)
const oldListener = onRequestProxyListener
if (oldListener && onRequest.hasListener(oldListener)) {
onRequest.removeListener(oldListener)
}

if (enable) {
// create new listener with the latest host:port
onRequestProxyListener = (request) => ({ type: 'http', host, port })

// register the listener
onRequest.addListener(onRequestProxyListener, {
urls: ['http://*.localhost/*'],
incognito: false
})
log(`enabled ${host}:${port} as HTTP proxy for *.localhost`)
return
}

// at this point we effectively disabled proxy
log('disabled HTTP proxy for *.localhost')
}

// registerSubdomainProxyChromium sets proxy using API available in Chromium
// https://developer.chrome.com/extensions/proxy
async function registerSubdomainProxyChromium (enable, proxyHost) {
const get = async (opts) => new Promise(resolve => chrome.proxy.settings.get(opts, resolve))
const set = async (opts) => new Promise(resolve => chrome.proxy.settings.set(opts, resolve))
const clear = async (opts) => new Promise(resolve => chrome.proxy.settings.clear(opts, resolve))
const scope = 'regular_only'

// read current proxy settings
const settings = await get({ incognito: false })

// set or update, if enabled
if (enable) {
// PAC script enables selective routing to PROXY at host+port
// here, PROXY is the same as HTTP API endpoint
const pacConfig = {
mode: 'pac_script',
pacScript: {
data: 'function FindProxyForURL(url, host) {\n' +
" if (shExpMatch(host, '*.localhost'))\n" +
` return 'PROXY ${proxyHost}';\n` +
" return 'DIRECT';\n" +
'}'
}
}
set({ value: pacConfig, scope })
log(`enabled ${proxyHost} as HTTP proxy for *.localhost`)
// log('updated chrome.proxy.settings', await get({ incognito: false }))
return
}

// else: remove any existing proxy settings
if (settings && settings.levelOfControl === 'controlled_by_this_extension') {
// remove any proxy settings ipfs-companion set up before
await clear({ scope })
log('disabled HTTP proxy for *.localhost')
}
}

module.exports.registerSubdomainProxy = registerSubdomainProxy
35 changes: 26 additions & 9 deletions add-on/src/lib/ipfs-companion.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ log.error = debug('ipfs-companion:main:error')
const browser = require('webextension-polyfill')
const toMultiaddr = require('uri-to-multiaddr')
const pMemoize = require('p-memoize')
const { optionDefaults, storeMissingOptions, migrateOptions } = require('./options')
const { optionDefaults, storeMissingOptions, migrateOptions, guiURLString } = require('./options')
const { initState, offlinePeerCount } = require('./state')
const { createIpfsPathValidator } = require('./ipfs-path')
const { createIpfsPathValidator, sameGateway } = require('./ipfs-path')
const createDnslinkResolver = require('./dnslink')
const { createRequestModifier } = require('./ipfs-request')
const { initIpfsClient, destroyIpfsClient } = require('./ipfs-client')
Expand All @@ -22,6 +22,7 @@ const createInspector = require('./inspector')
const { createRuntimeChecks } = require('./runtime-checks')
const { createContextMenus, findValueForContext, contextMenuCopyAddressAtPublicGw, contextMenuCopyRawCid, contextMenuCopyCanonicalAddress, contextMenuViewOnGateway } = require('./context-menus')
const createIpfsProxy = require('./ipfs-proxy')
const { registerSubdomainProxy } = require('./http-proxy')
const { showPendingLandingPages } = require('./on-installed')

// init happens on addon load in background/background.js
Expand Down Expand Up @@ -83,6 +84,7 @@ module.exports = async function init () {
ipfsProxyContentScript = await registerIpfsProxyContentScript()
log('register all listeners')
registerListeners()
await registerSubdomainProxy(getState, runtime)
await setApiStatusUpdateInterval(options.ipfsApiPollMs)
log('init done')
await showPendingLandingPages()
Expand Down Expand Up @@ -353,7 +355,7 @@ module.exports = async function init () {
// Chrome does not permit for both pageAction and browserAction to be enabled at the same time
// https://github.com/ipfs-shipyard/ipfs-companion/issues/398
if (runtime.isFirefox && ipfsPathValidator.isIpfsPageActionsContext(url)) {
if (url.startsWith(state.gwURLString) || url.startsWith(state.apiURLString)) {
if (sameGateway(url, state.gwURL) || sameGateway(url, state.apiURL)) {
await browser.pageAction.setIcon({ tabId: tabId, path: '/icons/ipfs-logo-on.svg' })
await browser.pageAction.setTitle({ tabId: tabId, title: browser.i18n.getMessage('pageAction_titleIpfsAtCustomGateway') })
} else {
Expand Down Expand Up @@ -619,7 +621,8 @@ module.exports = async function init () {
case 'customGatewayUrl':
state.gwURL = new URL(change.newValue)
state.gwURLString = state.gwURL.toString()
state.webuiRootUrl = `${state.gwURLString}ipfs/${state.webuiCid}/`
// TODO: for now we load webui from API port, should we remove this?
// state.webuiRootUrl = `${state.gwURLString}ipfs/${state.webuiCid}/`
break
case 'publicGatewayUrl':
state.pubGwURL = new URL(change.newValue)
Expand All @@ -632,8 +635,26 @@ module.exports = async function init () {
case 'useCustomGateway':
state.redirect = change.newValue
break
case 'useSubdomainProxy':
state[key] = change.newValue
// More work is needed, as this key decides how requests are routed
// to the gateway:
await browser.storage.local.set({
// We need to update the hostname in customGatewayUrl:
// 127.0.0.1 - path gateway
// localhost - subdomain gateway
customGatewayUrl: guiURLString(
state.gwURLString, {
useLocalhostName: state.useSubdomainProxy
}
)
})
// Finally, update proxy settings based on the state
await registerSubdomainProxy(getState, runtime)
break
case 'ipfsProxy':
state[key] = change.newValue
// This is window.ipfs proxy, requires update of the content script:
ipfsProxyContentScript = await registerIpfsProxyContentScript()
break
case 'dnslinkPolicy':
Expand All @@ -642,16 +663,12 @@ module.exports = async function init () {
await browser.storage.local.set({ detectIpfsPathHeader: true })
}
break
case 'recoverFailedHttpRequests':
state[key] = change.newValue
break
case 'logNamespaces':
shouldReloadExtension = true
state[key] = localStorage.debug = change.newValue
break
case 'recoverFailedHttpRequests':
case 'importDir':
state[key] = change.newValue
break
case 'linkify':
case 'catchUnhandledProtocols':
case 'displayNotifications':
Expand Down
Loading

0 comments on commit 43e6494

Please sign in to comment.