Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes window.ipfs is added after page load #368

Merged
merged 9 commits into from
Feb 7, 2018
Merged
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
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
FROM node:9.4.0-alpine
RUN apk add --update make gcc g++ python
RUN apk add --update make gcc g++ python git
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY package.json /usr/src/app/package.json
Expand Down
2 changes: 1 addition & 1 deletion add-on/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@
"description": "An option title for enabling/disabling the IPFS proxy (option_ipfsProxy_title)"
},
"option_ipfsProxy_description": {
"message": "Add IPFS to the window object on every page",
"message": "IPFS is added to the window object on every page. Enable/disable access to the functions it exposes.",
"description": "An option description for the IPFS proxy (option_ipfsProxy_description)"
},
"option_ipfsProxy_link_manage_permissions": {
Expand Down
16 changes: 14 additions & 2 deletions add-on/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,20 @@

"web_accessible_resources": [
"icons/ipfs-logo-on.svg",
"icons/ipfs-logo-off.svg",
"dist/contentScripts/ipfs-proxy/page.js"
"icons/ipfs-logo-off.svg"
],

"content_scripts": [
{
"all_frames": true,
"js": [
"dist/contentScripts/ipfs-proxy/content.js"
],
"matches": [
"<all_urls>"
],
"run_at": "document_start"
}
],

"protocol_handlers": [
Expand Down
14 changes: 6 additions & 8 deletions add-on/src/contentScripts/ipfs-proxy/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const browser = require('webextension-polyfill')
const injectScript = require('./inject-script')
const fs = require('fs')

function init () {
const port = browser.runtime.connect({ name: 'ipfs-proxy' })
Expand All @@ -15,13 +16,10 @@ function init () {
}
})

injectScript(browser.extension.getURL('dist/contentScripts/ipfs-proxy/page.js'))
}
// browserify inlines contents of this file
const code = fs.readFileSync(`${__dirname}/../../../dist/contentScripts/ipfs-proxy/page.js`, 'utf8')

// Only run this once for this window!
// URL can change (history API) which causes this script to be executed again,
// but it only needs to be setup once per window...
if (!window.__ipfsProxyContentInitialized) {
init()
window.__ipfsProxyContentInitialized = true
injectScript(code)
}

init()
12 changes: 5 additions & 7 deletions add-on/src/contentScripts/ipfs-proxy/inject-script.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
'use strict'

function injectScript (src, target, opts) {
function injectScript (code, opts) {
opts = opts || {}
const doc = opts.document || document

const scriptTag = doc.createElement('script')
scriptTag.src = src
scriptTag.onload = function () {
this.parentNode.removeChild(this)
}
const scriptTag = document.createElement('script')
scriptTag.innerHTML = code

target = doc.head || doc.documentElement
const target = opts.target || doc.head || doc.documentElement
target.appendChild(scriptTag)
scriptTag.parentNode.removeChild(scriptTag)
}

module.exports = injectScript
25 changes: 25 additions & 0 deletions add-on/src/lib/ipfs-proxy/acl-whitelist.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[
"block.get",
"block.stat",
"dag.get",
"dag.tree",
"dht.get",
"dht.findprovs",
"dht.findpeer",
"dht.query",
"files.cat",
"files.catPullStream",
"files.get",
"files.getReadableStream",
"files.getPullStream",
"object.get",
"object.data",
"object.links",
"object.stat",
"pubsub.subscribe",
"pubsub.unsubscribe",
"pubsub.peers",
"swarm.peers",
"swarm.addrs",
"swarm.localAddrs"
]
98 changes: 2 additions & 96 deletions add-on/src/lib/ipfs-proxy/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,69 +4,13 @@
const browser = require('webextension-polyfill')
const { createProxyServer, closeProxyServer } = require('ipfs-postmsg-proxy')
const AccessControl = require('./access-control')

// These are the functions that require an allow/deny decision by the user.
// All other exposed IPFS functions are available to call without authorization.
const ACL_FUNCTIONS = [
'block.put',
'config.set',
'config.get',
'config.replace',
'dag.put',
'dht.put',
'dht.provide',
'files.add',
'key.get',
'key.list',
'key.rename',
'key.rm',
'object.new',
'object.put',
'object.patch.addLink',
'object.patch.rmLink',
'object.patch.appendData',
'object.patch.setData',
'pin.add',
'pin.rm',
'pubsub.publish',
'swarm.connect',
'swarm.disconnect'
]
const createPreAcl = require('./pre-acl')

// Creates an object that manages the "server side" of the IPFS proxy
function createIpfsProxy (getIpfs, getState) {
let connections = []
const accessControl = new AccessControl(browser.storage)

// When a new URL is visited, we execute a content script, which creates
// a `window.ipfs` object on the page and opens up a new port so that the
// `window.ipfs` proxy can talk to us.
const onTabUpdated = (tabId, changeInfo, tab) => {
// Some devtools tabs do not have an ID
if (!tabId || tabId === browser.tabs.TAB_ID_NONE) return

// If IPFS proxy option is not enabled do not execute the content script
if (!getState().ipfsProxy) return

// Only inject on http(s): ipfs: or dweb:
if (!['http', 'ipfs', 'dweb'].some(p => (tab.url || '').startsWith(p))) return

// Only inject when loaded
if (changeInfo.status !== 'complete') return

try {
browser.tabs.executeScript(tab.id, {
file: '/dist/contentScripts/ipfs-proxy/content.js',
runAt: 'document_start',
allFrames: true
})
} catch (err) {
console.error('Failed to execute IPFS proxy content script', err)
}
}

browser.tabs.onUpdated.addListener(onTabUpdated)

// Port connection events are emitted when a content script attempts to
// communicate with us. Each new URL visited by the user will open a port.
// When a port is opened, we create a new IPFS proxy server to listen to the
Expand All @@ -81,10 +25,7 @@ function createIpfsProxy (getIpfs, getState) {
removeListener: (_, handler) => port.onMessage.removeListener(handler),
postMessage: (data) => port.postMessage(data),
getMessageData: (d) => d,
pre: ACL_FUNCTIONS.reduce((obj, permission) => {
obj[permission] = createAclPreCall(accessControl, origin, permission)
return obj
}, {})
pre: (fnName) => createPreAcl(getState, accessControl, origin, fnName)
})

const close = () => {
Expand All @@ -108,7 +49,6 @@ function createIpfsProxy (getIpfs, getState) {
const handle = {
destroy () {
connections.forEach(c => c.destroy)
browser.tabs.onUpdated.removeListener(onTabUpdated)
browser.runtime.onConnect.removeListener(onPortConnect)
}
}
Expand All @@ -117,37 +57,3 @@ function createIpfsProxy (getIpfs, getState) {
}

module.exports = createIpfsProxy

// Creates a "pre" function that is called prior to calling a real function
// on the IPFS instance. It will throw if access is denied, and ask the user if
// no access decision has been made yet.
function createAclPreCall (accessControl, origin, permission) {
return async (...args) => {
let access = await accessControl.getAccess(origin, permission)

if (!access) {
const { allow, blanket, remember } = await requestAccess(origin, permission)
access = await accessControl.setAccess(origin, blanket ? '*' : permission, allow, remember)
}

if (!access.allow) throw new Error(`User denied access to ${permission}`)

return args
}
}

async function requestAccess (origin, permission) {
const msg = `Allow ${origin} to access ipfs.${permission}?`

// TODO: add checkbox to allow all for this origin
let allow

try {
allow = window.confirm(msg)
} catch (err) {
console.warn('Failed to confirm, possibly not supported in this environment', err)
allow = false
}

return { allow, blanket: false }
}
31 changes: 31 additions & 0 deletions add-on/src/lib/ipfs-proxy/pre-acl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const defaultRequestAccess = require('./request-access')

// This are the function that DO NOT require an allow/deny decision by the user.
// All other IPFS functions require authorization.
const ACL_WHITELIST = Object.freeze(require('./acl-whitelist.json'))

// Creates a "pre" function that is called prior to calling a real function
// on the IPFS instance. It will throw if access is denied, and ask the user if
// no access decision has been made yet.
function createPreAcl (getState, accessControl, origin, permission, requestAccess = defaultRequestAccess) {
return async (...args) => {
// Check if all access to the IPFS node is disabled
if (!getState().ipfsProxy) throw new Error('User disabled access to IPFS')

// No need to verify access if permission is on the whitelist
if (ACL_WHITELIST.includes(permission)) return args

let access = await accessControl.getAccess(origin, permission)

if (!access) {
const { allow } = await requestAccess(origin, permission)
access = await accessControl.setAccess(origin, permission, allow)
}

if (!access.allow) throw new Error(`User denied access to ${permission}`)

return args
}
}

module.exports = createPreAcl
17 changes: 17 additions & 0 deletions add-on/src/lib/ipfs-proxy/request-access.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
async function requestAccess (origin, permission) {
const msg = `Allow ${origin} to access ipfs.${permission}?`

// TODO: add checkbox to allow all for this origin
let allow

try {
allow = window.confirm(msg)
} catch (err) {
console.warn('Failed to confirm, possibly not supported in this environment', err)
allow = false
}

return { allow }
}

module.exports = requestAccess
2 changes: 1 addition & 1 deletion add-on/src/lib/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const optionDefaults = Object.freeze({
customGatewayUrl: 'http://127.0.0.1:8080',
ipfsApiUrl: 'http://127.0.0.1:5001',
ipfsApiPollMs: 3000,
ipfsProxy: false
ipfsProxy: true
})

exports.optionDefaults = optionDefaults
Expand Down
24 changes: 15 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,19 @@
"build:copy:src": "shx mkdir -p add-on/dist && shx cp -R add-on/src/* add-on/dist",
"build:copy:wx-polyfill-lib": "shx cp node_modules/webextension-polyfill/dist/browser-polyfill.min.js add-on/dist/contentScripts/browser-polyfill.min.js",
"build:js": "browserify -p prundupify -t browserify-css -t [ browserify-package-json --global ] add-on/src/background/background.js add-on/src/options/options.js add-on/src/popup/browser-action/index.js add-on/src/popup/quick-upload.js add-on/src/pages/proxy-acl/index.js -p [ factor-bundle -o add-on/dist/background/background.js -o add-on/dist/options/options.js -o add-on/dist/popup/browser-action/browser-action.js -o add-on/dist/popup/quick-upload.js -o add-on/dist/pages/proxy-acl/proxy-acl.js ] -o add-on/dist/ipfs-companion-common.js",
"build:content-scripts": "run-p build:content-scripts:**",
"build:content-scripts:ipfs-proxy:content": "browserify -s IpfsProxyContent add-on/src/contentScripts/ipfs-proxy/content.js -o add-on/dist/contentScripts/ipfs-proxy/content.js",
"build:content-scripts:ipfs-proxy:page": "browserify -s IpfsProxyPage add-on/src/contentScripts/ipfs-proxy/page.js -o add-on/dist/contentScripts/ipfs-proxy/page.js",
"build:content-scripts": "run-p build:content-scripts:*",
"build:content-scripts:ipfs-proxy": "run-s build:content-scripts:ipfs-proxy:*",
"build:content-scripts:ipfs-proxy:page": "browserify -p prundupify -g uglifyify -t [ browserify-package-json --global ] add-on/src/contentScripts/ipfs-proxy/page.js -o add-on/dist/contentScripts/ipfs-proxy/page.js",
"build:content-scripts:ipfs-proxy:content": "browserify -p prundupify -g uglifyify -t brfs -t [ browserify-package-json --global ] -s IpfsProxyContent add-on/src/contentScripts/ipfs-proxy/content.js -o add-on/dist/contentScripts/ipfs-proxy/content.js",
"build:content-scripts:ipfs-proxy:cleanup": "shx rm add-on/dist/contentScripts/ipfs-proxy/page.js",
"build:minimize-dist": "shx rm -rf add-on/dist/lib",
"build:bundle-extension": "web-ext build -s add-on/ -i src/ -a build/",
"watch": "run-p watch:*",
"watch": "npm-run-all build:copy --parallel watch:*",
"watch:js": "watchify -p prundupify -t browserify-css -t [ browserify-package-json --global ] add-on/src/background/background.js add-on/src/options/options.js add-on/src/popup/browser-action/index.js add-on/src/popup/quick-upload.js add-on/src/pages/proxy-acl/index.js -p [ factor-bundle -o add-on/dist/background/background.js -o add-on/dist/options/options.js -o add-on/dist/popup/browser-action/browser-action.js -o add-on/dist/popup/quick-upload.js -o add-on/dist/pages/proxy-acl/proxy-acl.js ] -o add-on/dist/ipfs-companion-common.js -v",
"watch:content-scripts": "run-p watch:content-scripts:**",
"watch:content-scripts:ipfs-proxy:content": "watchify -s IpfsProxyContent add-on/src/contentScripts/ipfs-proxy/content.js -o add-on/dist/contentScripts/ipfs-proxy/content.js",
"watch:content-scripts:ipfs-proxy:page": "watchify -s IpfsProxyPage add-on/src/contentScripts/ipfs-proxy/page.js -o add-on/dist/contentScripts/ipfs-proxy/page.js",
"watch:content-scripts": "run-p watch:content-scripts:*",
"watch:content-scripts:ipfs-proxy": "run-s watch:content-scripts:ipfs-proxy:*",
"watch:content-scripts:ipfs-proxy:page": "watchify -p prundupify -t [ browserify-package-json --global ] add-on/src/contentScripts/ipfs-proxy/page.js -o add-on/dist/contentScripts/ipfs-proxy/page.js -v",
"watch:content-scripts:ipfs-proxy:content": "nodemon --exec \"browserify -p prundupify -t brfs -t [ browserify-package-json --global ] -s IpfsProxyContent add-on/src/contentScripts/ipfs-proxy/content.js -o add-on/dist/contentScripts/ipfs-proxy/content.js -v\" --watch add-on/src/contentScripts/ipfs-proxy/content.js --watch add-on/dist/contentScripts/ipfs-proxy/page.js",
"test": "run-s test:*",
"test:functional": " nyc --reporter=lcov --reporter=text mocha --require ignore-styles 'test/functional/**/*.test.js'",
"lint": "run-s lint:*",
Expand All @@ -43,6 +46,7 @@
"babel-preset-es2015": "6.24.1",
"babel-preset-es2017": "6.24.1",
"babelify": "8.0.0",
"brfs": "1.4.4",
"browserify": "14.5.0",
"browserify-css": "0.14.0",
"browserify-package-json": "1.0.1",
Expand All @@ -56,12 +60,14 @@
"ignore-styles": "5.0.1",
"mem-storage-area": "1.0.3",
"mocha": "4.0.1",
"nodemon": "1.14.11",
"npm-run-all": "4.1.2",
"nyc": "^11.4.1",
"nyc": "11.4.1",
"shx": "0.2.2",
"sinon": "4.1.2",
"sinon-chrome": "2.2.1",
"standard": "10.0.3",
"uglifyify": "^4.0.5",
"watchify": "3.9.0",
"web-ext": "2.3.2"
},
Expand All @@ -71,7 +77,7 @@
"file-type": "7.3.0",
"ipfs": "0.27.6",
"ipfs-api": "17.2.7",
"ipfs-postmsg-proxy": "2.1.0",
"ipfs-postmsg-proxy": "2.3.0",
"is-ipfs": "0.3.2",
"is-svg": "2.1.0",
"lru_map": "0.3.3",
Expand Down
Loading