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

Commit 6b42bbf

Browse files
harshjvdaviddias
authored andcommitted
feat: add HTTP Gateway to the js-ipfs daemon
1 parent bfc58d6 commit 6b42bbf

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+516
-8
lines changed

gulpfile.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const gulp = require('gulp')
44
const parallel = require('async/parallel')
55
const series = require('async/series')
66
const createTempRepo = require('./test/utils/create-repo-nodejs.js')
7-
const HTTPAPI = require('./src/http-api')
7+
const HTTPAPI = require('./src/http')
88
const leftPad = require('left-pad')
99

1010
let nodes = []

package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"test:unit:node": "gulp test:node",
2828
"test:unit:node:core": "TEST=core npm run test:unit:node",
2929
"test:unit:node:http": "TEST=http npm run test:unit:node",
30+
"test:unit:node:gateway": "TEST=gateway npm run test:unit:node",
3031
"test:unit:node:cli": "TEST=cli npm run test:unit:node",
3132
"test:unit:browser": "gulp test:browser",
3233
"test:interop": "npm run test:interop:node",
@@ -92,8 +93,10 @@
9293
"async": "^2.5.0",
9394
"bl": "^1.2.1",
9495
"boom": "^5.2.0",
95-
"cids": "~0.5.1",
9696
"debug": "^3.0.1",
97+
"cids": "^0.5.1",
98+
"file-type": "^6.1.0",
99+
"filesize": "^3.5.10",
97100
"fsm-event": "^2.1.0",
98101
"glob": "^7.1.2",
99102
"hapi": "^16.5.2",
@@ -126,6 +129,7 @@
126129
"lodash.sortby": "^4.7.0",
127130
"lodash.values": "^4.3.0",
128131
"mafmt": "^3.0.0",
132+
"mime-types": "^2.1.16",
129133
"mkdirp": "~0.5.1",
130134
"multiaddr": "^3.0.0",
131135
"multihashes": "~0.4.9",

src/cli/commands/daemon.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict'
22

3-
const HttpAPI = require('../../http-api')
3+
const HttpAPI = require('../../http')
44
const utils = require('../utils')
55
const print = utils.print
66

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

src/http/gateway/resolver.js

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
'use strict'
2+
3+
const mh = require('multihashes')
4+
const promisify = require('promisify-es6')
5+
const eachOfSeries = require('async/eachOfSeries')
6+
const debug = require('debug')
7+
const log = debug('jsipfs:http-gateway:resolver')
8+
log.error = debug('jsipfs:http-gateway:resolver:error')
9+
10+
const html = require('./utils/html')
11+
const PathUtil = require('./utils/path')
12+
13+
const INDEX_HTML_FILES = [ 'index.html', 'index.htm', 'index.shtml' ]
14+
15+
const resolveDirectory = promisify((ipfs, path, multihash, callback) => {
16+
if (!callback) {
17+
callback = noop
18+
}
19+
20+
mh.validate(mh.fromB58String(multihash))
21+
22+
ipfs
23+
.object
24+
.get(multihash, { enc: 'base58' })
25+
.then((DAGNode) => {
26+
const links = DAGNode.links
27+
const indexFiles = links.filter((link) => INDEX_HTML_FILES.indexOf(link.name) !== -1)
28+
29+
// found index file in links
30+
if (indexFiles.length > 0) {
31+
return callback(null, indexFiles)
32+
}
33+
34+
return callback(null, html.build(path, links))
35+
})
36+
})
37+
38+
const noop = function () {}
39+
40+
const resolveMultihash = promisify((ipfs, path, callback) => {
41+
if (!callback) {
42+
callback = noop
43+
}
44+
45+
const parts = PathUtil.splitPath(path)
46+
const partsLength = parts.length
47+
48+
let currentMultihash = parts[0]
49+
50+
eachOfSeries(parts, (multihash, currentIndex, next) => {
51+
// throws error when invalid multihash is passed
52+
mh.validate(mh.fromB58String(currentMultihash))
53+
log('currentMultihash: ', currentMultihash)
54+
log('currentIndex: ', currentIndex, '/', partsLength)
55+
56+
ipfs
57+
.object
58+
.get(currentMultihash, { enc: 'base58' })
59+
.then((DAGNode) => {
60+
// log('DAGNode: ', DAGNode)
61+
if (currentIndex === partsLength - 1) {
62+
// leaf node
63+
log('leaf node: ', currentMultihash)
64+
// log('DAGNode: ', DAGNode.links)
65+
66+
if (DAGNode.links &&
67+
DAGNode.links.length > 0 &&
68+
DAGNode.links[0].name.length > 0) {
69+
// this is a directory.
70+
let isDirErr = new Error('This dag node is a directory')
71+
// add currentMultihash as a fileName so it can be used by resolveDirectory
72+
isDirErr.fileName = currentMultihash
73+
return next(isDirErr)
74+
}
75+
76+
next()
77+
} else {
78+
// find multihash of requested named-file
79+
// in current DAGNode's links
80+
let multihashOfNextFile
81+
const nextFileName = parts[currentIndex + 1]
82+
const links = DAGNode.links
83+
84+
for (let link of links) {
85+
if (link.name === nextFileName) {
86+
// found multihash of requested named-file
87+
multihashOfNextFile = mh.toB58String(link.multihash)
88+
log('found multihash: ', multihashOfNextFile)
89+
break
90+
}
91+
}
92+
93+
if (!multihashOfNextFile) {
94+
log.error(`no link named "${nextFileName}" under ${currentMultihash}`)
95+
throw new Error(`no link named "${nextFileName}" under ${currentMultihash}`)
96+
}
97+
98+
currentMultihash = multihashOfNextFile
99+
next()
100+
}
101+
})
102+
}, (err) => {
103+
if (err) {
104+
log.error(err)
105+
return callback(err)
106+
}
107+
callback(null, {multihash: currentMultihash})
108+
})
109+
})
110+
111+
module.exports = {
112+
resolveDirectory,
113+
resolveMultihash
114+
}

src/http/gateway/resources/gateway.js

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
'use strict'
2+
3+
const debug = require('debug')
4+
const log = debug('jsipfs:http-gateway')
5+
log.error = debug('jsipfs:http-gateway:error')
6+
const pull = require('pull-stream')
7+
const toPull = require('stream-to-pull-stream')
8+
const fileType = require('file-type')
9+
const mime = require('mime-types')
10+
const GatewayResolver = require('../resolver')
11+
const PathUtils = require('../utils/path')
12+
const Stream = require('stream')
13+
14+
module.exports = {
15+
checkHash: (request, reply) => {
16+
if (!request.params.hash) {
17+
return reply({
18+
Message: 'Path Resolve error: path must contain at least one component',
19+
Code: 0
20+
}).code(400).takeover()
21+
}
22+
23+
return reply({
24+
ref: `/ipfs/${request.params.hash}`
25+
})
26+
},
27+
handler: (request, reply) => {
28+
const ref = request.pre.args.ref
29+
const ipfs = request.server.app.ipfs
30+
31+
return GatewayResolver
32+
.resolveMultihash(ipfs, ref)
33+
.then((data) => {
34+
ipfs
35+
.files
36+
.cat(data.multihash)
37+
.then((stream) => {
38+
if (ref.endsWith('/')) {
39+
// remove trailing slash for files
40+
return reply
41+
.redirect(PathUtils.removeTrailingSlash(ref))
42+
.permanent(true)
43+
} else {
44+
if (!stream._read) {
45+
stream._read = () => {}
46+
stream._readableState = {}
47+
}
48+
// response.continue()
49+
let filetypeChecked = false
50+
let stream2 = new Stream.PassThrough({highWaterMark: 1})
51+
let response = reply(stream2).hold()
52+
53+
pull(
54+
toPull.source(stream),
55+
pull.drain((chunk) => {
56+
// Check file type. do this once.
57+
if (chunk.length > 0 && !filetypeChecked) {
58+
log('got first chunk')
59+
let fileSignature = fileType(chunk)
60+
log('file type: ', fileSignature)
61+
62+
filetypeChecked = true
63+
const mimeType = mime.lookup((fileSignature) ? fileSignature.ext : null)
64+
log('ref ', ref)
65+
log('mime-type ', mimeType)
66+
67+
if (mimeType) {
68+
log('writing mimeType')
69+
70+
response
71+
.header('Content-Type', mime.contentType(mimeType))
72+
.header('Access-Control-Allow-Headers', 'X-Stream-Output, X-Chunked-Ouput')
73+
.header('Access-Control-Allow-Methods', 'GET')
74+
.header('Access-Control-Allow-Origin', '*')
75+
.header('Access-Control-Expose-Headers', 'X-Stream-Output, X-Chunked-Ouput')
76+
.send()
77+
} else {
78+
response
79+
.header('Access-Control-Allow-Headers', 'X-Stream-Output, X-Chunked-Ouput')
80+
.header('Access-Control-Allow-Methods', 'GET')
81+
.header('Access-Control-Allow-Origin', '*')
82+
.header('Access-Control-Expose-Headers', 'X-Stream-Output, X-Chunked-Ouput')
83+
.send()
84+
}
85+
}
86+
87+
stream2.write(chunk)
88+
}, (err) => {
89+
if (err) throw err
90+
log('stream ended.')
91+
stream2.end()
92+
})
93+
)
94+
}
95+
})
96+
.catch((err) => {
97+
if (err) {
98+
log.error(err)
99+
return reply(err.toString()).code(500)
100+
}
101+
})
102+
}).catch((err) => {
103+
log('err: ', err.toString(), ' fileName: ', err.fileName)
104+
105+
const errorToString = err.toString()
106+
if (errorToString === 'Error: This dag node is a directory') {
107+
return GatewayResolver
108+
.resolveDirectory(ipfs, ref, err.fileName)
109+
.then((data) => {
110+
if (typeof data === 'string') {
111+
// no index file found
112+
if (!ref.endsWith('/')) {
113+
// for a directory, if URL doesn't end with a /
114+
// append / and redirect permanent to that URL
115+
return reply.redirect(`${ref}/`).permanent(true)
116+
} else {
117+
// send directory listing
118+
return reply(data)
119+
}
120+
} else {
121+
// found index file
122+
// redirect to URL/<found-index-file>
123+
return reply.redirect(PathUtils.joinURLParts(ref, data[0].name))
124+
}
125+
}).catch((err) => {
126+
log.error(err)
127+
return reply(err.toString()).code(500)
128+
})
129+
} else if (errorToString.startsWith('Error: no link named')) {
130+
return reply(errorToString).code(404)
131+
} else if (errorToString.startsWith('Error: multihash length inconsistent') ||
132+
errorToString.startsWith('Error: Non-base58 character')) {
133+
return reply({Message: errorToString, code: 0}).code(400)
134+
} else {
135+
log.error(err)
136+
return reply({Message: errorToString, code: 0}).code(500)
137+
}
138+
})
139+
}
140+
}

src/http/gateway/resources/index.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use strict'
2+
3+
module.exports = {
4+
gateway: require('./gateway')
5+
}

src/http/gateway/routes/gateway.js

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use strict'
2+
3+
const resources = require('../resources')
4+
5+
module.exports = (server) => {
6+
const gateway = server.select('Gateway')
7+
8+
gateway.route({
9+
method: '*',
10+
path: '/ipfs/{hash*}',
11+
config: {
12+
pre: [
13+
{ method: resources.gateway.checkHash, assign: 'args' }
14+
],
15+
handler: resources.gateway.handler
16+
}
17+
})
18+
}

src/http/gateway/routes/index.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use strict'
2+
3+
module.exports = (server) => {
4+
require('./gateway')(server)
5+
}

0 commit comments

Comments
 (0)