This repository has been archived by the owner on Feb 12, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
gateway.js
202 lines (177 loc) · 8.1 KB
/
gateway.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
'use strict'
const debug = require('debug')
const uint8ArrayFromString = require('uint8arrays/from-string')
const uint8ArrayToString = require('uint8arrays/to-string')
const Boom = require('@hapi/boom')
const Ammo = require('@hapi/ammo') // HTTP Range processing utilities
const last = require('it-last')
const multibase = require('multibase')
const { resolver } = require('ipfs-http-response')
const detectContentType = require('ipfs-http-response/src/utils/content-type')
const isIPFS = require('is-ipfs')
const toStream = require('it-to-stream')
const PathUtils = require('../utils/path')
const { cidToString } = require('ipfs-core-utils/src/cid')
const log = debug('ipfs:http-gateway')
log.error = debug('ipfs:http-gateway:error')
module.exports = {
async handler (request, h) {
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
let ipfsPath = decodeURI(path.startsWith('/ipns/')
? await last(ipfs.name.resolve(path, { recursive: true }))
: path)
let directory = false
let data
try {
data = await resolver.cid(ipfs, ipfsPath)
} catch (err) {
const errorToString = err.toString()
log.error('err: ', errorToString, ' fileName: ', err.fileName)
// 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('/')) {
// add trailing slash for directory listings
return h.redirect(`${path}/`).permanent(true)
}
// send directory listing
return h.response(data)
}
// 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')):
case (errorToString.startsWith('Error: Non-base58 character')):
case (errorToString.startsWith('Error: invalid character')):
throw Boom.boomify(err, { statusCode: 400 })
default:
log.error(err)
throw err
}
}
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)
}
if (request.headers['service-worker'] === 'script') {
// Disallow Service Worker registration on /ipfs scope
// https://github.com/ipfs/go-ipfs/issues/4025
if (path.match(/^\/ip[nf]s\/[^/]+$/)) throw Boom.badRequest('navigator.serviceWorker: registration is not allowed for this scope')
}
// Support If-None-Match & Etag (Conditional Requests from RFC7232)
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
const etag = `"${data.cid}"`
const cachedEtag = request.headers['if-none-match']
if (cachedEtag === etag || cachedEtag === `W/${etag}`) {
return h.response().code(304) // Not Modified
}
// Immutable content produces 304 Not Modified for all values of If-Modified-Since
if (path.startsWith('/ipfs/') && request.headers['if-modified-since']) {
return h.response().code(304) // Not Modified
}
// This necessary to set correct Content-Length and validate Range requests
// Note: we need `size` (raw data), not `cumulativeSize` (data + DAGNodes)
const { size } = await ipfs.files.stat(`/ipfs/${data.cid}`)
// Handle Byte Range requests (https://tools.ietf.org/html/rfc7233#section-2.1)
const catOptions = {}
let rangeResponse = false
if (request.headers.range) {
// If-Range is respected (when present), but we compare it only against Etag
// (Last-Modified date is too weak for IPFS use cases)
if (!request.headers['if-range'] || request.headers['if-range'] === etag) {
const ranges = Ammo.header(request.headers.range, size)
if (!ranges) {
const error = Boom.rangeNotSatisfiable()
error.output.headers['content-range'] = `bytes */${size}`
throw error
}
if (ranges.length === 1) { // Ignore requests for multiple ranges (hard to map to ipfs.cat and not used in practice)
rangeResponse = true
const range = ranges[0]
catOptions.offset = range.from
catOptions.length = (range.to - range.from + 1)
}
}
}
const { source, contentType } = await detectContentType(ipfsPath, ipfs.cat(data.cid, catOptions))
const responseStream = toStream.readable((async function * () {
for await (const chunk of source) {
yield chunk.slice() // Convert BufferList to Buffer
}
})())
const res = h.response(responseStream).code(rangeResponse ? 206 : 200)
// Etag maps directly to an identifier for a specific version of a resource
// and enables smart client-side caching thanks to If-None-Match
res.header('etag', etag)
// Set headers specific to the immutable namespace
if (path.startsWith('/ipfs/')) {
res.header('Cache-Control', 'public, max-age=29030400, immutable')
}
log('HTTP path ', path)
log('IPFS path ', ipfsPath)
log('content-type ', contentType)
if (contentType) {
log('writing content-type header')
res.header('Content-Type', contentType)
}
if (rangeResponse) {
const from = catOptions.offset
const to = catOptions.offset + catOptions.length - 1
res.header('Content-Range', `bytes ${from}-${to}/${size}`)
res.header('Content-Length', catOptions.length)
} else {
// Announce support for Range requests
res.header('Accept-Ranges', 'bytes')
res.header('Content-Length', size)
}
// Support Content-Disposition via ?filename=foo parameter
// (useful for browser vendor to download raw CID into custom filename)
// Source: https://github.com/ipfs/go-ipfs/blob/v0.4.20/core/corehttp/gateway_handler.go#L232-L236
if (request.query.filename) {
res.header('Content-Disposition', `inline; filename*=UTF-8''${encodeURIComponent(request.query.filename)}`)
}
return res
},
afterHandler (request, h) {
const { response } = request
// Add headers to successfult responses (regular or range)
if (response.statusCode === 200 || response.statusCode === 206) {
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 = path.split('/')[2]
const ipfsOrigin = cidToString(rootCid, { base: 'base32' })
response.header('Suborigin', `ipfs000${ipfsOrigin}`)
} else if (path.startsWith('/ipns/')) {
// Suborigin for /ipns/: https://github.com/ipfs/in-web-browsers/issues/66
const root = path.split('/')[2]
// encode CID/FQDN in base32 (Suborigin allows only a-z)
const ipnsOrigin = isIPFS.cid(root)
? cidToString(root, { base: 'base32' })
: uint8ArrayToString(multibase.encode('base32', uint8ArrayFromString(root)))
response.header('Suborigin', `ipns000${ipnsOrigin}`)
}
}
return h.continue
}
}