git ssb

0+

cel / dillo-dat



Tree: 6b03ce0114b8fe5962fd9fc588cf8e0969892366

Files: 6b03ce0114b8fe5962fd9fc588cf8e0969892366 / dat.dpi

14538 bytesRaw
1#!/usr/bin/env node
2// vi: ft=javascript
3
4var net = require('net')
5var path = require('path')
6var fs = require('fs')
7var parseDatUrl = require('parse-dat-url')
8var parseUrl = require('url').parse
9var datDns = require('dat-dns')()
10var memo = require('asyncmemo')
11var hyperdrive = require('hyperdrive')
12var crypto = require('crypto')
13var h = require('hyperscript')
14var hyperdiscovery = require('hyperdiscovery')
15
16var dilloDir = path.join(process.env.HOME, '.dillo')
17var dpidKeysPath = path.join(dilloDir, 'dpid_comm_keys')
18var archivesPath = path.join(dilloDir, 'dat', 'archives')
19
20var dpidKeys = fs.readFileSync(dpidKeysPath, {encoding: 'ascii'}).split(/\s+/)
21
22function 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
34var archives = {}
35var 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
70function 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
78DpiReq.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
89DpiReq.prototype.onClose = function () {
90 this.closed = true
91}
92
93DpiReq.prototype.onError = function (err) {
94 // console.trace('[dat dpi]', err)
95 this.closed = true
96}
97
98DpiReq.prototype.reload = function (url) {
99 this.socket.end("<cmd='reload_request' url='" + url + "' '>")
100}
101
102DpiReq.prototype.sendStatus = function (msg) {
103 this.socket.write("<cmd='send_status_message' msg='" + msg + "' '>")
104}
105
106DpiReq.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
111DpiReq.prototype.serveError = function (err) {
112 this.writeHeader('text/plain')
113 this.socket.end(err && err.stack || err)
114}
115
116DpiReq.prototype.getAddress = function () {
117 return this.socket.remoteAddress + ':' + this.socket.remotePort
118}
119
120DpiReq.prototype.serveStat = function (st) {
121 this.writeHeader('text/plain')
122 this.socket.end(JSON.stringify(st, 0, 2))
123}
124
125function escapeHTML(str) {
126 return str.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
127}
128
129var 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
136function compareFileStats(a, b) {
137 return ((b.type === 'directory') - (a.type === 'directory'))
138 || (b.name.localeCompare(a.name))
139}
140
141DpiReq.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
196function 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
207function 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
225DpiReq.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
231DpiReq.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
240DpiReq.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
248DpiReq.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
259DpiReq.prototype.serveTimedout = function (urlp) {
260 this.writeHeader('text/html')
261 this.socket.end(h('h3', 'Timed out').outerHTML)
262}
263
264DpiReq.prototype.serveNotFound = function (urlp) {
265 this.writeHeader('text/html')
266 this.socket.end(h('h3', 'Not found').outerHTML)
267}
268
269function 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
277DpiReq.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
335DpiReq.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
356DpiReq.prototype.serveInternal = function () {
357 this.urlp = parseUrl(this.url, true)
358 if (this.urlp.pathname === '/dat/') return this.serveDashboard()
359 this.serveNotFound()
360}
361
362DpiReq.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
374function 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
383function 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
395function 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
405DpiReq.prototype.commands = [
406 [/^<cmd='auth' msg='([^']*)' '>/, DpiReq_onAuth],
407 [/^<cmd='open_url' url='(.*?)' '>/, DpiReq_onOpenUrl],
408 [/^<cmd='DpiBye' '>/, DpiReq_onBye],
409]
410
411net.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