#!/usr/bin/env node var http = require('http') var fs = require('fs') var proc = require('child_process') var path = require('path') var URL = require('url') var http = require('http') var https = require('https') var crypto = require('crypto') var Transform = require('stream').Transform var SsbNpmRegistry = require('../') var pullFile = require('pull-file') var lru = require('hashlru') var memo = require('asyncmemo') var ssbAppname = process.env.ssb_appname || 'ssb' var ssbPath = process.env.ssb_path || path.join(process.env.HOME, '.' + ssbAppname) var blobsPath = path.join(ssbPath, 'blobs') var configPath = path.join(ssbPath, 'config') var blobsTmpPath = path.join(blobsPath, 'tmp') var numTmpBlobs = 0 var host = null var port = null var msgsUrl = null var blobsUrl = null var server var listenHostname var branches = [] function shift(args) { if (!args.length) return usage(1) return args.shift() } function version() { var pkg = require('./package') console.log(pkg.name + ' ' + pkg.version) } function usage(code) { console.error(fs.readFileSync(path.join(__dirname, 'usage.txt'))) process.exit(code) } function main(args) { var viewerUrl = null var wsUrl = null var cmdArgs = [] while (args.length) { var arg = args.shift() switch (arg) { case '--help': return usage(0) case '--version': return version() case '--host': host = shift(args); break case '--port': port = shift(args); break case '--msgs-url': msgsUrl = shift(args); break case '--blobs-url': blobsUrl = shift(args); break case '--ws-url': wsUrl = shift(args); break case '--viewer-url': viewerUrl = shift(args); break case '--branch': branches.push(shift(args)); break case '--': cmdArgs.push.apply(cmdArgs, args.splice(0)); break default: cmdArgs.push(arg); break } } if (wsUrl && viewerUrl) { throw '--ws-url and --viewer-url options conflict' } if (wsUrl && (msgsUrl || blobsUrl)) { throw '--ws-url option conflicts with --msgs-url and --blobs-url' } if (viewerUrl && (msgsUrl || blobsUrl)) { throw '--viewer-url option conflicts with --msgs-url and --blobs-url' } if (wsUrl) { wsUrl = wsUrl.replace(/\/+$/, '') blobsUrl = wsUrl + '/blobs/get/%s' msgsUrl = wsUrl + '/msg/%s' } if (viewerUrl) { viewerUrl = viewerUrl.replace(/\/+$/, '') blobsUrl = viewerUrl + '/%s' msgsUrl = viewerUrl + '/%s.json' } if (!port && !cmdArgs.length) { port = 8989 } server = http.createServer(serve) server.listen(port, host, function () { if (cmdArgs.length) { var cmd = cmdArgs.shift() port = server.address().port var registryUrl = 'http://' + (host || 'localhost') + ':' + port + '/npm/' + branches.map(encodeURIComponent).join(',') + '/' var env = {} for (var k in process.env) env[k] = process.env[k] env.npm_config_registry = registryUrl if (cmd === 'npm') { cmdArgs.unshift('--no-update-notifier') cmdArgs.unshift('--fetch-retries=0') cmdArgs.unshift('--download=' + registryUrl + '-/prebuild/{name}-v{version}-{runtime}-v{abi}-{platform}{libc}-{arch}.tar.gz') } cmdArgs.forEach(function (arg, i) { cmdArgs[i] = arg.replace(/\{registry\}/g, registryUrl) }) var child = proc.spawn(cmd, cmdArgs, { env: env, stdio: 'inherit' }) child.on('exit', process.exit) process.on('SIGINT', function () { child.kill('SIGINT') }) process.on('SIGTERM', function () { child.kill('SIGTERM') }) process.on('uncaughtException', function (e) { console.error(e) child.kill('SIGKILL') process.exit(1) }) } else { printServerListening() } }) } function printServerListening() { var addr = server.address() listenHostname = typeof addr === 'string' ? 'unix:' + addr : addr.family === 'IPv6' ? '[' + addr.address + ']:' + addr.port : addr.address + ':' + addr.port console.log('Listening on http://' + listenHostname) } function serveStatus(res, code, message) { res.writeHead(code, message) res.end(message) } var blobIdRegex = /^&([A-Za-z0-9\/+]{43}=)\.sha256$/ function idToBuf(id) { var m = blobIdRegex.exec(id) if (!m) return null return new Buffer(m[1], 'base64') } function blobFilename(buf) { if (!buf) return null var str = buf.toString('hex') return path.join(blobsPath, 'sha256', str.slice(0, 2), str.slice(2)) } function getRemoteBlob(id, cb) { var url = blobsUrl.replace('%s', id) return /^https:\/\//.test(url) ? https.get(url, cb) : http.get(url, cb) } function getRemoteMsg(id, cb) { var url = msgsUrl.replace('%s', encodeURIComponent(id)) return /^https:\/\//.test(url) ? https.get(url, cb) : http.get(url, cb) } function mkdirp(dir, cb) { fs.stat(dir, function (err, stats) { if (!err) return cb() fs.mkdir(dir, function (err) { if (!err) return cb() mkdirp(path.dirname(dir), function (err) { if (err) return cb(err) fs.mkdir(dir, cb) }) }) }) } function rename(src, dest, cb) { mkdirp(path.dirname(dest), function (err) { if (err) return cb(err) fs.rename(src, dest, cb) }) } function hash(arr) { return arr.reduce(function (hash, item) { return hash.update(String(item)) }, crypto.createHash('sha256')).digest('base64') } function fetchAddBlob(id, hash, filename, opts, cb) { opts = opts || {} var readIt = opts.readIt !== false if (!blobsUrl) return cb(new Error('Missing blobs URL')) var req = getRemoteBlob(id, function (res) { req.removeListener('error', cb) if (res.statusCode !== 200) return cb(new Error(res.statusMessage)) mkdirp(blobsTmpPath, function (err) { if (err) return res.destroy(), cb(err) var blobTmpPath = path.join(blobsTmpPath, Date.now() + '-' + numTmpBlobs++) fs.open(blobTmpPath, 'w+', function (err, fd) { if (err) return res.destroy(), cb(err) var writeStream = fs.createWriteStream(null, { fd: fd, flags: 'w+', autoClose: false}) var hasher = crypto.createHash('sha256') var hashThrough = new Transform({ transform: function (data, encoding, cb) { hasher.update(data) cb(null, data) } }) res.pipe(hashThrough).pipe(writeStream, {end: false}) res.on('error', function (err) { writeStream.end(function (err1) { fs.unlink(blobTmpPath, function (err2) { cb(err || err1 || err2) }) }) }) hashThrough.on('end', function () { var receivedHash = hasher.digest() if (receivedHash.compare(hash)) { writeStream.end(function (err) { fs.unlink(blobTmpPath, function (err1) { cb(err1 || err || new Error('mismatched hash')) }) }) } else { res.unpipe(hashThrough) rename(blobTmpPath, filename, function (err) { if (err) return console.error(err) if (readIt) cb(null, fs.createReadStream(null, {fd: fd, start: 0})) else fs.close(fd, function (err) { if (err) return cb(err) cb(null) }) }) } }) }) }) }) req.on('error', cb) } function getAddBlob(id, hash, filename, cb) { fs.access(filename, fs.constants.R_OK, function (err) { if (err && err.code === 'ENOENT') return fetchAddBlob(id, hash, filename, {}, cb) if (err) return cb(err) cb(null, fs.createReadStream(filename)) }) } function serveBlobsGet(req, res, id) { try { id = decodeURIComponent(id) } catch (e) {} var hash = idToBuf(id) var filename = blobFilename(hash) getAddBlob(id, hash, filename, function (err, stream) { if (err) return serveStatus(res, 500, err.message) if (!stream) return serveStatus(res, 404, 'Blob Not Found') res.writeHead(200) stream.pipe(res) }) } function serveMsg(req, res, id) { try { id = decodeURIComponent(id) } catch (e) {} ssbGet(id, function (err, value) { if (err) return serveStatus(res, 400, err.message) if (!msg) return serveStatus(res, 404, 'Msg Not Found') res.writeHead(200, {'Content-Type': 'application/json'}) res.end(JSON.stringify({key: id, value: msg})) }) } function whoami(cb) { cb(null, {id: 'foo'}) } var ssbGet = memo({ cache: lru(1000) }, function (id, cb) { if (!msgsUrl) return cb(new Error('Missing msgs URL')) var req = getRemoteMsg(id, function (res) { req.removeListener('error', cb) if (res.statusCode !== 200) return cb(new Error('unable to get msg ' + id + ': ' + res.statusMessage)) var bufs = [] res.on('data', function (buf) { bufs.push(buf) }) res.on('error', onError) function onError(err) { cb(err) cb = null } res.on('end', function () { if (!cb) return res.removeListener('error', onError) var buf = Buffer.concat(bufs) try { var obj = JSON.parse(buf.toString('utf8')) if (Array.isArray(obj)) obj = obj[0] if (!obj) return cb(new Error('empty message')) if (obj.value) obj = obj.value gotMsg(obj, cb) } catch(e) { return cb(e) } }) }) req.removeListener('error', cb) function gotMsg(value, cb) { var encoded = new Buffer(JSON.stringify(value, null, 2), 'binary') var hash = crypto.createHash('sha256').update(encoded).digest('base64') var id1 = '%' + hash + '.sha256' if (id !== id1) return cb(new Error('mismatched hash ' + id + ' ' + id1)) cb(null, value) } }) function blobsSize(id, cb) { var filename = blobFilename(idToBuf(id)) if (!filename) return cb(new Error('bad id')) fs.stat(filename, function (err, stats) { if (err) return cb(err) cb(null, stats.size) }) } function blobsWant(id, cb) { var hash = idToBuf(id) var filename = blobFilename(hash) fetchAddBlob(id, hash, filename, {readIt: false}, function (err) { if (err) return cb(err) cb(null, true) }) } function blobsGet(id) { var filename = blobFilename(idToBuf(id)) if (!filename) return cb(new Error('bad id')) return pullFile(filename) } var ssbNpmConfig try { ssbNpmConfig = JSON.stringify(fs.readFileSync(configPath)).npm } catch(e) { } var serveNpm1 = SsbNpmRegistry.respond({ whoami: whoami, get: ssbGet, blobs: { size: blobsSize, want: blobsWant, get: blobsGet, } }, { ws: { port: port, host: host, }, npm: ssbNpmConfig }) function serveNpm(req, res, url) { req.url = url serveNpm1(req, res) } function serve(req, res) { var p = URL.parse(req.url) if (p.pathname.startsWith('/npm/')) return serveNpm(req, res, p.pathname.substr(4)) if (p.pathname.startsWith('/msg/')) return serveMsg(req, res, p.pathname.substr(5)) if (p.pathname.startsWith('/blobs/get/')) return serveBlobsGet(req, res, p.pathname.substr(11)) return serveStatus(res, 404, 'Not Found') } main(process.argv.slice(2))