Skip to content

Commit 5cc81be

Browse files
authored
Merge pull request #853 from feat/subdomain-proxy-support
feat: *.localhost subdomain gateway support with http proxy
2 parents 597fd6f + 5bce2a2 commit 5cc81be

28 files changed

+2319
-1611
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

+9-1
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_useSubdomains_title": {
283+
"message": "Use Subdomains",
284+
"description": "An option title on the Preferences screen (option_useSubdomains_title)"
285+
},
286+
"option_useSubdomains_description": {
287+
"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.",
288+
"description": "An option description on the Preferences screen (option_useSubdomains_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)"
@@ -296,7 +304,7 @@
296304
"description": "An option description on the Preferences screen (option_dnslinkDataPreload_description)"
297305
},
298306
"option_dnslinkRedirect_warning": {
299-
"message": "Redirecting to a path-based gateway breaks Origin-based security isolation of DNSLink websites. Make sure you understand related risks.",
307+
"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.",
300308
"description": "A warning on the Preferences screen, displayed when URL does not belong to Secure Context (option_customGatewayUrl_warning)"
301309
},
302310
"option_noIntegrationsHostnames_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.chromium.json

+13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
11
{
22
"minimum_chrome_version": "72",
3+
"permissions": [
4+
"<all_urls>",
5+
"idle",
6+
"tabs",
7+
"notifications",
8+
"storage",
9+
"unlimitedStorage",
10+
"contextMenus",
11+
"clipboardWrite",
12+
"webNavigation",
13+
"webRequest",
14+
"webRequestBlocking"
15+
],
316
"incognito": "not_allowed"
417
}

add-on/manifest.common.json

-13
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,6 @@
1111
"38": "icons/png/ipfs-logo-on_38.png",
1212
"128": "icons/png/ipfs-logo-on_128.png"
1313
},
14-
"permissions": [
15-
"<all_urls>",
16-
"idle",
17-
"tabs",
18-
"notifications",
19-
"storage",
20-
"unlimitedStorage",
21-
"contextMenus",
22-
"clipboardWrite",
23-
"webNavigation",
24-
"webRequest",
25-
"webRequestBlocking"
26-
],
2714
"background": {
2815
"page": "dist/background/background.html"
2916
},

add-on/manifest.firefox.json

+14
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,20 @@
1818
"default_title": "__MSG_pageAction_titleNonIpfs__",
1919
"default_popup": "dist/popup/page-action/index.html"
2020
},
21+
"permissions": [
22+
"<all_urls>",
23+
"idle",
24+
"tabs",
25+
"notifications",
26+
"proxy",
27+
"storage",
28+
"unlimitedStorage",
29+
"contextMenus",
30+
"clipboardWrite",
31+
"webNavigation",
32+
"webRequest",
33+
"webRequestBlocking"
34+
],
2135
"content_scripts": [ ],
2236
"protocol_handlers": [
2337
{

add-on/src/lib/dnslink.js

+21-16
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ const IsIpfs = require('is-ipfs')
99
const LRU = require('lru-cache')
1010
const { default: PQueue } = require('p-queue')
1111
const { offlinePeerCount } = require('./state')
12-
const { pathAtHttpGateway } = require('./ipfs-path')
13-
14-
// TODO: add Preferences toggle to disable redirect of DNSLink websites (while keeping async dnslink lookup)
12+
const { ipfsContentPath, sameGateway, pathAtHttpGateway } = require('./ipfs-path')
1513

1614
module.exports = function createDnslinkResolver (getState) {
1715
// DNSLink lookup result cache
@@ -47,11 +45,11 @@ module.exports = function createDnslinkResolver (getState) {
4745
return state.dnslinkPolicy &&
4846
requestUrl.startsWith('http') &&
4947
!IsIpfs.url(requestUrl) &&
50-
!requestUrl.startsWith(state.apiURLString) &&
51-
!requestUrl.startsWith(state.gwURLString)
48+
!sameGateway(requestUrl, state.apiURL) &&
49+
!sameGateway(requestUrl, state.gwURL)
5250
},
5351

54-
dnslinkRedirect (url, dnslink) {
52+
dnslinkAtGateway (url, dnslink) {
5553
if (typeof url === 'string') {
5654
url = new URL(url)
5755
}
@@ -61,9 +59,8 @@ module.exports = function createDnslinkResolver (getState) {
6159
// to load the correct path from IPFS
6260
// - https://github.com/ipfs/ipfs-companion/issues/298
6361
const ipnsPath = dnslinkResolver.convertToIpnsPath(url)
64-
const gateway = state.ipfsNodeType === 'embedded' ? state.pubGwURLString : state.gwURLString
65-
// TODO: redirect to `ipns://` if hasNativeProtocolHandler === true
66-
return { redirectUrl: pathAtHttpGateway(ipnsPath, gateway) }
62+
const gateway = state.localGwAvailable ? state.gwURLString : state.pubGwURLString
63+
return pathAtHttpGateway(ipnsPath, gateway)
6764
}
6865
},
6966

@@ -111,7 +108,7 @@ module.exports = function createDnslinkResolver (getState) {
111108
preloadUrlCache.set(url, true)
112109
const dnslink = await dnslinkResolver.resolve(url)
113110
if (!dnslink) return
114-
if (state.ipfsNodeType === 'embedded') return
111+
if (!state.localGwAvailable) return
115112
if (state.peerCount < 1) return
116113
return preloadQueue.add(async () => {
117114
const { pathname } = new URL(url)
@@ -128,7 +125,13 @@ module.exports = function createDnslinkResolver (getState) {
128125
let apiProvider
129126
// TODO: fix DNS resolver for ipfsNodeType='embedded:chromesockets', for now use ipfs.io
130127
if (!state.ipfsNodeType.startsWith('embedded') && state.peerCount !== offlinePeerCount) {
131-
apiProvider = state.apiURLString
128+
// Use gw port so it can be a GET:
129+
// Chromium does not execute onBeforeSendHeaders for synchronous calls
130+
// made from the same extension context as onBeforeSendHeaders
131+
// which means we are unable to fixup Origin on the fly for this
132+
// This will no longer be needed when we switch
133+
// to async lookup via ipfs.dns everywhere
134+
apiProvider = state.gwURLString
132135
} else {
133136
// fallback to resolver at public gateway
134137
apiProvider = 'https://ipfs.io/'
@@ -204,19 +207,21 @@ module.exports = function createDnslinkResolver (getState) {
204207
// in url.hostname OR in url.pathname (/ipns/<fqdn>)
205208
// and return matching FQDN if present
206209
findDNSLinkHostname (url) {
207-
const { hostname, pathname } = new URL(url)
208-
// check //foo.tld/ipns/<fqdn>
209-
if (IsIpfs.ipnsPath(pathname)) {
210+
if (!url) return
211+
// Normalize subdomain and path gateways to to /ipns/<fqdn>
212+
const contentPath = ipfsContentPath(url)
213+
if (IsIpfs.ipnsPath(contentPath)) {
210214
// we may have false-positives here, so we do additional checks below
211-
const ipnsRoot = pathname.match(/^\/ipns\/([^/]+)/)[1]
215+
const ipnsRoot = contentPath.match(/^\/ipns\/([^/]+)/)[1]
212216
// console.log('findDNSLinkHostname ==> inspecting IPNS root', ipnsRoot)
213217
// Ignore PeerIDs, match DNSLink only
214218
if (!IsIpfs.cid(ipnsRoot) && dnslinkResolver.readAndCacheDnslink(ipnsRoot)) {
215219
// console.log('findDNSLinkHostname ==> found DNSLink for FQDN in url.pathname: ', ipnsRoot)
216220
return ipnsRoot
217221
}
218222
}
219-
// check //<fqdn>/foo/bar
223+
// Check main hostname
224+
const { hostname } = new URL(url)
220225
if (dnslinkResolver.readAndCacheDnslink(hostname)) {
221226
// console.log('findDNSLinkHostname ==> found DNSLink for url.hostname', hostname)
222227
return hostname

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

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
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+
//
27+
// State in Q2 2020:
28+
// - Chromium hardcodes `localhost` name to point at local IP and proxy is not
29+
// really necessary. The code is here (inactivE) in case we need it in the future.
30+
// - Firefox requires proxy to avoid DNS lookup, but there is an open issue
31+
// that will remove that need at some point:
32+
// https://bugzilla.mozilla.org/show_bug.cgi?id=1220810
33+
async function registerSubdomainProxy (getState, runtime, notify) {
34+
// At the moment only firefox requires proxy registration
35+
if (!runtime.isFirefox) return
36+
37+
try {
38+
const { active, useSubdomains, gwURLString } = getState()
39+
const enable = active && useSubdomains
40+
41+
// HTTP Proxy feature is exposed on the gateway port
42+
// Just ensure we use localhost IP to remove any dependency on DNS
43+
const { hostname, port } = safeURL(gwURLString, { useLocalhostName: false })
44+
45+
// Firefox uses own APIs for selective proxying
46+
if (runtime.isFirefox) {
47+
return await registerSubdomainProxyFirefox(enable, hostname, port)
48+
}
49+
50+
// At this point we would asume Chromium, but its not needed atm
51+
// Uncomment below if ever needed (+ add 'proxy' permission to manifest.json)
52+
// return await registerSubdomainProxyChromium(enable, hostname, port)
53+
} catch (err) {
54+
// registerSubdomainProxy is just a failsafe, not necessary in most cases,
55+
// so we should not break init when it fails.
56+
// For now we just log error and exit as NOOP
57+
log.error('registerSubdomainProxy failed', err)
58+
// Show pop-up only the first time, during init() when notify is passed
59+
try {
60+
if (notify) notify('notify_addonIssueTitle', 'notify_addonIssueMsg')
61+
} catch (_) {
62+
}
63+
}
64+
}
65+
66+
// storing listener for later
67+
var onRequestProxyListener
68+
69+
// registerSubdomainProxyFirefox sets proxy using API available in Firefox
70+
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/proxy/onRequest
71+
async function registerSubdomainProxyFirefox (enable, hostname, port) {
72+
const { onRequest } = browser.proxy
73+
74+
// always remove the old listener (host and port could change)
75+
const oldListener = onRequestProxyListener
76+
if (oldListener && onRequest.hasListener(oldListener)) {
77+
onRequest.removeListener(oldListener)
78+
}
79+
80+
if (enable) {
81+
// create new listener with the latest host:port note: the listener is
82+
// handling requests made to all localhost ports (limitation of the API,
83+
// port is ignored) that is why we manually check port inside of the listener
84+
onRequestProxyListener = (request) => {
85+
if (new URL(request.url).port === port) {
86+
return { type: 'http', host: hostname, port }
87+
}
88+
return { type: 'direct' }
89+
}
90+
91+
// register the listener
92+
onRequest.addListener(onRequestProxyListener, {
93+
urls: ['http://*.localhost/*'],
94+
incognito: false
95+
})
96+
log(`enabled ${hostname}:${port} as HTTP proxy for *.localhost`)
97+
return
98+
}
99+
100+
// at this point we effectively disabled proxy
101+
log('disabled HTTP proxy for *.localhost')
102+
}
103+
104+
/*
105+
* Chromium 80 does not need proxy, so below is not used.
106+
* Uncomment below if ever needed (+ add 'proxy' permission to manifest.json)
107+
108+
// Helpers for converting callback chrome.* API to promises
109+
const cb = (resolve, reject) => (result) => {
110+
const err = chrome.runtime.lastError
111+
if (err) return reject(err)
112+
return resolve(result)
113+
}
114+
const get = async (opts) => new Promise((resolve, reject) => chrome.proxy.settings.get(opts, cb(resolve, reject)))
115+
const set = async (opts) => new Promise((resolve, reject) => chrome.proxy.settings.set(opts, cb(resolve, reject)))
116+
const clear = async (opts) => new Promise((resolve, reject) => chrome.proxy.settings.clear(opts, cb(resolve, reject)))
117+
118+
// registerSubdomainProxyChromium sets proxy using API available in Chromium
119+
// https://developer.chrome.com/extensions/proxy
120+
async function registerSubdomainProxyChromium (enable, hostname, port) {
121+
const scope = 'regular_only'
122+
123+
// read current proxy settings
124+
const settings = await get({ incognito: false })
125+
126+
// set or update, if enabled
127+
if (enable) {
128+
// PAC script enables selective routing to PROXY at host+port
129+
// here, PROXY is the same as HTTP API endpoint
130+
const pacConfig = {
131+
mode: 'pac_script',
132+
pacScript: {
133+
data: 'function FindProxyForURL(url, host) {\n' +
134+
` if (shExpMatch(host, '*.localhost:${port}'))\n` +
135+
` return 'PROXY ${hostname}:${port}';\n` +
136+
" return 'DIRECT';\n" +
137+
'}'
138+
}
139+
}
140+
await set({ value: pacConfig, scope })
141+
log(`enabled ${hostname}:${port} as HTTP proxy for *.localhost`)
142+
// log('updated chrome.proxy.settings', await get({ incognito: false }))
143+
return
144+
}
145+
146+
// else: remove any existing proxy settings
147+
if (settings && settings.levelOfControl === 'controlled_by_this_extension') {
148+
// remove any proxy settings ipfs-companion set up before
149+
await clear({ scope })
150+
log('disabled HTTP proxy for *.localhost')
151+
}
152+
}
153+
*/
154+
155+
module.exports.registerSubdomainProxy = registerSubdomainProxy

0 commit comments

Comments
 (0)