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