From e8d8d672b92eb06bbc0dde49fc09cdd3ed2f1aa9 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Mon, 3 Sep 2018 00:12:56 +0200 Subject: [PATCH] feat(gateway): X-Ipfs-Path, Etag, Cache-Control, Suborigin Return the same headers as HTTP Gateway exposed by go-ipfs: - X-Ipfs-Path: requested IPFS Path of returned resource - Etag: multihash of returned payload - Cache-Control: disable cache for directory listings and errors, enable heavy caching for immutable assets from /ipfs/ namespace - Suborigin: use root CID in base32 and literal prefix to conform to current suborigin spec License: MIT Signed-off-by: Marcin Rataj --- src/http/gateway/resources/gateway.js | 27 +++++++++++++ src/http/gateway/routes/gateway.js | 5 ++- test/gateway/index.js | 56 ++++++++++++++++++++++++++- 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/src/http/gateway/resources/gateway.js b/src/http/gateway/resources/gateway.js index 856bf4a8d9..4b6df5977f 100644 --- a/src/http/gateway/resources/gateway.js +++ b/src/http/gateway/resources/gateway.js @@ -8,6 +8,7 @@ const toPull = require('stream-to-pull-stream') const fileType = require('file-type') const mime = require('mime-types') const Stream = require('readable-stream') +const CID = require('cids') const { resolver } = require('ipfs-http-response') const PathUtils = require('../utils/path') @@ -41,6 +42,7 @@ module.exports = { ref: `/ipfs/${request.params.cid}` }) }, + handler: (request, reply) => { const ref = request.pre.args.ref const ipfs = request.server.app.ipfs @@ -120,6 +122,15 @@ module.exports = { let response = reply(stream2).hold() + // Etag maps directly to an identifier for a specific version of a resource + // TODO: change to .cid.toBaseEncodedString() after switch to new js-ipfs-http-response + response.header('Etag', `"${data.multihash}"`) + + // Set headers specific to the immutable namespace + if (ref.startsWith('/ipfs/')) { + response.header('Cache-Control', 'public, max-age=29030400, immutable') + } + pull( toPull.source(stream), pull.through((chunk) => { @@ -148,5 +159,21 @@ module.exports = { ) } }) + }, + + afterHandler: (request, reply) => { + const response = request.response + if (response.statusCode === 200) { + const ref = request.pre.args.ref + response.header('X-Ipfs-Path', ref) + if (ref.startsWith('/ipfs/')) { + const rootCid = ref.split('/')[2] + const ipfsOrigin = new CID(rootCid).toV1().toBaseEncodedString('base32') + response.header('Suborigin', 'ipfs000' + ipfsOrigin) + } + // TODO: we don't have case-insensitive solution for /ipns/ yet (https://github.com/ipfs/go-ipfs/issues/5287) + } + reply.continue() } + } diff --git a/src/http/gateway/routes/gateway.js b/src/http/gateway/routes/gateway.js index 5eddeb7c21..810a520437 100644 --- a/src/http/gateway/routes/gateway.js +++ b/src/http/gateway/routes/gateway.js @@ -12,7 +12,10 @@ module.exports = (server) => { pre: [ { method: resources.gateway.checkCID, assign: 'args' } ], - handler: resources.gateway.handler + handler: resources.gateway.handler, + ext: { + onPostHandler: { method: resources.gateway.afterHandler } + } } }) } diff --git a/test/gateway/index.js b/test/gateway/index.js index 1c319671e5..fde341d73e 100644 --- a/test/gateway/index.js +++ b/test/gateway/index.js @@ -93,7 +93,7 @@ describe('HTTP Gateway', function () { (cb) => { const expectedMultihash = 'QmT78zSuBmuS4z925WZfrqQ1qHaJ56DQaTfyMUF7F8ff5o' - http.api.node.files.add(Buffer.from('hello world' + '\n'), (err, res) => { + http.api.node.files.add(Buffer.from('hello world' + '\n'), {cidVersion: 0}, (err, res) => { expect(err).to.not.exist() const file = res[0] expect(file.path).to.equal(expectedMultihash) @@ -144,6 +144,10 @@ describe('HTTP Gateway', function () { }, (res) => { expect(res.statusCode).to.equal(400) expect(res.result.Message).to.be.a('string') + expect(res.headers['cache-control']).to.equal('no-cache') + expect(res.headers['etag']).to.equal(undefined) + expect(res.headers['x-ipfs-path']).to.equal(undefined) + expect(res.headers['suborigin']).to.equal(undefined) done() }) }) @@ -155,11 +159,15 @@ describe('HTTP Gateway', function () { }, (res) => { expect(res.statusCode).to.equal(400) expect(res.result.Message).to.be.a('string') + expect(res.headers['cache-control']).to.equal('no-cache') + expect(res.headers['etag']).to.equal(undefined) + expect(res.headers['x-ipfs-path']).to.equal(undefined) + expect(res.headers['suborigin']).to.equal(undefined) done() }) }) - it('valid hash', (done) => { + it('valid CIDv0', (done) => { gateway.inject({ method: 'GET', url: '/ipfs/QmT78zSuBmuS4z925WZfrqQ1qHaJ56DQaTfyMUF7F8ff5o' @@ -167,9 +175,33 @@ describe('HTTP Gateway', function () { expect(res.statusCode).to.equal(200) expect(res.rawPayload).to.eql(Buffer.from('hello world' + '\n')) expect(res.payload).to.equal('hello world' + '\n') + expect(res.headers['cache-control']).to.equal('public, max-age=29030400, immutable') + expect(res.headers['etag']).to.equal('"QmT78zSuBmuS4z925WZfrqQ1qHaJ56DQaTfyMUF7F8ff5o"') + expect(res.headers['x-ipfs-path']).to.equal('/ipfs/QmT78zSuBmuS4z925WZfrqQ1qHaJ56DQaTfyMUF7F8ff5o') + expect(res.headers['suborigin']).to.equal('ipfs000bafybeicg2rebjoofv4kbyovkw7af3rpiitvnl6i7ckcywaq6xjcxnc2mby') + + done() + }) + }) + + /* TODO when support for CIDv1 lands + it('valid CIDv1', (done) => { + gateway.inject({ + method: 'GET', + url: '/ipfs/TO-DO' + }, (res) => { + expect(res.statusCode).to.equal(200) + expect(res.rawPayload).to.eql(Buffer.from('hello world' + '\n')) + expect(res.payload).to.equal('hello world' + '\n') + expect(res.headers['etag']).to.equal(TO-DO) + expect(res.headers['x-ipfs-path']).to.equal(TO-DO) + expect(res.headers['suborigin']).to.equal(TO-DO) + expect(res.headers['cache-control']).to.equal('public, max-age=29030400, immutable') + done() }) }) + */ it('stream a large file', (done) => { let bigFileHash = 'Qme79tX2bViL26vNjPsF3DP1R9rMKMvnPYJiKTTKPrXJjq' @@ -193,6 +225,10 @@ describe('HTTP Gateway', function () { }, (res) => { expect(res.statusCode).to.equal(200) expect(res.headers['content-type']).to.equal('image/jpeg') + expect(res.headers['x-ipfs-path']).to.equal('/ipfs/' + kitty) + expect(res.headers['cache-control']).to.equal('public, max-age=29030400, immutable') + expect(res.headers['etag']).to.equal('"Qmd286K6pohQcTKYqnS1YhWrCiS4gz7Xi34sdwMe9USZ7u"') + expect(res.headers['suborigin']).to.equal('ipfs000bafybeidsg6t7ici2osxjkukisd5inixiunqdpq2q5jy4a2ruzdf6ewsqk4') let fileSignature = fileType(res.rawPayload) expect(fileSignature.mime).to.equal('image/jpeg') @@ -239,6 +275,10 @@ describe('HTTP Gateway', function () { }, (res) => { 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('/ipfs/' + dir) + expect(res.headers['cache-control']).to.equal('no-cache') + expect(res.headers['etag']).to.equal(undefined) + expect(res.headers['suborigin']).to.equal('ipfs000bafybeidsg6t7ici2osxjkukisd5inixiunqdpq2q5jy4a2ruzdf6ewsqk4') // check if the cat picture is in the payload as a way to check // if this is an index of this directory @@ -256,6 +296,11 @@ describe('HTTP Gateway', function () { url: '/ipfs/' + dir }, (res) => { 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('/ipfs/' + dir) + expect(res.headers['cache-control']).to.equal('public, max-age=29030400, immutable') + expect(res.headers['etag']).to.equal('"Qma6665X5k3zti8nKy7gmXK2BndNDSkgmANpV6k3FUjUeg"') + expect(res.headers['suborigin']).to.equal('ipfs000bafybeigccfheqv7upr4k64bkg5b5wiwelunyn2l2rbirmm43m34lcpuqqe') expect(res.rawPayload).to.deep.equal(directoryContent['index.html']) done() }) @@ -269,6 +314,11 @@ describe('HTTP Gateway', function () { url: '/ipfs/' + dir }, (res) => { 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('/ipfs/' + dir) + expect(res.headers['cache-control']).to.equal('public, max-age=29030400, immutable') + expect(res.headers['etag']).to.equal('"QmUBKGqJWiJYMrNed4bKsbo1nGYGmY418WCc2HgcwRvmHc"') + expect(res.headers['suborigin']).to.equal('ipfs000bafybeigccfheqv7upr4k64bkg5b5wiwelunyn2l2rbirmm43m34lcpuqqe') expect(res.rawPayload).to.deep.equal(directoryContent['nested-folder/nested.html']) done() }) @@ -283,6 +333,7 @@ describe('HTTP Gateway', function () { }, (res) => { expect(res.statusCode).to.equal(301) expect(res.headers['location']).to.equal('/ipfs/QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdsgaTQ/') + expect(res.headers['x-ipfs-path']).to.equal(undefined) done() }) }) @@ -296,6 +347,7 @@ describe('HTTP Gateway', function () { }, (res) => { expect(res.statusCode).to.equal(302) expect(res.headers['location']).to.equal('/ipfs/QmbQD7EMEL1zeebwBsWEfA3ndgSS6F7S6iTuwuqasPgVRi/index.html') + expect(res.headers['x-ipfs-path']).to.equal(undefined) done() }) })