Files: 14c89af8a6ac540abbb7936e49a5e70e4c474a2b / dat.dpi
13841 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.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.writeHeader = function (type) { |
103 | this.socket.write("<cmd='start_send_page' url='" + this.url + "' '>") |
104 | this.socket.write('Content-type: ' + type + '\r\n\r\n') |
105 | } |
106 | |
107 | DpiReq.prototype.serveError = function (err) { |
108 | this.writeHeader('text/plain') |
109 | this.socket.end(err && err.stack || err) |
110 | } |
111 | |
112 | DpiReq.prototype.getAddress = function () { |
113 | return this.socket.remoteAddress + ':' + this.socket.remotePort |
114 | } |
115 | |
116 | DpiReq.prototype.serveStat = function (st) { |
117 | this.writeHeader('text/plain') |
118 | this.socket.end(JSON.stringify(st, 0, 2)) |
119 | } |
120 | |
121 | function escapeHTML(str) { |
122 | return str.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') |
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 | // folder: h('img', {alt: 'folder', href: 'data:image/gif;base64,R0lGODdhFAAXAPAAMf///wAAACwAAAAAFAAXAAACOYSPqcvtD1OYNAZ1XcZtL1oZHpSNJGBqoYVO0QG+aJt263mTuUbL9AWcBYctoZEIdP1AzBisCc0UAAA7'}), |
128 | 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='}), |
129 | 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=='}), |
130 | } |
131 | |
132 | function compareFileStats(a, b) { |
133 | return ((b.type === 'directory') - (a.type === 'directory')) |
134 | || (b.name.localeCompare(a.name)) |
135 | } |
136 | |
137 | DpiReq.prototype.serveDirectory = function () { |
138 | if (this.closed) return |
139 | var files = [] |
140 | var self = this |
141 | var pathname = this.pathname |
142 | if (!/\/$/.test(pathname)) pathname += '/' |
143 | this.archive.readdir(pathname, {cached: true}, function (err, names) { |
144 | if (err) return self.serveError(err) |
145 | self.writeHeader('text/html') |
146 | ;(function next() { |
147 | var name = names.pop() |
148 | if (name == null) return gotStats() |
149 | self.archive.stat(pathname + name, function (err, st) { |
150 | if (err) { |
151 | self.socket.write(h('pre', err.stack || err).outerHTML) |
152 | st = {} |
153 | } else { |
154 | st.type = st.isDirectory() ? 'directory' : 'file' |
155 | } |
156 | if (st.isDirectory()) st.size = null |
157 | st.name = name |
158 | if (st.mtime.getTime() === 0) st.mtime = null |
159 | files.push(st) |
160 | next() |
161 | }) |
162 | }()) |
163 | }) |
164 | function gotStats() { |
165 | files.sort(compareFileStats) |
166 | if (pathname !== '/') { |
167 | files.unshift({ |
168 | name: '..', |
169 | type: 'updir' |
170 | }) |
171 | } |
172 | self.socket.end(h('html', [ |
173 | h('head', [ |
174 | h('title', pathname) |
175 | ]), |
176 | h('body', [ |
177 | h('h4', pathname), |
178 | h('table', files.map(function (file) { |
179 | var suffix = file.type === 'directory' ? '/' : '' |
180 | return h('tr', [ |
181 | h('td', (typeIcons[file.type] || '')), |
182 | h('td', h('a', {href: pathname + file.name + suffix}, file.name)), |
183 | h('td', file.mtime == null ? '' : file.mtime), |
184 | h('td', {align: 'right'}, file.size == null ? '' : file.size) |
185 | ]) |
186 | })) |
187 | ]) |
188 | ]).outerHTML) |
189 | } |
190 | } |
191 | |
192 | function detectContentType(pathname) { |
193 | var m = /[^.]*$/.exec(pathname) |
194 | switch (m && m[0]) { |
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 | return unswarm(q.key, function (err) { |
278 | if (err) return self.serveError(err) |
279 | return self.reload(q.reload) |
280 | }) |
281 | } |
282 | if (q.restart) { |
283 | self.reload(q.reload) |
284 | return process.exit(0) |
285 | } |
286 | |
287 | this.writeHeader('text/html') |
288 | self.socket.end(h('html', [ |
289 | h('head', [ |
290 | h('title', 'Dat') |
291 | ]), |
292 | h('body', [ |
293 | h('form', [ |
294 | h('input', {type: 'submit', name: 'restart', value: 'Restart'}), |
295 | h('input', {type: 'hidden', name: 'reload', value: self.url}) |
296 | ]), |
297 | h('h3', 'Archives'), |
298 | Object.keys(archives).length === 0 ? [ |
299 | h('p', 'Not swarming any archives currently') |
300 | ] : Object.keys(archives).map(function (key) { |
301 | var archive = archives[key] |
302 | var description = archive.manifest && archive.manifest.description |
303 | var title = archive.manifest && archive.manifest.title |
304 | return [ |
305 | h('p', [ |
306 | h('a', {href: 'dat://' + key}, title || h('code', key)), |
307 | description ? [' - ', String(description)] : '', |
308 | ]), |
309 | h('blockquote', [ |
310 | h('h3', 'Peers'), |
311 | archive.swarm.connections.length === 0 ? [ |
312 | 'None' |
313 | ] : archive.swarm.connections.map(function (stream) { |
314 | return h('div', h('code', stream.address)) |
315 | }), |
316 | h('form', [ |
317 | h('input', {type: 'hidden', name: 'key', value: key}), |
318 | h('input', {type: 'hidden', name: 'reload', value: self.url}), |
319 | h('input', {type: 'submit', name: 'unswarm', value: 'unswarm'}), |
320 | ]) |
321 | ]) |
322 | ] |
323 | }) |
324 | ]) |
325 | ]).outerHTML) |
326 | } |
327 | |
328 | DpiReq.prototype.serveDat = function () { |
329 | var self = this |
330 | self.urlp = parseDatUrl(self.url, true) |
331 | if (!self.urlp.host) return self.serveError('Archive not found') |
332 | datDns.resolveName(self.urlp.host, {ignoreCachedMiss: true}, function (err, key) { |
333 | if (err) return self.serveError(err) |
334 | if (!key) return cb(new TypeError('resolve failed')) |
335 | withTimeout(getArchive, 10000)(key, function (err, archive) { |
336 | if (err) return self.serveError(err) |
337 | if (!archive) return self.serveTimedout(self.urlp) |
338 | try { if (self.urlp.version) archive = archive.checkout(+self.urlp.version) } |
339 | catch(e) { return self.serveError(err) } |
340 | self.archive = archive |
341 | self.serve() |
342 | }) |
343 | }) |
344 | } |
345 | |
346 | DpiReq.prototype.serveInternal = function () { |
347 | this.urlp = parseUrl(this.url, true) |
348 | if (this.urlp.pathname === '/dat/') return this.serveDashboard() |
349 | this.serveNotFound() |
350 | } |
351 | |
352 | DpiReq.prototype.serve = function () { |
353 | var self = this |
354 | if (self.closed) return |
355 | self.stat(self.urlp.pathname || '/', function (err, st) { |
356 | if (err) return self.serveNotFound(err) |
357 | self.pathname = st.pathname |
358 | if (self.urlp.query.stat) return self.serveStat(st) |
359 | if (st.isDirectory()) return self.serveDirectory() |
360 | self.serveFile() |
361 | }) |
362 | } |
363 | |
364 | function DpiReq_onAuth(m) { |
365 | this.authed = (m[1] == dpidKeys[1]) |
366 | if (!this.authed) { |
367 | console.error('[dat dpi] bad auth from', this.getAddress()) |
368 | return this.socket.end() |
369 | } |
370 | this.authed = true |
371 | } |
372 | |
373 | function DpiReq_onOpenUrl(m) { |
374 | if (!this.authed) { |
375 | var addr = this.socket.remoteAddress + ':' + this.socket.remotePort |
376 | console.error('[dat dpi] un-authed request from', this.getAddress()) |
377 | return this.socket.end() |
378 | } |
379 | this.url = m[1] |
380 | if (this.url.startsWith('dat:')) return this.serveDat() |
381 | if (this.url.startsWith('dpi:/dat/')) return this.serveInternal() |
382 | this.serveError('Not found') |
383 | } |
384 | |
385 | DpiReq.prototype.commands = [ |
386 | [/^<cmd='auth' msg='([^']*)' '>/, DpiReq_onAuth], |
387 | [/^<cmd='open_url' url='(.*?)' '>/, DpiReq_onOpenUrl] |
388 | ] |
389 | |
390 | net.createServer({allowHalfOpen: true}, function (c) { |
391 | new DpiReq(c) |
392 | }).listen(process.stdin, function () { |
393 | console.log('[dat dpi] started') |
394 | }) |
395 |
Built with git-ssb-web