From 97d03fca03fe698721552847cdf9b36cad3bce4b Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Mon, 6 May 2019 12:11:19 +0200 Subject: [PATCH 1/7] feat: support /ipns/ at HTTP Gateway It requires below to PRs to land first: https://github.com/ipfs/js-ipfs/pull/2002 https://github.com/ipfs/js-ipfs-http-response/pull/19 https://github.com/ipfs/js-ipfs-mfs/pull/48 License: MIT Signed-off-by: Marcin Rataj --- package.json | 4 +-- src/http/api/routes/webui.js | 4 +-- src/http/gateway/resources/gateway.js | 29 +++++++++++++---- src/http/gateway/routes/gateway.js | 46 +++++++++++++++++++-------- src/http/gateway/routes/index.js | 2 +- 5 files changed, 60 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index ad8413336d..8ba905f84a 100644 --- a/package.json +++ b/package.json @@ -94,8 +94,8 @@ "ipfs-block": "~0.8.1", "ipfs-block-service": "~0.15.1", "ipfs-http-client": "^32.0.1", - "ipfs-http-response": "~0.3.0", - "ipfs-mfs": "~0.11.4", + "ipfs-http-response": "~0.3.1", + "ipfs-mfs": "~0.11.5", "ipfs-multipart": "~0.1.0", "ipfs-repo": "~0.26.6", "ipfs-unixfs": "~0.1.16", diff --git a/src/http/api/routes/webui.js b/src/http/api/routes/webui.js index e97c469bd0..05f170a25e 100644 --- a/src/http/api/routes/webui.js +++ b/src/http/api/routes/webui.js @@ -5,10 +5,10 @@ const resources = require('../../gateway/resources') module.exports = [ { method: '*', - path: '/ipfs/{cid*}', + path: '/ipfs/{immutableId*}', options: { pre: [ - { method: resources.gateway.checkCID, assign: 'args' } + { method: resources.gateway.checkImmutableId, assign: 'args' } ] }, handler: resources.gateway.handler diff --git a/src/http/gateway/resources/gateway.js b/src/http/gateway/resources/gateway.js index 8f538aa22c..8c47afdc8f 100644 --- a/src/http/gateway/resources/gateway.js +++ b/src/http/gateway/resources/gateway.js @@ -30,6 +30,12 @@ function detectContentType (ref, chunk) { return mime.contentType(mimeType) } +async function resolveIpns (ref, ipfs) { + const [ root ] = PathUtils.splitPath(ref) + const immutableRoot = await ipfs.name.resolve(root, { recursive: true }) + return ref.replace(`/ipns/${root}`, PathUtils.removeTrailingSlash(immutableRoot)) +} + // Enable streaming of compressed payload // https://github.com/hapijs/hapi/issues/3599 class ResponseStream extends PassThrough { @@ -45,21 +51,32 @@ class ResponseStream extends PassThrough { } module.exports = { - checkCID (request, h) { - if (!request.params.cid) { + checkImmutableId (request, h) { + if (!request.params.immutableId) { throw Boom.badRequest('Path Resolve error: path must contain at least one component') } - - return { ref: `/ipfs/${request.params.cid}` } + return { ref: `/ipfs/${request.params.immutableId}` } + }, + checkMutableId (request, h) { + if (!request.params.mutableId) { + throw Boom.badRequest('Path Resolve error: path must contain at least one component') + } + return { ref: `/ipns/${request.params.mutableId}` } }, async handler (request, h) { const { ref } = request.pre.args const { ipfs } = request.server.app + // The resolver from ipfs-http-response supports only immutable /ipfs/ for now, + // so we convert /ipns/ to /ipfs/ before passing it to the resolver ¯\_(ツ)_/¯ + // This can be removed if a solution proposed in + // https://github.com/ipfs/js-ipfs-http-response/issues/22 lands upstream + const immutableRef = ref.startsWith('/ipns/') ? await resolveIpns(ref, ipfs) : ref + let data try { - data = await resolver.cid(ipfs, ref) + data = await resolver.cid(ipfs, immutableRef) } catch (err) { const errorToString = err.toString() log.error('err: ', errorToString, ' fileName: ', err.fileName) @@ -67,7 +84,7 @@ module.exports = { // switch case with true feels so wrong. switch (true) { case (errorToString === 'Error: This dag node is a directory'): - data = await resolver.directory(ipfs, ref, err.cid) + data = await resolver.directory(ipfs, immutableRef, err.cid) if (typeof data === 'string') { // no index file found diff --git a/src/http/gateway/routes/gateway.js b/src/http/gateway/routes/gateway.js index 4fe5d640b0..94989bd473 100644 --- a/src/http/gateway/routes/gateway.js +++ b/src/http/gateway/routes/gateway.js @@ -2,19 +2,37 @@ const resources = require('../resources') -module.exports = { - method: '*', - path: '/ipfs/{cid*}', - options: { - handler: resources.gateway.handler, - pre: [ - { method: resources.gateway.checkCID, assign: 'args' } - ], - response: { - ranges: false // disable built-in support, we do it manually - }, - ext: { - onPostHandler: { method: resources.gateway.afterHandler } +module.exports = [ + { + method: '*', + path: '/ipfs/{immutableId*}', + options: { + handler: resources.gateway.handler, + pre: [ + { method: resources.gateway.checkImmutableId, assign: 'args' } + ], + response: { + ranges: false // disable built-in support, we do it manually + }, + ext: { + onPostHandler: { method: resources.gateway.afterHandler } + } + } + }, + { + method: '*', + path: '/ipns/{mutableId*}', + options: { + handler: resources.gateway.handler, + pre: [ + { method: resources.gateway.checkMutableId, assign: 'args' } + ], + response: { + ranges: false // disable built-in support, we do it manually + }, + ext: { + onPostHandler: { method: resources.gateway.afterHandler } + } } } -} +] diff --git a/src/http/gateway/routes/index.js b/src/http/gateway/routes/index.js index 2cbf163b04..8f796ab609 100644 --- a/src/http/gateway/routes/index.js +++ b/src/http/gateway/routes/index.js @@ -1,3 +1,3 @@ 'use strict' -module.exports = [require('./gateway')] +module.exports = [...require('./gateway')] From 99d92b75d512308a952b03224ad15f4b3cb04bdd Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Tue, 2 Jul 2019 21:35:55 +0200 Subject: [PATCH 2/7] style: apply name suggestions from review License: MIT Signed-off-by: Marcin Rataj --- src/http/api/routes/webui.js | 4 ++-- src/http/gateway/resources/gateway.js | 6 +++--- src/http/gateway/routes/gateway.js | 4 ++-- src/http/gateway/routes/index.js | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/http/api/routes/webui.js b/src/http/api/routes/webui.js index 089dff81cc..10e7533956 100644 --- a/src/http/api/routes/webui.js +++ b/src/http/api/routes/webui.js @@ -5,10 +5,10 @@ const resources = require('../../gateway/resources') module.exports = [ { method: '*', - path: '/ipfs/{immutableId*}', + path: '/ipfs/{cid*}', options: { pre: [ - { method: resources.gateway.checkImmutableId, assign: 'args' } + { method: resources.gateway.checkCID, assign: 'args' } ] }, handler: resources.gateway.handler diff --git a/src/http/gateway/resources/gateway.js b/src/http/gateway/resources/gateway.js index 8c47afdc8f..40c3159cf0 100644 --- a/src/http/gateway/resources/gateway.js +++ b/src/http/gateway/resources/gateway.js @@ -51,11 +51,11 @@ class ResponseStream extends PassThrough { } module.exports = { - checkImmutableId (request, h) { - if (!request.params.immutableId) { + checkCID (request, h) { + if (!request.params.cid) { throw Boom.badRequest('Path Resolve error: path must contain at least one component') } - return { ref: `/ipfs/${request.params.immutableId}` } + return { ref: `/ipfs/${request.params.cid}` } }, checkMutableId (request, h) { if (!request.params.mutableId) { diff --git a/src/http/gateway/routes/gateway.js b/src/http/gateway/routes/gateway.js index 94989bd473..44dc7be060 100644 --- a/src/http/gateway/routes/gateway.js +++ b/src/http/gateway/routes/gateway.js @@ -5,11 +5,11 @@ const resources = require('../resources') module.exports = [ { method: '*', - path: '/ipfs/{immutableId*}', + path: '/ipfs/{cid*}', options: { handler: resources.gateway.handler, pre: [ - { method: resources.gateway.checkImmutableId, assign: 'args' } + { method: resources.gateway.checkCID, assign: 'args' } ], response: { ranges: false // disable built-in support, we do it manually diff --git a/src/http/gateway/routes/index.js b/src/http/gateway/routes/index.js index 8f796ab609..24f57fbbd7 100644 --- a/src/http/gateway/routes/index.js +++ b/src/http/gateway/routes/index.js @@ -1,3 +1,3 @@ 'use strict' -module.exports = [...require('./gateway')] +module.exports = require('./gateway') From bb67cc29a2fa08572c660b3bb49100e74c55d021 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 3 Jul 2019 13:12:59 +0200 Subject: [PATCH 3/7] refactor(gateway): simplify path validation - remove custom arg passing and replace it with native Joi validation - decode escaped unicode in paths before passing to ipfs resolver - add tests for /ipns/ path (file and directory) License: MIT Signed-off-by: Marcin Rataj --- src/http/api/routes/webui.js | 21 ++++++++--- src/http/gateway/resources/gateway.js | 37 +++++++++---------- src/http/gateway/routes/gateway.js | 25 ++++++++----- test/gateway/index.js | 53 +++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 35 deletions(-) diff --git a/src/http/api/routes/webui.js b/src/http/api/routes/webui.js index 10e7533956..d300feae97 100644 --- a/src/http/api/routes/webui.js +++ b/src/http/api/routes/webui.js @@ -1,17 +1,26 @@ 'use strict' +const Joi = require('@hapi/joi') const resources = require('../../gateway/resources') module.exports = [ { method: '*', - path: '/ipfs/{cid*}', + path: '/ipfs/{cidPath*}', options: { - pre: [ - { method: resources.gateway.checkCID, assign: 'args' } - ] - }, - handler: resources.gateway.handler + handler: resources.gateway.handler, + validate: { + params: { + cidPath: Joi.string().required() + } + }, + response: { + ranges: false // disable built-in support, handler does it manually + }, + ext: { + onPostHandler: { method: resources.gateway.afterHandler } + } + } }, { method: '*', diff --git a/src/http/gateway/resources/gateway.js b/src/http/gateway/resources/gateway.js index 40c3159cf0..7dc50eb931 100644 --- a/src/http/gateway/resources/gateway.js +++ b/src/http/gateway/resources/gateway.js @@ -11,9 +11,11 @@ const Boom = require('boom') const Ammo = require('@hapi/ammo') // HTTP Range processing utilities const peek = require('buffer-peek-stream') +const multibase = require('multibase') const { resolver } = require('ipfs-http-response') const PathUtils = require('../utils/path') const { cidToString } = require('../../../utils/cid') +const isIPFS = require('is-ipfs') function detectContentType (ref, chunk) { let fileSignature @@ -51,28 +53,18 @@ class ResponseStream extends PassThrough { } module.exports = { - checkCID (request, h) { - if (!request.params.cid) { - throw Boom.badRequest('Path Resolve error: path must contain at least one component') - } - return { ref: `/ipfs/${request.params.cid}` } - }, - checkMutableId (request, h) { - if (!request.params.mutableId) { - throw Boom.badRequest('Path Resolve error: path must contain at least one component') - } - return { ref: `/ipns/${request.params.mutableId}` } - }, async handler (request, h) { - const { ref } = request.pre.args + const ref = request.path const { ipfs } = request.server.app // The resolver from ipfs-http-response supports only immutable /ipfs/ for now, // so we convert /ipns/ to /ipfs/ before passing it to the resolver ¯\_(ツ)_/¯ - // This can be removed if a solution proposed in + // This could be removed if a solution proposed in // https://github.com/ipfs/js-ipfs-http-response/issues/22 lands upstream - const immutableRef = ref.startsWith('/ipns/') ? await resolveIpns(ref, ipfs) : ref + const immutableRef = decodeURIComponent(ref.startsWith('/ipns/') + ? await resolveIpns(ref, ipfs) + : ref) let data try { @@ -217,18 +209,25 @@ module.exports = { const { response } = request // Add headers to successfult responses (regular or range) if (response.statusCode === 200 || response.statusCode === 206) { - const { ref } = request.pre.args + const ref = request.path response.header('X-Ipfs-Path', ref) if (ref.startsWith('/ipfs/')) { // "set modtime to a really long time ago, since files are immutable and should stay cached" // Source: https://github.com/ipfs/go-ipfs/blob/v0.4.20/core/corehttp/gateway_handler.go#L228-L229 response.header('Last-Modified', 'Thu, 01 Jan 1970 00:00:01 GMT') - // Suborigins: https://github.com/ipfs/in-web-browsers/issues/66 + // Suborigin for /ipfs/: https://github.com/ipfs/in-web-browsers/issues/66 const rootCid = ref.split('/')[2] const ipfsOrigin = cidToString(rootCid, { base: 'base32' }) - response.header('Suborigin', 'ipfs000' + ipfsOrigin) + response.header('Suborigin', `ipfs000${ipfsOrigin}`) + } else if (ref.startsWith('/ipns/')) { + // Suborigin for /ipns/: https://github.com/ipfs/in-web-browsers/issues/66 + const root = ref.split('/')[2] + // encode CID/FQDN in base32 (Suborigin allows only a-z) + const ipnsOrigin = isIPFS.cid(root) + ? cidToString(root, { base: 'base32' }) + : multibase.encode('base32', Buffer.from(root)).toString() + response.header('Suborigin', `ipns000${ipnsOrigin}`) } - // TODO: we don't have case-insensitive solution for /ipns/ yet (https://github.com/ipfs/go-ipfs/issues/5287) } return h.continue } diff --git a/src/http/gateway/routes/gateway.js b/src/http/gateway/routes/gateway.js index 44dc7be060..e2593d5e2b 100644 --- a/src/http/gateway/routes/gateway.js +++ b/src/http/gateway/routes/gateway.js @@ -1,18 +1,21 @@ 'use strict' +const Joi = require('@hapi/joi') const resources = require('../resources') module.exports = [ { method: '*', - path: '/ipfs/{cid*}', + path: '/ipfs/{cidPath*}', options: { handler: resources.gateway.handler, - pre: [ - { method: resources.gateway.checkCID, assign: 'args' } - ], + validate: { + params: { + cidPath: Joi.string().required() + } + }, response: { - ranges: false // disable built-in support, we do it manually + ranges: false // disable built-in support, handler does it manually }, ext: { onPostHandler: { method: resources.gateway.afterHandler } @@ -21,14 +24,16 @@ module.exports = [ }, { method: '*', - path: '/ipns/{mutableId*}', + path: '/ipns/{libp2pKeyOrFqdn*}', options: { handler: resources.gateway.handler, - pre: [ - { method: resources.gateway.checkMutableId, assign: 'args' } - ], + validate: { + params: { + libp2pKeyOrFqdn: Joi.string().required() + } + }, response: { - ranges: false // disable built-in support, we do it manually + ranges: false // disable built-in support, handler does it manually }, ext: { onPostHandler: { method: resources.gateway.afterHandler } diff --git a/test/gateway/index.js b/test/gateway/index.js index 22fc6ea7b6..1a8bdd55e9 100644 --- a/test/gateway/index.js +++ b/test/gateway/index.js @@ -12,6 +12,7 @@ const os = require('os') const path = require('path') const hat = require('hat') const fileType = require('file-type') +const CID = require('cids') const bigFile = loadFixture('test/fixtures/15mb.random', 'interface-ipfs-core') const directoryContent = { @@ -84,6 +85,8 @@ describe('HTTP Gateway', function () { content('unsniffable-folder/hexagons-xml.svg'), content('unsniffable-folder/hexagons.svg') ]) + // Publish QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdsgaTQ to IPNS using self key + await http.api._ipfs.name.publish('QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdsgaTQ', { resolve: false }) }) after(() => http.api.stop()) @@ -526,4 +529,54 @@ describe('HTTP Gateway', function () { expect(res.headers.location).to.equal('/ipfs/QmbQD7EMEL1zeebwBsWEfA3ndgSS6F7S6iTuwuqasPgVRi/index.html') expect(res.headers['x-ipfs-path']).to.equal(undefined) }) + + it('load a file from IPNS', async () => { + const { id } = await http.api._ipfs.id() + const ipnsPath = `/ipns/${id}/cat.jpg` + + const res = await gateway.inject({ + method: 'GET', + url: ipnsPath + }) + + const kittyDirectCid = 'Qmd286K6pohQcTKYqnS1YhWrCiS4gz7Xi34sdwMe9USZ7u' + + expect(res.statusCode).to.equal(200) + expect(res.headers['content-type']).to.equal('image/jpeg') + expect(res.headers['content-length']).to.equal(res.rawPayload.length).to.equal(443230) + expect(res.headers['x-ipfs-path']).to.equal(ipnsPath) + expect(res.headers['etag']).to.equal(`"${kittyDirectCid}"`) + expect(res.headers['cache-control']).to.equal('no-cache') // TODO: should be record TTL + expect(res.headers['last-modified']).to.equal(undefined) + expect(res.headers.etag).to.equal('"Qmd286K6pohQcTKYqnS1YhWrCiS4gz7Xi34sdwMe9USZ7u"') + expect(res.headers.suborigin).to.equal(`ipns000${new CID(id).toV1().toBaseEncodedString('base32')}`) + + let fileSignature = fileType(res.rawPayload) + expect(fileSignature.mime).to.equal('image/jpeg') + expect(fileSignature.ext).to.equal('jpg') + }) + + it('load a directory from IPNS', async () => { + const { id } = await http.api._ipfs.id() + const ipnsPath = `/ipns/${id}/` + + const res = await gateway.inject({ + method: 'GET', + url: ipnsPath + }) + + expect(res.statusCode).to.equal(200) + expect(res.headers['content-type']).to.equal('text/html; charset=utf-8') + expect(res.headers['x-ipfs-path']).to.equal(ipnsPath) + expect(res.headers['cache-control']).to.equal('no-cache') + expect(res.headers['last-modified']).to.equal(undefined) + expect(res.headers['content-length']).to.equal(res.rawPayload.length) + expect(res.headers.etag).to.equal(undefined) + expect(res.headers.suborigin).to.equal(`ipns000${new CID(id).toV1().toBaseEncodedString('base32')}`) + + // check if the cat picture is in the payload as a way to check + // if this is an index of this directory + let listedFile = res.payload.match(/\/cat\.jpg/g) + expect(listedFile).to.have.lengthOf(1) + }) }) From eebde0bc8591f63066e665d65f81c8646a15e083 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 3 Jul 2019 15:26:49 +0200 Subject: [PATCH 4/7] style: unify route param names License: MIT Signed-off-by: Marcin Rataj --- src/http/api/routes/webui.js | 4 ++-- src/http/gateway/routes/gateway.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/http/api/routes/webui.js b/src/http/api/routes/webui.js index d300feae97..cf9cb91d1b 100644 --- a/src/http/api/routes/webui.js +++ b/src/http/api/routes/webui.js @@ -6,12 +6,12 @@ const resources = require('../../gateway/resources') module.exports = [ { method: '*', - path: '/ipfs/{cidPath*}', + path: '/ipfs/{ipfsPath*}', options: { handler: resources.gateway.handler, validate: { params: { - cidPath: Joi.string().required() + ipfsPath: Joi.string().required() } }, response: { diff --git a/src/http/gateway/routes/gateway.js b/src/http/gateway/routes/gateway.js index e2593d5e2b..02ac285a76 100644 --- a/src/http/gateway/routes/gateway.js +++ b/src/http/gateway/routes/gateway.js @@ -6,12 +6,12 @@ const resources = require('../resources') module.exports = [ { method: '*', - path: '/ipfs/{cidPath*}', + path: '/ipfs/{ipfsPath*}', options: { handler: resources.gateway.handler, validate: { params: { - cidPath: Joi.string().required() + ipfsPath: Joi.string().required() } }, response: { @@ -24,12 +24,12 @@ module.exports = [ }, { method: '*', - path: '/ipns/{libp2pKeyOrFqdn*}', + path: '/ipns/{ipnsPath*}', options: { handler: resources.gateway.handler, validate: { params: { - libp2pKeyOrFqdn: Joi.string().required() + ipnsPath: Joi.string().required() } }, response: { From c21709d9a215cf18e916468f4e453a1aa81e258e Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 3 Jul 2019 17:24:25 +0200 Subject: [PATCH 5/7] test(gateway): load from URI-encoded path License: MIT Signed-off-by: Marcin Rataj --- src/http/gateway/resources/gateway.js | 2 +- test/gateway/index.js | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/http/gateway/resources/gateway.js b/src/http/gateway/resources/gateway.js index 7dc50eb931..bc52cd0bed 100644 --- a/src/http/gateway/resources/gateway.js +++ b/src/http/gateway/resources/gateway.js @@ -62,7 +62,7 @@ module.exports = { // so we convert /ipns/ to /ipfs/ before passing it to the resolver ¯\_(ツ)_/¯ // This could be removed if a solution proposed in // https://github.com/ipfs/js-ipfs-http-response/issues/22 lands upstream - const immutableRef = decodeURIComponent(ref.startsWith('/ipns/') + const immutableRef = decodeURI(ref.startsWith('/ipns/') ? await resolveIpns(ref, ipfs) : ref) diff --git a/test/gateway/index.js b/test/gateway/index.js index 1a8bdd55e9..9cab5b6d7d 100644 --- a/test/gateway/index.js +++ b/test/gateway/index.js @@ -21,6 +21,7 @@ const directoryContent = { 'nested-folder/ipfs.txt': loadFixture('test/gateway/test-folder/nested-folder/ipfs.txt'), 'nested-folder/nested.html': loadFixture('test/gateway/test-folder/nested-folder/nested.html'), 'cat-folder/cat.jpg': loadFixture('test/gateway/test-folder/cat-folder/cat.jpg'), + 'utf8/cat-with-óąśśł-and-أعظم._.jpg': loadFixture('test/gateway/test-folder/cat-folder/cat.jpg'), 'unsniffable-folder/hexagons-xml.svg': loadFixture('test/gateway/test-folder/unsniffable-folder/hexagons-xml.svg'), 'unsniffable-folder/hexagons.svg': loadFixture('test/gateway/test-folder/unsniffable-folder/hexagons.svg') } @@ -85,6 +86,8 @@ describe('HTTP Gateway', function () { content('unsniffable-folder/hexagons-xml.svg'), content('unsniffable-folder/hexagons.svg') ]) + // QmaRdtkDark8TgXPdDczwBneadyF44JvFGbrKLTkmTUhHk + await http.api._ipfs.add([content('utf8/cat-with-óąśśł-and-أعظم._.jpg')]) // Publish QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdsgaTQ to IPNS using self key await http.api._ipfs.name.publish('QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdsgaTQ', { resolve: false }) }) @@ -530,6 +533,25 @@ describe('HTTP Gateway', function () { expect(res.headers['x-ipfs-path']).to.equal(undefined) }) + it('test(gateway): load from URI-encoded path', async () => { + // non-ascii characters will be URI-encoded by the browser + const utf8path = '/ipfs/QmaRdtkDark8TgXPdDczwBneadyF44JvFGbrKLTkmTUhHk/cat-with-óąśśł-and-أعظم._.jpg' + const escapedPath = encodeURI(utf8path) // this is what will be actually requested + const res = await gateway.inject({ + method: 'GET', + url: escapedPath + }) + + expect(res.statusCode).to.equal(200) + expect(res.headers['content-type']).to.equal('image/jpeg') + expect(res.headers['x-ipfs-path']).to.equal(escapedPath) + expect(res.headers['cache-control']).to.equal('public, max-age=29030400, immutable') + expect(res.headers['last-modified']).to.equal('Thu, 01 Jan 1970 00:00:01 GMT') + expect(res.headers['content-length']).to.equal(res.rawPayload.length) + expect(res.headers.etag).to.equal('"Qmd286K6pohQcTKYqnS1YhWrCiS4gz7Xi34sdwMe9USZ7u"') + expect(res.headers.suborigin).to.equal('ipfs000bafybeiftsm4u7cn24bn2suwg3x7sldx2uplvfylsk3e4bgylyxwjdevhqm') + }) + it('load a file from IPNS', async () => { const { id } = await http.api._ipfs.id() const ipnsPath = `/ipns/${id}/cat.jpg` From 7e3e7b262d1aea62dc6819931be1dc1c716487e0 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 3 Jul 2019 17:52:26 +0200 Subject: [PATCH 6/7] style: unify names related to content path License: MIT Signed-off-by: Marcin Rataj --- src/http/api/routes/webui.js | 4 +- src/http/gateway/resources/gateway.js | 56 ++++++++++++--------------- src/http/gateway/routes/gateway.js | 8 ++-- 3 files changed, 31 insertions(+), 37 deletions(-) diff --git a/src/http/api/routes/webui.js b/src/http/api/routes/webui.js index cf9cb91d1b..9d6926f6aa 100644 --- a/src/http/api/routes/webui.js +++ b/src/http/api/routes/webui.js @@ -6,12 +6,12 @@ const resources = require('../../gateway/resources') module.exports = [ { method: '*', - path: '/ipfs/{ipfsPath*}', + path: '/ipfs/{path*}', options: { handler: resources.gateway.handler, validate: { params: { - ipfsPath: Joi.string().required() + path: Joi.string().required() } }, response: { diff --git a/src/http/gateway/resources/gateway.js b/src/http/gateway/resources/gateway.js index bc52cd0bed..653e3fa0a8 100644 --- a/src/http/gateway/resources/gateway.js +++ b/src/http/gateway/resources/gateway.js @@ -17,27 +17,21 @@ const PathUtils = require('../utils/path') const { cidToString } = require('../../../utils/cid') const isIPFS = require('is-ipfs') -function detectContentType (ref, chunk) { +function detectContentType (path, chunk) { let fileSignature // try to guess the filetype based on the first bytes // note that `file-type` doesn't support svgs, therefore we assume it's a svg if ref looks like it - if (!ref.endsWith('.svg')) { + if (!path.endsWith('.svg')) { fileSignature = fileType(chunk) } - // if we were unable to, fallback to the `ref` which might contain the extension - const mimeType = mime.lookup(fileSignature ? fileSignature.ext : ref) + // if we were unable to, fallback to the path which might contain the extension + const mimeType = mime.lookup(fileSignature ? fileSignature.ext : path) return mime.contentType(mimeType) } -async function resolveIpns (ref, ipfs) { - const [ root ] = PathUtils.splitPath(ref) - const immutableRoot = await ipfs.name.resolve(root, { recursive: true }) - return ref.replace(`/ipns/${root}`, PathUtils.removeTrailingSlash(immutableRoot)) -} - // Enable streaming of compressed payload // https://github.com/hapijs/hapi/issues/3599 class ResponseStream extends PassThrough { @@ -55,20 +49,20 @@ class ResponseStream extends PassThrough { module.exports = { async handler (request, h) { - const ref = request.path const { ipfs } = request.server.app + const path = request.path // The resolver from ipfs-http-response supports only immutable /ipfs/ for now, // so we convert /ipns/ to /ipfs/ before passing it to the resolver ¯\_(ツ)_/¯ // This could be removed if a solution proposed in // https://github.com/ipfs/js-ipfs-http-response/issues/22 lands upstream - const immutableRef = decodeURI(ref.startsWith('/ipns/') - ? await resolveIpns(ref, ipfs) - : ref) + const immutablePath = decodeURI(path.startsWith('/ipns/') + ? await ipfs.name.resolve(path, { recursive: true }) + : path) let data try { - data = await resolver.cid(ipfs, immutableRef) + data = await resolver.cid(ipfs, immutablePath) } catch (err) { const errorToString = err.toString() log.error('err: ', errorToString, ' fileName: ', err.fileName) @@ -76,14 +70,14 @@ module.exports = { // switch case with true feels so wrong. switch (true) { case (errorToString === 'Error: This dag node is a directory'): - data = await resolver.directory(ipfs, immutableRef, err.cid) + data = await resolver.directory(ipfs, immutablePath, err.cid) if (typeof data === 'string') { // no index file found - if (!ref.endsWith('/')) { + if (!path.endsWith('/')) { // for a directory, if URL doesn't end with a / // append / and redirect permanent to that URL - return h.redirect(`${ref}/`).permanent(true) + return h.redirect(`${path}/`).permanent(true) } // send directory listing return h.response(data) @@ -91,7 +85,7 @@ module.exports = { // found index file // redirect to URL/ - return h.redirect(PathUtils.joinURLParts(ref, data[0].Name)) + return h.redirect(PathUtils.joinURLParts(path, data[0].Name)) case (errorToString.startsWith('Error: no link named')): throw Boom.boomify(err, { statusCode: 404 }) case (errorToString.startsWith('Error: multihash length inconsistent')): @@ -103,9 +97,9 @@ module.exports = { } } - if (ref.endsWith('/')) { + if (path.endsWith('/')) { // remove trailing slash for files - return h.redirect(PathUtils.removeTrailingSlash(ref)).permanent(true) + return h.redirect(PathUtils.removeTrailingSlash(path)).permanent(true) } // Support If-None-Match & Etag (Conditional Requests from RFC7232) @@ -117,7 +111,7 @@ module.exports = { } // Immutable content produces 304 Not Modified for all values of If-Modified-Since - if (ref.startsWith('/ipfs/') && request.headers['if-modified-since']) { + if (path.startsWith('/ipfs/') && request.headers['if-modified-since']) { return h.response().code(304) // Not Modified } @@ -159,7 +153,7 @@ module.exports = { log.error(err) return reject(err) } - resolve({ peekedStream, contentType: detectContentType(ref, streamHead) }) + resolve({ peekedStream, contentType: detectContentType(path, streamHead) }) }) }) @@ -172,11 +166,11 @@ module.exports = { res.header('etag', etag) // Set headers specific to the immutable namespace - if (ref.startsWith('/ipfs/')) { + if (path.startsWith('/ipfs/')) { res.header('Cache-Control', 'public, max-age=29030400, immutable') } - log('ref ', ref) + log('path ', path) log('content-type ', contentType) if (contentType) { @@ -209,19 +203,19 @@ module.exports = { const { response } = request // Add headers to successfult responses (regular or range) if (response.statusCode === 200 || response.statusCode === 206) { - const ref = request.path - response.header('X-Ipfs-Path', ref) - if (ref.startsWith('/ipfs/')) { + const path = request.path + response.header('X-Ipfs-Path', path) + if (path.startsWith('/ipfs/')) { // "set modtime to a really long time ago, since files are immutable and should stay cached" // Source: https://github.com/ipfs/go-ipfs/blob/v0.4.20/core/corehttp/gateway_handler.go#L228-L229 response.header('Last-Modified', 'Thu, 01 Jan 1970 00:00:01 GMT') // Suborigin for /ipfs/: https://github.com/ipfs/in-web-browsers/issues/66 - const rootCid = ref.split('/')[2] + const rootCid = path.split('/')[2] const ipfsOrigin = cidToString(rootCid, { base: 'base32' }) response.header('Suborigin', `ipfs000${ipfsOrigin}`) - } else if (ref.startsWith('/ipns/')) { + } else if (path.startsWith('/ipns/')) { // Suborigin for /ipns/: https://github.com/ipfs/in-web-browsers/issues/66 - const root = ref.split('/')[2] + const root = path.split('/')[2] // encode CID/FQDN in base32 (Suborigin allows only a-z) const ipnsOrigin = isIPFS.cid(root) ? cidToString(root, { base: 'base32' }) diff --git a/src/http/gateway/routes/gateway.js b/src/http/gateway/routes/gateway.js index 02ac285a76..57d563aa6b 100644 --- a/src/http/gateway/routes/gateway.js +++ b/src/http/gateway/routes/gateway.js @@ -6,12 +6,12 @@ const resources = require('../resources') module.exports = [ { method: '*', - path: '/ipfs/{ipfsPath*}', + path: '/ipfs/{path*}', options: { handler: resources.gateway.handler, validate: { params: { - ipfsPath: Joi.string().required() + path: Joi.string().required() } }, response: { @@ -24,12 +24,12 @@ module.exports = [ }, { method: '*', - path: '/ipns/{ipnsPath*}', + path: '/ipns/{path*}', options: { handler: resources.gateway.handler, validate: { params: { - ipnsPath: Joi.string().required() + path: Joi.string().required() } }, response: { From f1da001412087abe6e471485c39d361178bf4513 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Wed, 3 Jul 2019 18:00:50 +0200 Subject: [PATCH 7/7] =?UTF-8?q?style:=20immutablePath=20=E2=86=92=20ipfsPa?= =?UTF-8?q?th?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit License: MIT Signed-off-by: Marcin Rataj --- src/http/gateway/resources/gateway.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/http/gateway/resources/gateway.js b/src/http/gateway/resources/gateway.js index 653e3fa0a8..6f2a42b6c2 100644 --- a/src/http/gateway/resources/gateway.js +++ b/src/http/gateway/resources/gateway.js @@ -56,13 +56,13 @@ module.exports = { // so we convert /ipns/ to /ipfs/ before passing it to the resolver ¯\_(ツ)_/¯ // This could be removed if a solution proposed in // https://github.com/ipfs/js-ipfs-http-response/issues/22 lands upstream - const immutablePath = decodeURI(path.startsWith('/ipns/') + const ipfsPath = decodeURI(path.startsWith('/ipns/') ? await ipfs.name.resolve(path, { recursive: true }) : path) let data try { - data = await resolver.cid(ipfs, immutablePath) + data = await resolver.cid(ipfs, ipfsPath) } catch (err) { const errorToString = err.toString() log.error('err: ', errorToString, ' fileName: ', err.fileName) @@ -70,7 +70,7 @@ module.exports = { // switch case with true feels so wrong. switch (true) { case (errorToString === 'Error: This dag node is a directory'): - data = await resolver.directory(ipfs, immutablePath, err.cid) + data = await resolver.directory(ipfs, ipfsPath, err.cid) if (typeof data === 'string') { // no index file found