Skip to content
This repository has been archived by the owner on Feb 12, 2024. It is now read-only.

fix(gateway): keep pretty URL and return implicit index.html #2217

Merged
merged 1 commit into from
Jul 5, 2019
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
25 changes: 16 additions & 9 deletions src/http/gateway/resources/gateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,11 @@ 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 ipfsPath = decodeURI(path.startsWith('/ipns/')
let ipfsPath = decodeURI(path.startsWith('/ipns/')
? await ipfs.name.resolve(path, { recursive: true })
: path)

let directory = false
Copy link
Member Author

@lidel lidel Jul 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with explicit flag to make it easier to review/modify code paths related to directories (with and without index.html)

let data
try {
data = await resolver.cid(ipfs, ipfsPath)
Expand All @@ -70,22 +71,23 @@ module.exports = {
// switch case with true feels so wrong.
switch (true) {
case (errorToString === 'Error: This dag node is a directory'):
directory = true
data = await resolver.directory(ipfs, ipfsPath, err.cid)

if (typeof data === 'string') {
// no index file found
if (!path.endsWith('/')) {
// for a directory, if URL doesn't end with a /
// append / and redirect permanent to that URL
// add trailing slash for directory listings
return h.redirect(`${path}/`).permanent(true)
}
// send directory listing
return h.response(data)
}

// found index file
// redirect to URL/<found-index-file>
return h.redirect(PathUtils.joinURLParts(path, data[0].Name))
// found index file: return <ipfsPath>/<found-index-file>
ipfsPath = PathUtils.joinURLParts(ipfsPath, data[0].Name)
data = await resolver.cid(ipfs, ipfsPath)
break
case (errorToString.startsWith('Error: no link named')):
throw Boom.boomify(err, { statusCode: 404 })
case (errorToString.startsWith('Error: multihash length inconsistent')):
Expand All @@ -97,10 +99,14 @@ module.exports = {
}
}

if (path.endsWith('/')) {
if (!directory && path.endsWith('/')) {
// remove trailing slash for files
return h.redirect(PathUtils.removeTrailingSlash(path)).permanent(true)
}
if (directory && !path.endsWith('/')) {
// add trailing slash for directories with implicit index.html
return h.redirect(`${path}/`).permanent(true)
}

// Support If-None-Match & Etag (Conditional Requests from RFC7232)
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
Expand Down Expand Up @@ -153,7 +159,7 @@ module.exports = {
log.error(err)
return reject(err)
}
resolve({ peekedStream, contentType: detectContentType(path, streamHead) })
resolve({ peekedStream, contentType: detectContentType(ipfsPath, streamHead) })
Copy link
Member Author

@lidel lidel Jul 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ipfsPath instead of path because if we are returning implicit index.html we want to detect HTML content type of the file, not the dir.

})
})

Expand All @@ -170,7 +176,8 @@ module.exports = {
res.header('Cache-Control', 'public, max-age=29030400, immutable')
}

log('path ', path)
log('HTTP path ', path)
log('IPFS path ', ipfsPath)
log('content-type ', contentType)

if (contentType) {
Expand Down
30 changes: 25 additions & 5 deletions test/gateway/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -519,20 +519,40 @@ describe('HTTP Gateway', function () {
expect(res.headers['x-ipfs-path']).to.equal(undefined)
})

// TODO: check if interop for this exists and if not, match behavior of go-ipfs
it('redirect to webpage index.html', async () => {
const dir = 'QmbQD7EMEL1zeebwBsWEfA3ndgSS6F7S6iTuwuqasPgVRi/'
it('redirect to a directory with index.html', async () => {
const dir = 'QmbQD7EMEL1zeebwBsWEfA3ndgSS6F7S6iTuwuqasPgVRi' // note lack of '/' at the end

const res = await gateway.inject({
method: 'GET',
url: '/ipfs/' + dir
})

expect(res.statusCode).to.equal(302)
expect(res.headers.location).to.equal('/ipfs/QmbQD7EMEL1zeebwBsWEfA3ndgSS6F7S6iTuwuqasPgVRi/index.html')
// we expect redirect to the same path but with '/' at the end
expect(res.statusCode).to.equal(301)
expect(res.headers.location).to.equal(`/ipfs/${dir}/`)
expect(res.headers['x-ipfs-path']).to.equal(undefined)
})

it('load a directory with index.html', async () => {
const dir = 'QmbQD7EMEL1zeebwBsWEfA3ndgSS6F7S6iTuwuqasPgVRi/' // note '/' at the end

const res = await gateway.inject({
method: 'GET',
url: '/ipfs/' + dir
})

// confirm payload is index.html
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['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('"Qma6665X5k3zti8nKy7gmXK2BndNDSkgmANpV6k3FUjUeg"')
expect(res.headers.suborigin).to.equal('ipfs000bafybeigccfheqv7upr4k64bkg5b5wiwelunyn2l2rbirmm43m34lcpuqqe')
expect(res.rawPayload).to.deep.equal(directoryContent['index.html'])
})

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'
Expand Down