git ssb

3+

cel / ssb-npm-registry



Tree: bf2c8915dee53c4a685241c0b75ac581a663ae81

Files: bf2c8915dee53c4a685241c0b75ac581a663ae81 / bootstrap / bin.js

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

Built with git-ssb-web