diff --git a/add-on/_locales/en/messages.json b/add-on/_locales/en/messages.json
index c3a696c24..6ae6c8578 100644
--- a/add-on/_locales/en/messages.json
+++ b/add-on/_locales/en/messages.json
@@ -355,22 +355,22 @@
"description": "Message displayed when no permissions have been granted (page_proxyAcl_no_perms)"
},
"page_proxyAcl_confirm_revoke": {
- "message": "Revoke permission $PERMISSION$ for $ORIGIN$?",
- "description": "Confirmation message for revoking a permission for an origin (page_proxyAcl_confirm_revoke)",
+ "message": "Revoke permission $PERMISSION$ for $SCOPE$?",
+ "description": "Confirmation message for revoking a permission for a scope (page_proxyAcl_confirm_revoke)",
"placeholders": {
"permission": {
"content": "$1"
},
- "origin": {
+ "scope": {
"content": "$2"
}
}
},
"page_proxyAcl_confirm_revoke_all": {
- "message": "Revoke all permissions for $ORIGIN$?",
- "description": "Confirmation message for revoking all permissions for an origin (page_proxyAcl_confirm_revoke_all)",
+ "message": "Revoke all permissions for $SCOPE$?",
+ "description": "Confirmation message for revoking all permissions for an scope (page_proxyAcl_confirm_revoke_all)",
"placeholders": {
- "origin": {
+ "scope": {
"content": "$1"
}
}
@@ -401,10 +401,10 @@
}
},
"page_proxyAccessDialog_title": {
- "message": "Allow $ORIGIN$ to access ipfs.$PERMISSION$?",
+ "message": "Allow $SCOPE$ to access ipfs.$PERMISSION$?",
"description": "Main title of the access permission dialog (page_proxyAccessDialog_title)",
"placeholders": {
- "origin": {
+ "scope": {
"content": "$1"
},
"permission": {
@@ -413,13 +413,8 @@
}
},
"page_proxyAccessDialog_wildcardCheckbox_label": {
- "message": "Apply to all permissions for $ORIGIN$",
- "description": "Label for the apply permissions to all checkbox (page_proxyAccessDialog_wildcardCheckbox_label)",
- "placeholders": {
- "origin": {
- "content": "$1"
- }
- }
+ "message": "Apply this decision to all permissions in this scope",
+ "description": "Label for the apply permissions to all checkbox (page_proxyAccessDialog_wildcardCheckbox_label)"
},
"page_proxyAcl_revoke_all_button_title": {
"message": "Revoke all permissions",
diff --git a/add-on/src/lib/ipfs-proxy/access-control.js b/add-on/src/lib/ipfs-proxy/access-control.js
index 08a4959e0..c8f2f243b 100644
--- a/add-on/src/lib/ipfs-proxy/access-control.js
+++ b/add-on/src/lib/ipfs-proxy/access-control.js
@@ -16,67 +16,108 @@ class AccessControl extends EventEmitter {
async _onStorageChange (changes) {
const prefix = this._storageKeyPrefix
- const aclChangeKeys = Object.keys(changes).filter((key) => key.startsWith(prefix))
+ const scopesKey = this._getScopesKey()
+ const aclChangeKeys = Object.keys(changes).filter((key) => {
+ return key !== scopesKey && key.startsWith(prefix)
+ })
if (!aclChangeKeys.length) return
- // Map { origin => Map { permission => allow } }
+ // Map { scope => Map { permission => allow } }
this.emit('change', aclChangeKeys.reduce((aclChanges, key) => {
return aclChanges.set(
- key.slice(prefix.length + 1),
+ key.slice(prefix.length + ('.access'.length) + 1),
new Map(JSON.parse(changes[key].newValue))
)
}, new Map()))
}
- _getGrantsKey (origin) {
- return `${this._storageKeyPrefix}.${origin}`
+ _getScopesKey () {
+ return `${this._storageKeyPrefix}.scopes`
}
- // Get a Map of granted permissions for a given origin
- // e.g. Map { 'files.add' => true, 'object.new' => false }
- async _getGrants (origin) {
- const key = this._getGrantsKey(origin)
- return new Map(
+ // Get the list of scopes stored in the acl
+ async _getScopes () {
+ const key = this._getScopesKey()
+ return new Set(
JSON.parse((await this._storage.local.get({ [key]: '[]' }))[key])
)
}
- async _setGrants (origin, grants) {
- const key = this._getGrantsKey(origin)
- return this._storage.local.set({ [key]: JSON.stringify(Array.from(grants)) })
+ async _addScope (scope) {
+ const scopes = await this._getScopes()
+ scopes.add(scope)
+
+ const key = this._getScopesKey()
+ await this._storage.local.set({ [key]: JSON.stringify(Array.from(scopes)) })
+ }
+
+ // ordered by longest first
+ async _getMatchingScopes (scope) {
+ const scopes = await this._getScopes()
+ const origin = new URL(scope).origin
+
+ return Array.from(scopes)
+ .filter(s => {
+ if (origin !== new URL(s).origin) return false
+ return scope.startsWith(s)
+ })
+ .sort((a, b) => b.length - a.length)
}
- async getAccess (origin, permission) {
- if (!isOrigin(origin)) throw new TypeError('Invalid origin')
+ _getAccessKey (scope) {
+ return `${this._storageKeyPrefix}.access.${scope}`
+ }
+
+ // Get a Map of granted permissions for a given scope
+ // e.g. Map { 'files.add' => true, 'object.new' => false }
+ async _getAllAccess (scope) {
+ const key = this._getAccessKey(scope)
+ return new Map(
+ JSON.parse((await this._storage.local.get({ [key]: '[]' }))[key])
+ )
+ }
+
+ async getAccess (scope, permission) {
+ if (!isScope(scope)) throw new TypeError('Invalid scope')
if (!isString(permission)) throw new TypeError('Invalid permission')
- const grants = await this._getGrants(origin)
+ const matchingScopes = await this._getMatchingScopes(scope)
+
+ let allow = null
+ let matchingScope
+
+ for (matchingScope of matchingScopes) {
+ const allAccess = await this._getAllAccess(matchingScope)
+
+ if (allAccess.has('*')) {
+ allow = allAccess.get('*')
+ break
+ }
- if (grants.has('*')) {
- return { origin, permission, allow: grants.get('*') }
+ if (allAccess.has(permission)) {
+ allow = allAccess.get(permission)
+ break
+ }
}
- return grants.has(permission)
- ? { origin, permission, allow: grants.get(permission) }
- : null
+ return allow == null ? null : { scope: matchingScope, permission, allow }
}
- async setAccess (origin, permission, allow) {
- if (!isOrigin(origin)) throw new TypeError('Invalid origin')
+ async setAccess (scope, permission, allow) {
+ if (!isScope(scope)) throw new TypeError('Invalid scope')
if (!isString(permission)) throw new TypeError('Invalid permission')
if (!isBoolean(allow)) throw new TypeError('Invalid allow')
return this._writeQ.add(async () => {
- const access = { origin, permission, allow }
- const grants = await this._getGrants(origin)
+ const allAccess = await this._getAllAccess(scope)
// Trying to set access for non-wildcard permission, when wildcard
// permission is already granted?
- if (grants.has('*') && permission !== '*') {
- if (grants.get('*') === allow) {
+ if (allAccess.has('*') && permission !== '*') {
+ if (allAccess.get('*') === allow) {
// Noop if requested access is the same as access for wildcard grant
- return access
+ return { scope, permission, allow }
} else {
// Fail if requested access is the different to access for wildcard grant
throw new Error(`Illegal set access for ${permission} when wildcard exists`)
@@ -85,47 +126,54 @@ class AccessControl extends EventEmitter {
// If setting a wildcard permission, remove existing grants
if (permission === '*') {
- grants.clear()
+ allAccess.clear()
}
- grants.set(permission, allow)
- await this._setGrants(origin, grants)
+ allAccess.set(permission, allow)
+
+ const accessKey = this._getAccessKey(scope)
+ await this._storage.local.set({ [accessKey]: JSON.stringify(Array.from(allAccess)) })
+
+ await this._addScope(scope)
- return access
+ return { scope, permission, allow }
})
}
- // Map { origin => Map { permission => allow } }
+ // Map { scope => Map { permission => allow } }
async getAcl () {
- const data = await this._storage.local.get()
- const prefix = this._storageKeyPrefix
+ const scopes = await this._getScopes()
+ const acl = new Map()
+
+ await Promise.all(Array.from(scopes).map(scope => {
+ return (async () => {
+ const allAccess = await this._getAllAccess(scope)
+ acl.set(scope, allAccess)
+ })()
+ }))
- return Object.keys(data)
- .reduce((acl, key) => {
- return key.startsWith(prefix)
- ? acl.set(key.slice(prefix.length + 1), new Map(JSON.parse(data[key])))
- : acl
- }, new Map())
+ return acl
}
// Revoke access to the given permission
// if permission is null, revoke all access
- async revokeAccess (origin, permission = null) {
- if (!isOrigin(origin)) throw new TypeError('Invalid origin')
+ async revokeAccess (scope, permission = null) {
+ if (!isScope(scope)) throw new TypeError('Invalid scope')
if (permission && !isString(permission)) throw new TypeError('Invalid permission')
return this._writeQ.add(async () => {
- let grants
+ let allAccess
if (permission) {
- grants = await this._getGrants(origin)
- if (!grants.has(permission)) return
- grants.delete(permission)
+ allAccess = await this._getAllAccess(scope)
+ if (!allAccess.has(permission)) return
+ allAccess.delete(permission)
} else {
- grants = new Map()
+ allAccess = new Map()
}
- await this._setGrants(origin, grants)
+ const key = this._getAccessKey(scope)
+ await this._storage.local.set({ [key]: JSON.stringify(Array.from(allAccess)) })
})
}
@@ -136,7 +184,7 @@ class AccessControl extends EventEmitter {
module.exports = AccessControl
-const isOrigin = (value) => {
+const isScope = (value) => {
if (!isString(value)) return false
let url
@@ -147,7 +195,7 @@ const isOrigin = (value) => {
return false
}
- return url.origin === value
+ return url.origin + url.pathname === value
}
const isString = (value) => Object.prototype.toString.call(value) === '[object String]'
diff --git a/add-on/src/lib/ipfs-proxy/index.js b/add-on/src/lib/ipfs-proxy/index.js
index f58e9bacf..a9bb00430 100644
--- a/add-on/src/lib/ipfs-proxy/index.js
+++ b/add-on/src/lib/ipfs-proxy/index.js
@@ -20,14 +20,18 @@ function createIpfsProxy (getIpfs, getState) {
const onPortConnect = (port) => {
if (port.name !== 'ipfs-proxy') return
- const { origin } = new URL(port.sender.url)
+ const getScope = async () => {
+ const tab = await browser.tabs.get(port.sender.tab.id)
+ const { origin, pathname } = new URL(tab.url)
+ return origin + pathname
+ }
const proxy = createProxyServer(getIpfs, {
addListener: (_, handler) => port.onMessage.addListener(handler),
removeListener: (_, handler) => port.onMessage.removeListener(handler),
postMessage: (data) => port.postMessage(data),
getMessageData: (d) => d,
- pre: (fnName) => createPreAcl(getState, accessControl, origin, fnName, requestAccess)
+ pre: (fnName) => createPreAcl(getState, accessControl, getScope, fnName, requestAccess)
})
const close = () => {
diff --git a/add-on/src/lib/ipfs-proxy/pre-acl.js b/add-on/src/lib/ipfs-proxy/pre-acl.js
index 60d9015a7..6ecfcc2b6 100644
--- a/add-on/src/lib/ipfs-proxy/pre-acl.js
+++ b/add-on/src/lib/ipfs-proxy/pre-acl.js
@@ -5,7 +5,7 @@ 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) {
+function createPreAcl (getState, accessControl, getScope, permission, requestAccess) {
return async (...args) => {
// Check if all access to the IPFS node is disabled
if (!getState().ipfsProxy) throw new Error('User disabled access to IPFS')
@@ -13,11 +13,12 @@ function createPreAcl (getState, accessControl, origin, permission, requestAcces
// 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)
+ const scope = await getScope()
+ let access = await accessControl.getAccess(scope, permission)
if (!access) {
- const { allow, wildcard } = await requestAccess(origin, permission)
- access = await accessControl.setAccess(origin, wildcard ? '*' : permission, allow)
+ const { allow, wildcard } = await requestAccess(scope, permission)
+ access = await accessControl.setAccess(scope, wildcard ? '*' : permission, allow)
}
if (!access.allow) throw new Error(`User denied access to ${permission}`)
diff --git a/add-on/src/lib/ipfs-proxy/request-access.js b/add-on/src/lib/ipfs-proxy/request-access.js
index 59f27dc30..631a7bf75 100644
--- a/add-on/src/lib/ipfs-proxy/request-access.js
+++ b/add-on/src/lib/ipfs-proxy/request-access.js
@@ -1,29 +1,41 @@
'use strict'
const DIALOG_WIDTH = 540
-const DIALOG_HEIGHT = 200
+const DIALOG_HEIGHT = 220
const DIALOG_PATH = 'dist/pages/proxy-access-dialog/index.html'
const DIALOG_PORT_NAME = 'proxy-access-dialog'
function createRequestAccess (browser, screen) {
- return async function requestAccess (origin, permission, opts) {
+ return async function requestAccess (scope, permission, opts) {
opts = opts || {}
- const width = opts.dialogWidth || DIALOG_WIDTH
- const height = opts.dialogHeight || DIALOG_HEIGHT
-
const url = browser.extension.getURL(opts.dialogPath || DIALOG_PATH)
- const currentWin = await browser.windows.getCurrent()
-
- const top = Math.round(((screen.width / 2) - (width / 2)) + currentWin.left)
- const left = Math.round(((screen.height / 2) - (height / 2)) + currentWin.top)
- const { tabs } = await browser.windows.create({ url, width, height, top, left, type: 'popup' })
+ let dialogTabId
+ if (browser && browser.windows && browser.windows.create) {
+ // display modal dialog in a centered popup window
+ const currentWin = await browser.windows.getCurrent()
+ const width = opts.dialogWidth || DIALOG_WIDTH
+ const height = opts.dialogHeight || DIALOG_HEIGHT
+ const top = Math.round(((screen.width / 2) - (width / 2)) + currentWin.left)
+ const left = Math.round(((screen.height / 2) - (height / 2)) + currentWin.top)
+
+ const dialogWindow = await browser.windows.create({ url, width, height, top, left, type: 'popup' })
+ dialogTabId = dialogWindow.tabs[0].id
+ // Fix for Fx57 bug where bundled page loaded using
+ // browser.windows.create won't show contents unless resized.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1402110
+ await browser.windows.update(dialogWindow.id, {width: dialogWindow.width + 1})
+ } else {
+ // fallback: opening dialog as a new active tab
+ // (runtimes without browser.windows.create, eg. Andorid)
+ dialogTabId = (await browser.tabs.create({active: true, url: url})).id
+ }
// Resolves with { allow, wildcard }
- const userResponse = getUserResponse(tabs[0].id, origin, permission, opts)
+ const userResponse = getUserResponse(dialogTabId, scope, permission, opts)
// Never resolves, might reject if user closes the tab
- const userTabRemoved = getUserTabRemoved(tabs[0].id, origin, permission)
+ const userTabRemoved = getUserTabRemoved(dialogTabId, scope, permission)
let response
@@ -35,12 +47,12 @@ function createRequestAccess (browser, screen) {
userResponse.destroy()
}
- await browser.tabs.remove(tabs[0].id)
+ await browser.tabs.remove(dialogTabId)
return response
}
- function getUserResponse (tabId, origin, permission, opts) {
+ function getUserResponse (tabId, scope, permission, opts) {
opts = opts || {}
const dialogPortName = opts.dialogPortName || DIALOG_PORT_NAME
@@ -52,8 +64,8 @@ function createRequestAccess (browser, screen) {
browser.runtime.onConnect.removeListener(onPortConnect)
- // Tell the dialog what origin/permission it is about
- port.postMessage({ origin, permission })
+ // Tell the dialog what scope/permission it is about
+ port.postMessage({ scope, permission })
// Wait for the user response
const onMessage = ({ allow, wildcard }) => {
@@ -75,11 +87,11 @@ function createRequestAccess (browser, screen) {
// Since the dialog is a tab not a real dialog it can be closed by the user
// with no response, this function creates a promise that will reject if the tab
// is removed.
- function getUserTabRemoved (tabId, origin, permission) {
+ function getUserTabRemoved (tabId, scope, permission) {
let onTabRemoved
const userTabRemoved = new Promise((resolve, reject) => {
- onTabRemoved = () => reject(new Error(`Failed to obtain access response for ${permission} at ${origin}`))
+ onTabRemoved = () => reject(new Error(`Failed to obtain access response for ${permission} at ${scope}`))
browser.tabs.onRemoved.addListener(onTabRemoved)
})
diff --git a/add-on/src/pages/proxy-access-dialog/index.html b/add-on/src/pages/proxy-access-dialog/index.html
index 40855149b..9b34d8587 100644
--- a/add-on/src/pages/proxy-access-dialog/index.html
+++ b/add-on/src/pages/proxy-access-dialog/index.html
@@ -8,7 +8,7 @@
-
+
diff --git a/add-on/src/pages/proxy-access-dialog/page.js b/add-on/src/pages/proxy-access-dialog/page.js
index 12985cda6..e5bf2f693 100644
--- a/add-on/src/pages/proxy-access-dialog/page.js
+++ b/add-on/src/pages/proxy-access-dialog/page.js
@@ -8,20 +8,20 @@ function createProxyAccessDialogPage (i18n) {
const onDeny = () => emit('deny')
const onWildcardToggle = () => emit('wildcardToggle')
- const { loading, origin, permission } = state
+ const { loading, scope, permission } = state
return html`
-
+
${loading ? null : html`
diff --git a/add-on/src/pages/proxy-access-dialog/store.js b/add-on/src/pages/proxy-access-dialog/store.js
index 78daebf84..05efc8f5a 100644
--- a/add-on/src/pages/proxy-access-dialog/store.js
+++ b/add-on/src/pages/proxy-access-dialog/store.js
@@ -2,7 +2,7 @@
function createProxyAccessDialogStore (i18n, runtime) {
return function proxyAccessDialogStore (state, emitter) {
- state.origin = null
+ state.scope = null
state.permission = null
state.loading = true
state.wildcard = false
@@ -10,10 +10,10 @@ function createProxyAccessDialogStore (i18n, runtime) {
const port = runtime.connect({ name: 'proxy-access-dialog' })
const onMessage = (data) => {
- if (!data || !data.origin || !data.permission) return
+ if (!data || !data.scope || !data.permission) return
port.onMessage.removeListener(onMessage)
- state.origin = data.origin
+ state.scope = data.scope
state.permission = data.permission
state.loading = false
diff --git a/add-on/src/pages/proxy-acl/page.js b/add-on/src/pages/proxy-acl/page.js
index cdeb95710..a8c3c16c7 100644
--- a/add-on/src/pages/proxy-acl/page.js
+++ b/add-on/src/pages/proxy-acl/page.js
@@ -9,8 +9,8 @@ function createProxyAclPage (i18n) {
const onToggleAllow = (e) => emit('toggleAllow', e)
const { acl } = state
- const origins = Array.from(state.acl.keys())
- const hasGrants = origins.some((origin) => !!acl.get(origin).size)
+ const scopes = Array.from(state.acl.keys())
+ const hasGrants = scopes.some((scope) => !!acl.get(scope).size)
return html`
@@ -32,16 +32,16 @@ function createProxyAclPage (i18n) {
${hasGrants ? html`
- ${origins.reduce((rows, origin) => {
- const permissions = acl.get(origin)
+ ${scopes.reduce((rows, scope) => {
+ const permissions = acl.get(scope)
if (!permissions.size) return rows
return rows.concat(
- originRow({ onRevoke, origin, i18n }),
+ scopeRow({ onRevoke, scope, i18n }),
Array.from(permissions.keys())
.sort()
- .map((permission) => accessRow({ origin, permission, allow: permissions.get(permission), onRevoke, onToggleAllow, i18n }))
+ .map((permission) => accessRow({ scope, permission, allow: permissions.get(permission), onRevoke, onToggleAllow, i18n }))
)
}, [])}
@@ -56,16 +56,16 @@ function createProxyAclPage (i18n) {
module.exports = createProxyAclPage
-function originRow ({ origin, onRevoke, i18n }) {
+function scopeRow ({ scope, onRevoke, i18n }) {
return html`
- ${origin} |
- ${revokeButton({ onRevoke, origin, i18n })} |
+ ${scope} |
+ ${revokeButton({ onRevoke, scope, i18n })} |
`
}
-function accessRow ({ origin, permission, allow, onRevoke, onToggleAllow, i18n }) {
+function accessRow ({ scope, permission, allow, onRevoke, onToggleAllow, i18n }) {
const title = i18n.getMessage(
allow
? 'page_proxyAcl_toggle_to_deny_button_title'
@@ -78,7 +78,7 @@ function accessRow ({ origin, permission, allow, onRevoke, onToggleAllow, i18n }
class="f5 white ph3 pv2 ${allow ? 'bg-green' : 'bg-red'} tc bb b--white-10 pointer"
style="width: 75px"
onclick=${onToggleAllow}
- data-origin="${origin}"
+ data-scope="${scope}"
data-permission="${permission}"
data-allow=${allow}
title="${title}">
@@ -89,14 +89,14 @@ function accessRow ({ origin, permission, allow, onRevoke, onToggleAllow, i18n }
${permission} |
- ${revokeButton({ onRevoke, origin, permission, i18n })}
+ ${revokeButton({ onRevoke, scope, permission, i18n })}
|
`
}
-function revokeButton ({ onRevoke, origin, permission = null, i18n }) {
+function revokeButton ({ onRevoke, scope, permission = null, i18n }) {
const title = permission
? i18n.getMessage('page_proxyAcl_revoke_button_title', permission)
: i18n.getMessage('page_proxyAcl_revoke_all_button_title')
@@ -105,7 +105,7 @@ function revokeButton ({ onRevoke, origin, permission = null, i18n }) {