git ssb

3+

cel / ssb-npm-registry



Tree: 1570ad9df58d43e3838545d51222ead7e6503c5c

Files: 1570ad9df58d43e3838545d51222ead7e6503c5c / bootstrap / bin.js

11121 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 msgIds = []
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 '--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
131function 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
139function serveStatus(res, code, message) {
140 res.writeHead(code, message)
141 res.end(message)
142}
143
144var blobIdRegex = /^&([A-Za-z0-9\/+]{43}=)\.sha256$/
145
146function idToBuf(id) {
147 var m = blobIdRegex.exec(id)
148 if (!m) return null
149 return new Buffer(m[1], 'base64')
150}
151
152function 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
158function 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
163function 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
168function 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
181function 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
188function hash(arr) {
189 return arr.reduce(function (hash, item) {
190 return hash.update(String(item))
191 }, crypto.createHash('sha256')).digest('base64')
192}
193
194function 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
249function 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
257function 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
270function 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
281function whoami(cb) {
282 cb(null, {id: 'foo'})
283}
284
285var 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
327function 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
336function 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
346function blobsGet(id) {
347 var filename = blobFilename(idToBuf(id))
348 if (!filename) return cb(new Error('bad id'))
349 return pullFile(filename)
350}
351
352var ssbNpmConfig
353try {
354 ssbNpmConfig = JSON.stringify(fs.readFileSync(configPath)).npm
355} catch(e) {
356}
357
358var 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
374function serveNpm(req, res, url) {
375 req.url = url
376 serveNpm1(req, res)
377}
378
379function 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
387main(process.argv.slice(2))
388

Built with git-ssb-web