git ssb

3+

cel / ssb-npm-registry



Tree: 92267ec828419ae24882f44ece54c5fb0b5ec4d3

Files: 92267ec828419ae24882f44ece54c5fb0b5ec4d3 / bootstrap / bin.js

11499 bytesRaw
1#!/usr/bin/env node
2
3var http = require('http')
4var fs = require('fs')
5var proc = require('child_process')
6var path = require('path')
7var URL = require('url')
8var http = require('http')
9var https = require('https')
10var crypto = require('crypto')
11var Transform = require('stream').Transform
12var SsbNpmRegistry = require('../')
13var pullFile = require('pull-file')
14var lru = require('hashlru')
15var memo = require('asyncmemo')
16
17var ssbAppname = process.env.ssb_appname || 'ssb'
18var ssbPath = process.env.ssb_path || path.join(process.env.HOME, '.' + ssbAppname)
19var blobsPath = path.join(ssbPath, 'blobs')
20var configPath = path.join(ssbPath, 'config')
21var blobsTmpPath = path.join(blobsPath, 'tmp')
22var numTmpBlobs = 0
23
24var host = null
25var port = null
26var msgsUrl = null
27var blobsUrl = null
28var server
29var listenHostname
30var branches = []
31
32function shift(args) {
33 if (!args.length) return usage(1)
34 return args.shift()
35}
36
37function version() {
38 var pkg = require('../package')
39 console.log(pkg.name + ' ' + pkg.version)
40}
41
42function usage(code) {
43 (code ? process.stderr : process.stdout)
44 .write(fs.readFileSync(path.join(__dirname, 'usage.txt'), 'utf8'))
45 process.exit(code)
46}
47
48function 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
153function 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
161function serveStatus(res, code, message) {
162 res.writeHead(code, message)
163 res.end(message)
164}
165
166var blobIdRegex = /^&([A-Za-z0-9\/+]{43}=)\.sha256$/
167
168function idToBuf(id) {
169 var m = blobIdRegex.exec(id)
170 if (!m) return null
171 return new Buffer(m[1], 'base64')
172}
173
174function 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
180function 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
185function 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
190function 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
203function 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
210function hash(arr) {
211 return arr.reduce(function (hash, item) {
212 return hash.update(String(item))
213 }, crypto.createHash('sha256')).digest('base64')
214}
215
216function 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
271function 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
279function 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
292function 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
303function whoami(cb) {
304 cb(null, {id: 'foo'})
305}
306
307var 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
349function 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
358function 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
367function blobsGet(id) {
368 var filename = blobFilename(idToBuf(id))
369 if (!filename) return cb(new Error('bad id'))
370 return pullFile(filename)
371}
372
373var ssbNpmConfig
374try {
375 ssbNpmConfig = JSON.stringify(fs.readFileSync(configPath)).npm
376} catch(e) {
377}
378
379var serveNpm1
380function serveNpm(req, res, url) {
381 req.url = url
382 serveNpm1(req, res)
383}
384
385function 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
394main(process.argv.slice(2))
395

Built with git-ssb-web