git ssb

0+

cel / dillo-dat



Tree: 8ae5cfd3842505847e8c879d7fba2bef2f655068

Files: 8ae5cfd3842505847e8c879d7fba2bef2f655068 / dat.dpi

14257 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
125var 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
131function compareFileStats(a, b) {
132 return ((b.type === 'directory') - (a.type === 'directory'))
133 || (b.name.localeCompare(a.name))
134}
135
136DpiReq.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
191function 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
203function 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
221DpiReq.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
227DpiReq.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
236DpiReq.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
244DpiReq.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
255DpiReq.prototype.serveTimedout = function (urlp) {
256 this.writeHeader('text/html')
257 this.socket.end(h('h3', 'Timed out').outerHTML)
258}
259
260DpiReq.prototype.serveNotFound = function (urlp) {
261 this.writeHeader('text/html')
262 this.socket.end(h('h3', 'Not found').outerHTML)
263}
264
265function 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
273DpiReq.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.restart) {
284 self.sendStatus('restarting')
285 self.reload(q.reload)
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: 'restart', value: 'Restart'}),
297 h('input', {type: 'hidden', name: 'reload', value: self.url})
298 ]),
299 h('h3', 'Archives'),
300 Object.keys(archives).length === 0 ? [
301 h('p', 'None')
302 ] : Object.keys(archives).map(function (key) {
303 var archive = archives[key]
304 var description = archive.manifest && archive.manifest.description
305 var title = archive.manifest && archive.manifest.title
306 var href = 'dat://' + key
307 var versionedHref = href + '+' + archive.version
308 return [
309 h('p', [
310 h('a', {href: href}, title || h('code', key)),
311 description ? [' - ', String(description)] : '', h('br'),
312 'version ', h('a', {href: versionedHref}, archive.version)
313 ]),
314 h('blockquote', [
315 h('h3', 'Peers'),
316 archive.swarm.connections.length === 0 ? [
317 'None'
318 ] : archive.swarm.connections.map(function (stream) {
319 return h('div', h('code', stream.address))
320 }),
321 h('form', {action: ''}, [
322 h('input', {type: 'hidden', name: 'key', value: key}),
323 h('input', {type: 'hidden', name: 'reload', value: self.url}),
324 h('input', {type: 'submit', name: 'unswarm', value: 'unswarm'}),
325 ])
326 ])
327 ]
328 })
329 ])
330 ]).outerHTML)
331}
332
333DpiReq.prototype.serveDat = function () {
334 var self = this
335 self.urlp = parseDatUrl(self.url, true)
336 if (!self.urlp.host) return self.serveError('Archive not found')
337 self.sendStatus('resolving name')
338 datDns.resolveName(self.urlp.host, {ignoreCachedMiss: true}, function (err, key) {
339 if (err) return self.serveError(err)
340 if (!key) return cb(new TypeError('resolve failed'))
341 self.sendStatus('getting archive')
342 withTimeout(getArchive, 10000)(key, function (err, archive) {
343 if (err) return self.serveError(err)
344 if (!archive) return self.serveTimedout(self.urlp)
345 try { if (self.urlp.version) archive = archive.checkout(+self.urlp.version) }
346 catch(e) { return self.serveError(err) }
347 self.archive = archive
348 self.sendStatus('serving file')
349 self.serve()
350 })
351 })
352}
353
354DpiReq.prototype.serveInternal = function () {
355 this.urlp = parseUrl(this.url, true)
356 if (this.urlp.pathname === '/dat/') return this.serveDashboard()
357 this.serveNotFound()
358}
359
360DpiReq.prototype.serve = function () {
361 var self = this
362 if (self.closed) return
363 self.stat(self.urlp.pathname || '/', function (err, st) {
364 if (err) return self.serveNotFound(err)
365 self.pathname = st.pathname
366 if (self.urlp.query.stat) return self.serveStat(st)
367 if (st.isDirectory()) return self.serveDirectory()
368 self.serveFile()
369 })
370}
371
372function DpiReq_onAuth(m) {
373 this.authed = (m[1] == dpidKeys[1])
374 if (!this.authed) {
375 console.error('[dat dpi] bad auth from', this.getAddress())
376 return this.socket.end()
377 }
378 this.authed = true
379}
380
381function DpiReq_onOpenUrl(m) {
382 if (!this.authed) {
383 console.error('[dat dpi] un-authed request from', this.getAddress())
384 return this.socket.end()
385 }
386 this.url = m[1]
387 if (this.url.startsWith('dat:')) return this.serveDat()
388 if (this.url.startsWith('dpi:/dat/')) return this.serveInternal()
389 this.serveError('Not found')
390}
391
392function DpiReq_onBye(m) {
393 if (!this.authed) {
394 console.error('[dat dpi] un-authed bye from', this.getAddress())
395 return this.socket.end()
396 }
397 console.log('[dat dpi] stopping')
398 process.exit(0)
399}
400
401DpiReq.prototype.commands = [
402 [/^<cmd='auth' msg='([^']*)' '>/, DpiReq_onAuth],
403 [/^<cmd='open_url' url='(.*?)' '>/, DpiReq_onOpenUrl],
404 [/^<cmd='DpiBye' '>/, DpiReq_onBye],
405]
406
407net.createServer({allowHalfOpen: true}, function (c) {
408 new DpiReq(c)
409}).listen(process.stdin, function () {
410 console.log('[dat dpi] started')
411})
412

Built with git-ssb-web