Skip to content
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

feat: Poor Man's Protocol Handlers #283

Merged
merged 8 commits into from
Sep 27, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion add-on/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "IPFS Companion",
"short_name": "IPFS Companion",
"version" : "2.0.9",
"version" : "2.0.10",

"description": "Browser extension that simplifies access to IPFS resources",
"homepage_url": "https://github.com/ipfs/ipfs-companion",
Expand Down
147 changes: 117 additions & 30 deletions add-on/src/lib/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ async function initStates (options) {
state.automaticMode = options.automaticMode
state.linkify = options.linkify
state.dnslink = options.dnslink
state.catchUnhandledProtocols = options.catchUnhandledProtocols
state.displayNotifications = options.displayNotifications
state.dnslinkCache = /* global LRUMap */ new LRUMap(1000)
}

Expand All @@ -45,6 +47,8 @@ function registerListeners () {
browser.webRequest.onBeforeRequest.addListener(onBeforeRequest, {urls: ['<all_urls>']}, ['blocking'])
browser.storage.onChanged.addListener(onStorageChange)
browser.tabs.onUpdated.addListener(onUpdatedTab)
browser.runtime.onMessage.addListener(onRuntimeMessage)
browser.runtime.onConnect.addListener(onRuntimeConnect)
}

// REDIRECT
Expand All @@ -54,25 +58,14 @@ function publicIpfsResource (url) {
return window.IsIpfs.url(url) && !url.startsWith(state.gwURLString) && !url.startsWith(state.apiURLString)
}

function redirectToCustomGateway (request) {
const url = new URL(request.url)
function redirectToCustomGateway (requestUrl) {
const url = new URL(requestUrl)
url.protocol = state.gwURL.protocol
url.host = state.gwURL.host
url.port = state.gwURL.port
return { redirectUrl: url.toString() }
}

function redirectToNormalizedPath (request) {
const url = new URL(request.url)
let path = decodeURIComponent(url.pathname)
path = path.replace(/^\/web\+fs:[/]*/i, '/') // web+fs://ipfs/Qm → /ipfs/Qm
path = path.replace(/^\/web\+dweb:[/]*/i, '/') // web+dweb://ipfs/Qm → /ipfs/Qm
path = path.replace(/^\/web\+([^:]+):[/]*/i, '/$1/') // web+foo://Qm → /foo/Qm
path = path.replace(/^\/ip([^/]+)\/ip[^/]+\//, '/ip$1/') // /ipfs/ipfs/Qm → /ipfs/Qm
url.pathname = path
return { redirectUrl: url.toString() }
}

// HTTP Request Hooks
// ===================================================================

Expand All @@ -95,14 +88,28 @@ function onBeforeSendHeaders (request) {
}

function onBeforeRequest (request) {
if (request.url.startsWith('https://ipfs.io/web%2B')) {
// poor-mans protocol handlers - https://github.com/ipfs/ipfs-companion/issues/164#issuecomment-328374052
if (state.catchUnhandledProtocols && mayContainUnhandledIpfsProtocol(request)) {
const fix = normalizedUnhandledIpfsProtocol(request)
if (fix) {
return fix
}
}

// handler for protocol_handlers from manifest.json
if (webPlusProtocolRequest(request)) {
// fix path passed via custom protocol
return redirectToNormalizedPath(request)
const fix = normalizedWebPlusRequest(request)
if (fix) {
return fix
}
}

// handle redirects to custom gateway
if (state.redirect) {
// IPFS resources
if (publicIpfsResource(request.url)) {
return redirectToCustomGateway(request)
return redirectToCustomGateway(request.url)
}
// Look for dnslink in TXT records of visited sites
if (isDnslookupEnabled(request)) {
Expand All @@ -111,6 +118,61 @@ function onBeforeRequest (request) {
}
}

// PROTOCOL HANDLERS: web+ in Firefox (protocol_handlers from manifest.json)
// ===================================================================

const webPlusProtocolHandler = 'https://ipfs.io/web%2B'

function webPlusProtocolRequest (request) {
return request.url.startsWith(webPlusProtocolHandler)
}

function pathAtPublicGw (path) {
return new URL(`https://ipfs.io${path}`).toString()
}

function normalizedWebPlusRequest (request) {
const oldPath = decodeURIComponent(new URL(request.url).pathname)
let path = oldPath
path = path.replace(/^\/web\+dweb:\//i, '/') // web+dweb:/ipfs/Qm → /ipfs/Qm
path = path.replace(/^\/web\+ipfs:\/\//i, '/ipfs/') // web+ipfs://Qm → /ipfs/Qm
path = path.replace(/^\/web\+ipns:\/\//i, '/ipns/') // web+ipns://Qm → /ipns/Qm
if (oldPath !== path && window.IsIpfs.path(path)) {
return { redirectUrl: pathAtPublicGw(path) }
}
return null
}

// PROTOCOL HANDLERS: UNIVERSAL FALLBACK FOR UNHANDLED PROTOCOLS
// ===================================================================

const unhandledIpfsRE = /=(?:web%2B|)(ipfs(?=%3A%2F%2F)|ipns(?=%3A%2F%2F)|dweb(?=%3A%2Fip[f|n]s))%3A(?:%2F%2F|%2F)([^&]+)/

function mayContainUnhandledIpfsProtocol (request) {
// TODO: run only for google, bing, duckduckgo etc
// TODO: add checkbox under experiments
return request.url.includes('%3A%2F')
}

function unhandledIpfsPath (requestUrl) {
const unhandled = requestUrl.match(unhandledIpfsRE)
if (unhandled && unhandled.length > 1) {
const unhandledProtocol = decodeURIComponent(unhandled[1])
const unhandledPath = `/${decodeURIComponent(unhandled[2])}`
return window.IsIpfs.path(unhandledPath) ? unhandledPath : `/${unhandledProtocol}${unhandledPath}`
}
return null
}

function normalizedUnhandledIpfsProtocol (request) {
const path = unhandledIpfsPath(request.url)
if (window.IsIpfs.path(path)) {
// replace search query with fake request to the public gateway
// (will be redirected later, if needed)
return { redirectUrl: pathAtPublicGw(path) }
}
}

// DNSLINK
// ===================================================================

Expand Down Expand Up @@ -199,7 +261,18 @@ function readDnslinkTxtRecordFromApi (fqdn) {
})
}

// PORTS
// RUNTIME MESSAGES (one-off messaging)
// ===================================================================
// https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/sendMessage

function onRuntimeMessage (request, sender) {
// console.log((sender.tab ? 'Message from a content script:' + sender.tab.url : 'Message from the extension'), request)
if (request.isIpfsPath) {
return Promise.resolve({isIpfsPath: window.IsIpfs.path(request.isIpfsPath)})
}
}

// PORTS (connection-based messaging)
// ===================================================================
// https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/runtime/connect
// Make a connection between different contexts inside the add-on,
Expand All @@ -209,15 +282,15 @@ function readDnslinkTxtRecordFromApi (fqdn) {
const browserActionPortName = 'browser-action-port'
var browserActionPort

browser.runtime.onConnect.addListener(port => {
function onRuntimeConnect (port) {
// console.log('onConnect', port)
if (port.name === browserActionPortName) {
browserActionPort = port
browserActionPort.onMessage.addListener(handleMessageFromBrowserAction)
browserActionPort.onDisconnect.addListener(() => { browserActionPort = null })
sendStatusUpdateToBrowserAction()
}
})
}

function handleMessageFromBrowserAction (message) {
// console.log('In background script, received message from browser action', message)
Expand Down Expand Up @@ -252,19 +325,22 @@ async function sendStatusUpdateToBrowserAction () {
// ===================================================================

function notify (titleKey, messageKey, messageParam) {
const title = browser.i18n.getMessage(titleKey)
let message
if (messageKey.startsWith('notify_')) {
message = messageParam ? browser.i18n.getMessage(messageKey, messageParam) : browser.i18n.getMessage(messageKey)
} else {
message = messageKey
}

browser.notifications.create({
'type': 'basic',
'iconUrl': browser.extension.getURL('icons/ipfs-logo-on.svg'),
'title': browser.i18n.getMessage(titleKey),
'message': message
})
if (state.displayNotifications) {
browser.notifications.create({
'type': 'basic',
'iconUrl': browser.extension.getURL('icons/ipfs-logo-on.svg'),
'title': title,
'message': message
})
}
console.log(`[ipfs-companion] ${title}: ${message}`)
}

// contextMenus
Expand Down Expand Up @@ -346,17 +422,24 @@ function isIpfsPageActionsContext (url) {
}

async function onUpdatedTab (tabId, changeInfo, tab) {
if (tab && tab.url) {
if (tab && tab.url && !tab.url.startsWith('chrome://')) {
if (state.linkify && changeInfo.status === 'complete') {
console.log(`Running linkfyDOM for ${tab.url}`)
console.log(`[ipfs-companion] Running linkfyDOM for ${tab.url}`)
try {
await browser.tabs.executeScript(tabId, {
file: '/src/lib/npm/browser-polyfill.min.js',
matchAboutBlank: false,
allFrames: true,
runAt: 'document_start'
})
await browser.tabs.executeScript(tabId, {
file: '/src/lib/linkifyDOM.js',
matchAboutBlank: false,
allFrames: true
allFrames: true,
runAt: 'document_idle'
})
} catch (error) {
console.error(`Unable to linkify DOM at '${tab.url}' due to ${error}`)
console.error(`Unable to linkify DOM at '${tab.url}' due to`, error)
}
}
}
Expand Down Expand Up @@ -548,6 +631,10 @@ function onStorageChange (changes, area) { // eslint-disable-line no-unused-vars
state.redirect = change.newValue
} else if (key === 'linkify') {
state.linkify = change.newValue
} else if (key === 'catchUnhandledProtocols') {
state.catchUnhandledProtocols = change.newValue
} else if (key === 'displayNotifications') {
state.displayNotifications = change.newValue
} else if (key === 'automaticMode') {
state.automaticMode = change.newValue
} else if (key === 'dnslink') {
Expand Down
Loading