From 2648c22dd0f969f0028887347fd5675718cf7453 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Sun, 5 Nov 2017 18:55:34 +0100 Subject: [PATCH] feat: PoC URL Shortener - HTML-based redirect - publish static HTML page to IPFS - Javascript-based redirect (which replaces History entry) with meta-header as a noscript fallback at a public gateway - PoC uses 32bit murmur3 as a preferred Multihash backend (this may change, if the risk of collisions is too high) --- add-on/_locales/en/messages.json | 12 ++++ add-on/src/lib/common.js | 101 ++++++++++++++++++++++++++++-- add-on/src/lib/option-defaults.js | 1 + add-on/src/options/options.html | 9 +++ 4 files changed, 119 insertions(+), 4 deletions(-) diff --git a/add-on/_locales/en/messages.json b/add-on/_locales/en/messages.json index c0b1342e4..9623c5ffb 100644 --- a/add-on/_locales/en/messages.json +++ b/add-on/_locales/en/messages.json @@ -67,6 +67,10 @@ "message": "Upload to IPFS", "description": "An item in right-click context menu" }, + "contextMenu_createShortUrl": { + "message": "Create Short URL (Experimental)", + "description": "An item in right-click context menu" + }, "notify_addonIssueTitle": { "message": "IPFS Add-on Issue", "description": "A title of system notification" @@ -232,6 +236,14 @@ "message": "DNSLINK Support", "description": "An option title on the Preferences screen" }, + "option_preloadAtPublicGateway_title": { + "message": "Preload Uploads", + "description": "An option title on the Preferences screen" + }, + "option_preloadAtPublicGateway_description": { + "message": "Enables automatic preload of uploaded assets via asynchronous HTTP HEAD request to a Public Gateway", + "description": "An option description on the Preferences screen" + }, "option_dnslink_description": { "message": "Perform DNS lookup for every visited website and use Custom Gateway if DNSLINK is present in its DNS TXT record (known to slow down the browser)", "description": "An option description on the Preferences screen" diff --git a/add-on/src/lib/common.js b/add-on/src/lib/common.js index 304c71071..0c1824024 100644 --- a/add-on/src/lib/common.js +++ b/add-on/src/lib/common.js @@ -41,6 +41,7 @@ function initStates (options) { state.automaticMode = options.automaticMode state.linkify = options.linkify state.dnslink = options.dnslink + state.preloadAtPublicGateway = options.preloadAtPublicGateway state.catchUnhandledProtocols = options.catchUnhandledProtocols state.displayNotifications = options.displayNotifications state.dnslinkCache = /* global LRUMap */ new LRUMap(1000) @@ -152,6 +153,10 @@ function onBeforeRequest (request) { // handle redirects to custom gateway if (state.redirect) { + // Ignore preload requests + if (request.method === 'HEAD' && state.preloadAtPublicGateway && request.url.startsWith(state.pubGwURLString)) { + return + } // Detect valid /ipfs/ and /ipns/ on any site if (publicIpfsOrIpnsResource(request.url)) { return redirectToCustomGateway(request.url) @@ -388,6 +393,7 @@ function notify (titleKey, messageKey, messageParam) { // contextMenus // ------------------------------------------------------------------- const contextMenuUploadToIpfs = 'contextMenu_UploadToIpfs' +const contextMenuCreateShortUrl = 'contextMenu_createShortUrl' const contextMenuCopyIpfsAddress = 'panelCopy_currentIpfsAddress' const contextMenuCopyPublicGwUrl = 'panel_copyCurrentPublicGwUrl' @@ -395,6 +401,7 @@ browser.contextMenus.create({ id: contextMenuUploadToIpfs, title: browser.i18n.getMessage(contextMenuUploadToIpfs), contexts: ['image', 'video', 'audio'], + documentUrlPatterns: [''], enabled: false, onclick: addFromURL }) @@ -412,12 +419,88 @@ browser.contextMenus.create({ documentUrlPatterns: ['*://*/ipfs/*', '*://*/ipns/*'], onclick: copyAddressAtPublicGw }) +browser.contextMenus.create({ + id: contextMenuCreateShortUrl, + title: browser.i18n.getMessage(contextMenuCreateShortUrl), + contexts: ['page', 'image', 'video', 'audio', 'link'], + documentUrlPatterns: [''], + enabled: false, + onclick: copyShortAddressAtPublicGw +}) function inFirefox () { return !!navigator.userAgent.match('Firefox') } +// URL Shortener +// ------------------------------------------------------------------- + +async function copyShortAddressAtPublicGw (info) { + let longUrl = await findUrlForContext(info) + if (longUrl.startsWith(state.gwURLString)) { + // normalize local URL to point at the public GW + const rawIpfsAddress = longUrl.replace(/^.+(\/ip(f|n)s\/.+)/, '$1') + longUrl = urlAtPublicGw(rawIpfsAddress) + } + const redirectHtml = ` + + + + ${longUrl} + + + Redirecting to ${longUrl} + ` + // console.log('html for redirect', redirectHtml) + const buffer = ipfs.Buffer.from(redirectHtml, 'utf-8') + const opts = {hash: 'murmur3'} + console.log('[ipfs-companion] shortening URL to a MurmurHash3', longUrl) + ipfs.add(buffer, opts, urlShorteningResultHandler) +} + +function urlShorteningResultHandler (err, result) { + if (err || !result) { + console.error('ipfs add error', err, result) + notify('notify_uploadErrorTitle', 'notify_inlineErrorMsg', `${err}`) + return + } + result.forEach(function (file) { + if (file && file.hash) { + const path = `/ipfs/${file.hash}` + const shortUrlAtPubGw = urlAtPublicGw(path) + copyTextToClipboard(shortUrlAtPubGw) + notify('notify_copiedPublicURLTitle', shortUrlAtPubGw) + if (state.preloadAtPublicGateway) { + preloadAtPublicGateway(path) + } + } + }) +} + +function preloadAtPublicGateway (path) { + // asynchronous HTTP HEAD request preloads triggers content without downloading it + return new Promise((resolve, reject) => { + const http = new XMLHttpRequest() + http.open('HEAD', urlAtPublicGw(path)) + http.onreadystatechange = function () { + if (this.readyState === this.DONE) { + console.log(`[ipfs-companion] preloadAtPublicGateway(${path}):`, this.statusText) + if (this.status === 200) { + resolve(this.statusText) + } else { + reject(new Error(this.statusText)) + } + } + } + http.send() + }) +} + +// URL Uploader +// ------------------------------------------------------------------- + async function addFromURL (info) { + const srcUrl = await findUrlForContext(info) try { if (inFirefox()) { // workaround due to https://github.com/ipfs/ipfs-companion/issues/227 @@ -427,7 +510,7 @@ async function addFromURL (info) { } // console.log('addFromURL.info', info) // console.log('addFromURL.fetchOptions', fetchOptions) - const response = await fetch(info.srcUrl, fetchOptions) + const response = await fetch(srcUrl, fetchOptions) const reader = new FileReader() reader.onloadend = () => { const buffer = ipfs.Buffer.from(reader.result) @@ -435,7 +518,7 @@ async function addFromURL (info) { } reader.readAsArrayBuffer(await response.blob()) } else { - ipfs.util.addFromURL(info.srcUrl, uploadResultHandler) + ipfs.util.addFromURL(srcUrl, uploadResultHandler) } } catch (error) { console.error(`Error for ${contextMenuUploadToIpfs}`, error) @@ -459,14 +542,21 @@ function uploadResultHandler (err, result) { } result.forEach(function (file) { if (file && file.hash) { + const path = `/ipfs/${file.hash}` browser.tabs.create({ - 'url': new URL(state.gwURLString + '/ipfs/' + file.hash).toString() + 'url': new URL(state.gwURLString + path).toString() }) - console.log('successfully stored', file.hash) + console.log('successfully stored', path) + if (state.preloadAtPublicGateway) { + preloadAtPublicGateway(path) + } } }) } +// Copying URLs +// ------------------------------------------------------------------- + async function findUrlForContext (context) { if (context) { if (context.linkUrl) { @@ -533,6 +623,7 @@ async function copyTextToClipboard (copyText) { async function updateContextMenus (changedTabId) { await browser.contextMenus.update(contextMenuUploadToIpfs, {enabled: state.peerCount > 0}) + await browser.contextMenus.update(contextMenuCreateShortUrl, {enabled: state.peerCount > 0}) if (changedTabId) { // recalculate tab-dependant menu items const currentTab = await browser.tabs.query({active: true, currentWindow: true}).then(tabs => tabs[0]) @@ -807,6 +898,8 @@ function onStorageChange (changes, area) { // eslint-disable-line no-unused-vars state.automaticMode = change.newValue } else if (key === 'dnslink') { state.dnslink = change.newValue + } else if (key === 'preloadAtPublicGateway') { + state.preloadAtPublicGateway = change.newValue } } } diff --git a/add-on/src/lib/option-defaults.js b/add-on/src/lib/option-defaults.js index 0ca64ec06..56063e810 100644 --- a/add-on/src/lib/option-defaults.js +++ b/add-on/src/lib/option-defaults.js @@ -7,6 +7,7 @@ const optionDefaults = Object.freeze({ // eslint-disable-line no-unused-vars automaticMode: true, linkify: false, dnslink: false, + preloadAtPublicGateway: true, catchUnhandledProtocols: true, displayNotifications: true, customGatewayUrl: 'http://127.0.0.1:8080', diff --git a/add-on/src/options/options.html b/add-on/src/options/options.html index dca2f02dc..e0eac8bbd 100644 --- a/add-on/src/options/options.html +++ b/add-on/src/options/options.html @@ -173,6 +173,15 @@ +
+ + +