var http = require('http') var os = require('os') var fs = require('fs') 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 proc = require('child_process') function usage(code) { console.error('Usage: ' + process.argv[0] + ' ' + process.argv[1] + '\n' + ' [--help] [--verbose]\n' + ' [--blobs-host ] [--blobs-port ]\n' + ' [--registry-host ] [--registry-port ]\n' + ' [--blobs-remote ]\n' + ' [--] [ ]') process.exit(code) } var ssbAppname = process.env.ssb_appname || 'ssb' var ssbPath = process.env.ssb_path || path.join(os.homedir(), '.' + ssbAppname) var blobsPath = path.join(ssbPath, 'blobs') var blobsTmpPath = path.join(blobsPath, 'tmp') var numTmpBlobs = 0 var remotes = [] function blobFilename(buf) { var str = buf.toString('hex') return path.join(blobsPath, 'sha256', str.slice(0, 2), str.slice(2)) } function formatHttpAddr(addr) { return 'http://' + (typeof addr === 'string' ? 'unix:' + addr : addr.family === 'IPv6' ? '[' + addr.address + ']:' + addr.port : addr.address + ':' + addr.port) } function serveBlobs(opts, req, res) { var p = URL.parse(req.url) if (p.pathname === '/') return serveStatus(res, 204) var m = /^\/blobs\/get\/(&([A-Za-z0-9\/+]{43}=)\.sha256)$/.exec(p.pathname) if (m) return serveBlobsGet(req, res, opts.remote, m[1], new Buffer(m[2], 'base64')) return serveStatus(res, 404, 'Not Found') } function serveRegistry(opts, req, res) { var p = URL.parse(req.url) var pathname = req.url.replace(/\?.*/, '') if (pathname === '/') return serveStatus(res, 204) if (pathname === '/-/ping') return serveStatus(res, 200, null, '"pong"') if (/^\/-\//.test(pathname)) return serveStatus(404) return servePkg(opts.pkgs, req, res, pathname.substr(1)) } function serveStatus(res, code, message, body) { res.writeHead(code, message) res.end(body || message) } function serveStop(opts, req, res) { var addr = req.socket.remoteAddress if (addr !== '::1' && addr !== '127.0.0.1' && addr !== '::ffff:127.0.0.1') { return serveStatus(res, 403) } serveStatus(res, 200) opts.registryServer.close() opts.blobsServer.close() } function serveBlobsGet(req, res, remote, id, hash) { var filename = blobFilename(hash) getAddBlob(remote, id, hash, filename, function (err, stream) { if (err) return serveStatus(res, 500, null, err.message) if (!stream) return serveStatus(res, 404, 'Blob Not Found') res.writeHead(200) stream.pipe(res) }) } function getAddBlob(remote, id, hash, filename, cb) { fs.access(filename, fs.constants.R_OK, function (err) { if (err && err.code === 'ENOENT') { return fetchAddBlob(remote, id, hash, filename, cb) } if (err) return cb(err) cb(null, fs.createReadStream(filename)) }) } function getRemoteBlob(remote, id, cb) { if (/^https:\/\//.test(remote)) return https.get(remote + id, cb) if (/^http:\/\//.test(remote)) return https.get(remote + id, cb) return http.get('http://' + remote + '/blobs/get/' + id, 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 fetchAddBlob(remote, id, hash, filename, cb) { var req = getRemoteBlob(remote, 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 (hash.compare(receivedHash)) { writeStream.end(function (err) { fs.unlink(blobTmpPath, function (err1) { cb(err || err1) }) }) } else { res.unpipe(hashThrough) rename(blobTmpPath, filename, function (err) { if (err) return console.error(err) cb(null, fs.createReadStream(null, {fd: fd, start: 0})) }) } }) }) }) }) req.on('error', cb) } function servePkg(pkgs, req, res, pathname) { var self = this var parts = pathname.split('/') var pkgName = parts.shift() if (parts[0] === '-rev') return serveStatus(res, 501, 'Unpublish is not supported') if (parts.length > 0) return serveStatus(res, 404) var pkg = pkgs[pkgName] || {} serveStatus(res, 200, null, JSON.stringify(pkg, 0, 2)) } function startServers(opts, cb) { var waiting = 2 opts.blobsServer = http.createServer(serveBlobs.bind(null, opts)) .listen(opts.blobsPort, opts.blobsHost, function () { if (opts.verbose) console.error('blobs listening on ' + formatHttpAddr(this.address())) if (!--waiting) cb() }) opts.registryServer = http.createServer(serveRegistry.bind(null, opts)) .listen(opts.registryPort, opts.registryHost, function () { if (opts.verbose) console.error('registry listening on ' + formatHttpAddr(this.address())) if (!--waiting) cb() }) } process.nextTick(function () { var args = process.argv.slice(2) var opts = { pkgs: exports.pkgs || {}, blobsPort: 8989, registryPort: 8043, } var cmd getopts: while (args.length) { var arg = args.shift() switch (arg) { case '--help': return usage(0) case '--verbose': opts.verbose = true; break case '--blobs-host': opts.blobsHost = args.shift(); break case '--blobs-port': opts.blobsPort = args.shift(); break case '--registry-port': opts.registryPort = args.shift(); break case '--registry-host': opts.registryHost = args.shift(); break case '--blobs-remote': opts.remote = args.shift(); break case '--': cmd = args.shift(); break getopts default: if (/^--/.test(arg)) return usage(1); cmd = arg; break getopts } } startServers(opts, function (err) { if (err) throw err if (cmd) { var regHost = opts.registryHost || 'localhost' var regHostname = regHost + ':' + opts.registryPort if (cmd === 'npm' || cmd === 'npx') { args.push('--registry=http://' + regHostname + '/') args.push('--//' + regHostname + '/:_authToken=1') } var child = proc.spawn(cmd, args, { 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) }) } }) })