diff --git a/lib/gateways.js b/lib/gateways.js index a7d86156c..b96954c52 100644 --- a/lib/gateways.js +++ b/lib/gateways.js @@ -99,3 +99,9 @@ Object.defineProperty(exports, 'linkify', { return prefs.linkify } }) + +Object.defineProperty(exports, 'fsUris', { + get: function () { + return prefs.fsUris + } +}) diff --git a/lib/main.js b/lib/main.js index ef3a5fed2..e0cbea8bd 100644 --- a/lib/main.js +++ b/lib/main.js @@ -7,6 +7,7 @@ const parent = require('sdk/remote/parent') exports.main = function (options, callbacks) { // eslint-disable-line no-unused-vars require('./redirects.js') + require('./request-fix.js') require('./peer-watch.js') protocols.register() parent.remoteRequire('./child-main.js', module) diff --git a/lib/protocols.js b/lib/protocols.js index 750f1f607..27a744c8b 100644 --- a/lib/protocols.js +++ b/lib/protocols.js @@ -35,14 +35,39 @@ const HANDLERS = { 'web+ipns': WebIpnsProtocolHandler } +/* + nsIURI does not not resolve relative paths properly + nsIStandardURL does, but always uses /// for authority-less URLs + thus we convert back and forth +*/ +function resolveRelative (base, relative) { + let newBase = Cc['@mozilla.org/network/standard-url;1'].createInstance(Ci.nsIStandardURL) + newBase.QueryInterface(Ci.nsIURI) + newBase.init(Ci.nsIStandardURL.URLTYPE_NO_AUTHORITY, -1, base.spec, null, null) + let resolved = Cc['@mozilla.org/network/standard-url;1'].createInstance(Ci.nsIStandardURL) + resolved.init(Ci.nsIStandardURL.URLTYPE_NO_AUTHORITY, -1, relative, null, newBase) + resolved.QueryInterface(Ci.nsIURI) + + // console.log("rel", base, relative, newBase, resolved) + + return resolved.spec +} + CommonProtocolHandler.prototype = Object.freeze({ defaultPort: -1, allowPort: function (port, scheme) { // eslint-disable-line no-unused-vars return false }, + /* + since ipfs is a public network and pages could just embed their own js-ipfs code anyway to fetch the data + we allow any page to embed ipfs uris and waive CORS restrictions + + TODO: consider adding URI_SAFE_TO_LOAD_IN_SECURE_CONTEXT (discussion needed) + */ protocolFlags: Ci.nsIProtocolHandler.URI_NOAUTH | - Ci.nsIProtocolHandler.URI_LOADABLE_BY_ANYONE, + Ci.nsIProtocolHandler.URI_LOADABLE_BY_ANYONE | + Ci.nsIProtocolHandler.URI_FETCHABLE_BY_ANYONE, normalizedIpfsPath: function (uriSpec) { let schemeExp = this.scheme.replace(/\+/, '\\+') // fix for web+fs etc @@ -56,41 +81,61 @@ CommonProtocolHandler.prototype = Object.freeze({ newURI: function (aSpec, aOriginCharset, aBaseURI) { // console.info('Detected newURI with IPFS protocol: ' + aSpec) + // console.log(aSpec, aOriginCharset, aBaseURI && aBaseURI.spec) if (aBaseURI && aBaseURI.scheme === this.scheme) { // presence of aBaseURI means a dependent resource or a relative link - // and we need to return correct http URI - let http = gw.publicUri.spec + this.pathPrefix + aBaseURI.path - let base = ioservice.newURI(http, null, null) - let uri = ioservice.newURI(aSpec, aOriginCharset, base) - return uri + // resolve relative within the current protocol + // leave the decision to convert to http to the next steps + aSpec = resolveRelative(aBaseURI, aSpec) } - /* Left for future use (if we enable channel.originalURI in newChannel method) - let uri = Cc['@mozilla.org/network/simple-uri;1'].createInstance(Ci.nsIURI) - uri.spec = aSpec - return uri - */ + let normalized = this.normalizedIpfsPath(aSpec) + + // console.log("norm", normalized) + + if (gw.fsUris && this.scheme === FS_SCHEME && new RegExp(`^${this.scheme}:`).test(aSpec)) { + let uri = Cc['@mozilla.org/network/simple-uri;1'].createInstance(Ci.nsIURI) + uri.spec = this.scheme + ':/' + normalized + return uri + } - let http = gw.publicUri.spec + this.normalizedIpfsPath(aSpec) - let uri = ioservice.newURI(http, aOriginCharset, null) + let uri = ioservice.newURI(normalized, aOriginCharset, gw.publicUri) // console.info('newURI routed to HTTP gateway: ' + uri.spec) return uri }, - newChannel: function (aURI) { + newChannel2: function (aURI, loadInfo) { // console.info('Detected newChannel for IPFS protocol: ' + aURI.spec) - let http = gw.publicUri.spec + this.pathPrefix + aURI.path - let channel = ioservice.newChannel(http, aURI.originCharset, null) + let normalized = this.normalizedIpfsPath(this.pathPrefix + aURI.path) + let gwUri = gw.publicUri + let httpUri = ioservice.newURI(normalized, aURI.originCharset, gwUri) + let channel = null + + if (loadInfo !== null) { + channel = ioservice.newChannelFromURIWithLoadInfo(httpUri, loadInfo) + } else { + channel = ioservice.newChannelFromURI(httpUri) + } - // line below would keep nice protocol URL in GUI - // but is disabled for now due to issues like - // https://github.com/lidel/ipfs-firefox-addon/issues/3 - // channel.originalURI = aURI + if (gw.fsUris && this.scheme === FS_SCHEME) { + channel.originalURI = aURI + channel.loadFlags &= ~Ci.nsIChannel.LOAD_REPLACE + // channel.loadFlags |= Ci.nsIChannel.LOAD_ANONYMOUS + } + + channel.loadInfo = loadInfo + + channel.QueryInterface(Ci.nsIWritablePropertyBag) + channel.QueryInterface(Ci.nsIWritablePropertyBag2) + channel.setPropertyAsAString('ipfs-uri', aURI.spec) // console.info('newChannel routed to HTTP gateway: ' + channel.URI.spec) return channel + }, + newChannel: function (aUri) { + return this.newChannel2(aUri, null) } }) diff --git a/lib/request-fix.js b/lib/request-fix.js new file mode 100644 index 000000000..21ea7f4be --- /dev/null +++ b/lib/request-fix.js @@ -0,0 +1,49 @@ +'use strict' + +const {Ci} = require('chrome') +const events = require('sdk/system/events') +const gw = require('./gateways.js') + +/* +const catman = Cc['@mozilla.org/categorymanager;1'].getService(CI.nsICategoryManager) +// category ("net-channel-event-sinks") +catman.addCategoryEntry(in string aCategory, in string aEntry, in string aValue, in boolean aPersist, in boolean aReplace) +*/ + +function fixupRequest (e) { + if (e.type !== 'http-on-modify-request') { + return + } + if (!gw.fsUris) { + return + } + const channel = e.subject.QueryInterface(Ci.nsIHttpChannel) + + channel.QueryInterface(Ci.nsIHttpChannelInternal) + channel.QueryInterface(Ci.nsIPropertyBag2) + channel.QueryInterface(Ci.nsIPropertyBag) + + let isIpfsReq = null + try { + isIpfsReq = channel.hasKey('ipfs-uri') + } catch (e) { + // console.log(e) + } + + if (isIpfsReq && channel.originalURI.scheme === 'fs') { + /* + // TODO: investigate effects of the following flags + // cookies make no sense in the ipfs context, we don't want to carry different gateway cookies into the page + channel.loadFlags |= Ci.nsIRequest.LOAD_ANONYMOUS + // should only do that for /ipfs/ paths since those are stable + channel.loadFlags |= Ci.nsIRequest.VALIDATE_NEVER + */ + + // prevent redirects from replacing the effective URI + channel.loadFlags &= ~Ci.nsIChannel.LOAD_REPLACE + + } + +} + +events.on('http-on-modify-request', fixupRequest) diff --git a/package.json b/package.json index 510eccf47..64a43090f 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,12 @@ "title": "Extended IPFS Link Support", "type": "bool", "value": false + }, + { + "name": "fsUris", + "title": "[Experimental] display fs:/ URIs as-is instead of rewriting to http://", + "type": "bool", + "value": false } ], "permissions": { diff --git a/test/test-canonical-fs-uris.js b/test/test-canonical-fs-uris.js new file mode 100644 index 000000000..3ca6a4f67 --- /dev/null +++ b/test/test-canonical-fs-uris.js @@ -0,0 +1,79 @@ +'use strict' + +const tabs = require('sdk/tabs') +const protocols = require('../lib/protocols.js') +const fsFactory = protocols.fs + +protocols.register() + +const fs = fsFactory.createInstance() +const gw = require('../lib/gateways.js') +const self = require('sdk/self') +const testpage = self.data.url('linkify-demo.html') +const sripage = 'fs:/ipfs/QmSrCRJmzE4zE1nAfWPbzVfanKQNBhp7ZWmMnEdbiLvYNh/mdown#sample.md' +const parent = require('sdk/remote/parent') + +const childMain = require.resolve('../lib/child-main.js') + +parent.remoteRequire(childMain) + +const {Cc, Ci} = require('chrome') +const ioservice = Cc['@mozilla.org/network/io-service;1'].getService(Ci.nsIIOService) + +ioservice.newURI('fs:/ipns/foo', null, null) + +exports['test newURI'] = function (assert) { + require('sdk/simple-prefs').prefs.fsUris = true + + assert.equal(fs.newURI('fs:/ipns/foo', null, null).spec, 'fs:/ipns/foo', 'keeps fs:/ uris as-is') +} + +exports['test newChannel'] = function (assert) { + require('sdk/simple-prefs').prefs.fsUris = true + + let uri = fs.newURI('fs:///ipns/foo', null, null) + let chan = fs.newChannel(uri) + + assert.equal(chan.originalURI.spec, 'fs:/ipns/foo', "keeps fs: URI as channel's originalURI") + + // double and triple slashes lead to gateway redirects, which cause CORS troubles -> check normalization + assert.equal(chan.URI.spec, 'https://ipfs.io/ipns/foo', 'channel has normalized http urls') +} + +// https://github.com/lidel/ipfs-firefox-addon/issues/3 +exports['test subresource loading'] = function (assert, done) { + require('sdk/simple-prefs').prefs.fsUris = true + gw.redirectEnabled = false + + tabs.open({ + url: testpage, + onReady: (tab) => { + + // first load somehow doesn't have protocol handlers registered. so load resource:// first, then redirect to fs:/ page + if (tab.url !== sripage) { + tab.url = sripage + tab.reload() + return + } + + let worker = tab.attach({ + contentScript: ` + let obs = new MutationObserver(function(mutations) { + let result = (document.querySelector("#ipfs-markdown-reader") instanceof HTMLHeadingElement) + self.port.emit("test result", {result: result}) + }) + obs.observe(document.body,{childList: true}) + ` + }) + worker.port.on('test result', (msg) => { + assert.equal(msg.result, true, 'subresource loaded successfully') + + require('sdk/simple-prefs').prefs.fsUris = false + tab.close(done) + }) + } + }) +} + +require('./prefs-util.js').isolateTestCases(exports) +require('sdk/test').run(exports) diff --git a/test/test-linkify.js b/test/test-linkify.js index 5f9af176f..361f038e2 100644 --- a/test/test-linkify.js +++ b/test/test-linkify.js @@ -4,12 +4,14 @@ const tabs = require('sdk/tabs') const parent = require('sdk/remote/parent') const self = require('sdk/self') const testpage = self.data.url('linkify-demo.html') +const prefs = require('sdk/simple-prefs').prefs require('../lib/rewrite-pages.js') parent.remoteRequire('resource://ipfs-firefox-addon-at-lidel-dot-org/lib/rewrite-pages.js') exports['test link processing, plain text conversion'] = function (assert, done) { - require('sdk/simple-prefs').prefs.linkify = true + prefs.linkify = true + prefs.fsUris = true tabs.open({ url: testpage,