git ssb

0+

cel / dillo-dat



Tree: 14c89af8a6ac540abbb7936e49a5e70e4c474a2b

Files: 14c89af8a6ac540abbb7936e49a5e70e4c474a2b / dat.dpi

13841 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.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.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
107DpiReq.prototype.serveError = function (err) {
108 this.writeHeader('text/plain')
109 this.socket.end(err && err.stack || err)
110}
111
112DpiReq.prototype.getAddress = function () {
113 return this.socket.remoteAddress + ':' + this.socket.remotePort
114}
115
116DpiReq.prototype.serveStat = function (st) {
117 this.writeHeader('text/plain')
118 this.socket.end(JSON.stringify(st, 0, 2))
119}
120
121function escapeHTML(str) {
122 return str.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
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 // 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
132function compareFileStats(a, b) {
133 return ((b.type === 'directory') - (a.type === 'directory'))
134 || (b.name.localeCompare(a.name))
135}
136
137DpiReq.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
192function 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
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 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
328DpiReq.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
346DpiReq.prototype.serveInternal = function () {
347 this.urlp = parseUrl(this.url, true)
348 if (this.urlp.pathname === '/dat/') return this.serveDashboard()
349 this.serveNotFound()
350}
351
352DpiReq.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
364function 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
373function 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
385DpiReq.prototype.commands = [
386 [/^<cmd='auth' msg='([^']*)' '>/, DpiReq_onAuth],
387 [/^<cmd='open_url' url='(.*?)' '>/, DpiReq_onOpenUrl]
388]
389
390net.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