Skip to content

Commit

Permalink
feat: PoC URL Shortener
Browse files Browse the repository at this point in the history
- 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)
  • Loading branch information
lidel committed Nov 5, 2017
1 parent 83ff53e commit 2648c22
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 4 deletions.
12 changes: 12 additions & 0 deletions add-on/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
101 changes: 97 additions & 4 deletions add-on/src/lib/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -388,13 +393,15 @@ function notify (titleKey, messageKey, messageParam) {
// contextMenus
// -------------------------------------------------------------------
const contextMenuUploadToIpfs = 'contextMenu_UploadToIpfs'
const contextMenuCreateShortUrl = 'contextMenu_createShortUrl'
const contextMenuCopyIpfsAddress = 'panelCopy_currentIpfsAddress'
const contextMenuCopyPublicGwUrl = 'panel_copyCurrentPublicGwUrl'

browser.contextMenus.create({
id: contextMenuUploadToIpfs,
title: browser.i18n.getMessage(contextMenuUploadToIpfs),
contexts: ['image', 'video', 'audio'],
documentUrlPatterns: ['<all_urls>'],
enabled: false,
onclick: addFromURL
})
Expand All @@ -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: ['<all_urls>'],
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 = `<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<noscript><meta http-equiv='refresh' content='0; URL=${longUrl}' /></noscript>
<title>${longUrl}</title>
</head>
<body onload='window.location.replace("${longUrl}")'>
Redirecting to <a href='${longUrl}'>${longUrl}</a>
`
// 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
Expand All @@ -427,15 +510,15 @@ 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)
ipfs.add(buffer, uploadResultHandler)
}
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)
Expand All @@ -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) {
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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
}
}
}
Expand Down
1 change: 1 addition & 0 deletions add-on/src/lib/option-defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
9 changes: 9 additions & 0 deletions add-on/src/options/options.html
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,15 @@
</label>
<input type="checkbox" id="displayNotifications" />
</div>
<div>
<label for="preloadAtPublicGateway">
<dl>
<dt data-i18n="option_preloadAtPublicGateway_title"><dt>
<dd data-i18n="option_preloadAtPublicGateway_description"></dd>
</dl>
</label>
<input type="checkbox" id="preloadAtPublicGateway" />
</div>
<div>
<label for="catchUnhandledProtocols">
<dl>
Expand Down

0 comments on commit 2648c22

Please sign in to comment.