#!/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 = 'localhost' 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) { (code ? process.stderr : process.stdout) .write(fs.readFileSync(path.join(__dirname, 'usage.txt'), 'utf8')) process.exit(code) } function main(args) { var viewerUrl = null var wsUrl = null var cmd 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 '--exec': cmd = 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' } var doExec = cmdArgs.length > 0 || cmd != null if (!doExec) port = 8044 server = http.createServer() server.listen(port, host, function () { port = server.address().port server.on('request', SsbNpmRegistry.respond({ whoami: whoami, get: ssbGet, blobs: { size: blobsSize, want: blobsWant, get: blobsGet, } }, { npm: ssbNpmConfig })) if (doExec) { var registryUrl = 'http://' + host + ':' + port + '/' + branches.map(encodeURIComponent).join(',') var env = {} for (var k in process.env) env[k] = process.env[k] env.npm_config_registry = registryUrl if (!cmd) cmd = 'npm' if (cmd === 'npm') { if (process.platform === 'win32') cmd = 'npm.cmd' cmdArgs.unshift('--no-update-notifier') cmdArgs.unshift('--download={registry}/-/prebuild/{name}-v{version}-{runtime}-v{abi}-{platform}{libc}-{arch}.tar.gz') cmdArgs.unshift('--' + registryUrl.replace(/^https?:/, '') + ':_authToken=1') cmdArgs.unshift('--registry={registry}') } else if (cmd === 'yarn') { cmdArgs.unshift('--registry={registry}') } 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 readNext(fn) { var next return function (end, cb) { if (next) return next(end, cb) try { fn(function (err, _next) { if (err) return cb(err) next = _next next(null, cb) }) } catch(e) { cb(e) } } } 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 Buffer.from(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(err2 || err1 || err) }) }) }) 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, toPull(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, pullFile(filename)) }) } 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 = Buffer.from(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) { return readNext(function (cb) { var hash = idToBuf(id) var filename = blobFilename(hash) getAddBlob(id, hash, filename, cb) }) } var ssbNpmConfig try { ssbNpmConfig = JSON.stringify(fs.readFileSync(configPath)).npm } catch(e) { } main(process.argv.slice(2))