bootstrap/bin.jsView |
---|
| 1 … | +#!/usr/bin/env node |
| 2 … | + |
| 3 … | +var http = require('http') |
| 4 … | +var fs = require('fs') |
| 5 … | +var proc = require('child_process') |
| 6 … | +var path = require('path') |
| 7 … | +var URL = require('url') |
| 8 … | +var http = require('http') |
| 9 … | +var https = require('https') |
| 10 … | +var crypto = require('crypto') |
| 11 … | +var Transform = require('stream').Transform |
| 12 … | +var SsbNpmRegistry = require('../') |
| 13 … | +var pullFile = require('pull-file') |
| 14 … | +var lru = require('hashlru') |
| 15 … | +var memo = require('asyncmemo') |
| 16 … | + |
| 17 … | +var ssbAppname = process.env.ssb_appname || 'ssb' |
| 18 … | +var ssbPath = process.env.ssb_path || path.join(process.env.HOME, '.' + ssbAppname) |
| 19 … | +var blobsPath = path.join(ssbPath, 'blobs') |
| 20 … | +var configPath = path.join(ssbPath, 'config') |
| 21 … | +var blobsTmpPath = path.join(blobsPath, 'tmp') |
| 22 … | +var numTmpBlobs = 0 |
| 23 … | + |
| 24 … | +var host = null |
| 25 … | +var port = null |
| 26 … | +var msgsUrl = null |
| 27 … | +var blobsUrl = null |
| 28 … | +var server |
| 29 … | +var listenHostname |
| 30 … | +var msgIds = [] |
| 31 … | + |
| 32 … | +function shift(args) { |
| 33 … | + if (!args.length) return usage(1) |
| 34 … | + return args.shift() |
| 35 … | +} |
| 36 … | + |
| 37 … | +function version() { |
| 38 … | + var pkg = require('./package') |
| 39 … | + console.log(pkg.name + ' ' + pkg.version) |
| 40 … | +} |
| 41 … | + |
| 42 … | +function usage(code) { |
| 43 … | + console.error(fs.readFileSync(path.join(__dirname, 'usage.txt'))) |
| 44 … | + process.exit(code) |
| 45 … | +} |
| 46 … | + |
| 47 … | +function main(args) { |
| 48 … | + var viewerUrl = null |
| 49 … | + var wsUrl = null |
| 50 … | + |
| 51 … | + var cmdArgs = [] |
| 52 … | + while (args.length) { |
| 53 … | + var arg = args.shift() |
| 54 … | + switch (arg) { |
| 55 … | + case '--help': return usage(0) |
| 56 … | + case '--version': return version() |
| 57 … | + case '--host': host = shift(args); break |
| 58 … | + case '--port': port = shift(args); break |
| 59 … | + case '--msgs-url': msgsUrl = shift(args); break |
| 60 … | + case '--blobs-url': blobsUrl = shift(args); break |
| 61 … | + case '--ws-url': wsUrl = shift(args); break |
| 62 … | + case '--viewer-url': viewerUrl = shift(args); break |
| 63 … | + case '--msg': msgIds.push(shift(args)); break |
| 64 … | + case '--': cmdArgs.push.apply(cmdArgs, args.splice(0)); break |
| 65 … | + default: cmdArgs.push(arg); break |
| 66 … | + } |
| 67 … | + } |
| 68 … | + |
| 69 … | + if (wsUrl && viewerUrl) { |
| 70 … | + throw '--ws-url and --viewer-url options conflict' |
| 71 … | + } |
| 72 … | + if (wsUrl && (msgsUrl || blobsUrl)) { |
| 73 … | + throw '--ws-url option conflicts with --msgs-url and --blobs-url' |
| 74 … | + } |
| 75 … | + if (viewerUrl && (msgsUrl || blobsUrl)) { |
| 76 … | + throw '--viewer-url option conflicts with --msgs-url and --blobs-url' |
| 77 … | + } |
| 78 … | + |
| 79 … | + if (wsUrl) { |
| 80 … | + wsUrl = wsUrl.replace(/\/+$/, '') |
| 81 … | + blobsUrl = wsUrl + '/blobs/get/%s' |
| 82 … | + msgsUrl = wsUrl + '/msg/%s' |
| 83 … | + } |
| 84 … | + |
| 85 … | + if (viewerUrl) { |
| 86 … | + viewerUrl = viewerUrl.replace(/\/+$/, '') |
| 87 … | + blobsUrl = viewerUrl + '/%s' |
| 88 … | + msgsUrl = viewerUrl + '/%s.json' |
| 89 … | + } |
| 90 … | + |
| 91 … | + if (!port && !cmdArgs.length) { |
| 92 … | + port = 8989 |
| 93 … | + } |
| 94 … | + |
| 95 … | + server = http.createServer(serve) |
| 96 … | + server.listen(port, host, function () { |
| 97 … | + if (cmdArgs.length) { |
| 98 … | + var cmd = cmdArgs.shift() |
| 99 … | + port = server.address().port |
| 100 … | + var registryUrl = 'http://' + (host || 'localhost') + ':' + port |
| 101 … | + + '/npm/' + msgIds.map(encodeURIComponent).join(',') + '/' |
| 102 … | + var env = {} |
| 103 … | + for (var k in process.env) env[k] = process.env[k] |
| 104 … | + env.npm_config_registry = registryUrl |
| 105 … | + if (cmd === 'npm') { |
| 106 … | + cmdArgs.unshift('--no-update-notifier') |
| 107 … | + cmdArgs.unshift('--fetch-retries=0') |
| 108 … | + cmdArgs.unshift('--download=' + registryUrl + '-/prebuild/{name}-v{version}-{runtime}-v{abi}-{platform}{libc}-{arch}.tar.gz') |
| 109 … | + } |
| 110 … | + cmdArgs.forEach(function (arg, i) { |
| 111 … | + cmdArgs[i] = arg.replace(/\{registry\}/g, registryUrl) |
| 112 … | + }) |
| 113 … | + var child = proc.spawn(cmd, cmdArgs, { |
| 114 … | + env: env, |
| 115 … | + stdio: 'inherit' |
| 116 … | + }) |
| 117 … | + child.on('exit', process.exit) |
| 118 … | + process.on('SIGINT', function () { child.kill('SIGINT') }) |
| 119 … | + process.on('SIGTERM', function () { child.kill('SIGTERM') }) |
| 120 … | + process.on('uncaughtException', function (e) { |
| 121 … | + console.error(e) |
| 122 … | + child.kill('SIGKILL') |
| 123 … | + process.exit(1) |
| 124 … | + }) |
| 125 … | + } else { |
| 126 … | + printServerListening() |
| 127 … | + } |
| 128 … | + }) |
| 129 … | +} |
| 130 … | + |
| 131 … | +function printServerListening() { |
| 132 … | + var addr = server.address() |
| 133 … | + listenHostname = typeof addr === 'string' ? 'unix:' + addr |
| 134 … | + : addr.family === 'IPv6' ? '[' + addr.address + ']:' + addr.port |
| 135 … | + : addr.address + ':' + addr.port |
| 136 … | + console.log('Listening on http://' + listenHostname) |
| 137 … | +} |
| 138 … | + |
| 139 … | +function serveStatus(res, code, message) { |
| 140 … | + res.writeHead(code, message) |
| 141 … | + res.end(message) |
| 142 … | +} |
| 143 … | + |
| 144 … | +var blobIdRegex = /^&([A-Za-z0-9\/+]{43}=)\.sha256$/ |
| 145 … | + |
| 146 … | +function idToBuf(id) { |
| 147 … | + var m = blobIdRegex.exec(id) |
| 148 … | + if (!m) return null |
| 149 … | + return new Buffer(m[1], 'base64') |
| 150 … | +} |
| 151 … | + |
| 152 … | +function blobFilename(buf) { |
| 153 … | + if (!buf) return null |
| 154 … | + var str = buf.toString('hex') |
| 155 … | + return path.join(blobsPath, 'sha256', str.slice(0, 2), str.slice(2)) |
| 156 … | +} |
| 157 … | + |
| 158 … | +function getRemoteBlob(id, cb) { |
| 159 … | + var url = blobsUrl.replace('%s', id) |
| 160 … | + return /^https:\/\//.test(url) ? https.get(url, cb) : http.get(url, cb) |
| 161 … | +} |
| 162 … | + |
| 163 … | +function getRemoteMsg(id, cb) { |
| 164 … | + var url = msgsUrl.replace('%s', encodeURIComponent(id)) |
| 165 … | + return /^https:\/\//.test(url) ? https.get(url, cb) : http.get(url, cb) |
| 166 … | +} |
| 167 … | + |
| 168 … | +function mkdirp(dir, cb) { |
| 169 … | + fs.stat(dir, function (err, stats) { |
| 170 … | + if (!err) return cb() |
| 171 … | + fs.mkdir(dir, function (err) { |
| 172 … | + if (!err) return cb() |
| 173 … | + mkdirp(path.dirname(dir), function (err) { |
| 174 … | + if (err) return cb(err) |
| 175 … | + fs.mkdir(dir, cb) |
| 176 … | + }) |
| 177 … | + }) |
| 178 … | + }) |
| 179 … | +} |
| 180 … | + |
| 181 … | +function rename(src, dest, cb) { |
| 182 … | + mkdirp(path.dirname(dest), function (err) { |
| 183 … | + if (err) return cb(err) |
| 184 … | + fs.rename(src, dest, cb) |
| 185 … | + }) |
| 186 … | +} |
| 187 … | + |
| 188 … | +function hash(arr) { |
| 189 … | + return arr.reduce(function (hash, item) { |
| 190 … | + return hash.update(String(item)) |
| 191 … | + }, crypto.createHash('sha256')).digest('base64') |
| 192 … | +} |
| 193 … | + |
| 194 … | +function fetchAddBlob(id, hash, filename, opts, cb) { |
| 195 … | + opts = opts || {} |
| 196 … | + var readIt = opts.readIt !== false |
| 197 … | + if (!blobsUrl) return cb(new Error('Missing blobs URL')) |
| 198 … | + var req = getRemoteBlob(id, function (res) { |
| 199 … | + req.removeListener('error', cb) |
| 200 … | + if (res.statusCode !== 200) return cb(new Error(res.statusMessage)) |
| 201 … | + mkdirp(blobsTmpPath, function (err) { |
| 202 … | + if (err) return res.destroy(), cb(err) |
| 203 … | + var blobTmpPath = path.join(blobsTmpPath, Date.now() + '-' + numTmpBlobs++) |
| 204 … | + fs.open(blobTmpPath, 'w+', function (err, fd) { |
| 205 … | + if (err) return res.destroy(), cb(err) |
| 206 … | + var writeStream = fs.createWriteStream(null, { |
| 207 … | + fd: fd, flags: 'w+', autoClose: false}) |
| 208 … | + var hasher = crypto.createHash('sha256') |
| 209 … | + var hashThrough = new Transform({ |
| 210 … | + transform: function (data, encoding, cb) { |
| 211 … | + hasher.update(data) |
| 212 … | + cb(null, data) |
| 213 … | + } |
| 214 … | + }) |
| 215 … | + res.pipe(hashThrough).pipe(writeStream, {end: false}) |
| 216 … | + res.on('error', function (err) { |
| 217 … | + writeStream.end(function (err1) { |
| 218 … | + fs.unlink(blobTmpPath, function (err2) { |
| 219 … | + cb(err || err1 || err2) |
| 220 … | + }) |
| 221 … | + }) |
| 222 … | + }) |
| 223 … | + hashThrough.on('end', function () { |
| 224 … | + var receivedHash = hasher.digest() |
| 225 … | + if (receivedHash.compare(hash)) { |
| 226 … | + writeStream.end(function (err) { |
| 227 … | + fs.unlink(blobTmpPath, function (err1) { |
| 228 … | + cb(err1 || err || new Error('mismatched hash')) |
| 229 … | + }) |
| 230 … | + }) |
| 231 … | + } else { |
| 232 … | + res.unpipe(hashThrough) |
| 233 … | + rename(blobTmpPath, filename, function (err) { |
| 234 … | + if (err) return console.error(err) |
| 235 … | + if (readIt) cb(null, fs.createReadStream(null, {fd: fd, start: 0})) |
| 236 … | + else fs.close(fd, function (err) { |
| 237 … | + if (err) return cb(err) |
| 238 … | + cb(null) |
| 239 … | + }) |
| 240 … | + }) |
| 241 … | + } |
| 242 … | + }) |
| 243 … | + }) |
| 244 … | + }) |
| 245 … | + }) |
| 246 … | + req.on('error', cb) |
| 247 … | +} |
| 248 … | + |
| 249 … | +function getAddBlob(id, hash, filename, cb) { |
| 250 … | + fs.access(filename, fs.constants.R_OK, function (err) { |
| 251 … | + if (err && err.code === 'ENOENT') return fetchAddBlob(id, hash, filename, {}, cb) |
| 252 … | + if (err) return cb(err) |
| 253 … | + cb(null, fs.createReadStream(filename)) |
| 254 … | + }) |
| 255 … | +} |
| 256 … | + |
| 257 … | +function serveBlobsGet(req, res, id) { |
| 258 … | + try { id = decodeURIComponent(id) } |
| 259 … | + catch (e) {} |
| 260 … | + var hash = idToBuf(id) |
| 261 … | + var filename = blobFilename(hash) |
| 262 … | + getAddBlob(id, hash, filename, function (err, stream) { |
| 263 … | + if (err) return serveStatus(res, 500, err.message) |
| 264 … | + if (!stream) return serveStatus(res, 404, 'Blob Not Found') |
| 265 … | + res.writeHead(200) |
| 266 … | + stream.pipe(res) |
| 267 … | + }) |
| 268 … | +} |
| 269 … | + |
| 270 … | +function serveMsg(req, res, id) { |
| 271 … | + try { id = decodeURIComponent(id) } |
| 272 … | + catch (e) {} |
| 273 … | + ssbGet(id, function (err, value) { |
| 274 … | + if (err) return serveStatus(res, 400, err.message) |
| 275 … | + if (!msg) return serveStatus(res, 404, 'Msg Not Found') |
| 276 … | + res.writeHead(200, {'Content-Type': 'application/json'}) |
| 277 … | + res.end(JSON.stringify({key: id, value: msg})) |
| 278 … | + }) |
| 279 … | +} |
| 280 … | + |
| 281 … | +function whoami(cb) { |
| 282 … | + cb(null, {id: 'foo'}) |
| 283 … | +} |
| 284 … | + |
| 285 … | +var ssbGet = memo({ |
| 286 … | + cache: lru(1000) |
| 287 … | +}, function (id, cb) { |
| 288 … | + if (!msgsUrl) return cb(new Error('Missing msgs URL')) |
| 289 … | + var req = getRemoteMsg(id, function (res) { |
| 290 … | + req.removeListener('error', cb) |
| 291 … | + if (res.statusCode !== 200) |
| 292 … | + return cb(new Error('unable to get msg ' + id + ': ' + res.statusMessage)) |
| 293 … | + var bufs = [] |
| 294 … | + res.on('data', function (buf) { |
| 295 … | + bufs.push(buf) |
| 296 … | + }) |
| 297 … | + res.on('error', onError) |
| 298 … | + function onError(err) { |
| 299 … | + cb(err) |
| 300 … | + cb = null |
| 301 … | + } |
| 302 … | + res.on('end', function () { |
| 303 … | + if (!cb) return |
| 304 … | + res.removeListener('error', onError) |
| 305 … | + var buf = Buffer.concat(bufs) |
| 306 … | + try { |
| 307 … | + var obj = JSON.parse(buf.toString('utf8')) |
| 308 … | + if (Array.isArray(obj)) obj = obj[0] |
| 309 … | + if (!obj) return cb(new Error('empty message')) |
| 310 … | + if (obj.value) obj = obj.value |
| 311 … | + gotMsg(obj, cb) |
| 312 … | + } catch(e) { |
| 313 … | + return cb(e) |
| 314 … | + } |
| 315 … | + }) |
| 316 … | + }) |
| 317 … | + req.removeListener('error', cb) |
| 318 … | + function gotMsg(value, cb) { |
| 319 … | + var encoded = new Buffer(JSON.stringify(value, null, 2), 'binary') |
| 320 … | + var hash = crypto.createHash('sha256').update(encoded).digest('base64') |
| 321 … | + var id1 = '%' + hash + '.sha256' |
| 322 … | + if (id !== id1) return cb(new Error('mismatched hash ' + id + ' ' + id1)) |
| 323 … | + cb(null, value) |
| 324 … | + } |
| 325 … | +}) |
| 326 … | + |
| 327 … | +function blobsSize(id, cb) { |
| 328 … | + var filename = blobFilename(idToBuf(id)) |
| 329 … | + if (!filename) return cb(new Error('bad id')) |
| 330 … | + fs.stat(filename, function (err, stats) { |
|
| 331 … | + if (err) return cb(err) |
| 332 … | + cb(null, stats.size) |
| 333 … | + }) |
| 334 … | +} |
| 335 … | + |
| 336 … | +function blobsWant(id, cb) { |
| 337 … | + var hash = idToBuf(id) |
| 338 … | + var filename = blobFilename(hash) |
| 339 … | + cb(new Error('not implemented')) |
| 340 … | + fetchAddBlob(id, hash, filename, {readIt: false}, function (err) { |
| 341 … | + if (err) return cb(err) |
| 342 … | + cb(null, true) |
| 343 … | + }) |
| 344 … | +} |
| 345 … | + |
| 346 … | +function blobsGet(id) { |
| 347 … | + var filename = blobFilename(idToBuf(id)) |
| 348 … | + if (!filename) return cb(new Error('bad id')) |
| 349 … | + return pullFile(filename) |
| 350 … | +} |
| 351 … | + |
| 352 … | +var ssbNpmConfig |
| 353 … | +try { |
| 354 … | + ssbNpmConfig = JSON.stringify(fs.readFileSync(configPath)).npm |
| 355 … | +} catch(e) { |
| 356 … | +} |
| 357 … | + |
| 358 … | +var serveNpm1 = SsbNpmRegistry.respond({ |
| 359 … | + whoami: whoami, |
| 360 … | + get: ssbGet, |
| 361 … | + blobs: { |
| 362 … | + size: blobsSize, |
| 363 … | + want: blobsWant, |
| 364 … | + get: blobsGet, |
| 365 … | + } |
| 366 … | +}, { |
| 367 … | + ws: { |
| 368 … | + port: port, |
| 369 … | + host: host, |
| 370 … | + }, |
| 371 … | + npm: ssbNpmConfig |
| 372 … | +}) |
| 373 … | + |
| 374 … | +function serveNpm(req, res, url) { |
| 375 … | + req.url = url |
| 376 … | + serveNpm1(req, res) |
| 377 … | +} |
| 378 … | + |
| 379 … | +function serve(req, res) { |
| 380 … | + var p = URL.parse(req.url) |
| 381 … | + if (p.pathname.startsWith('/npm/')) return serveNpm(req, res, p.pathname.substr(4)) |
| 382 … | + if (p.pathname.startsWith('/msg/')) return serveMsg(req, res, p.pathname.substr(5)) |
| 383 … | + if (p.pathname.startsWith('/blobs/get/')) return serveBlobsGet(req, res, p.pathname.substr(11)) |
| 384 … | + return serveStatus(res, 404, 'Not Found') |
| 385 … | +} |
| 386 … | + |
| 387 … | +main(process.argv.slice(2)) |