Files: 803a5f17c3e8e82e1c1a7d22d26205a2a4239ad9 / dat.dpi
14330 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 | var typeIcons = { |
126 | updir: h('img', {alt: 'up', src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAKxJREFUeNpi/P//PwMlgImBQjAMDGBBF2BkZISz09LSwCE8a9YsuCBGoIMEkDEMJCUl/b90+QoYg9i41LNgc1ZycvL/hMQkhgcPH4H5iUnJIJf9nzt3LiNBL2RkZPwPj4hk4BMUYuDh44MEFDMLQ0xsHAMrKyvIJYyEwuDLiuXLeP7+/Qv3EihcmJmZGZiYmL5gqEcPFKBiAyDFjCPQ/wLVX8BrwGhSJh0ABBgAsetR5KBfw9EAAAAASUVORK5CYII='}), |
127 | 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='}), |
128 | 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=='}), |
129 | } |
130 | |
131 | function compareFileStats(a, b) { |
132 | return ((b.type === 'directory') - (a.type === 'directory')) |
133 | || (b.name.localeCompare(a.name)) |
134 | } |
135 | |
136 | DpiReq.prototype.serveDirectory = function () { |
137 | if (this.closed) return |
138 | var files = [] |
139 | var self = this |
140 | var pathname = this.pathname |
141 | if (!/\/$/.test(pathname)) pathname += '/' |
142 | this.archive.readdir(pathname, {cached: true}, function (err, names) { |
143 | if (err) return self.serveError(err) |
144 | self.writeHeader('text/html') |
145 | ;(function next() { |
146 | var name = names.pop() |
147 | if (name == null) return gotStats() |
148 | self.archive.stat(pathname + name, function (err, st) { |
149 | if (err) { |
150 | self.socket.write(h('pre', err.stack || err).outerHTML) |
151 | st = {} |
152 | } else { |
153 | st.type = st.isDirectory() ? 'directory' : 'file' |
154 | } |
155 | if (st.isDirectory()) st.size = null |
156 | st.name = name |
157 | if (st.mtime.getTime() === 0) st.mtime = null |
158 | files.push(st) |
159 | next() |
160 | }) |
161 | }()) |
162 | }) |
163 | function gotStats() { |
164 | files.sort(compareFileStats) |
165 | if (pathname !== '/') { |
166 | files.unshift({ |
167 | name: '..', |
168 | type: 'updir' |
169 | }) |
170 | } |
171 | self.socket.end('<!doctype html>' + h('html', [ |
172 | h('head', [ |
173 | h('title', pathname) |
174 | ]), |
175 | h('body', [ |
176 | h('h4', pathname), |
177 | h('table', files.map(function (file) { |
178 | var suffix = file.type === 'directory' ? '/' : '' |
179 | return h('tr', [ |
180 | h('td', (typeIcons[file.type] || '')), |
181 | h('td', h('a', {href: pathname + file.name + suffix}, file.name)), |
182 | h('td', file.mtime == null ? '' : file.mtime), |
183 | h('td', {align: 'right'}, file.size == null ? '' : file.size) |
184 | ]) |
185 | })) |
186 | ]) |
187 | ]).outerHTML) |
188 | } |
189 | } |
190 | |
191 | function detectContentType(pathname) { |
192 | var m = /[^.]*$/.exec(pathname) |
193 | switch (m && m[0]) { |
194 | case 'htm': |
195 | case 'html': return 'text/html' |
196 | case 'png': return 'image/png' |
197 | case 'gif': return 'image/gif' |
198 | case 'jpg': return 'image/jpeg' |
199 | default: return 'text/plain' |
200 | } |
201 | } |
202 | |
203 | function withTimeout(fn, ms) { |
204 | return function (arg, cb) { |
205 | var timeout = setTimeout(function () { |
206 | if (!cb) return |
207 | var _cb = cb |
208 | cb = null |
209 | _cb() |
210 | }, ms) |
211 | fn(arg, function (err, res) { |
212 | if (!cb) return |
213 | clearTimeout(timeout) |
214 | var _cb = cb |
215 | cb = null |
216 | _cb(err, res) |
217 | }) |
218 | } |
219 | } |
220 | |
221 | DpiReq.prototype.serveFile = function () { |
222 | if (this.closed) return |
223 | this.writeHeader(detectContentType(this.pathname)) |
224 | this.archive.createReadStream(this.pathname).pipe(this.socket) |
225 | } |
226 | |
227 | DpiReq.prototype.statFile = function (pathname, cb) { |
228 | var self = this |
229 | this.archive.stat(pathname, function (err, st) { |
230 | if (err) return cb(err) |
231 | st.pathname = pathname |
232 | cb(null, st) |
233 | }) |
234 | } |
235 | |
236 | DpiReq.prototype.statDirectory = function (pathname, cb) { |
237 | var self = this |
238 | self.statFile(pathname + '/index.html', function (err, st) { |
239 | if (!err) return cb(null, st) |
240 | self.statFile(pathname, cb) |
241 | }) |
242 | } |
243 | |
244 | DpiReq.prototype.stat = function (pathname, cb) { |
245 | var self = this |
246 | if (/\/\/$/.test(pathname)) return self.statFile(pathname.replace(/\/$/, ''), cb) |
247 | if (/\/$/.test(pathname)) return self.statDirectory(pathname, cb) |
248 | self.statFile(pathname, function (err, st) { |
249 | if (err) return self.statFile(pathname + '.html', cb) |
250 | if (st.isDirectory()) return self.statDirectory(pathname, cb) |
251 | cb(null, st) |
252 | }) |
253 | } |
254 | |
255 | DpiReq.prototype.serveTimedout = function (urlp) { |
256 | this.writeHeader('text/html') |
257 | this.socket.end(h('h3', 'Timed out').outerHTML) |
258 | } |
259 | |
260 | DpiReq.prototype.serveNotFound = function (urlp) { |
261 | this.writeHeader('text/html') |
262 | this.socket.end(h('h3', 'Not found').outerHTML) |
263 | } |
264 | |
265 | function unswarm(key, cb) { |
266 | var archive = archives[key] |
267 | if (!archive) return cb(new Error('missing archive')) |
268 | delete archives[key] |
269 | archive.swarm.close() |
270 | archive.close(cb) |
271 | } |
272 | |
273 | DpiReq.prototype.serveDashboard = function () { |
274 | var self = this |
275 | var q = self.urlp.query |
276 | if (q.unswarm) { |
277 | self.sendStatus('unswarming ' + q.key) |
278 | return unswarm(q.key, function (err) { |
279 | if (err) return self.serveError(err) |
280 | return self.reload(q.reload) |
281 | }) |
282 | } |
283 | if (q.quit) { |
284 | this.writeHeader('text/plain') |
285 | self.socket.end('Quitting') |
286 | return process.exit(0) |
287 | } |
288 | |
289 | this.writeHeader('text/html') |
290 | self.socket.end('<!doctype html>' + h('html', [ |
291 | h('head', [ |
292 | h('title', 'Dat') |
293 | ]), |
294 | h('body', [ |
295 | h('form', {action: ''}, [ |
296 | h('input', {type: 'submit', name: 'quit', value: 'Quit'}) |
297 | ]), |
298 | h('h3', 'Archives'), |
299 | Object.keys(archives).length === 0 ? [ |
300 | h('p', 'None') |
301 | ] : Object.keys(archives).map(function (key) { |
302 | var archive = archives[key] |
303 | var description = archive.manifest && archive.manifest.description |
304 | var title = archive.manifest && archive.manifest.title |
305 | var href = 'dat://' + key |
306 | var versionedHref = href + '+' + archive.version |
307 | return [ |
308 | h('p', [ |
309 | h('a', {href: href}, title || h('code', key)), |
310 | description ? [' - ', String(description)] : '', h('br'), |
311 | 'version ', h('a', {href: versionedHref}, archive.version) |
312 | ]), |
313 | h('blockquote', [ |
314 | h('h3', 'Peers'), |
315 | archive.swarm.connections.length === 0 ? [ |
316 | 'None' |
317 | ] : archive.swarm.connections.map(function (stream) { |
318 | return h('div', h('code', stream.address)) |
319 | }), |
320 | h('form', {action: ''}, [ |
321 | h('input', {type: 'hidden', name: 'key', value: key}), |
322 | h('input', {type: 'hidden', name: 'reload', value: self.url}), |
323 | h('input', {type: 'submit', name: 'unswarm', value: 'unswarm'}), |
324 | ]) |
325 | ]) |
326 | ] |
327 | }) |
328 | ]) |
329 | ]).outerHTML) |
330 | } |
331 | |
332 | DpiReq.prototype.serveDat = function () { |
333 | var self = this |
334 | self.urlp = parseDatUrl(self.url, true) |
335 | if (!self.urlp.host) return self.serveError('Archive not found') |
336 | self.sendStatus('resolving name') |
337 | datDns.resolveName(self.urlp.host, { |
338 | ignoreCachedMiss: true, |
339 | noDnsOverHttps: true |
340 | }, 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 | var pathname = self.urlp.pathname || '/' |
366 | try { pathname = decodeURIComponent(pathname) } |
367 | catch(e) {} |
368 | self.stat(pathname, function (err, st) { |
369 | if (err) return self.serveNotFound(err) |
370 | self.pathname = st.pathname |
371 | if (self.urlp.query.stat) return self.serveStat(st) |
372 | if (st.isDirectory()) return self.serveDirectory() |
373 | self.serveFile() |
374 | }) |
375 | } |
376 | |
377 | function DpiReq_onAuth(m) { |
378 | this.authed = (m[1] == dpidKeys[1]) |
379 | if (!this.authed) { |
380 | console.error('[dat dpi] bad auth from', this.getAddress()) |
381 | return this.socket.end() |
382 | } |
383 | this.authed = true |
384 | } |
385 | |
386 | function DpiReq_onOpenUrl(m) { |
387 | if (!this.authed) { |
388 | console.error('[dat dpi] un-authed request from', this.getAddress()) |
389 | return this.socket.end() |
390 | } |
391 | this.url = m[1].replace(/''/g, '\'') |
392 | if (this.url.startsWith('dat:')) return this.serveDat() |
393 | if (this.url.startsWith('dpi:/dat/')) return this.serveInternal() |
394 | this.serveError('Not found') |
395 | } |
396 | |
397 | function DpiReq_onBye(m) { |
398 | if (!this.authed) { |
399 | console.error('[dat dpi] un-authed bye from', this.getAddress()) |
400 | return this.socket.end() |
401 | } |
402 | console.log('[dat dpi] stopping') |
403 | process.exit(0) |
404 | } |
405 | |
406 | DpiReq.prototype.commands = [ |
407 | [/^<cmd='auth' msg='([^']*)' '>/, DpiReq_onAuth], |
408 | [/^<cmd='open_url' url='(.*?)' '>/, DpiReq_onOpenUrl], |
409 | [/^<cmd='DpiBye' '>/, DpiReq_onBye], |
410 | ] |
411 | |
412 | net.createServer({allowHalfOpen: true}, function (c) { |
413 | new DpiReq(c) |
414 | }).listen(process.stdin, function () { |
415 | console.log('[dat dpi] started') |
416 | }) |
417 |
Built with git-ssb-web