@@ -11,21 +11,23 @@ const Boom = require('boom')
11
11
const Ammo = require ( '@hapi/ammo' ) // HTTP Range processing utilities
12
12
const peek = require ( 'buffer-peek-stream' )
13
13
14
+ const multibase = require ( 'multibase' )
14
15
const { resolver } = require ( 'ipfs-http-response' )
15
16
const PathUtils = require ( '../utils/path' )
16
17
const { cidToString } = require ( '../../../utils/cid' )
18
+ const isIPFS = require ( 'is-ipfs' )
17
19
18
- function detectContentType ( ref , chunk ) {
20
+ function detectContentType ( path , chunk ) {
19
21
let fileSignature
20
22
21
23
// try to guess the filetype based on the first bytes
22
24
// note that `file-type` doesn't support svgs, therefore we assume it's a svg if ref looks like it
23
- if ( ! ref . endsWith ( '.svg' ) ) {
25
+ if ( ! path . endsWith ( '.svg' ) ) {
24
26
fileSignature = fileType ( chunk )
25
27
}
26
28
27
- // if we were unable to, fallback to the `ref` which might contain the extension
28
- const mimeType = mime . lookup ( fileSignature ? fileSignature . ext : ref )
29
+ // if we were unable to, fallback to the path which might contain the extension
30
+ const mimeType = mime . lookup ( fileSignature ? fileSignature . ext : path )
29
31
30
32
return mime . contentType ( mimeType )
31
33
}
@@ -45,44 +47,45 @@ class ResponseStream extends PassThrough {
45
47
}
46
48
47
49
module . exports = {
48
- checkCID ( request , h ) {
49
- if ( ! request . params . cid ) {
50
- throw Boom . badRequest ( 'Path Resolve error: path must contain at least one component' )
51
- }
52
-
53
- return { ref : `/ipfs/${ request . params . cid } ` }
54
- } ,
55
50
56
51
async handler ( request , h ) {
57
- const { ref } = request . pre . args
58
52
const { ipfs } = request . server . app
53
+ const path = request . path
54
+
55
+ // The resolver from ipfs-http-response supports only immutable /ipfs/ for now,
56
+ // so we convert /ipns/ to /ipfs/ before passing it to the resolver ¯\_(ツ)_/¯
57
+ // This could be removed if a solution proposed in
58
+ // https://github.com/ipfs/js-ipfs-http-response/issues/22 lands upstream
59
+ const ipfsPath = decodeURI ( path . startsWith ( '/ipns/' )
60
+ ? await ipfs . name . resolve ( path , { recursive : true } )
61
+ : path )
59
62
60
63
let data
61
64
try {
62
- data = await resolver . cid ( ipfs , ref )
65
+ data = await resolver . cid ( ipfs , ipfsPath )
63
66
} catch ( err ) {
64
67
const errorToString = err . toString ( )
65
68
log . error ( 'err: ' , errorToString , ' fileName: ' , err . fileName )
66
69
67
70
// switch case with true feels so wrong.
68
71
switch ( true ) {
69
72
case ( errorToString === 'Error: This dag node is a directory' ) :
70
- data = await resolver . directory ( ipfs , ref , err . cid )
73
+ data = await resolver . directory ( ipfs , ipfsPath , err . cid )
71
74
72
75
if ( typeof data === 'string' ) {
73
76
// no index file found
74
- if ( ! ref . endsWith ( '/' ) ) {
77
+ if ( ! path . endsWith ( '/' ) ) {
75
78
// for a directory, if URL doesn't end with a /
76
79
// append / and redirect permanent to that URL
77
- return h . redirect ( `${ ref } /` ) . permanent ( true )
80
+ return h . redirect ( `${ path } /` ) . permanent ( true )
78
81
}
79
82
// send directory listing
80
83
return h . response ( data )
81
84
}
82
85
83
86
// found index file
84
87
// redirect to URL/<found-index-file>
85
- return h . redirect ( PathUtils . joinURLParts ( ref , data [ 0 ] . Name ) )
88
+ return h . redirect ( PathUtils . joinURLParts ( path , data [ 0 ] . Name ) )
86
89
case ( errorToString . startsWith ( 'Error: no link named' ) ) :
87
90
throw Boom . boomify ( err , { statusCode : 404 } )
88
91
case ( errorToString . startsWith ( 'Error: multihash length inconsistent' ) ) :
@@ -94,9 +97,9 @@ module.exports = {
94
97
}
95
98
}
96
99
97
- if ( ref . endsWith ( '/' ) ) {
100
+ if ( path . endsWith ( '/' ) ) {
98
101
// remove trailing slash for files
99
- return h . redirect ( PathUtils . removeTrailingSlash ( ref ) ) . permanent ( true )
102
+ return h . redirect ( PathUtils . removeTrailingSlash ( path ) ) . permanent ( true )
100
103
}
101
104
102
105
// Support If-None-Match & Etag (Conditional Requests from RFC7232)
@@ -108,7 +111,7 @@ module.exports = {
108
111
}
109
112
110
113
// Immutable content produces 304 Not Modified for all values of If-Modified-Since
111
- if ( ref . startsWith ( '/ipfs/' ) && request . headers [ 'if-modified-since' ] ) {
114
+ if ( path . startsWith ( '/ipfs/' ) && request . headers [ 'if-modified-since' ] ) {
112
115
return h . response ( ) . code ( 304 ) // Not Modified
113
116
}
114
117
@@ -150,7 +153,7 @@ module.exports = {
150
153
log . error ( err )
151
154
return reject ( err )
152
155
}
153
- resolve ( { peekedStream, contentType : detectContentType ( ref , streamHead ) } )
156
+ resolve ( { peekedStream, contentType : detectContentType ( path , streamHead ) } )
154
157
} )
155
158
} )
156
159
@@ -163,11 +166,11 @@ module.exports = {
163
166
res . header ( 'etag' , etag )
164
167
165
168
// Set headers specific to the immutable namespace
166
- if ( ref . startsWith ( '/ipfs/' ) ) {
169
+ if ( path . startsWith ( '/ipfs/' ) ) {
167
170
res . header ( 'Cache-Control' , 'public, max-age=29030400, immutable' )
168
171
}
169
172
170
- log ( 'ref ' , ref )
173
+ log ( 'path ' , path )
171
174
log ( 'content-type ' , contentType )
172
175
173
176
if ( contentType ) {
@@ -200,18 +203,25 @@ module.exports = {
200
203
const { response } = request
201
204
// Add headers to successfult responses (regular or range)
202
205
if ( response . statusCode === 200 || response . statusCode === 206 ) {
203
- const { ref } = request . pre . args
204
- response . header ( 'X-Ipfs-Path' , ref )
205
- if ( ref . startsWith ( '/ipfs/' ) ) {
206
+ const path = request . path
207
+ response . header ( 'X-Ipfs-Path' , path )
208
+ if ( path . startsWith ( '/ipfs/' ) ) {
206
209
// "set modtime to a really long time ago, since files are immutable and should stay cached"
207
210
// Source: https://github.com/ipfs/go-ipfs/blob/v0.4.20/core/corehttp/gateway_handler.go#L228-L229
208
211
response . header ( 'Last-Modified' , 'Thu, 01 Jan 1970 00:00:01 GMT' )
209
- // Suborigins : https://github.com/ipfs/in-web-browsers/issues/66
210
- const rootCid = ref . split ( '/' ) [ 2 ]
212
+ // Suborigin for /ipfs/ : https://github.com/ipfs/in-web-browsers/issues/66
213
+ const rootCid = path . split ( '/' ) [ 2 ]
211
214
const ipfsOrigin = cidToString ( rootCid , { base : 'base32' } )
212
- response . header ( 'Suborigin' , 'ipfs000' + ipfsOrigin )
215
+ response . header ( 'Suborigin' , `ipfs000${ ipfsOrigin } ` )
216
+ } else if ( path . startsWith ( '/ipns/' ) ) {
217
+ // Suborigin for /ipns/: https://github.com/ipfs/in-web-browsers/issues/66
218
+ const root = path . split ( '/' ) [ 2 ]
219
+ // encode CID/FQDN in base32 (Suborigin allows only a-z)
220
+ const ipnsOrigin = isIPFS . cid ( root )
221
+ ? cidToString ( root , { base : 'base32' } )
222
+ : multibase . encode ( 'base32' , Buffer . from ( root ) ) . toString ( )
223
+ response . header ( 'Suborigin' , `ipns000${ ipnsOrigin } ` )
213
224
}
214
- // TODO: we don't have case-insensitive solution for /ipns/ yet (https://github.com/ipfs/go-ipfs/issues/5287)
215
225
}
216
226
return h . continue
217
227
}
0 commit comments