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 5 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 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
15 changes: 7 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,11 @@ function init () {
}
})

injectScript(browser.extension.getURL('dist/contentScripts/ipfs-proxy/page.js'))
}
// browserify inlines contents of this file
// eslint-disable-next-line
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"
]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too permissive?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a whitelist right? Any other calls will by default be blocked? Think they are all fine, but I do worry about for example MFS being totally open for everyone with access to window.ipfs.

Copy link
Member

@lidel lidel Feb 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we remove all *.ls from whitelist for now. Adding / reading specific hash can be done without explicit permission, but listing internal node states should require explicit confirmation.

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
18 changes: 11 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@
"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:ipfs-proxy": "run-s build:content-scripts:ipfs-proxy:*",
"build:content-scripts:ipfs-proxy:page": "browserify add-on/src/contentScripts/ipfs-proxy/page.js -o add-on/dist/contentScripts/ipfs-proxy/page.js",
"build:content-scripts:ipfs-proxy:content": "browserify -t brfs -s IpfsProxyContent add-on/src/contentScripts/ipfs-proxy/content.js -o add-on/dist/contentScripts/ipfs-proxy/content.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:ipfs-proxy": "run-s watch:content-scripts:ipfs-proxy:*",
"watch:content-scripts:ipfs-proxy:page": "watchify 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 -t brfs -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 +45,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,8 +59,9 @@
"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",
Expand All @@ -71,7 +75,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