@@ -8,6 +8,7 @@ const fileType = require('file-type')
8
8
const mime = require ( 'mime-types' )
9
9
const { PassThrough } = require ( 'readable-stream' )
10
10
const Boom = require ( 'boom' )
11
+ const Ammo = require ( '@hapi/ammo' ) // HTTP Range processing utilities
11
12
const peek = require ( 'buffer-peek-stream' )
12
13
13
14
const { resolver } = require ( 'ipfs-http-response' )
@@ -98,7 +99,47 @@ module.exports = {
98
99
return h . redirect ( PathUtils . removeTrailingSlash ( ref ) ) . permanent ( true )
99
100
}
100
101
101
- const rawStream = ipfs . catReadableStream ( data . cid )
102
+ // Support If-None-Match & Etag (Conditional Requests from RFC7232)
103
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
104
+ const etag = `"${ data . cid } "`
105
+ const cachedEtag = request . headers [ 'if-none-match' ]
106
+ if ( cachedEtag === etag || cachedEtag === `W/${ etag } ` ) {
107
+ return h . response ( ) . code ( 304 ) // Not Modified
108
+ }
109
+
110
+ // Immutable content produces 304 Not Modified for all values of If-Modified-Since
111
+ if ( ref . startsWith ( '/ipfs/' ) && request . headers [ 'if-modified-since' ] ) {
112
+ return h . response ( ) . code ( 304 ) // Not Modified
113
+ }
114
+
115
+ // This necessary to set correct Content-Length and validate Range requests
116
+ // Note: we need `size` (raw data), not `cumulativeSize` (data + DAGNodes)
117
+ const { size } = await ipfs . files . stat ( `/ipfs/${ data . cid } ` )
118
+
119
+ // Handle Byte Range requests (https://tools.ietf.org/html/rfc7233#section-2.1)
120
+ const catOptions = { }
121
+ let rangeResponse = false
122
+ if ( request . headers . range ) {
123
+ // If-Range is respected (when present), but we compare it only against Etag
124
+ // (Last-Modified date is too weak for IPFS use cases)
125
+ if ( ! request . headers [ 'if-range' ] || request . headers [ 'if-range' ] === etag ) {
126
+ const ranges = Ammo . header ( request . headers . range , size )
127
+ if ( ! ranges ) {
128
+ const error = Boom . rangeNotSatisfiable ( )
129
+ error . output . headers [ 'content-range' ] = `bytes */${ size } `
130
+ throw error
131
+ }
132
+
133
+ if ( ranges . length === 1 ) { // Ignore requests for multiple ranges (hard to map to ipfs.cat and not used in practice)
134
+ rangeResponse = true
135
+ const range = ranges [ 0 ]
136
+ catOptions . offset = range . from
137
+ catOptions . length = ( range . to - range . from + 1 )
138
+ }
139
+ }
140
+ }
141
+
142
+ const rawStream = ipfs . catReadableStream ( data . cid , catOptions )
102
143
const responseStream = new ResponseStream ( )
103
144
104
145
// Pass-through Content-Type sniffing over initial bytes
@@ -119,10 +160,11 @@ module.exports = {
119
160
}
120
161
} )
121
162
122
- const res = h . response ( responseStream )
163
+ const res = h . response ( responseStream ) . code ( rangeResponse ? 206 : 200 )
123
164
124
165
// Etag maps directly to an identifier for a specific version of a resource
125
- res . header ( 'Etag' , `"${ data . cid } "` )
166
+ // and enables smart client-side caching thanks to If-None-Match
167
+ res . header ( 'etag' , etag )
126
168
127
169
// Set headers specific to the immutable namespace
128
170
if ( ref . startsWith ( '/ipfs/' ) ) {
@@ -137,15 +179,38 @@ module.exports = {
137
179
res . header ( 'Content-Type' , contentType )
138
180
}
139
181
182
+ if ( rangeResponse ) {
183
+ const from = catOptions . offset
184
+ const to = catOptions . offset + catOptions . length - 1
185
+ res . header ( 'Content-Range' , `bytes ${ from } -${ to } /${ size } ` )
186
+ res . header ( 'Content-Length' , catOptions . length )
187
+ } else {
188
+ // Announce support for Range requests
189
+ res . header ( 'Accept-Ranges' , 'bytes' )
190
+ res . header ( 'Content-Length' , size )
191
+ }
192
+
193
+ // Support Content-Disposition via ?filename=foo parameter
194
+ // (useful for browser vendor to download raw CID into custom filename)
195
+ // Source: https://github.com/ipfs/go-ipfs/blob/v0.4.20/core/corehttp/gateway_handler.go#L232-L236
196
+ if ( request . query . filename ) {
197
+ res . header ( 'Content-Disposition' , `inline; filename*=UTF-8''${ encodeURIComponent ( request . query . filename ) } ` )
198
+ }
199
+
140
200
return res
141
201
} ,
142
202
143
203
afterHandler ( request , h ) {
144
204
const { response } = request
145
- if ( response . statusCode === 200 ) {
205
+ // Add headers to successfult responses (regular or range)
206
+ if ( response . statusCode === 200 || response . statusCode === 206 ) {
146
207
const { ref } = request . pre . args
147
208
response . header ( 'X-Ipfs-Path' , ref )
148
209
if ( ref . startsWith ( '/ipfs/' ) ) {
210
+ // "set modtime to a really long time ago, since files are immutable and should stay cached"
211
+ // Source: https://github.com/ipfs/go-ipfs/blob/v0.4.20/core/corehttp/gateway_handler.go#L228-L229
212
+ response . header ( 'Last-Modified' , 'Thu, 01 Jan 1970 00:00:01 GMT' )
213
+ // Suborigins: https://github.com/ipfs/in-web-browsers/issues/66
149
214
const rootCid = ref . split ( '/' ) [ 2 ]
150
215
const ipfsOrigin = cidToString ( rootCid , { base : 'base32' } )
151
216
response . header ( 'Suborigin' , 'ipfs000' + ipfsOrigin )
0 commit comments