Skip to content

Commit

Permalink
Merge pull request #853 from feat/subdomain-proxy-support
Browse files Browse the repository at this point in the history
feat: *.localhost subdomain gateway support with http proxy
  • Loading branch information
lidel authored Apr 5, 2020
2 parents 597fd6f + 5bce2a2 commit 5cc81be
Show file tree
Hide file tree
Showing 28 changed files with 2,319 additions and 1,611 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
10 changes: 9 additions & 1 deletion 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_useSubdomains_title": {
"message": "Use Subdomains",
"description": "An option title on the Preferences screen (option_useSubdomains_title)"
},
"option_useSubdomains_description": {
"message": "Isolate content roots from each other by loading them from subdomains at *.localhost and creating a unique Origin for each CID, IPNS or DNSLink record. Requires a local go-ipfs 0.5.0 or later.",
"description": "An option description on the Preferences screen (option_useSubdomains_description)"
},
"option_dnslinkRedirect_title": {
"message": "Load websites from Custom Gateway",
"description": "An option title on the Preferences screen (option_dnslinkRedirect_title)"
Expand All @@ -296,7 +304,7 @@
"description": "An option description on the Preferences screen (option_dnslinkDataPreload_description)"
},
"option_dnslinkRedirect_warning": {
"message": "Redirecting to a path-based gateway breaks Origin-based security isolation of DNSLink websites. Make sure you understand related risks.",
"message": "Avoid using this if your IPFS Node does not support *.ipfs.localhost. Redirecting to a path-based gateway breaks Origin-based security isolation of DNSLink websites. Make sure you understand related risks.",
"description": "A warning on the Preferences screen, displayed when URL does not belong to Secure Context (option_customGatewayUrl_warning)"
},
"option_noIntegrationsHostnames_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
13 changes: 13 additions & 0 deletions add-on/manifest.chromium.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
{
"minimum_chrome_version": "72",
"permissions": [
"<all_urls>",
"idle",
"tabs",
"notifications",
"storage",
"unlimitedStorage",
"contextMenus",
"clipboardWrite",
"webNavigation",
"webRequest",
"webRequestBlocking"
],
"incognito": "not_allowed"
}
13 changes: 0 additions & 13 deletions add-on/manifest.common.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,6 @@
"38": "icons/png/ipfs-logo-on_38.png",
"128": "icons/png/ipfs-logo-on_128.png"
},
"permissions": [
"<all_urls>",
"idle",
"tabs",
"notifications",
"storage",
"unlimitedStorage",
"contextMenus",
"clipboardWrite",
"webNavigation",
"webRequest",
"webRequestBlocking"
],
"background": {
"page": "dist/background/background.html"
},
Expand Down
14 changes: 14 additions & 0 deletions add-on/manifest.firefox.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@
"default_title": "__MSG_pageAction_titleNonIpfs__",
"default_popup": "dist/popup/page-action/index.html"
},
"permissions": [
"<all_urls>",
"idle",
"tabs",
"notifications",
"proxy",
"storage",
"unlimitedStorage",
"contextMenus",
"clipboardWrite",
"webNavigation",
"webRequest",
"webRequestBlocking"
],
"content_scripts": [ ],
"protocol_handlers": [
{
Expand Down
37 changes: 21 additions & 16 deletions add-on/src/lib/dnslink.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ const IsIpfs = require('is-ipfs')
const LRU = require('lru-cache')
const { default: PQueue } = require('p-queue')
const { offlinePeerCount } = require('./state')
const { pathAtHttpGateway } = require('./ipfs-path')

// TODO: add Preferences toggle to disable redirect of DNSLink websites (while keeping async dnslink lookup)
const { ipfsContentPath, sameGateway, pathAtHttpGateway } = require('./ipfs-path')

module.exports = function createDnslinkResolver (getState) {
// DNSLink lookup result cache
Expand Down Expand Up @@ -47,11 +45,11 @@ module.exports = function createDnslinkResolver (getState) {
return state.dnslinkPolicy &&
requestUrl.startsWith('http') &&
!IsIpfs.url(requestUrl) &&
!requestUrl.startsWith(state.apiURLString) &&
!requestUrl.startsWith(state.gwURLString)
!sameGateway(requestUrl, state.apiURL) &&
!sameGateway(requestUrl, state.gwURL)
},

dnslinkRedirect (url, dnslink) {
dnslinkAtGateway (url, dnslink) {
if (typeof url === 'string') {
url = new URL(url)
}
Expand All @@ -61,9 +59,8 @@ module.exports = function createDnslinkResolver (getState) {
// to load the correct path from IPFS
// - https://github.com/ipfs/ipfs-companion/issues/298
const ipnsPath = dnslinkResolver.convertToIpnsPath(url)
const gateway = state.ipfsNodeType === 'embedded' ? state.pubGwURLString : state.gwURLString
// TODO: redirect to `ipns://` if hasNativeProtocolHandler === true
return { redirectUrl: pathAtHttpGateway(ipnsPath, gateway) }
const gateway = state.localGwAvailable ? state.gwURLString : state.pubGwURLString
return pathAtHttpGateway(ipnsPath, gateway)
}
},

Expand Down Expand Up @@ -111,7 +108,7 @@ module.exports = function createDnslinkResolver (getState) {
preloadUrlCache.set(url, true)
const dnslink = await dnslinkResolver.resolve(url)
if (!dnslink) return
if (state.ipfsNodeType === 'embedded') return
if (!state.localGwAvailable) return
if (state.peerCount < 1) return
return preloadQueue.add(async () => {
const { pathname } = new URL(url)
Expand All @@ -128,7 +125,13 @@ module.exports = function createDnslinkResolver (getState) {
let apiProvider
// TODO: fix DNS resolver for ipfsNodeType='embedded:chromesockets', for now use ipfs.io
if (!state.ipfsNodeType.startsWith('embedded') && state.peerCount !== offlinePeerCount) {
apiProvider = state.apiURLString
// Use gw port so it can be a GET:
// Chromium does not execute onBeforeSendHeaders for synchronous calls
// made from the same extension context as onBeforeSendHeaders
// which means we are unable to fixup Origin on the fly for this
// This will no longer be needed when we switch
// to async lookup via ipfs.dns everywhere
apiProvider = state.gwURLString
} else {
// fallback to resolver at public gateway
apiProvider = 'https://ipfs.io/'
Expand Down Expand Up @@ -204,19 +207,21 @@ module.exports = function createDnslinkResolver (getState) {
// in url.hostname OR in url.pathname (/ipns/<fqdn>)
// and return matching FQDN if present
findDNSLinkHostname (url) {
const { hostname, pathname } = new URL(url)
// check //foo.tld/ipns/<fqdn>
if (IsIpfs.ipnsPath(pathname)) {
if (!url) return
// Normalize subdomain and path gateways to to /ipns/<fqdn>
const contentPath = ipfsContentPath(url)
if (IsIpfs.ipnsPath(contentPath)) {
// we may have false-positives here, so we do additional checks below
const ipnsRoot = pathname.match(/^\/ipns\/([^/]+)/)[1]
const ipnsRoot = contentPath.match(/^\/ipns\/([^/]+)/)[1]
// console.log('findDNSLinkHostname ==> inspecting IPNS root', ipnsRoot)
// Ignore PeerIDs, match DNSLink only
if (!IsIpfs.cid(ipnsRoot) && dnslinkResolver.readAndCacheDnslink(ipnsRoot)) {
// console.log('findDNSLinkHostname ==> found DNSLink for FQDN in url.pathname: ', ipnsRoot)
return ipnsRoot
}
}
// check //<fqdn>/foo/bar
// Check main hostname
const { hostname } = new URL(url)
if (dnslinkResolver.readAndCacheDnslink(hostname)) {
// console.log('findDNSLinkHostname ==> found DNSLink for url.hostname', hostname)
return hostname
Expand Down
155 changes: 155 additions & 0 deletions add-on/src/lib/http-proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
'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
//
// State in Q2 2020:
// - Chromium hardcodes `localhost` name to point at local IP and proxy is not
// really necessary. The code is here (inactivE) in case we need it in the future.
// - Firefox requires proxy to avoid DNS lookup, but there is an open issue
// that will remove that need at some point:
// https://bugzilla.mozilla.org/show_bug.cgi?id=1220810
async function registerSubdomainProxy (getState, runtime, notify) {
// At the moment only firefox requires proxy registration
if (!runtime.isFirefox) return

try {
const { active, useSubdomains, gwURLString } = getState()
const enable = active && useSubdomains

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

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

// At this point we would asume Chromium, but its not needed atm
// Uncomment below if ever needed (+ add 'proxy' permission to manifest.json)
// return await registerSubdomainProxyChromium(enable, hostname, port)
} catch (err) {
// registerSubdomainProxy is just a failsafe, not necessary in most cases,
// so we should not break init when it fails.
// For now we just log error and exit as NOOP
log.error('registerSubdomainProxy failed', err)
// Show pop-up only the first time, during init() when notify is passed
try {
if (notify) notify('notify_addonIssueTitle', 'notify_addonIssueMsg')
} catch (_) {
}
}
}

// 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, hostname, 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 note: the listener is
// handling requests made to all localhost ports (limitation of the API,
// port is ignored) that is why we manually check port inside of the listener
onRequestProxyListener = (request) => {
if (new URL(request.url).port === port) {
return { type: 'http', host: hostname, port }
}
return { type: 'direct' }
}

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

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

/*
* Chromium 80 does not need proxy, so below is not used.
* Uncomment below if ever needed (+ add 'proxy' permission to manifest.json)
// Helpers for converting callback chrome.* API to promises
const cb = (resolve, reject) => (result) => {
const err = chrome.runtime.lastError
if (err) return reject(err)
return resolve(result)
}
const get = async (opts) => new Promise((resolve, reject) => chrome.proxy.settings.get(opts, cb(resolve, reject)))
const set = async (opts) => new Promise((resolve, reject) => chrome.proxy.settings.set(opts, cb(resolve, reject)))
const clear = async (opts) => new Promise((resolve, reject) => chrome.proxy.settings.clear(opts, cb(resolve, reject)))
// registerSubdomainProxyChromium sets proxy using API available in Chromium
// https://developer.chrome.com/extensions/proxy
async function registerSubdomainProxyChromium (enable, hostname, port) {
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:${port}'))\n` +
` return 'PROXY ${hostname}:${port}';\n` +
" return 'DIRECT';\n" +
'}'
}
}
await set({ value: pacConfig, scope })
log(`enabled ${hostname}:${port} 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
Loading

0 comments on commit 5cc81be

Please sign in to comment.