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

Commit cc41624

Browse files
harshjvdaviddias
authored andcommitted
feat: add gateway route to daemon
1 parent 54be08b commit cc41624

File tree

7 files changed

+328
-1
lines changed

7 files changed

+328
-1
lines changed

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,14 @@
126126
"lodash.get": "^4.4.2",
127127
"lodash.sortby": "^4.7.0",
128128
"lodash.values": "^4.3.0",
129+
"mime-types": "^2.1.13",
129130
"mafmt": "^2.1.8",
130131
"mkdirp": "^0.5.1",
131132
"multiaddr": "^2.3.0",
132133
"multihashes": "~0.4.5",
133134
"once": "^1.4.0",
134135
"path-exists": "^3.0.0",
136+
"promised-for": "^1.0.0",
135137
"peer-book": "^0.5.0",
136138
"peer-id": "^0.9.0",
137139
"peer-info": "^0.10.0",
@@ -204,4 +206,4 @@
204206
205207
"ᴠɪᴄᴛᴏʀ ʙᴊᴇʟᴋʜᴏʟᴍ <[email protected]>"
206208
]
207-
}
209+
}

src/http-api/gateway/resolver.js

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
'use strict'
2+
3+
const mh = require('multihashes')
4+
const pf = require('promised-for')
5+
6+
const html = require('./utils/html')
7+
const PathUtil = require('./utils/path')
8+
9+
const INDEX_HTML_FILES = [ 'index.html', 'index.htm', 'index.shtml' ]
10+
11+
const resolveDirectory = (ipfs, path, multihash) => {
12+
return ipfs
13+
.object
14+
.get(multihash, { enc: 'base58' })
15+
.then((DAGNode) => {
16+
const links = DAGNode.links
17+
const indexFiles = links.filter((link) => INDEX_HTML_FILES.indexOf(link.name) !== -1)
18+
19+
// found index file in links
20+
if (indexFiles.length > 0) {
21+
return indexFiles
22+
}
23+
24+
return html.build(path, links)
25+
})
26+
}
27+
28+
const resolveMultihash = (ipfs, path) => {
29+
const parts = PathUtil.splitPath(path)
30+
const partsLength = parts.length
31+
32+
return pf(
33+
{
34+
multihash: parts[0],
35+
index: 0
36+
},
37+
(i) => i.index < partsLength,
38+
(i) => {
39+
const currentIndex = i.index
40+
const currentMultihash = i.multihash
41+
42+
// throws error when invalid multihash is passed
43+
mh.validate(mh.fromB58String(currentMultihash))
44+
45+
return ipfs
46+
.object
47+
.get(currentMultihash, { enc: 'base58' })
48+
.then((DAGNode) => {
49+
if (currentIndex === partsLength - 1) {
50+
// leaf node
51+
return {
52+
multihash: currentMultihash,
53+
index: currentIndex + 1
54+
}
55+
} else {
56+
// find multihash of requested named-file
57+
// in current DAGNode's links
58+
let multihashOfNextFile
59+
const nextFileName = parts[currentIndex + 1]
60+
const links = DAGNode.links
61+
62+
for (let link of links) {
63+
if (link.name === nextFileName) {
64+
// found multihash of requested named-file
65+
multihashOfNextFile = mh.toB58String(link.multihash)
66+
break
67+
}
68+
}
69+
70+
if (!multihashOfNextFile) {
71+
throw new Error(`no link named "${nextFileName}" under ${currentMultihash}`)
72+
}
73+
74+
return {
75+
multihash: multihashOfNextFile,
76+
index: currentIndex + 1
77+
}
78+
}
79+
})
80+
})
81+
}
82+
83+
module.exports = {
84+
resolveDirectory,
85+
resolveMultihash
86+
}

src/http-api/gateway/utils/html.js

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
'use strict'
2+
3+
const filesize = require('filesize')
4+
5+
const HTML_PAGE_STYLE = require('./style')
6+
const PathUtil = require('./path')
7+
8+
const getParentDirectoryURL = (originalParts) => {
9+
const parts = originalParts.splice()
10+
11+
if (parts.length > 1) {
12+
parts.pop()
13+
}
14+
15+
return [ '', 'ipfs' ].concat(parts).join('/')
16+
}
17+
18+
const buildFilesList = (path, links) => {
19+
const rows = links.map((link) => {
20+
let row = [
21+
`<div class="ipfs-icon ipfs-_blank">&nbsp;</div>`,
22+
`<a href="${PathUtil.joinURLParts(path, link.name)}">${link.name}</a>`,
23+
filesize(link.size)
24+
]
25+
26+
row = row.map((cell) => `<td>${cell}</td>`).join('')
27+
28+
return `<tr>${row}</tr>`
29+
})
30+
31+
return rows.join('')
32+
}
33+
34+
const buildTable = (path, links) => {
35+
const parts = PathUtil.splitPath(path)
36+
let parentDirectoryURL = getParentDirectoryURL(parts)
37+
38+
return `
39+
<table class="table table-striped">
40+
<tbody>
41+
<tr>
42+
<td class="narrow">
43+
<div class="ipfs-icon ipfs-_blank">&nbsp;</div>
44+
</td>
45+
<td class="padding">
46+
<a href="${parentDirectoryURL}">..</a>
47+
</td>
48+
<td></td>
49+
</tr>
50+
${buildFilesList(path, links)}
51+
</tbody>
52+
</table>
53+
`
54+
}
55+
56+
module.exports.build = (path, links) => {
57+
return `
58+
<!DOCTYPE html>
59+
<html>
60+
<head>
61+
<meta charset="utf-8">
62+
<title>${path}</title>
63+
<style>${HTML_PAGE_STYLE}</style>
64+
</head>
65+
<body>
66+
<div id="header" class="row">
67+
<div class="col-xs-2">
68+
<div id="logo" class="ipfs-logo"></div>
69+
</div>
70+
</div>
71+
<br>
72+
<div class="col-xs-12">
73+
<div class="panel panel-default">
74+
<div class="panel-heading">
75+
<strong>Index of ${path}</strong>
76+
</div>
77+
${buildTable(path, links)}
78+
</div>
79+
</div>
80+
</body>
81+
</html>
82+
`
83+
}

src/http-api/gateway/utils/path.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict'
2+
3+
const splitPath = (path) => {
4+
if (path[path.length - 1] === '/') path = path.substring(0, path.length - 1)
5+
return path.substring(6).split('/')
6+
}
7+
8+
const removeLeadingSlash = (url) => {
9+
if (url[0] === '/') url = url.substring(1)
10+
return url
11+
}
12+
13+
const removeTrailingSlash = (url) => {
14+
if (url.endsWith('/')) url = url.substring(0, url.length - 1)
15+
return url
16+
}
17+
18+
const removeSlashFromBothEnds = (url) => {
19+
url = removeLeadingSlash(url)
20+
url = removeTrailingSlash(url)
21+
return url
22+
}
23+
24+
const joinURLParts = (...urls) => {
25+
urls = urls.filter((url) => url.length > 0)
26+
urls = [ '' ].concat(urls.map((url) => removeSlashFromBothEnds(url)))
27+
28+
return urls.join('/')
29+
}
30+
31+
module.exports = {
32+
splitPath,
33+
removeTrailingSlash,
34+
joinURLParts
35+
}

src/http-api/gateway/utils/style.js

+16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/http-api/resources/files.js

+93
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ const toPull = require('stream-to-pull-stream')
1111
const pushable = require('pull-pushable')
1212
const EOL = require('os').EOL
1313
const toStream = require('pull-stream-to-stream')
14+
const mime = require('mime-types')
15+
16+
const GatewayResolver = require('../gateway/resolver')
17+
const PathUtils = require('../gateway/utils/path')
1418

1519
exports = module.exports
1620

@@ -213,3 +217,92 @@ exports.add = {
213217
)
214218
}
215219
}
220+
221+
exports.gateway = {
222+
checkHash: (request, reply) => {
223+
if (!request.params.hash) {
224+
return reply('Path Resolve error: path must contain at least one component').code(400).takeover()
225+
}
226+
227+
return reply({
228+
ref: `/ipfs/${request.params.hash}`
229+
})
230+
},
231+
handler: (request, reply) => {
232+
const ref = request.pre.args.ref
233+
const ipfs = request.server.app.ipfs
234+
235+
return GatewayResolver
236+
.resolveMultihash(ipfs, ref)
237+
.then((data) => {
238+
ipfs
239+
.files
240+
.cat(data.multihash)
241+
.then((stream) => {
242+
if (ref.endsWith('/')) {
243+
// remove trailing slash for files
244+
return reply
245+
.redirect(PathUtils.removeTrailingSlash(ref))
246+
.permanent(true)
247+
} else {
248+
const mimeType = mime.lookup(ref)
249+
250+
if (!stream._read) {
251+
stream._read = () => {}
252+
stream._readableState = {}
253+
}
254+
255+
if (mimeType) {
256+
return reply(stream)
257+
.header('Content-Type', mime.contentType(mimeType))
258+
.header('X-Stream-Output', '1')
259+
} else {
260+
return reply(stream)
261+
.header('X-Stream-Output', '1')
262+
}
263+
}
264+
})
265+
.catch((err) => {
266+
if (err.toString() === 'Error: This dag node is a directory') {
267+
return GatewayResolver
268+
.resolveDirectory(ipfs, ref, data.multihash)
269+
.then((data) => {
270+
if (typeof data === 'string') {
271+
// no index file found
272+
if (!ref.endsWith('/')) {
273+
// for a directory, if URL doesn't end with a /
274+
// append / and redirect permanent to that URL
275+
return reply.redirect(`${ref}/`).permanent(true)
276+
} else {
277+
// send directory listing
278+
return reply(data)
279+
}
280+
} else {
281+
// found index file
282+
// redirect to URL/<found-index-file>
283+
return reply.redirect(PathUtils.joinURLParts(ref, data[0].name))
284+
}
285+
}).catch((err) => {
286+
log.error(err)
287+
return reply(err.toString()).code(500)
288+
})
289+
} else {
290+
log.error(err)
291+
return reply(err.toString()).code(500)
292+
}
293+
})
294+
}).catch((err) => {
295+
const errorToString = err.toString()
296+
297+
if (errorToString.startsWith('Error: no link named')) {
298+
return reply(errorToString).code(404)
299+
} else if (errorToString.startsWith('Error: multihash length inconsistent') ||
300+
errorToString.startsWith('Error: Non-base58 character')) {
301+
return reply(errorToString).code(400)
302+
} else {
303+
log.error(err)
304+
return reply(errorToString).code(500)
305+
}
306+
})
307+
}
308+
}

src/http-api/routes/files.js

+12
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const resources = require('./../resources')
44

55
module.exports = (server) => {
66
const api = server.select('API')
7+
const gateway = server.select('Gateway')
78

89
api.route({
910
// TODO fix method
@@ -41,4 +42,15 @@ module.exports = (server) => {
4142
handler: resources.files.add.handler
4243
}
4344
})
45+
46+
gateway.route({
47+
method: '*',
48+
path: '/ipfs/{hash*}',
49+
config: {
50+
pre: [
51+
{ method: resources.files.gateway.checkHash, assign: 'args' }
52+
],
53+
handler: resources.files.gateway.handler
54+
}
55+
})
4456
}

0 commit comments

Comments
 (0)