-
Notifications
You must be signed in to change notification settings - Fork 324
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
window.ipfs #333
window.ipfs #333
Conversation
add-on/manifest.json
Outdated
"http://*/*", | ||
"https://*/*" | ||
], | ||
"match_about_blank": true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't think it's necessary to inject on blank pages no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Amazing! Thanks for starting to implementing this! Will be a great feature.
Looks like something that can work, at least as a beginning. The most important part is to get streaming responses to work over the Ports, and is also where I got stuck. Try to implement the .cat
method with streaming response, so we can make sure it's working before adding code for window.ipfs.
add-on/manifest.json
Outdated
{ | ||
"matches": [ | ||
"file://*/*", | ||
"http://*/*", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll eventually inject programmatically due to the need to turn this feature off, but yes that would be better.
Extracting this proxy idea for |
cd992d9
to
5518545
Compare
bd84eb7
to
0e42bdf
Compare
cc5db5c
to
9b29486
Compare
const injectScript = require('./inject-script') | ||
|
||
function init () { | ||
const port = browser.runtime.connect({ name: 'ipfs-proxy' }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If other extensions know this name, is it possible they can connect to it? That would be bad, as they could bypass the permissions you've setup, if I understand correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would! I'll check it out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...although thinking about it, the access control is all done in the extension background process, so it doesn't matter who's connecting and asking for stuff, it still has to be authorised by the user.
I'll double check but I think it might be ok.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok so, the background process is listening to runtime.onConnect
:
Fired when a connection is made with either an extension process or a content script.
https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/onConnect
It would need to listen to runtime.onConnectExternal
to receive connections from other extensions.
Aside from that, even if connections from other extensions were allowed, the authorization is done in the background process, so the user would still need to have approved the function to be called before the proxy server let it through to the IPFS instance.
registerListeners() | ||
await setApiStatusUpdateInterval(options.ipfsApiPollMs) | ||
await storeMissingOptions(options, optionDefaults, browser.storage.local) | ||
await storeMissingOptions( | ||
await browser.storage.local.get(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain this change? The code looks fine, just curious.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes! Previously storeMissingOptions
wasn't doing anything.
On first run, options
will be the same as optionDefaults
. Due to the line earlier:
const options = await browser.storage.local.get(optionDefaults)
Regarding the parameter to get
:
A key (string) or keys (an array of strings or an object specifying default values) to identify the item(s) to be retrieved from storage
https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/storage/StorageArea/get#Parameters
...so, it finds {}
in storage and then assigns optionDefaults
to it.
So storeMissingOptions
is given two identical objects and does nothing.
Similarly for subsequent runs, options will be assigned any missing defaults so there's never anything to store!
Instead, we get a fresh copy of the current options, without defaults applied.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice catch! I think I've made this bug while porting from legacy SDK to WebExtension APIs.
Due to the "fallback to defaults" it worked anyway, so we never noticed :):)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...probably should be in a separate PR - apologies
const acl = await this.getAcl() | ||
|
||
acl[origin] = acl[origin] || {} | ||
acl[origin][permission] = allow |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard against the forbidden origins like __proto__
? 👻
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's less pretty, but might be safer to structure the acl with data that comes from the outside as values rather than keys...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a Map
instead of an plain old object would be safer right?
add-on/src/lib/ipfs-proxy/index.js
Outdated
'swarm.disconnect' | ||
] | ||
|
||
function createIpfsProxy (getIpfs, getState) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function is doing some intricate plumbing. It's very cool. Please add some docs to pin down some of the helpful background context that is in your head so future us can pick it up.
Things like "this is when a tab is created, but also caveat, caveat" and "This end is talking from proxy to ipfs node, this end is content script to proxy.", those sort of things.
package.json
Outdated
@@ -37,7 +40,10 @@ | |||
"private": true, | |||
"preferGlobal": false, | |||
"devDependencies": { | |||
"babel-preset-env": "^1.6.1", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pin those deps.
@@ -18,13 +15,19 @@ | |||
"build:copy": "run-s build:copy:*", | |||
"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 -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/ipfs-companion-common.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", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to self... capture the browserify config in a file so we can re-use it for watchify.
expect(acl).to.deep.equal({}) | ||
|
||
const sets = [ | ||
['http://ipfs.io', 'ipfs.files.add', true], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Worth ensuring that urls are normalised correctly...
are
http://ipfs.io
http://ipfs.io/
http://ipfs.io/ipfs/QmHash
https://ipfs.io/
equivalent for permissions purposes? are
https://www.ipfs.io
https://ipfs.io
different for permissions purposes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The origin for permissions purposes is extracted out of the URL that is reported by the port so this information doesn't come from "outside", and nor does the permission name (comes from ACL_FUNCTIONS
) or allow/deny decision (comes from window.confirm
).
So the origin is normalised already by virtue of parsing the URL and extracting the origin. For permissions we're using the same-origin policy, so URLs for a different origin need different approval.
An origin is defined as a combination of URI scheme, host name, and port number
https://en.wikipedia.org/wiki/Same-origin_policy
http://ipfs.io
andhttp://ipfs.io/
are normalised tohttp://ipfs.io
http://ipfs.io/ipfs/QmHash
not supported as a separate "origin" yet (mentioned in "What's still todo (in a future PR)?" in the description) so origin would behttp://ipfs.io
https://ipfs.io
is a different origin for permissions (because scheme differs)https://www.ipfs.io
andhttps://ipfs.io
, also a different origin for permissions (because host differs)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah sure, yes, I was trying to say that there is an implicit dependency for this to function correctly on the url having been normalised before it gets passed on to the ACL, but there is no test to assert it.
With the new changes the ACL will error out if the user tries to set a value that isn't a valid origin as normalised by URL
, so i guess that covers it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✨ 🚀 ✨
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice work @alanshaw
Ignore me. I didn't have the latest commit. It builds fine under both npm and yarn now. |
No |
@olizilla review feedback done, is good now? |
add-on/src/lib/ipfs-proxy/index.js
Outdated
@@ -5,6 +5,8 @@ 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps one for a next PR, but ongoing maintenance would be simpler if we flip this to a whitelist of methods that can be called without access control... as the ipfs api changes we can jump to the latest release and then figure out later if a new api should be whitelisted.
It's working in Chrome again now! Is very nice. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fantastic work @alanshaw 🚀 👍
Added some notes in code and below:
acls
Before we merge this I think we should switch ACLs from whitelist-by-default to prompt-by-default. Better to sort this out from the beginning.
window.confirm
I don't think a window.confirm
prompt is enough in the long run, even if we ignore issue with Firefox.
User should be able to select one of four options:
(i) allow method (ii) deny method
(iii) allow everything for this origin (iv) deny everything for this origin
We could simplify UI for this by going with two buttons (allow aX/deny X) and a checkbox "remember choice for all API functions for this origin", which when checked changes "X" from function name to "everything", or something like that.
I was thinking about providing custom page via browserAction, but we can't trigger "onclick" on it programmatically.
Here's an idea: what if we display a prompt in.. a new tab, that is closed right after user picks allow/deny?
add-on/_locales/en/messages.json
Outdated
@@ -265,6 +265,18 @@ | |||
"message": "DNSLINK Support", | |||
"description": "An option title on the Preferences screen (option_dnslink_title)" | |||
}, | |||
"option_ipfsProxy_title": { | |||
"message": "window.ipfs", | |||
"description": "An option title for enabling/disabling the IPFS proxy" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please append key names at the end of descriptions, eg:
"description": "An option title for enabling/disabling the IPFS proxy (option_ipfsProxy_title)"
This is so that crowdin users can see it as additional hint about message's context.
(Crowdin UI hides key names for some reason 🤔 )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure 👍
const _Buffer = Buffer | ||
|
||
window.Buffer = window.Buffer || _Buffer | ||
window.Ipfs = window.Ipfs || Ipfs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should expose both window.Ipfs
and window.ipfs
.
Naming is quite unfortunate :'(
Is there a good use case for exposing Ipfs
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you want an IPFS node configured with particular swarm peers so using the one provided by the extension is not possible? ...and you don't want to download the IPFS lib because you're on a poor connection?
Maybe...for experiments?
Are they good enough?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say yes, it would make developer's life easier. 👍
But perhaps it should be renamed to window.IpfsApi
or something like that?
@diasdavid, would appreciate your thoughts on this from js-ipfs-api
perspective
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While this is fine for a PoC, real life implementation has to be the other way around.
Namely, we should require ACL for everything by default, and have a short whitelist of safe functions that do not require permission.
That way we will not introduce security issues when version bump of js-ipfs(-api) introduces a new function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree and I'm happy to switch, but can this be done in a separate PR so we can get this merged?
Just FYI, bumping js-ipfs alone wouldn't expose new functions, you'd have to also add the new functions to ipfs-postmsg-proxy for them to become available.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it can be a separate PR.
As long as the checkbox enabling window.ipfs is disabled by default, we can merge it. 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, window.ipfs checkbox is disabled by default
package.json
Outdated
@@ -48,8 +54,11 @@ | |||
"fs-promise": "2.0.3", | |||
"geckodriver": "1.10.0", | |||
"husky": "0.14.3", | |||
"ignore-styles": "^5.0.1", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's drop ^
:)
(here, in nyc and p-queue)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, no problem
constructor (storage, key = 'ipfsProxyAcl') { | ||
super() | ||
this._storage = storage | ||
this._key = key |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if we store ACL keys with a prefix?
eg. set this._key = 'ipfsProxyAcl.' + key
(or add/remove a prefix in getAcl
and _setAcl
)
It removes risk of overriding storage keys + makes it easier to browse/sort local storage keys when debugging the extension.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, just to confirm our keys would look like 'ipfsProxyAcl.http://google.co.uk
for example?
Currently things look sort of like this:
{
ipfsProxyAcl: {
'http://google.co.uk': {
'files.add': true
},
'https://ipfs.io': {
'files.add': false,
'object.new': false
}
}
}
...and we'd be altering it to be something like:
{
'ipfsProxyAcl.http://google.co.uk': {
'files.add': true
},
'ipfsProxyAcl.https://ipfs.io': {
'files.add': false,
'object.new': false
}
}
I like this and I think it makes things simpler, the only issue I can think of is getting the entire ACL for the manage permissions page might be tricky - we'd have to read everything in storage into memory and filter just the keys we need.
I think that's a better trade of though since atm we're reading the entire ACL into memory for every getAccess
and setAccess
, which will be far more commonly called than getAcl
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds like a plan 👌
I think its the only thing that blocks merging this PR, other things can be addressed in separate PRs
let allow | ||
|
||
try { | ||
allow = window.confirm(msg) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just FYI, but just like you said this won't work on Firefox. Modals are not allowed from background page.
Confirmed with ipfs.add
– it results in no prompt, access is automatically denied:
Browser Console confirms prompt was not allowed:
There are related tickets at bugzilla tl;dr this is by design and they suggest to display prompt via content script:
We don't intend to support alert() in background pages.
If you want to alert data based on a context menu item, you can do so from a content script.
– https://bugzilla.mozilla.org/show_bug.cgi?id=1203394#c41
I'm telling you that it won't be accessible from background pages. Alerts are modal dialogs, and they're meant to be tied to a specific, user-visible window. If you want to trigger an alert from a context menu operation, it needs to be triggered from the appropriate content window.
– https://bugzilla.mozilla.org/show_bug.cgi?id=1203394#c43
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, as I also mentioned in the description, the workaround for Firefox right now will be to go to the "manage permissions" page and change the "deny" to "allow".
I will send a separate PR for a custom confirm dialog with options to allow/deny all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alanshaw I've noticed and found how Greasemonkey displays confirmation dialog (under Firefox):
(HTML page loaded in new browser window with 'type': 'popup'
)
What is also interesting is Android detection via chrome.runtime.getPlatformInfo
:)
@lidel is it good to go now? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
Yes! 💯 Fantastic work @alanshaw! 👌 🚀 ✨ |
This is basically the good work of @victorbjelkholm done in 6b0ac8b but cleaned up a little.
refs #330
This is a first pass effort at getting this to work.
Demo: https://youtu.be/t1ldUp_mjDk
What's in this PR?
window.ipfs
property in your web pages!nyc
for code coverageWhat's still todo (in a future PR)?
window.confirm
from the background process so it fails - all access towindow.ipfs
on Firefox will be automatically denied, however as a temporary workaround, Firefox users can use the manage permissions page to enable permissions after they have been auto deniedwindow.confirm
can't cover these UX requirementsHow does it work?
https://www.youtube.com/watch?v=aXLERp9-Mis