#!/usr/bin/env node // vi: ft=javascript var net = require('net') var path = require('path') var fs = require('fs') var parseDatUrl = require('parse-dat-url') var parseUrl = require('url').parse var datDns = require('dat-dns')() var memo = require('asyncmemo') var hyperdrive = require('hyperdrive') var crypto = require('crypto') var h = require('hyperscript') var hyperdiscovery = require('hyperdiscovery') var dilloDir = path.join(process.env.HOME, '.dillo') var dpidKeysPath = path.join(dilloDir, 'dpid_comm_keys') var archivesPath = path.join(dilloDir, 'dat', 'archives') var dpidKeys = fs.readFileSync(dpidKeysPath, {encoding: 'ascii'}).split(/\s+/) function readManifest(archive, cb) { archive.readFile('dat.json', function (err, data) { var manifest if (!err) try { manifest = JSON.parse(data) } catch(e) {} if (manifest) return cb(null, manifest) archive.readFile('CNAME', 'ascii', function (err, data) { if (err) return cb('Not found') cb(null, {title: data}) }) }) } var archives = {} var getArchive = memo({cache: { has: function (key) { return key in archives }, get: function (key) { return archives[key] }, set: function (key, value) { archives[key] = value}, }}, function (key, cb) { var archive try { var archivePath = path.join(archivesPath, key.substr(0, 2), key.substr(2)) archive = hyperdrive(archivePath, key, {sparse: true}) } catch(e) { return cb(e) } archive.ready(function (err) { if (err) return cb(err) console.log('[dat dpi] swarming', archive.key.toString('hex')) archive.swarm = hyperdiscovery(archive, { stream: function (info) { var stream = archive.replicate({ live: true }) stream.address = info.type + ':' + (info.host || '?') + ':' + (info.port || '?') return stream } }) }) archive.on('content', function () { cb(null, archive) readManifest(archive, function (err, manifest) { if (err) return archive.manifest = manifest }) }) archive.on('error', cb) }) function DpiReq(socket) { this.socket = socket this._inBuffer = '' socket.on('data', this.onData.bind(this)) socket.on('close', this.onClose.bind(this)) socket.on('error', this.onError.bind(this)) } DpiReq.prototype.onData = function (data) { this._inBuffer += data.toString('ascii') for (var i = 0; i < this.commands.length; i++) { var cmd = this.commands[i] var m = cmd[0].exec(this._inBuffer) if (!m) continue this._inBuffer = this._inBuffer.slice(m[0].length) cmd[1].call(this, m) } } DpiReq.prototype.onClose = function () { this.closed = true } DpiReq.prototype.onError = function (err) { // console.trace('[dat dpi]', err) this.closed = true } DpiReq.prototype.reload = function (url) { this.socket.end("") } DpiReq.prototype.sendStatus = function (msg) { this.socket.write("") } DpiReq.prototype.writeHeader = function (type) { this.socket.write("") this.socket.write('Content-type: ' + type + '\r\n\r\n') } DpiReq.prototype.serveError = function (err) { this.writeHeader('text/plain') this.socket.end(err && err.stack || err) } DpiReq.prototype.getAddress = function () { return this.socket.remoteAddress + ':' + this.socket.remotePort } DpiReq.prototype.serveStat = function (st) { this.writeHeader('text/plain') this.socket.end(JSON.stringify(st, 0, 2)) } var typeIcons = { updir: h('img', {alt: 'up', src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAKxJREFUeNpi/P//PwMlgImBQjAMDGBBF2BkZISz09LSwCE8a9YsuCBGoIMEkDEMJCUl/b90+QoYg9i41LNgc1ZycvL/hMQkhgcPH4H5iUnJIJf9nzt3LiNBL2RkZPwPj4hk4BMUYuDh44MEFDMLQ0xsHAMrKyvIJYyEwuDLiuXLeP7+/Qv3EihcmJmZGZiYmL5gqEcPFKBiAyDFjCPQ/wLVX8BrwGhSJh0ABBgAsetR5KBfw9EAAAAASUVORK5CYII='}), 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='}), 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=='}), } function compareFileStats(a, b) { return ((b.type === 'directory') - (a.type === 'directory')) || (b.name.localeCompare(a.name)) } DpiReq.prototype.serveDirectory = function () { if (this.closed) return var files = [] var self = this var pathname = this.pathname if (!/\/$/.test(pathname)) pathname += '/' this.archive.readdir(pathname, {cached: true}, function (err, names) { if (err) return self.serveError(err) self.writeHeader('text/html') ;(function next() { var name = names.pop() if (name == null) return gotStats() self.archive.stat(pathname + name, function (err, st) { if (err) { self.socket.write(h('pre', err.stack || err).outerHTML) st = {} } else { st.type = st.isDirectory() ? 'directory' : 'file' } if (st.isDirectory()) st.size = null st.name = name if (st.mtime.getTime() === 0) st.mtime = null files.push(st) next() }) }()) }) function gotStats() { files.sort(compareFileStats) if (pathname !== '/') { files.unshift({ name: '..', type: 'updir' }) } self.socket.end('' + h('html', [ h('head', [ h('title', pathname) ]), h('body', [ h('h4', pathname), h('table', files.map(function (file) { var suffix = file.type === 'directory' ? '/' : '' return h('tr', [ h('td', (typeIcons[file.type] || '')), h('td', h('a', {href: pathname + file.name + suffix}, file.name)), h('td', file.mtime == null ? '' : file.mtime), h('td', {align: 'right'}, file.size == null ? '' : file.size) ]) })) ]) ]).outerHTML) } } function detectContentType(pathname) { var m = /[^.]*$/.exec(pathname) switch (m && m[0]) { case 'htm': case 'html': return 'text/html' case 'png': return 'image/png' case 'gif': return 'image/gif' case 'jpg': return 'image/jpeg' default: return 'text/plain' } } function withTimeout(fn, ms) { return function (arg, cb) { var timeout = setTimeout(function () { if (!cb) return var _cb = cb cb = null _cb() }, ms) fn(arg, function (err, res) { if (!cb) return clearTimeout(timeout) var _cb = cb cb = null _cb(err, res) }) } } DpiReq.prototype.serveFile = function () { if (this.closed) return this.writeHeader(detectContentType(this.pathname)) this.archive.createReadStream(this.pathname).pipe(this.socket) } DpiReq.prototype.statFile = function (pathname, cb) { var self = this this.archive.stat(pathname, function (err, st) { if (err) return cb(err) st.pathname = pathname cb(null, st) }) } DpiReq.prototype.statDirectory = function (pathname, cb) { var self = this self.statFile(pathname + '/index.html', function (err, st) { if (!err) return cb(null, st) self.statFile(pathname, cb) }) } DpiReq.prototype.stat = function (pathname, cb) { var self = this if (/\/\/$/.test(pathname)) return self.statFile(pathname.replace(/\/$/, ''), cb) if (/\/$/.test(pathname)) return self.statDirectory(pathname, cb) self.statFile(pathname, function (err, st) { if (err) return self.statFile(pathname + '.html', cb) if (st.isDirectory()) return self.statDirectory(pathname, cb) cb(null, st) }) } DpiReq.prototype.serveTimedout = function (urlp) { this.writeHeader('text/html') this.socket.end(h('h3', 'Timed out').outerHTML) } DpiReq.prototype.serveNotFound = function (urlp) { this.writeHeader('text/html') this.socket.end(h('h3', 'Not found').outerHTML) } function unswarm(key, cb) { var archive = archives[key] if (!archive) return cb(new Error('missing archive')) delete archives[key] archive.swarm.close() archive.close(cb) } DpiReq.prototype.serveDashboard = function () { var self = this var q = self.urlp.query if (q.unswarm) { self.sendStatus('unswarming ' + q.key) return unswarm(q.key, function (err) { if (err) return self.serveError(err) return self.reload(q.reload) }) } if (q.restart) { self.sendStatus('restarting') self.reload(q.reload) return process.exit(0) } this.writeHeader('text/html') self.socket.end('' + h('html', [ h('head', [ h('title', 'Dat') ]), h('body', [ h('form', {action: ''}, [ h('input', {type: 'submit', name: 'restart', value: 'Restart'}), h('input', {type: 'hidden', name: 'reload', value: self.url}) ]), h('h3', 'Archives'), Object.keys(archives).length === 0 ? [ h('p', 'None') ] : Object.keys(archives).map(function (key) { var archive = archives[key] var description = archive.manifest && archive.manifest.description var title = archive.manifest && archive.manifest.title var href = 'dat://' + key var versionedHref = href + '+' + archive.version return [ h('p', [ h('a', {href: href}, title || h('code', key)), description ? [' - ', String(description)] : '', h('br'), 'version ', h('a', {href: versionedHref}, archive.version) ]), h('blockquote', [ h('h3', 'Peers'), archive.swarm.connections.length === 0 ? [ 'None' ] : archive.swarm.connections.map(function (stream) { return h('div', h('code', stream.address)) }), h('form', {action: ''}, [ h('input', {type: 'hidden', name: 'key', value: key}), h('input', {type: 'hidden', name: 'reload', value: self.url}), h('input', {type: 'submit', name: 'unswarm', value: 'unswarm'}), ]) ]) ] }) ]) ]).outerHTML) } DpiReq.prototype.serveDat = function () { var self = this self.urlp = parseDatUrl(self.url, true) if (!self.urlp.host) return self.serveError('Archive not found') self.sendStatus('resolving name') datDns.resolveName(self.urlp.host, { ignoreCachedMiss: true, noDnsOverHttps: true }, function (err, key) { if (err) return self.serveError(err) if (!key) return cb(new TypeError('resolve failed')) self.sendStatus('getting archive') withTimeout(getArchive, 10000)(key, function (err, archive) { if (err) return self.serveError(err) if (!archive) return self.serveTimedout(self.urlp) try { if (self.urlp.version) archive = archive.checkout(+self.urlp.version) } catch(e) { return self.serveError(err) } self.archive = archive self.sendStatus('serving file') self.serve() }) }) } DpiReq.prototype.serveInternal = function () { this.urlp = parseUrl(this.url, true) if (this.urlp.pathname === '/dat/') return this.serveDashboard() this.serveNotFound() } DpiReq.prototype.serve = function () { var self = this if (self.closed) return var pathname = self.urlp.pathname || '/' try { pathname = decodeURIComponent(pathname) } catch(e) {} self.stat(pathname, function (err, st) { if (err) return self.serveNotFound(err) self.pathname = st.pathname if (self.urlp.query.stat) return self.serveStat(st) if (st.isDirectory()) return self.serveDirectory() self.serveFile() }) } function DpiReq_onAuth(m) { this.authed = (m[1] == dpidKeys[1]) if (!this.authed) { console.error('[dat dpi] bad auth from', this.getAddress()) return this.socket.end() } this.authed = true } function DpiReq_onOpenUrl(m) { if (!this.authed) { console.error('[dat dpi] un-authed request from', this.getAddress()) return this.socket.end() } this.url = m[1] if (this.url.startsWith('dat:')) return this.serveDat() if (this.url.startsWith('dpi:/dat/')) return this.serveInternal() this.serveError('Not found') } function DpiReq_onBye(m) { if (!this.authed) { console.error('[dat dpi] un-authed bye from', this.getAddress()) return this.socket.end() } console.log('[dat dpi] stopping') process.exit(0) } DpiReq.prototype.commands = [ [/^/, DpiReq_onAuth], [/^/, DpiReq_onOpenUrl], [/^/, DpiReq_onBye], ] net.createServer({allowHalfOpen: true}, function (c) { new DpiReq(c) }).listen(process.stdin, function () { console.log('[dat dpi] started') })