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

[PoC] libdweb experiment: protocol handler API #533

Closed
wants to merge 35 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
51da0a5
feat: add libdweb-build target
lidel Jul 16, 2018
43bdb5f
feat: PoC for browser.protocol.registerProtocol
lidel Jul 17, 2018
08ef6f3
docs: notes on eveloping with libdweb
lidel Jul 17, 2018
a9f2b4d
fix: crashes caused by omitted contentType in protocol handler
lidel Jul 17, 2018
33cf210
feat: PoC of browser.protocol with directory listing
lidel Jul 17, 2018
101966e
feat: streaming in ipfs://
lidel Jul 20, 2018
dd870ee
fix: basic error handling for libdweb protocol
lidel Jul 22, 2018
0a305c8
Merge remote-tracking branch 'origin/master' into libdweb
lidel Aug 2, 2018
1d814f7
PoC IPNS support
lidel Aug 2, 2018
2ddf9ea
docs: simplified libdweb dev notes
lidel Aug 2, 2018
252776a
fix: make ipns:// work with embedded js-ipfs
lidel Aug 2, 2018
3378c1a
fix: meaningful error when global toggle is off
lidel Aug 2, 2018
c488120
feat(libdweb): download nightly if missing
lidel Aug 3, 2018
f4486de
fix(libdweb): udpate web-ext to fix addon-lint
lidel Aug 3, 2018
8beea27
feat(libdweb): run libdweb:build on win as a part of CI
lidel Aug 3, 2018
1d18fc5
feat(libdweb): run libdweb:build on mac during CI
lidel Aug 3, 2018
3133ad0
fix(libdweb/ci): switch to dev yarn build
lidel Aug 3, 2018
34eafa4
fix(libdweb/ci): network lock and submodule sync
lidel Aug 3, 2018
0a0b685
feat(libdweb): remove dependency on git submodule
lidel Aug 4, 2018
720debc
Merge remote-tracking branch 'origin/master' into libdweb
lidel Aug 8, 2018
9a91aca
fix(libdweb/ci): ipfs-protocol-muon-brave.test.js
lidel Aug 8, 2018
1a830dd
feat(libdweb): reuse dnslink cache for ipns://
lidel Aug 8, 2018
0ea698e
refactor(libdweb): reuse dir listing from js-ipfs
lidel Aug 10, 2018
a986ddb
feat(libdweb): switch to async handler
lidel Aug 12, 2018
b394646
fix(libdweb): detection of image/svg+xml
lidel Aug 12, 2018
e4c4bec
fix(libdweb): render markdown as plain text
lidel Aug 13, 2018
31cb664
docs(libdweb): add note about fix for yarn caching github deps
lidel Aug 13, 2018
3e1fd75
fix(libdweb): decodeURI(path) before IPFS lookup
lidel Aug 13, 2018
87ef474
fix(libdweb): allow loading of cross-origin assets
lidel Aug 15, 2018
8c87835
fix(libdweb): non-unwrappable cross-compartment wrapper promise error
lidel Aug 16, 2018
1bf413f
fix: missing content-sniffer
lidel Sep 14, 2018
eb399fd
Merge branch 'master' into libdweb
lidel Sep 14, 2018
39b2d0e
fix: workaround for mused module types
lidel Oct 11, 2018
72cb11a
Merge remote-tracking branch 'origin/master' into libdweb
lidel Oct 11, 2018
d6dfc0c
chore: switch to ipfs-http-response v0.2.0
lidel Oct 16, 2018
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
4 changes: 3 additions & 1 deletion .babelrc
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
{
"plugins": [
"transform-es2015-modules-commonjs",
"syntax-async-generators"
],
"presets": [
[
"@babel/preset-env",
{
"debug": true,
"targets": {
"firefox": 61,
"firefox": 63,
"chrome": 67
}
}
Expand Down
24 changes: 24 additions & 0 deletions add-on/manifest.firefox-libdweb.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"applications": {
"gecko": {
"id": "[email protected]"
}
},
"protocol_handlers": [],
"experiment_apis": {
"protocol": {
"schema": "../node_modules/libdweb/src/protocol/protocol.json",
"child": {
"scopes": ["addon_child"],
"paths": [["protocol"]],
"script": "../node_modules/libdweb/src/protocol/client.js"
},
"parent": {
"events": ["startup"],
"scopes": ["addon_parent"],
"paths": [["protocol"]],
"script": "../node_modules/libdweb/src/protocol/host.js"
}
}
}
}
2 changes: 1 addition & 1 deletion add-on/manifest.firefox.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"applications": {
"gecko": {
"id": "[email protected]",
"strict_min_version": "59.0"
"strict_min_version": "63.0"
}
},
"page_action": {
Expand Down
11 changes: 8 additions & 3 deletions add-on/src/lib/ipfs-companion.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ const { createIpfsPathValidator, pathAtHttpGateway } = require('./ipfs-path')
const createDnslinkResolver = require('./dnslink')
const { createRequestModifier, redirectOptOutHint } = require('./ipfs-request')
const { initIpfsClient, destroyIpfsClient } = require('./ipfs-client')
const { createIpfsUrlProtocolHandler } = require('./ipfs-protocol')
const createNotifier = require('./notifier')
const createCopier = require('./copier')
const createRuntimeChecks = require('./runtime-checks')
Expand Down Expand Up @@ -106,9 +105,15 @@ module.exports = async function init () {
browser.runtime.onMessage.addListener(onRuntimeMessage)
browser.runtime.onConnect.addListener(onRuntimeConnect)

if (runtime.hasNativeProtocolHandler) {
console.log('[ipfs-companion] registerStringProtocol available. Adding ipfs:// handler')
if (runtime.hasNativeProtocolHandler && browser.protocol.registerStringProtocol) {
console.log('[ipfs-companion] registerStringProtocol from Muon-Brave is available. Adding ipfs:// handler')
const { createIpfsUrlProtocolHandler } = require('./ipfs-protocol-muon-brave')
browser.protocol.registerStringProtocol('ipfs', createIpfsUrlProtocolHandler(() => ipfs))
} else if (runtime.hasNativeProtocolHandler && browser.protocol.registerProtocol) {
console.log('[ipfs-companion] registerProtocol from mozilla/libdweb is available. Adding ipfs:// handler')
const { createIpfsUrlProtocolHandler } = require('./ipfs-protocol-libdweb')
browser.protocol.registerProtocol('ipfs', createIpfsUrlProtocolHandler(getIpfs, dnslinkResolver))
browser.protocol.registerProtocol('ipns', createIpfsUrlProtocolHandler(getIpfs, dnslinkResolver))
} else {
console.log('[ipfs-companion] browser.protocol.registerStringProtocol not available, native protocol will not be registered')
}
Expand Down
188 changes: 188 additions & 0 deletions add-on/src/lib/ipfs-protocol-libdweb.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
'use strict'
/* eslint-env browser, webextensions */

import { asyncIterateStream } from 'async-iterate-stream/asyncIterateStream'
// import streamHead from 'stream-head'

const { resolver } = require('ipfs-http-response')
const { mimeSniff } = require('./mime-sniff')
// const detectContentType = require('ipfs-http-response/src/utils/content-type')
const toArrayBuffer = require('to-arraybuffer')
const peek = require('buffer-peek-stream')
// const dirView = require('./dir-view')
const PathUtils = require('ipfs/src/http/gateway/utils/path')
const isStream = require('is-stream')

const textBuffer = (data) => new TextEncoder('utf-8').encode(data).buffer

/* protocol handler for mozilla/libdweb */

exports.createIpfsUrlProtocolHandler = (getIpfs, dnslinkResolver) => {
return async (request) => {
console.time('[ipfs-companion] LibdwebProtocolHandler')
console.log(`[ipfs-companion] handling ${request.url}`)
try {
const ipfs = getIpfs()
const url = request.url
// Get /ipfs/ path for URL (resolve to immutable snapshot if ipns://)
const path = immutableIpfsPath(url, dnslinkResolver)
// Then, fetch response from IPFS
const { content, contentType, contentEncoding } = await getResponse(ipfs, url, path)

// console.log(`contentType=${contentType}, contentEncoding=${contentEncoding}, content`, content)

return {
contentEncoding,
contentType,
content: streamRespond(content)
}
} catch (err) {
console.error(`[ipfs-companion] failed to get data for ${request.url}`, err)
return {
contentEncoding: 'utf-8',
contentType: 'text/plain',
content: streamRespond(toErrorResponse(request, err))
}
} finally {
console.timeEnd('[ipfs-companion] LibdwebProtocolHandler')
}
}
}

async function * streamRespond (response) {
if (isStream(response)) {
for await (const chunk of asyncIterateStream(response, false)) {
// Buffer to ArrayBuffer
yield toArrayBuffer(chunk)
}
} else {
// just a buffer
yield response
}
}

// Prepare response with a meaningful error
function toErrorResponse (request, error) {
// console.error(`IPFS Error while getting response for ${request.url}`, error)
// TODO
// - create proper error page
// - find a way to communicate common errors (eg. not found, invalid CID, timeout)
if (error.message === 'file does not exist') {
// eg. when trying to access a non-existing file in a directory (basically http 404)
return textBuffer('Not found')
} else if (error.message === 'selected encoding not supported') {
// eg. when trying to access invalid CID
return textBuffer('IPFS Error: selected encoding is not supported in browser context. Make sure your CID is a valid CIDv1 in Base32.')
}
return textBuffer(`Unable to produce IPFS response for "${request.url}": ${error}`)
}

function immutableIpfsPath (url, dnslinkResolver) {
// TODO:
// - detect invalid addrs and display error page

// Move protocol to IPFS-like path
let path = url.replace(/^([^:]+):\/\/*/, '/$1/')
// Unescape special characters, eg. ipns://tr.wikipedia-on-ipfs.org/wiki/G%C3%BCne%C5%9F_r%C3%BCzg%C3%A2r%C4%B1.html
path = decodeURI(path)
// Handle IPNS (if present)
if (path.startsWith('/ipns/')) {
// js-ipfs does not implement ipfs.name.resolve yet, so we only do dnslink lookup
// const response = await ipfs.name.resolve(path, {recursive: true, nocache: false})
const fqdn = path.split('/')[2]
const dnslinkRecord = dnslinkResolver.readAndCacheDnslink(fqdn)
if (!dnslinkRecord) {
throw new Error(`Missing DNS TXT with dnslink for '${fqdn}'`)
}
path = path.replace(`/ipns/${fqdn}`, dnslinkRecord)
}
return path
}

async function getResponse (ipfs, url, path) {
let cid
try {
// try direct resolver, then fallback to manual one
const dag = await resolver.cid(ipfs, path)
cid = dag.cid
console.log('resolver.cid', cid.toBaseEncodedString())
} catch (err) {
// console.error('error in resolver.cid', err)
// handle directories
if (err.cid && err.message === 'This dag node is a directory') {
const dirCid = err.cid
console.log('resolver.directory', dirCid.toBaseEncodedString())
const data = await resolver.directory(ipfs, url, dirCid)
console.log('resolver.directory', Array.isArray(data) ? data : `returned '${typeof data}'`)
// TODO: redirect so directory always end with `/`
if (typeof data === 'string') {
// return HTML with directory listing
return {
content: textBuffer(data),
contentType: 'text/html',
contentEncoding: 'utf-8'
}
} else if (Array.isArray(data)) {
console.log('resolver.directory.indexes', data)
// return first index file
path = PathUtils.joinURLParts(path, data[0].name)
return getResponse(ipfs, url, path)
}
throw new Error('Invalid output of resolver.directory')
} else if (err.parentDagNode && err.missingLinkName) {
// It may be legitimate error, but it could also be a part of hamt-sharded-directory
// (example: ipns://tr.wikipedia-on-ipfs.org/wiki/Anasayfa.html)
// which is not supported by resolver.cid from ipfs-http-response at this time.
// Until ipfs.resolve support with sharding is added upstream, we use fallback below.
// TODO remove this after ipfs-http-response switch to ipfs.resolve
// or sharding is supported by some other means
try {
const matchingLink = (await ipfs.ls(err.parentDagNode, { resolveType: false })).find(item => item.name === err.missingLinkName)
if (matchingLink) {
console.log('resolver.cid.err.matchingLink', matchingLink)
path = path.replace(matchingLink.path, matchingLink.hash)
console.log('resolver.cid.err.path.after.matchingLink', path)
cid = path
// return getResponse(ipfs, url, path)
} else {
throw err
}
} catch (err) {
console.error('Trying to recover from Error while resolver.cid', err)
if (err.message === 'invalid node type') {
throw new Error('hamt-sharded-directory support is spotty with js-ipfs at this time, try go-ipfs until a fix is found')
} else {
// TODO: investigate issue with js-ipfs and ipns://tr.wikipedia-on-ipfs.org/wiki/Anasayfa.html
// (probably edge case relate to sharding)
// For now we fallback to ipfs.files.catReadableStream(full path)
cid = path
}
}
} else {
// unexpected issue, return error
throw err
}
}

// Efficient mime-sniff over initial bytes
// const { stream, head } = await streamHead(ipfs.files.catReadableStream(cid), {bytes: 512})
// const contentType = mimeSniff(head, new URL(url).pathname) || undefined
// below old version with buffer-peek-stream
const { stream, contentType } = await new Promise((resolve, reject) => {
try {
console.log(`ipfs.files.catReadableStream(${cid})`)
const catStream = ipfs.files.catReadableStream(cid)
peek(catStream, 512, (err, data, stream) => {
if (err) return reject(err)
const contentType = mimeSniff(data, new URL(url).pathname) || undefined
// TODO: switch to upstream const contentType = detectContentType(new URL(url).pathname, data) || 'application/octet-stream'
resolve({ stream, contentType })
})
} catch (err) {
reject(err)
}
})
console.log(`[ipfs-companion] [ipfs://] handler read ${path} and internally mime-sniffed it as ${contentType}`)

return { content: stream, contentType }
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* this file requires Muon-Brave */
const { mimeSniff } = require('./mime-sniff')
const dirView = require('./dir-view')
const PathUtils = require('ipfs/src/http/gateway/utils/path')
Expand Down
22 changes: 16 additions & 6 deletions add-on/src/lib/mime-sniff.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
'use strict'
/* eslint-env browser, webextensions */

const docSniff = require('doc-sniff')
const fileType = require('file-type')
const isSvg = require('is-svg')
const mime = require('mime-types')

/*
* A quick, best effort mime sniffing fn, via:
* @see https://github.com/sindresorhus/file-type
* @see https://github.com/sindresorhus/is-svg
* @see https://github.com/bitinn/doc-sniff
*
* buffer => 'mime/type'
Expand All @@ -16,16 +17,25 @@ const mime = require('mime-types')
exports.mimeSniff = function (buff, path) {
// deals with buffers, and uses magic number detection
const fileTypeRes = fileType(buff)
if (fileTypeRes) return fileTypeRes.mime
if (fileTypeRes) {
const pathSniffRes = mime.lookup(path)
if (fileTypeRes.mime === 'application/xml' && pathSniffRes === 'image/svg+xml') {
// detected SVGs
return pathSniffRes
}
return fileTypeRes.mime
}

const str = buff.toString('utf8')

// You gotta read the file to figure out if something is an svg
if (isSvg(str)) return 'image/svg+xml'

// minimal whatwg style doc sniff.
const docSniffRes = docSniff(false, str)

if (docSniffRes === 'text/plain' && mime.lookup(path) === 'text/markdown') {
// force plain text, otherwise browser triggers download of .md files
return 'text/plain'
}

if (!docSniffRes || docSniffRes === 'text/plain') {
// fallback to guessing by file extension
return mime.lookup(path)
Expand Down
2 changes: 1 addition & 1 deletion add-on/src/lib/runtime-checks.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ async function createRuntimeChecks (browser) {
const browserInfo = await getBrowserInfo(browser)
const runtimeBrowserName = browserInfo ? browserInfo.name : 'unknown'
const runtimeIsFirefox = !!runtimeBrowserName.match('Firefox')
const runtimeHasNativeProtocol = !!(browser && browser.protocol && browser.protocol.registerStringProtocol)
const runtimeHasNativeProtocol = !!(browser && browser.protocol && (browser.protocol.registerProtocol || browser.protocol.registerStringProtocol))
// platform
const platformInfo = await getPlatformInfo(browser)
const runtimeIsAndroid = platformInfo ? platformInfo.os === 'android' : false
Expand Down
8 changes: 6 additions & 2 deletions ci/Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ timeout(time: 1, unit: 'HOURS') { parallel(
// https://github.com/yarnpkg/yarn/issues/3110#issuecomment-293347341
bat 'npx [email protected] config set msvs_version 2015 --global'
// run developer build as a smoke-test for windows
bat 'npm run dev-build'
// bat 'npm run dev-build'
bat 'npx [email protected] --mutex network --no-lockfile'
bat 'npm run libdweb:build'
bat 'dir /s /b /o:gn build'
}
stage('win:test') {
Expand Down Expand Up @@ -91,7 +93,9 @@ timeout(time: 1, unit: 'HOURS') { parallel(
stage('mac:dev-build') {
sh 'rm -rf node_modules/'
// run developer build as a smoke-test
sh 'npm run dev-build'
// sh 'npm run dev-build'
sh 'npx [email protected] --mutex network --no-lockfile'
sh 'npm run libdweb:build'
sh 'ls -Rlh build'
}
stage('mac:test') {
Expand Down
5 changes: 5 additions & 0 deletions docs/developer-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* [Build and Run in Firefox](#build-and-run-in-firefox)
* [Build and Manual Install in Chromium](#build-and-manual-install-in-chromium)
* [Firefox for Android](#firefox-for-android)
* [Libdweb with Firefox Nightly](#libdweb)
* [Useful Tasks](#useful-tasks)
* [Tips](#tips)

Expand Down Expand Up @@ -62,6 +63,10 @@ Then open up `chrome://extensions` in Chromium-based browser, enable "Developer

See [`docs/firefox-for-android.md`](firefox-for-android.md)

### libdweb

See [`docs/libdweb.md`](libdweb.md)

## Useful Tasks

Each `npm` task can be run separately, but for most of time `dev-build`, `test` and `fix:lint` are all you need.
Expand Down
Loading