Files: 6b03ce0114b8fe5962fd9fc588cf8e0969892366 / dat.dpi
14538 bytesRaw
1 | #!/usr/bin/env node |
2 | // vi: ft=javascript |
3 | |
4 | var net = require('net') |
5 | var path = require('path') |
6 | var fs = require('fs') |
7 | var parseDatUrl = require('parse-dat-url') |
8 | var parseUrl = require('url').parse |
9 | var datDns = require('dat-dns')() |
10 | var memo = require('asyncmemo') |
11 | var hyperdrive = require('hyperdrive') |
12 | var crypto = require('crypto') |
13 | var h = require('hyperscript') |
14 | var hyperdiscovery = require('hyperdiscovery') |
15 | |
16 | var dilloDir = path.join(process.env.HOME, '.dillo') |
17 | var dpidKeysPath = path.join(dilloDir, 'dpid_comm_keys') |
18 | var archivesPath = path.join(dilloDir, 'dat', 'archives') |
19 | |
20 | var dpidKeys = fs.readFileSync(dpidKeysPath, {encoding: 'ascii'}).split(/\s+/) |
21 | |
22 | function readManifest(archive, cb) { |
23 | archive.readFile('dat.json', function (err, data) { |
24 | var manifest |
25 | if (!err) try { manifest = JSON.parse(data) } catch(e) {} |
26 | if (manifest) return cb(null, manifest) |
27 | archive.readFile('CNAME', 'ascii', function (err, data) { |
28 | if (err) return cb('Not found') |
29 | cb(null, {title: data}) |
30 | }) |
31 | }) |
32 | } |
33 | |
34 | var archives = {} |
35 | var getArchive = memo({cache: { |
36 | has: function (key) { return key in archives }, |
37 | get: function (key) { return archives[key] }, |
38 | set: function (key, value) { archives[key] = value}, |
39 | }}, function (key, cb) { |
40 | var archive |
41 | try { |
42 | var archivePath = path.join(archivesPath, key.substr(0, 2), key.substr(2)) |
43 | archive = hyperdrive(archivePath, key, {sparse: true}) |
44 | } catch(e) { |
45 | return cb(e) |
46 | } |
47 | archive.ready(function (err) { |
48 | if (err) return cb(err) |
49 | console.log('[dat dpi] swarming', archive.key.toString('hex')) |
50 | archive.swarm = hyperdiscovery(archive, { |
51 | stream: function (info) { |
52 | var stream = archive.replicate({ |
53 | live: true |
54 | }) |
55 | stream.address = info.type + ':' + (info.host || '?') + ':' + (info.port || '?') |
56 | return stream |
57 | } |
58 | }) |
59 | }) |
60 | archive.on('content', function () { |
61 | cb(null, archive) |
62 | readManifest(archive, function (err, manifest) { |
63 | if (err) return |
64 | archive.manifest = manifest |
65 | }) |
66 | }) |
67 | archive.on('error', cb) |
68 | }) |
69 | |
70 | function DpiReq(socket) { |
71 | this.socket = socket |
72 | this._inBuffer = '' |
73 | socket.on('data', this.onData.bind(this)) |
74 | socket.on('close', this.onClose.bind(this)) |
75 | socket.on('error', this.onError.bind(this)) |
76 | } |
77 | |
78 | DpiReq.prototype.onData = function (data) { |
79 | this._inBuffer += data.toString('ascii') |
80 | for (var i = 0; i < this.commands.length; i++) { |
81 | var cmd = this.commands[i] |
82 | var m = cmd[0].exec(this._inBuffer) |
83 | if (!m) continue |
84 | this._inBuffer = this._inBuffer.slice(m[0].length) |
85 | cmd[1].call(this, m) |
86 | } |
87 | } |
88 | |
89 | DpiReq.prototype.onClose = function () { |
90 | this.closed = true |
91 | } |
92 | |
93 | DpiReq.prototype.onError = function (err) { |
94 | // console.trace('[dat dpi]', err) |
95 | this.closed = true |
96 | } |
97 | |
98 | DpiReq.prototype.reload = function (url) { |
99 | this.socket.end("<cmd='reload_request' url='" + url + "' '>") |
100 | } |
101 | |
102 | DpiReq.prototype.sendStatus = function (msg) { |
103 | this.socket.write("<cmd='send_status_message' msg='" + msg + "' '>") |
104 | } |
105 | |
106 | DpiReq.prototype.writeHeader = function (type) { |
107 | this.socket.write("<cmd='start_send_page' url='" + this.url + "' '>") |
108 | this.socket.write('Content-type: ' + type + '\r\n\r\n') |
109 | } |
110 | |
111 | DpiReq.prototype.serveError = function (err) { |
112 | this.writeHeader('text/plain') |
113 | this.socket.end(err && err.stack || err) |
114 | } |
115 | |
116 | DpiReq.prototype.getAddress = function () { |
117 | return this.socket.remoteAddress + ':' + this.socket.remotePort |
118 | } |
119 | |
120 | DpiReq.prototype.serveStat = function (st) { |
121 | this.writeHeader('text/plain') |
122 | this.socket.end(JSON.stringify(st, 0, 2)) |
123 | } |
124 | |
125 | function escapeHTML(str) { |
126 | return str.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') |
127 | } |
128 | |
129 | var typeIcons = { |
130 | updir: h('img', {alt: 'up', src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAKxJREFUeNpi/P//PwMlgImBQjAMDGBBF2BkZISz09LSwCE8a9YsuCBGoIMEkDEMJCUl/b90+QoYg9i41LNgc1ZycvL/hMQkhgcPH4H5iUnJIJf9nzt3LiNBL2RkZPwPj4hk4BMUYuDh44MEFDMLQ0xsHAMrKyvIJYyEwuDLiuXLeP7+/Qv3EihcmJmZGZiYmL5gqEcPFKBiAyDFjCPQ/wLVX8BrwGhSJh0ABBgAsetR5KBfw9EAAAAASUVORK5CYII='}), |
131 | // folder: h('img', {alt: 'folder', href: 'data:image/gif;base64,R0lGODdhFAAXAPAAMf///wAAACwAAAAAFAAXAAACOYSPqcvtD1OYNAZ1XcZtL1oZHpSNJGBqoYVO0QG+aJt263mTuUbL9AWcBYctoZEIdP1AzBisCc0UAAA7'}), |
132 | directory: h('img', {alt: 'directory', src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAAXdEVYdEF1dGhvcgBMYXBvIENhbGFtYW5kcmVp35EaKgAAACl0RVh0RGVzY3JpcHRpb24AQmFzZWQgb2YgSmFrdWIgU3RlaW5lciBkZXNpZ26ghAVzAAABbElEQVQ4jaWQO0tDQRCFz2x2A8YHQoogaKFW2qSysbATsdAIWgrWlhIFBRvLoFhZW/gb0vgPRBAStEgExZA2VR7X3Nw7MxY3BhUjCU6zMOz5zrcL/HPo/HDzREFnZMj1tgoI1FPm/ePL/M2fgNxRxltaXh8xxkCEoSIQYQQdH6XHO6/T8ZePL/PFfgBLCifCqJQfesswDNBoNhAEnQQRFXLZjV+qAefiRQsAba/e27MIWl4Ta1t7SE3N9lVXEVxfnaYtyJjS0z04DCMlF8fK6jaSyRQatUpfwFhypvsEUrOze4CxiUmoAlBF4LfwXq/1DUcG3UJhRmJ0HI1a9c/AzxGOAAYApEsbCiBfAMrDA5T5nwb8zYCHN/j8RABQFYAINGgYgEhUamPGKLOQiyciCFH3NABRdFsFqhoVqUJV4bebiBmjNmZd8eW5kJ6bXxhUAADw9lpWY12BLrKZRWNjt0EYTA8DsM5Vw7a/9gEhN65EVGzVRQAAAABJRU5ErkJggg='}), |
133 | file: h('img', {src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAASdEVYdFRpdGxlAFBhcGVyIFNoZWV0c7mvkfkAAAAXdEVYdEF1dGhvcgBMYXBvIENhbGFtYW5kcmVp35EaKgAAACd0RVh0RGVzY3JpcHRpb24Ad2l0aCBhIEhVR0UgaGVscCBmcm9tIEpha3VihlQHswAAAhNJREFUOI11kstqU1EUhr91ctI2A2uTNsRaOxDEkeILiIgTL6CCAx+iUnTSgQPBRxAFSxWhA8XiBQst7aQjUV+kMWlzOaeJVZvsy3JwctK0wQWLvQabb/3/v7eoKuubqzdFZMk5PwuKqqIKoAB/Qba8d8/v3b2/xfFSVVbXPpWbUUO990Pd7Xa0Uv2paxurf1Y+vnucwA87AOh0OjP5iQL7v/dptWOacZ1ao0plZ5vdepV2q8Wt67dzxanik7fvlxcGBQQAxlgAqpUK5e0KO5Ua9d2IuNlmL/pFuVwhCAKuXrmWGx0Ze/pm+dXlFBAmAANAYSqPcy5p73DO4pwjE8OHzyuMZXNcvHAp9/3H1wXgWx9gjQGURi3CWjuU01S+xMkTBbxYgiCQg4ODGy9ePsvMzz1yfQUKTBTGcc7iVVHv8T5V4hhhFJExzp09z8bmesarzwIpINkaN1s454YUpCWBkC706gcysEkG+clxnPNo7y/0PsMhQHoAa1CvwyFCQBAoipBcFY4eyWCtxTt/FCBAHO3h7P8tZMIMpeI0xlh8z+pABkLpVBG0J1UGVKQKVBARrDH9rAaeERq1iG63298YhiFnZmf63rWXiTEGd9wCwOmZaUTkaA8ooJfpEEBEqnEcTRcKk//1n1a73QIkMtZ0EluqzD98cCfMhoum2y2pgpI84fEZlGx2pG6MmVtafP0F4B+wR1eZMTEGTgAAAABJRU5ErkJggg=='}), |
134 | } |
135 | |
136 | function compareFileStats(a, b) { |
137 | return ((b.type === 'directory') - (a.type === 'directory')) |
138 | || (b.name.localeCompare(a.name)) |
139 | } |
140 | |
141 | DpiReq.prototype.serveDirectory = function () { |
142 | if (this.closed) return |
143 | var files = [] |
144 | var self = this |
145 | var pathname = this.pathname |
146 | if (!/\/$/.test(pathname)) pathname += '/' |
147 | this.archive.readdir(pathname, {cached: true}, function (err, names) { |
148 | if (err) return self.serveError(err) |
149 | self.writeHeader('text/html') |
150 | ;(function next() { |
151 | var name = names.pop() |
152 | if (name == null) return gotStats() |
153 | self.archive.stat(pathname + name, function (err, st) { |
154 | if (err) { |
155 | self.socket.write(h('pre', err.stack || err).outerHTML) |
156 | st = {} |
157 | } else { |
158 | st.type = st.isDirectory() ? 'directory' : 'file' |
159 | } |
160 | if (st.isDirectory()) st.size = null |
161 | st.name = name |
162 | if (st.mtime.getTime() === 0) st.mtime = null |
163 | files.push(st) |
164 | next() |
165 | }) |
166 | }()) |
167 | }) |
168 | function gotStats() { |
169 | files.sort(compareFileStats) |
170 | if (pathname !== '/') { |
171 | files.unshift({ |
172 | name: '..', |
173 | type: 'updir' |
174 | }) |
175 | } |
176 | self.socket.end(h('html', [ |
177 | h('head', [ |
178 | h('title', pathname) |
179 | ]), |
180 | h('body', [ |
181 | h('h4', pathname), |
182 | h('table', files.map(function (file) { |
183 | var suffix = file.type === 'directory' ? '/' : '' |
184 | return h('tr', [ |
185 | h('td', (typeIcons[file.type] || '')), |
186 | h('td', h('a', {href: pathname + file.name + suffix}, file.name)), |
187 | h('td', file.mtime == null ? '' : file.mtime), |
188 | h('td', {align: 'right'}, file.size == null ? '' : file.size) |
189 | ]) |
190 | })) |
191 | ]) |
192 | ]).outerHTML) |
193 | } |
194 | } |
195 | |
196 | function detectContentType(pathname) { |
197 | var m = /[^.]*$/.exec(pathname) |
198 | switch (m && m[0]) { |
199 | case 'html': return 'text/html' |
200 | case 'png': return 'image/png' |
201 | case 'gif': return 'image/gif' |
202 | case 'jpg': return 'image/jpeg' |
203 | default: return 'text/plain' |
204 | } |
205 | } |
206 | |
207 | function withTimeout(fn, ms) { |
208 | return function (arg, cb) { |
209 | var timeout = setTimeout(function () { |
210 | if (!cb) return |
211 | var _cb = cb |
212 | cb = null |
213 | _cb() |
214 | }, ms) |
215 | fn(arg, function (err, res) { |
216 | if (!cb) return |
217 | clearTimeout(timeout) |
218 | var _cb = cb |
219 | cb = null |
220 | _cb(err, res) |
221 | }) |
222 | } |
223 | } |
224 | |
225 | DpiReq.prototype.serveFile = function () { |
226 | if (this.closed) return |
227 | this.writeHeader(detectContentType(this.pathname)) |
228 | this.archive.createReadStream(this.pathname).pipe(this.socket) |
229 | } |
230 | |
231 | DpiReq.prototype.statFile = function (pathname, cb) { |
232 | var self = this |
233 | this.archive.stat(pathname, function (err, st) { |
234 | if (err) return cb(err) |
235 | st.pathname = pathname |
236 | cb(null, st) |
237 | }) |
238 | } |
239 | |
240 | DpiReq.prototype.statDirectory = function (pathname, cb) { |
241 | var self = this |
242 | self.statFile(pathname + '/index.html', function (err, st) { |
243 | if (!err) return cb(null, st) |
244 | self.statFile(pathname, cb) |
245 | }) |
246 | } |
247 | |
248 | DpiReq.prototype.stat = function (pathname, cb) { |
249 | var self = this |
250 | if (/\/\/$/.test(pathname)) return self.statFile(pathname.replace(/\/$/, ''), cb) |
251 | if (/\/$/.test(pathname)) return self.statDirectory(pathname, cb) |
252 | self.statFile(pathname, function (err, st) { |
253 | if (err) return self.statFile(pathname + '.html', cb) |
254 | if (st.isDirectory()) return self.statDirectory(pathname, cb) |
255 | cb(null, st) |
256 | }) |
257 | } |
258 | |
259 | DpiReq.prototype.serveTimedout = function (urlp) { |
260 | this.writeHeader('text/html') |
261 | this.socket.end(h('h3', 'Timed out').outerHTML) |
262 | } |
263 | |
264 | DpiReq.prototype.serveNotFound = function (urlp) { |
265 | this.writeHeader('text/html') |
266 | this.socket.end(h('h3', 'Not found').outerHTML) |
267 | } |
268 | |
269 | function unswarm(key, cb) { |
270 | var archive = archives[key] |
271 | if (!archive) return cb(new Error('missing archive')) |
272 | delete archives[key] |
273 | archive.swarm.close() |
274 | archive.close(cb) |
275 | } |
276 | |
277 | DpiReq.prototype.serveDashboard = function () { |
278 | var self = this |
279 | var q = self.urlp.query |
280 | if (q.unswarm) { |
281 | self.sendStatus('unswarming ' + q.key) |
282 | return unswarm(q.key, function (err) { |
283 | if (err) return self.serveError(err) |
284 | return self.reload(q.reload) |
285 | }) |
286 | } |
287 | if (q.restart) { |
288 | self.sendStatus('restarting') |
289 | self.reload(q.reload) |
290 | return process.exit(0) |
291 | } |
292 | |
293 | this.writeHeader('text/html') |
294 | self.socket.end(h('html', [ |
295 | h('head', [ |
296 | h('title', 'Dat') |
297 | ]), |
298 | h('body', [ |
299 | h('form', [ |
300 | h('input', {type: 'submit', name: 'restart', value: 'Restart'}), |
301 | h('input', {type: 'hidden', name: 'reload', value: self.url}) |
302 | ]), |
303 | h('h3', 'Archives'), |
304 | Object.keys(archives).length === 0 ? [ |
305 | h('p', 'Not swarming any archives currently') |
306 | ] : Object.keys(archives).map(function (key) { |
307 | var archive = archives[key] |
308 | var description = archive.manifest && archive.manifest.description |
309 | var title = archive.manifest && archive.manifest.title |
310 | var href = 'dat://' + key + '+' + archive.version |
311 | return [ |
312 | h('p', [ |
313 | h('a', {href: href}, title || h('code', key)), |
314 | description ? [' - ', String(description)] : '', |
315 | ]), |
316 | h('blockquote', [ |
317 | h('h3', 'Peers'), |
318 | archive.swarm.connections.length === 0 ? [ |
319 | 'None' |
320 | ] : archive.swarm.connections.map(function (stream) { |
321 | return h('div', h('code', stream.address)) |
322 | }), |
323 | h('form', [ |
324 | h('input', {type: 'hidden', name: 'key', value: key}), |
325 | h('input', {type: 'hidden', name: 'reload', value: self.url}), |
326 | h('input', {type: 'submit', name: 'unswarm', value: 'unswarm'}), |
327 | ]) |
328 | ]) |
329 | ] |
330 | }) |
331 | ]) |
332 | ]).outerHTML) |
333 | } |
334 | |
335 | DpiReq.prototype.serveDat = function () { |
336 | var self = this |
337 | self.urlp = parseDatUrl(self.url, true) |
338 | if (!self.urlp.host) return self.serveError('Archive not found') |
339 | self.sendStatus('resolving name') |
340 | datDns.resolveName(self.urlp.host, {ignoreCachedMiss: true}, function (err, key) { |
341 | if (err) return self.serveError(err) |
342 | if (!key) return cb(new TypeError('resolve failed')) |
343 | self.sendStatus('getting archive') |
344 | withTimeout(getArchive, 10000)(key, function (err, archive) { |
345 | if (err) return self.serveError(err) |
346 | if (!archive) return self.serveTimedout(self.urlp) |
347 | try { if (self.urlp.version) archive = archive.checkout(+self.urlp.version) } |
348 | catch(e) { return self.serveError(err) } |
349 | self.archive = archive |
350 | self.sendStatus('serving file') |
351 | self.serve() |
352 | }) |
353 | }) |
354 | } |
355 | |
356 | DpiReq.prototype.serveInternal = function () { |
357 | this.urlp = parseUrl(this.url, true) |
358 | if (this.urlp.pathname === '/dat/') return this.serveDashboard() |
359 | this.serveNotFound() |
360 | } |
361 | |
362 | DpiReq.prototype.serve = function () { |
363 | var self = this |
364 | if (self.closed) return |
365 | self.stat(self.urlp.pathname || '/', function (err, st) { |
366 | if (err) return self.serveNotFound(err) |
367 | self.pathname = st.pathname |
368 | if (self.urlp.query.stat) return self.serveStat(st) |
369 | if (st.isDirectory()) return self.serveDirectory() |
370 | self.serveFile() |
371 | }) |
372 | } |
373 | |
374 | function DpiReq_onAuth(m) { |
375 | this.authed = (m[1] == dpidKeys[1]) |
376 | if (!this.authed) { |
377 | console.error('[dat dpi] bad auth from', this.getAddress()) |
378 | return this.socket.end() |
379 | } |
380 | this.authed = true |
381 | } |
382 | |
383 | function DpiReq_onOpenUrl(m) { |
384 | if (!this.authed) { |
385 | var addr = this.socket.remoteAddress + ':' + this.socket.remotePort |
386 | console.error('[dat dpi] un-authed request from', this.getAddress()) |
387 | return this.socket.end() |
388 | } |
389 | this.url = m[1] |
390 | if (this.url.startsWith('dat:')) return this.serveDat() |
391 | if (this.url.startsWith('dpi:/dat/')) return this.serveInternal() |
392 | this.serveError('Not found') |
393 | } |
394 | |
395 | function DpiReq_onBye(m) { |
396 | if (!this.authed) { |
397 | var addr = this.socket.remoteAddress + ':' + this.socket.remotePort |
398 | console.error('[dat dpi] un-authed bye from', this.getAddress()) |
399 | return this.socket.end() |
400 | } |
401 | console.log('[dat dpi] stopping') |
402 | process.exit(0) |
403 | } |
404 | |
405 | DpiReq.prototype.commands = [ |
406 | [/^<cmd='auth' msg='([^']*)' '>/, DpiReq_onAuth], |
407 | [/^<cmd='open_url' url='(.*?)' '>/, DpiReq_onOpenUrl], |
408 | [/^<cmd='DpiBye' '>/, DpiReq_onBye], |
409 | ] |
410 | |
411 | net.createServer({allowHalfOpen: true}, function (c) { |
412 | new DpiReq(c) |
413 | }).listen(process.stdin, function () { |
414 | console.log('[dat dpi] started') |
415 | }) |
416 |
Built with git-ssb-web