git ssb

3+

cel / ssb-npm-registry



Tree: c3f0d046e99501aa45f129bac5527c631b0b8c8c

Files: c3f0d046e99501aa45f129bac5527c631b0b8c8c / index.js

39043 bytesRaw
1var http = require('http')
2var os = require('os')
3var path = require('path')
4var fs = require('fs')
5var crypto = require('crypto')
6var pkg = require('./package')
7var semver = require('semver')
8var toPull = require('stream-to-pull-stream')
9var pull = require('pull-stream')
10var tar = require('tar-stream')
11var zlib = require('zlib')
12var cat = require('pull-cat')
13var hash = require('pull-hash')
14var multicb = require('multicb')
15var memo = require('asyncmemo')
16var lru = require('hashlru')
17var chunkedBlobs = require('./largeblobs')
18
19function escapeHTML(str) {
20 return String(str)
21 .replace(/</g, '&lt;')
22 .replace(/>/g, '&gt;')
23}
24
25function idToHex(id) {
26 var b64 = String(id).replace(/^[%#&]|\.[a-z0-9]*$/g, '')
27 return Buffer.from(b64, 'base64').toString('hex')
28}
29
30function onceify(fn, self) {
31 var cbs = [], err, data
32 return function (cb) {
33 if (fn) {
34 cbs.push(cb)
35 fn.call(self, function (_err, _data) {
36 err = _err, data = _data
37 var _cbs = cbs
38 cbs = null
39 while (_cbs.length) _cbs.shift()(err, data)
40 })
41 fn = null
42 } else if (cbs) {
43 cbs.push(cb)
44 } else {
45 cb(err, data)
46 }
47 }
48}
49
50function once(cb) {
51 var done
52 return function (err, result) {
53 if (done) {
54 if (err) console.trace(err)
55 } else {
56 done = true
57 cb(err, result)
58 }
59 }
60}
61
62function npmLogin(registryAddress, cb) {
63 var tokenLine = registryAddress.replace(/^http:/, '') + ':_authToken=1'
64 var filename = path.join(os.homedir(), '.npmrc')
65 fs.readFile(filename, 'utf8', function (err, data) {
66 if (err && err.code === 'ENOENT') data = ''
67 else if (err) return cb(new Error(err.stack))
68 var lines = data ? data.split('\n') : []
69 if (lines.indexOf(tokenLine) > -1) return cb()
70 var trailingNewline = (lines.length === 0 || lines[lines.length-1] === '')
71 var line = trailingNewline ? tokenLine + '\n' : '\n' + tokenLine
72 fs.appendFile(filename, line, cb)
73 })
74}
75
76function formatHost(host) {
77 if (host === '::1') return 'localhost'
78 host = host.replace(/^::ffff:/, '')
79 return host[0] !== '[' && /:.*:/.test(host) ? '[' + host + ']' : host
80}
81
82var wantWarnTime = Number(process.env.WANT_WARN_TIME) || 60e3
83
84exports.name = 'npm-registry'
85exports.version = '1.0.0'
86exports.manifest = {
87 getAddress: 'async'
88}
89exports.init = function (sbot, config) {
90 var conf = config.npm || {}
91 var port = conf.port || 8043
92 var host = conf.host || 'localhost'
93 var autoAuth = conf.autoAuth !== false
94 var listenUrl
95
96 var server = http.createServer(exports.respond(sbot, config))
97 var getAddress = onceify(function (cb) {
98 server.on('error', cb)
99 server.listen(port, host, function () {
100 server.removeListener('error', cb)
101 var addr = this.address()
102 var listenHost = addr.address
103 var regHost = listenHost === '::' ? '::1' :
104 listenHost === '0.0.0.0' ? '127.0.0.1' :
105 listenHost
106 var regUrl = 'http://' + formatHost(regHost) + ':' + addr.port
107 listenUrl = 'http://' + formatHost(listenHost) + ':' + addr.port
108 if (autoAuth) npmLogin(regUrl, next)
109 else next()
110 function next(err) {
111 cb(err, regUrl)
112 }
113 })
114 sbot.on('close', function () {
115 server.close()
116 })
117 })
118
119 /* getAddress called by local RPC is used to discover the local
120 * ssb-npm-registry address that can be given to local npm clients. However,
121 * when running the server we output the address the server is listening on,
122 * to avoid misleading situations like saying listening on localhost but
123 * actually listening on a wildcard address. */
124 getAddress(function (err) {
125 if (err) return console.error('[npm-registry]', err.stack || err)
126 console.log('[npm-registry] Listening on ' + listenUrl)
127 })
128
129 return {
130 getAddress: getAddress
131 }
132}
133
134exports.respond = function (sbot, config) {
135 var reg = new SsbNpmRegistryServer(sbot, config)
136 return function (req, res) {
137 new Req(reg, req, res).serve()
138 }
139}
140
141function publishMsg(sbot, value, cb) {
142 var gotExpectedPrevious = false
143 sbot.publish(value, function next(err, msg) {
144 if (err && /^expected previous:/.test(err.message)) {
145 // retry once on this error
146 if (gotExpectedPrevious) return cb(err)
147 gotExpectedPrevious = true
148 return sbot.publish(value, next)
149 }
150 cb(err, msg)
151 })
152}
153
154function getDependencyBranches(sbot, id, cb) {
155 // get ids of heads of tree of dependencyBranch message links to include all
156 // the dependencies of the given tarball id.
157 var getPackageJsonCached = memo(getPackageJsonFromTarballBlob, sbot)
158 var msgs = {}
159 var branched = {}
160 var blobs = {}
161
162 function addPkgById(id, cb) {
163 if (blobs[id]) return cb()
164 blobs[id] = true
165 getPackageJsonCached(id, function (err, pkg) {
166 if (err) return cb(err)
167 var done = multicb()
168 for (var name in pkg.dependencies || {}) {
169 addPkgBySpec(name, pkg.dependencies[name], done())
170 }
171 for (var name in pkg.optionalDependencies || {}) {
172 addPkgBySpec(name, pkg.optionalDependencies[name], done())
173 }
174 done(cb)
175 })
176 }
177
178 function addPkgBySpec(name, spec, cb) {
179 var versions = {}
180 var distTags = {}
181 pull(
182 getMentions(sbot.links2, {$prefix: 'npm:' + name + ':'}),
183 pull.filter(mentionMatches(null, name, spec)),
184 pull.map(function (mention) {
185 // query to get the messages since links2 does not include the msg id
186 return sbot.links({
187 source: mention.author,
188 dest: mention.link,
189 rel: 'mentions',
190 values: true,
191 })
192 }),
193 pull.flatten(),
194 pull.drain(function (msg) {
195 var c = msg && msg.value && msg.value.content
196 if (!c || !Array.isArray(c.mentions)) return
197 c.mentions.forEach(function (link) {
198 var data = link && link.name && decodeName(link.name)
199 if (!data || data.name !== name) return
200 versions[data.version] = {msg: msg, mention: link, mentionData: data}
201 if (data.distTag) distTags[data.distTag] = data.version
202 })
203 }, function (err) {
204 if (err) return cb(err)
205 var version = distTags[spec]
206 || semver.maxSatisfying(Object.keys(versions), spec)
207 var item = versions[version]
208 if (!item) return cb(new Error('Dependency version not found: ' + name + '@' + spec))
209 msgs[item.msg.key] = item.msg.value
210 var c = item.msg.value.content
211 if (Array.isArray(c.dependencyBranch)) {
212 for (var k = 0; k < c.dependencyBranch.length; k++) {
213 branched[c.dependencyBranch[k]] = true
214 }
215 }
216 // console.log('add', item.msg.key, item.mentionData.name, item.mentionData.version)
217 addPkgById(item.mention.link, cb)
218 })
219 )
220 }
221
222 addPkgById(id, function (err) {
223 if (err) return cb(err)
224 var ids = []
225 for (var key in msgs) {
226 if (!branched[key]) ids.push(key)
227 }
228 cb(null, ids)
229 })
230}
231
232function packageLinks(sbot, feed, id, name, spec) {
233 var matches = mentionMatches(id, name, spec)
234 return pull(
235 sbot.links({
236 source: feed,
237 dest: id,
238 rel: 'mentions',
239 values: true,
240 }),
241 pull.filter(function (msg) {
242 var c = msg && msg.value && msg.value.content
243 return c && Array.isArray(c.mentions) && c.mentions.some(matches)
244 })
245 )
246}
247
248function getVersionBranches(sbot, link, cb) {
249 var data = decodeName(link.name)
250 var msgs = {}, branched = {}
251 pull(
252 getMentions(sbot.links2, {$prefix: 'npm:' + data.name + ':'}),
253 pull.map(function (mention) {
254 return packageLinks(sbot, mention.author, mention.link, data.name)
255 }),
256 pull.flatten(),
257 pull.drain(function (msg) {
258 var c = msg && msg.value && msg.value.content
259 if (!c) return
260 msgs[msg.key] = msg.value
261 if (Array.isArray(c.versionBranch)) {
262 for (var k = 0; k < c.versionBranch.length; k++) {
263 branched[c.versionBranch[k]] = true
264 }
265 }
266 }, function (err) {
267 if (err) return cb(err)
268 var ids = []
269 for (var key in msgs) {
270 if (!branched[key]) ids.push(key)
271 }
272 cb(null, ids)
273 })
274 )
275}
276
277// For each dependency that is not a bundledDependency, get message ids for
278// that dependency name + version.
279function publishSingleMention(sbot, mention, cb) {
280 if (!sbot.links2) return cb(new Error('ssb-links plugin is required to publish ssb-npm packages'))
281 // Calculate dependencyBranch and versionBranch message ids.
282 var value = {
283 type: 'npm-packages',
284 mentions: [mention]
285 }
286 var done = multicb({pluck: 1, spread: true})
287 getDependencyBranches(sbot, mention.link, done())
288 getVersionBranches(sbot, mention, done())
289 done(function (err, dependencyBranches, versionBranches) {
290 if (err) return cb(err)
291 value.dependencyBranch = dependencyBranches || undefined
292 value.versionBranch = versionBranches || undefined
293 publishMsg(sbot, value, cb)
294 })
295}
296
297function publishMentions(sbot, mentions, cb) {
298 // console.error("publishing %s mentions", mentions.length)
299 if (mentions.length === 0) return cb(new Error('Empty mentions list'))
300 // if it is just one mention, fetch and add useful metadata
301 if (mentions.length === 1) return publishSingleMention(sbot, mentions[0], cb)
302 publishMsg(sbot, {
303 type: 'npm-packages',
304 mentions: mentions,
305 }, cb)
306}
307
308exports.publishPkgMentions = function (sbot, mentions, cb) {
309 // try to fit the mentions into as few messages as possible,
310 // while fitting under the message size limit.
311 var msgs = []
312 ;(function next(i, chunks) {
313 if (i >= mentions.length) return cb(null, msgs)
314 var chunkLen = Math.ceil(mentions.length / chunks)
315 publishMentions(sbot, mentions.slice(i, i + chunkLen), function (err, msg) {
316 if (err && /must not be large/.test(err.message)) return next(i, chunks + 1)
317 if (err && msgs.length) return onPartialPublish(err)
318 if (err) return cb(err)
319 msgs.push(msg)
320 next(i + chunkLen, chunks)
321 })
322 })(0, 1)
323 function onPartialPublish(err) {
324 var remaining = mentions.length - i
325 return cb(new Error('Published messages ' +
326 msgs.map(function (msg) { return msg.key }).join(', ') + ' ' +
327 'but failed to publish remaining ' + remaining + ': ' + (err.stack || err)))
328 }
329}
330
331exports.expandPkgMentions = function (sbot, mentions, props, cb) {
332 cb = once(cb)
333 var waiting = 0
334 var expandedMentions = mentions.map(function (link) {
335 var id = link && link.link
336 if (!id) return link
337 waiting++
338 var newLink = {}
339 for (var k in link) newLink[k] = link[k]
340 getPackageJsonFromTarballBlob(sbot, id, function (err, pkg) {
341 if (err) return cb(err)
342 for (var k in props) newLink[k] = pkg[k]
343 if (props.shasum && !pkg.shasum) newLink.shasum = pkg._shasum
344 if (!--waiting) next()
345 })
346 return newLink
347 })
348 if (!waiting) next()
349 function next() {
350 cb(null, expandedMentions)
351 }
352}
353
354function SsbNpmRegistryServer(sbot, config) {
355 this.sbot = sbot
356 this.config = config
357 this.npmConfig = config.npm || {}
358 this.host = this.npmConfig.host || 'localhost'
359 this.fetchAll = this.npmConfig.fetchAll
360 this.needShasum = this.npmConfig.needShasum
361 this.getMsg = memo({cache: lru(100)}, this.getMsg)
362 this.getFeedName = memo({cache: lru(100)}, this.getFeedName)
363 if (sbot.ws) {
364 var wsPort = config.ws && Number(config.ws.port) || '8989'
365 this.wsUrl = 'http://localhost:' + wsPort
366 }
367}
368
369SsbNpmRegistryServer.prototype = Object.create(http.Server.prototype)
370SsbNpmRegistryServer.prototype.constructor = SsbNpmRegistryServer
371
372SsbNpmRegistryServer.prototype.pushBlobs = function (ids, cb) {
373 var self = this
374 if (!self.sbot.blobs.push) return cb(new Error('missing blobs.push'))
375 ;(function next(i) {
376 if (i >= ids.length) return cb()
377 self.sbot.blobs.push(ids[i], function (err) {
378 if (err) return cb(err)
379 next(i+1)
380 })
381 })(0)
382}
383
384function getBlob(sbot, id, cb) {
385 var blobs = sbot.blobs
386 blobs.size(id, function (err, size) {
387 if (err && err.code !== 'ENOENT') return cb(err)
388 if (typeof size === 'number') return cb(null, blobs.get(id), size)
389 var chunks = chunkedBlobs[id]
390 if (chunks) return getLargeBlob(sbot, chunks, cb)
391 var timeout = wantWarnTime > 0 && setTimeout(function () {
392 console.error('Blob taking a long time to fetch:', id)
393 }, wantWarnTime)
394 blobs.want(id, function (err, got) {
395 if (timeout) clearTimeout(timeout)
396 if (err) cb(err)
397 else if (!got) cb('missing blob ' + id)
398 else blobs.size(id, function (err, size) {
399 if (err) return cb(err)
400 cb(null, blobs.get(id), size)
401 })
402 })
403 })
404}
405
406function getLargeBlob(sbot, ids, cb) {
407 var done = multicb({pluck: 1})
408 var totalSize = 0
409 ids.forEach(function (id) {
410 var cb = done()
411 getBlob(sbot, id, function (err, source, size) {
412 if (err) return cb(err)
413 totalSize += size
414 cb(null, source)
415 })
416 })
417 done(function (err, sources) {
418 if (err) return cb(err)
419 cb(null, cat(sources), totalSize)
420 })
421}
422
423function getMentions(links2, name) {
424 return links2.read({
425 query: [
426 {$filter: {rel: ['mentions', name, {$gt: true}]}},
427 {$filter: {dest: {$prefix: '&'}}},
428 {$map: {
429 name: ['rel', 1],
430 size: ['rel', 2],
431 link: 'dest',
432 author: 'source',
433 ts: 'ts'
434 }}
435 ]
436 })
437}
438
439function mentionMatches(id, name, spec) {
440 return function (mention) {
441 var data = mention
442 && (!id || id === mention.link)
443 && mention.name
444 && decodeName(mention.name)
445 return data
446 && data.name === name
447 && (spec ? semver.satisfies(data.version, spec) : true)
448 }
449}
450
451SsbNpmRegistryServer.prototype.getMentions = function (name) {
452 if (!this.sbot.links2) return pull.empty()
453 return getMentions(this.sbot.links2, name)
454}
455
456SsbNpmRegistryServer.prototype.getMsg = function (id, cb) {
457 if (this.sbot.ooo) {
458 var timeout = wantWarnTime > 0 && setTimeout(function () {
459 console.error('Message taking a long time to fetch:', id)
460 }, wantWarnTime)
461 return this.sbot.ooo.get({id: id, timeout: 0}, timeout ? function (err, msg) {
462 clearTimeout(timeout)
463 cb(err, msg)
464 } : cb)
465 }
466 else this.sbot.get(id, function (err, value) {
467 if (err) console.error('Unable to get message:', id)
468 if (err) return cb(err)
469 cb(null, {key: id, value: value})
470 })
471}
472
473SsbNpmRegistryServer.prototype.getFeedName = function (id, cb) {
474 var self = this
475 if (self.sbot.names && self.sbot.names.getSignifier) {
476 self.sbot.names.getSignifier(id, function (err, name) {
477 if (err || !name) tryLinks()
478 else cb(null, name)
479 })
480 } else {
481 tryLinks()
482 }
483 function tryLinks() {
484 if (!self.sbot.links) return nope()
485 pull(
486 self.sbot.links({
487 source: id,
488 dest: id,
489 rel: 'about',
490 values: true,
491 limit: 1,
492 reverse: true,
493 keys: false,
494 meta: false
495 }),
496 pull.map(function (value) {
497 return value && value.content && value.content.name
498 }),
499 pull.filter(Boolean),
500 pull.take(1),
501 pull.collect(function (err, names) {
502 if (err || !names.length) return nope()
503 cb(null, names[0])
504 })
505 )
506 }
507 function nope() {
508 cb(null, '')
509 }
510}
511
512SsbNpmRegistryServer.prototype.streamTree = function (heads, prop) {
513 var self = this
514 var stack = heads.slice()
515 var seen = {}
516 return function (abort, cb) {
517 if (abort) return cb(abort)
518 for (var id; stack.length && seen[id = stack.pop()];);
519 if (!id) return cb(true)
520 seen[id] = true
521 // TODO: use DFS
522 self.getMsg(id, function (err, msg) {
523 if (err) return cb(new Error(err.stack || err))
524 var c = msg && msg.value && msg.value.content
525 var links = c && c[prop]
526 if (Array.isArray(links)) {
527 stack.push.apply(stack, links)
528 } else if (links) {
529 stack.push(links)
530 }
531 cb(null, msg)
532 })
533 }
534}
535
536function Req(server, req, res) {
537 this.server = server
538 this.req = req
539 this.res = res
540 this.blobsToPush = []
541 this.fetchAll = server.fetchAll != null ? server.fetchAll : true
542 var ua = this.req.headers['user-agent']
543 var m = /\bnpm\/([0-9]*)/.exec(ua)
544 var npmVersion = m && m[1]
545 this.needShasum = server.needShasum != null ? server.needShasum :
546 (npmVersion && npmVersion < 5)
547 this.baseUrl = this.server.npmConfig.baseUrl
548 if (this.baseUrl) {
549 this.baseUrl = this.baseUrl.replace(/\/+$/, '')
550 } else {
551 var hostname = req.headers.host
552 || (formatHost(req.socket.localAddress) + ':' + req.socket.localPort)
553 this.baseUrl = 'http://' + hostname
554 }
555 this.blobBaseUrl = this.baseUrl + '/-/blobs/get/'
556}
557
558Req.prototype.serve = function () {
559 if (process.env.DEBUG) {
560 console.log(this.req.method, this.req.url, formatHost(this.req.socket.remoteAddress))
561 }
562 this.res.setTimeout(0)
563 var pathname = this.req.url.replace(/\?.*/, '')
564 var m
565 if ((m = /^\/(\^|%5[Ee])?(%25.*sha256)+(\/.*)$/.exec(pathname))) {
566 try {
567 this.headMsgIds = decodeURIComponent(m[2]).split(',')
568 this.headMsgPlus = !!m[1]
569 // ^ means also include packages published after the head message id
570 } catch(e) {
571 return this.respondError(400, e.stack || e)
572 }
573 pathname = m[3]
574 }
575 if (pathname === '/') return this.serveHome()
576 if (pathname === '/robots.txt') return this.serveRobots()
577 if (pathname === '/-/bootstrap') return this.serveBootstrap()
578 if (pathname === '/-/whoami') return this.serveWhoami()
579 if (pathname === '/-/ping') return this.respond(200, true)
580 if (pathname === '/-/user/org.couchdb.user:1') return this.serveUser1()
581 if (pathname.startsWith('/-/blobs/get/')) return this.serveBlob(pathname.substr(13))
582 if (pathname.startsWith('/-/msg/')) return this.serveMsg(pathname.substr(7))
583 if ((m = /^\/-\/prebuild\/(.*)$/.exec(pathname))) return this.servePrebuild(m[1])
584 if (!/^\/-\//.test(pathname)) return this.servePkg(pathname.substr(1))
585 return this.respond(404, {error: 'Not found'})
586}
587
588Req.prototype.respond = function (status, message) {
589 this.res.writeHead(status, {'content-type': 'application/json'})
590 this.res.end(message && JSON.stringify(message, 0, 2))
591}
592
593Req.prototype.respondError = function (status, message) {
594 this.respond(status, {error: message})
595}
596
597Req.prototype.respondErrorStr = function (status, err) {
598 this.res.writeHead(status, {'content-type': 'text/plain'})
599 this.res.end(err.stack || err)
600}
601
602Req.prototype.respondRaw = function (status, body) {
603 this.res.writeHead(status)
604 this.res.end(body)
605}
606
607Req.prototype.serveHome = function () {
608 var self = this
609 self.res.writeHead(200, {'content-type': 'text/html'})
610 self.res.end('<!doctype html><html><head><meta charset=utf-8>' +
611 '<title>' + escapeHTML(pkg.name) + '</title></head><body>' +
612 '<h1>' + escapeHTML(pkg.name) + '</h1>\n' +
613 '<p><a href="/-/bootstrap">Bootstrap</a></p>\n' +
614 '</body></html>')
615}
616
617Req.prototype.blobDist = function (id) {
618 var m = /^&([^.]+)\.([a-z0-9]+)$/.exec(id)
619 if (!m) throw new Error('bad blob id: ' + id)
620 return {
621 integrity: m[2] + '-' + m[1],
622 tarball: this.blobBaseUrl + id
623 }
624}
625
626Req.prototype.getMsgIdForBlobMention = function (blobId, feedId, cb) {
627 var self = this
628 pull(
629 self.server.sbot.links({
630 source: feedId,
631 dest: blobId,
632 rel: 'mentions',
633 values: true,
634 }),
635 pull.filter(function (msg) {
636 var c = msg && msg.value && msg.value.content
637 return c.type === 'npm-packages'
638 }),
639 pull.collect(function (err, msgs) {
640 if (err) return cb(err)
641 if (msgs.length === 0) return cb(new Error('Unable to find message id for mention ' + blobId + ' ' + feedId))
642 if (msgs.length > 1) console.warn('Warning: multiple messages mentioning blob id ' + blobId + ' ' + feedId)
643 // TODO: make a smarter decision about which message id to use
644 cb(null, msgs.pop().key)
645 })
646 )
647}
648
649Req.prototype.resolvePkg = function (pkgSpec, cb) {
650 var m = /(.[^@]*)(?:@(.*))?/.exec(pkgSpec)
651 if (!m) return cb(new Error('unable to parse spec: \'' + pkgSpec + '\''))
652 var self = this
653 var pkgName = m[1]
654 var spec = m[2] || '*'
655 var versions = {}
656 var distTags = {}
657 pull(
658 self.getMentions({$prefix: 'npm:' + pkgName + ':'}),
659 pull.drain(function (mention) {
660 var data = decodeName(mention.name)
661 if (!data.version) return
662 if (data.distTag) {
663 distTags[data.distTag] = data.version
664 }
665 versions[data.version] = {
666 author: mention.author,
667 name: pkgName,
668 version: data.version,
669 blobId: mention.link
670 }
671 }, function (err) {
672 if (err) return cb(err)
673 var version = distTags[spec]
674 || semver.maxSatisfying(Object.keys(versions), spec)
675 var item = versions[version]
676 if (!item) return cb(new Error('Version not found: ' + pkgName + '@' + spec))
677 self.getMsgIdForBlobMention(item.blobId, item.author, function (err, id) {
678 if (err) return cb(err)
679 item.msgId = id
680 cb(null, item)
681 })
682 })
683 )
684}
685
686Req.prototype.resolvePkgs = function (specs, cb) {
687 var done = multicb({pluck: 1})
688 var self = this
689 specs.forEach(function (spec) {
690 self.resolvePkg(spec, done())
691 })
692 done(cb)
693}
694
695Req.prototype.serveBootstrap = function () {
696 var self = this
697 var pkgs = self.server.npmConfig.defaultPkgs || ['ssb-npm']
698 var postInstallCmd = self.server.npmConfig.postInstallCmd
699 if (postInstallCmd == null) postInstallCmd = 'sbot server'
700 var ssbNpmRegistryName = require('./package.json').name
701 var ssbNpmRegistryVersion = require('./package.json').version
702 var ssbNpmRegistrySpec = ssbNpmRegistryName + '@^' + ssbNpmRegistryVersion
703 var done = multicb({pluck: 1, spread: true})
704 self.resolvePkg(ssbNpmRegistrySpec, done())
705 self.resolvePkgs(pkgs, done())
706 done(function (err, ssbNpmRegistryPkgInfo, pkgsInfo) {
707 if (err) return self.respondErrorStr(500, err.stack || err)
708 if (!ssbNpmRegistryPkgInfo) return self.respondErrorStr(500, 'Missing ssb-npm-registry package')
709 var ssbNpmRegistryBlobId = ssbNpmRegistryPkgInfo.blobId
710 var ssbNpmRegistryBlobHex = idToHex(ssbNpmRegistryBlobId)
711 var ssbNpmRegistryNameVersion = ssbNpmRegistryPkgInfo.name + '-' + ssbNpmRegistryPkgInfo.version
712 var pkgMsgs = pkgsInfo.map(function (info) { return info.msgId })
713 var globalPkgs = pkgsInfo.map(function (info) {
714 return info.name + '@' + info.version
715 }).join(' ')
716 var npmCmd = 'install -g ' + globalPkgs
717 var tmpDir = '/tmp/' + encodeURIComponent(ssbNpmRegistryNameVersion)
718 var wsUrl = self.baseUrl + '/-'
719 var tarballLink = wsUrl + '/blobs/get/' + ssbNpmRegistryBlobId
720
721 var script =
722 'mkdir -p ' + tmpDir + ' && cd ' + tmpDir + ' &&\n' +
723 'wget -q \'' + tarballLink + '\' -O package.tgz &&\n' +
724 'echo \'' + ssbNpmRegistryBlobHex + ' package.tgz\' | sha256sum -c &&\n' +
725 'tar xzf package.tgz &&\n' +
726 './*/bootstrap/bin.js --ws-url ' + wsUrl + ' \\\n' +
727 pkgMsgs.map(function (id) {
728 return ' --branch ' + id + ' \\\n'
729 }).join('') +
730 ' -- ' + npmCmd +
731 (postInstallCmd ? ' &&\n' +postInstallCmd : '') + '\n'
732
733 self.res.writeHead(200, {'Content-type': 'text/plain'})
734 self.res.end(script)
735 })
736}
737
738Req.prototype.serveRobots = function () {
739 this.res.writeHead(200, {'Content-type': 'text/plain'})
740 this.res.end('User-agent: *\nDisallow: /\n')
741}
742
743Req.prototype.serveWhoami = function () {
744 var self = this
745 self.server.sbot.whoami(function (err, feed) {
746 if (err) return self.respondError(500, err.stack || err)
747 self.respond(200, {username: feed.id})
748 })
749}
750
751Req.prototype.serveUser1 = function () {
752 this.respond(this.req.method === 'PUT' ? 201 : 200, {token: '1'})
753}
754
755Req.prototype.serveBlob = function (id) {
756 var self = this
757 if (self.req.headers['if-none-match'] === id) return self.respondRaw(304)
758 getBlob(self.server.sbot, id, function (err, readBlob, size) {
759 if (err) {
760 if (/^invalid/.test(err.message)) return self.respondErrorStr(400, err.message)
761 else return self.respondErrorStr(500, err.message || err)
762 }
763 self.res.writeHead(200, {
764 'Cache-Control': 'public, max-age=315360000',
765 'Content-Length': size,
766 'etag': id
767 })
768 pull(
769 readBlob,
770 toPull(self.res, function (err) {
771 if (err) console.error('[npm-registry]', err)
772 })
773 )
774 })
775}
776
777Req.prototype.serveMsg = function (id) {
778 var self = this
779 try { id = decodeURIComponent(id) }
780 catch (e) {}
781 if (self.req.headers['if-none-match'] === id) return self.respondRaw(304)
782 self.server.getMsg(id, function (err, msg) {
783 if (err) return self.respondError(500, err.message || err)
784 var out = Buffer.from(JSON.stringify(msg, null, 2), 'utf8')
785 self.res.writeHead(200, {
786 'Content-Type': 'application/json',
787 'Cache-Control': 'public, max-age=315360000',
788 'Content-Length': out.length,
789 'etag': id
790 })
791 self.res.end(out)
792 })
793}
794
795function decodeName(name) {
796 var parts = String(name).replace(/\.tgz$/, '').split(':')
797 return {
798 name: parts[1],
799 version: parts[2],
800 distTag: parts[3],
801 }
802}
803
804Req.prototype.getMsgMentions = function (name) {
805 return pull(
806 this.server.streamTree(this.headMsgIds, 'dependencyBranch'),
807 // decryption could be done here
808 pull.map(function (msg) {
809 var c = msg.value && msg.value.content
810 if (!c.mentions || !Array.isArray(c.mentions)) return []
811 return c.mentions.map(function (mention) {
812 return {
813 name: mention.name,
814 size: mention.size,
815 link: mention.link,
816 author: msg.value.author,
817 ts: msg.value.timestamp,
818 }
819 })
820 }),
821 pull.flatten(),
822 pull.filter(typeof name === 'string' ? function (link) {
823 return link.name === name
824 } : name && name.$prefix ? function (link) {
825 return link.name.substr(0, name.$prefix.length) === name.$prefix
826 } : function () {
827 throw new TypeError('unsupported name filter')
828 })
829 )
830}
831
832Req.prototype.getMentions = function (name) {
833 var useMsgMentions = this.headMsgIds
834 var useServerMentions = !this.headMsgIds || this.headMsgPlus
835 if (useServerMentions && !this.server.sbot.links2) {
836 return this.headMsgPlus
837 ? pull.error(new Error('ssb-links plugin is needed for ^msgid queries'))
838 : pull.error(new Error('ssb-links plugin is needed for non-msgid queries'))
839 }
840 return cat([
841 useMsgMentions ? this.getMsgMentions(name) : pull.empty(),
842 useServerMentions ? this.server.getMentions(name) : pull.empty()
843 ])
844}
845
846Req.prototype.getMentionLinks = function (blobId) {
847 return pull(
848 this.headMsgIds
849 ? this.server.streamTree(this.headMsgIds, 'dependencyBranch')
850 : this.server.sbot.links({
851 dest: blobId,
852 rel: 'mentions',
853 values: true,
854 }),
855 // decryption could be done here
856 pull.map(function (msg) {
857 var c = msg.value && msg.value.content
858 return c && Array.isArray(c.mentions) && c.mentions || []
859 }),
860 pull.flatten(),
861 pull.filter(function (link) {
862 return link && link.link === blobId
863 })
864 )
865}
866
867Req.prototype.servePkg = function (pathname) {
868 var self = this
869 var parts = pathname.split('/')
870 var pkgName = parts.shift().replace(/%2f/i, '/')
871 if (parts[0] === '-rev') return this.respondError(501, 'Unpublish is not supported')
872 var spec = parts.shift()
873 if (spec) try { spec = decodeURIComponent(spec) } finally {}
874 if (parts.length > 0) return this.respondError(404)
875 if (self.req.method === 'PUT') return self.publishPkg(pkgName)
876 var obj = {
877 _id: pkgName,
878 name: pkgName,
879 'dist-tags': {},
880 versions: {},
881 time: {}
882 }
883 var distTags = {/* <tag>: {version, ts}*/}
884 pull(
885 self.getMentions({$prefix: 'npm:' + pkgName + ':'}),
886 pull.drain(function (mention) {
887 var data = decodeName(mention.name)
888 if (!data.version) return
889 if (data.distTag) {
890 var tag = distTags[data.distTag]
891 if (!tag || mention.ts > tag.ts) {
892 /* TODO: sort by causal order instead of only by timestamps */
893 distTags[data.distTag] = {ts: mention.ts, version: data.version}
894 }
895 }
896 obj.versions[data.version] = {
897 author: {
898 url: mention.author
899 },
900 name: pkgName,
901 version: data.version,
902 dist: self.blobDist(mention.link)
903 }
904 var ts = new Date(mention.ts)
905 if (ts > obj.time.updated || !obj.time.updated) obj.time.updated = ts
906 if (ts < obj.time.created || !obj.time.created) obj.time.created = ts
907 obj.time[data.version] = ts.toISOString()
908 }, function (err) {
909 if (err) return self.respondError(500, err.stack || err)
910 for (var tag in distTags) {
911 obj['dist-tags'][tag] = distTags[tag].version
912 }
913 if (spec) resolveSpec()
914 else if (self.fetchAll) resolveAll()
915 else resolved()
916 })
917 )
918 function resolveSpec() {
919 var version = obj['dist-tags'][spec]
920 || semver.maxSatisfying(Object.keys(obj.versions), spec)
921 obj = obj.versions[version]
922 if (!obj) return self.respondError(404, 'version not found: ' + spec)
923 self.populatePackageJson(obj, function (err, pkg) {
924 if (err) return resolved(err)
925 obj = pkg || obj
926 resolved()
927 })
928 }
929 function resolveVersion(version, cb) {
930 self.populatePackageJson(obj.versions[version], function (err, pkg) {
931 if (err) return cb(err)
932 if (pkg) obj.versions[version] = pkg
933 if (pkg && pkg.license && !obj.license) obj.license = pkg.license
934 cb()
935 })
936 }
937 function resolveAll() {
938 var done = multicb()
939 for (var version in obj.versions) {
940 resolveVersion(version, done())
941 }
942 done(resolved)
943 }
944 function resolved(err) {
945 if (err) return self.respondError(500, err.stack || err)
946 self.respond(200, obj)
947 }
948}
949
950Req.prototype.servePrebuild = function (name) {
951 var self = this
952 var getMention = self.getMentions('prebuild:' + name)
953 var blobsByAuthor = {/* <author>: BlobId */}
954 pull(getMention, pull.drain(function (link) {
955 blobsByAuthor[link.author] = link.link
956 }, function (err) {
957 if (err) return self.respondError(500, err.stack || err)
958 var authorsByLink = {/* <BlobId>: [FeedId...] */}
959 var blobId
960 for (var feed in blobsByAuthor) {
961 var blob = blobId = blobsByAuthor[feed]
962 var feeds = authorsByLink[blob] || (authorsByLink[blob] = [])
963 feeds.push(feed)
964 }
965 switch (Object.keys(authorsByLink).length) {
966 case 0:
967 return self.respondError(404, 'Not Found')
968 case 1:
969 return self.serveBlob(blobId)
970 default:
971 return self.respond(300, {choices: authorsByLink})
972 }
973 }))
974}
975
976var localhosts = {
977 '::1': true,
978 '127.0.0.1': true,
979 '::ffff:127.0.0.1': true,
980}
981
982Req.prototype.publishPkg = function (pkgName) {
983 var self = this
984 var remoteAddress = self.req.socket.remoteAddress
985 if (!(remoteAddress in localhosts)) {
986 return self.respondError(403, 'You may not publish as this user.')
987 }
988
989 var chunks = []
990 self.req.on('data', function (data) {
991 chunks.push(data)
992 })
993 self.req.on('end', function () {
994 var data
995 try {
996 data = JSON.parse(Buffer.concat(chunks))
997 } catch(e) {
998 return self.respondError(400, e.stack)
999 }
1000 return self.publishPkg2(pkgName, data || {})
1001 })
1002}
1003
1004Req.prototype.publishPkg2 = function (name, data) {
1005 var self = this
1006 if (data.users) console.trace('[npm-registry] users property is not supported')
1007 var attachments = data._attachments || {}
1008 var links = {/* <name>-<version>.tgz: {link: <BlobId>, size: number} */}
1009 var done = multicb()
1010 function addAttachmentAsBlob(filename, cb) {
1011 var data = attachments[filename].data
1012 var tarball = Buffer.from(data, 'base64')
1013 var length = attachments[filename].length
1014 if (length && length !== tarball.length) return self.respondError(400,
1015 'Length mismatch for attachment \'' + filename + '\'')
1016 self.server.sbot.blobs.add(function (err, id) {
1017 if (err) return cb(err)
1018 self.blobsToPush.push(id)
1019 var shasum = crypto.createHash('sha1').update(tarball).digest('hex')
1020 links[filename] = {link: id, size: tarball.length, shasum: shasum}
1021 cb()
1022 })(pull.once(tarball))
1023 }
1024 for (var filename in attachments) {
1025 addAttachmentAsBlob(filename, done())
1026 }
1027 done(function (err) {
1028 if (err) return self.respondError(500, err.stack || err)
1029 try {
1030 self.publishPkg3(name, data, links)
1031 } catch(e) {
1032 self.respondError(500, e.stack || e)
1033 }
1034 })
1035}
1036
1037Req.prototype.publishPkg3 = function (name, data, links) {
1038 var self = this
1039 var versions = data.versions || {}
1040 var linksByVersion = {/* <version>: link */}
1041
1042 // associate tarball blobs with versions
1043 for (var version in versions) {
1044 var pkg = versions[version]
1045 if (!pkg) return self.respondError(400, 'Bad package object')
1046 if (!pkg.dist) return self.respondError(400, 'Missing package dist property')
1047 if (!pkg.dist.tarball) return self.respondError(400, 'Missing dist.tarball property')
1048 if (pkg.deprecated) return self.respondError(501, 'Deprecation is not supported')
1049 var m = /\/-\/([^\/]+|@[^\/]+\/[^\/]+)$/.exec(pkg.dist.tarball)
1050 if (!m) return self.respondError(400, 'Bad tarball URL \'' + pkg.dist.tarball + '\'')
1051 var filename = m[1]
1052 var link = links[filename]
1053 if (!link) return self.respondError(501, 'Unable to find attachment \'' + filename + '\'')
1054 // TODO?: try to find missing tarball mentioned in other messages
1055 if (pkg.version && pkg.version !== version)
1056 return self.respondError(400, 'Mismatched package version: ' + [pkg.version, version])
1057 linksByVersion[version] = link
1058 link.version = version
1059 link.license = pkg.license
1060 link.dependencies = pkg.dependencies || {}
1061 link.optionalDependencies = pkg.optionalDependencies
1062 link.bundledDependencies = pkg.bundledDependencies || pkg.bundleDependencies
1063 }
1064
1065 // associate blobs with dist-tags
1066 var tags = data['dist-tags'] || {}
1067 for (var tag in tags) {
1068 var version = tags[tag]
1069 var link = linksByVersion[version]
1070 if (!link) return self.respondError(501, 'Setting a dist-tag for a version not being published is not supported.')
1071 // TODO?: support setting dist-tag without version,
1072 // by looking up a tarball blob for the version
1073 link.tag = tag
1074 }
1075
1076 // compute blob links to publish
1077 var mentions = []
1078 for (var filename in links) {
1079 var link = links[filename] || {}
1080 if (!link.version) return self.respondError(400, 'Attachment ' + filename + ' was not linked to in the package metadata')
1081 mentions.push({
1082 name: 'npm:' + name + ':' + link.version + (link.tag ? ':' + link.tag : ''),
1083 link: link.link,
1084 size: link.size,
1085 shasum: link.shasum,
1086 license: link.license,
1087 dependencies: link.dependencies,
1088 optionalDependencies: link.optionalDependencies,
1089 bundledDependencies: link.bundledDependencies,
1090 })
1091 }
1092 return self.publishPkgs(mentions)
1093}
1094
1095Req.prototype.publishPkgs = function (mentions) {
1096 var self = this
1097 exports.publishPkgMentions(self.server.sbot, mentions, function (err, msgs) {
1098 if (err) return self.respondError(500, err.stack || err)
1099 self.server.pushBlobs(self.blobsToPush, function (err) {
1100 if (err) console.error('[npm-registry] Failed to push blob ' + id + ': ' + (err.stack || err))
1101 self.respond(201)
1102 console.log(msgs.map(function (msg) { return msg.key }).join('\n'))
1103 })
1104 })
1105}
1106
1107Req.prototype.populatePackageJson = function (obj, cb) {
1108 var self = this
1109 var blobId = obj.dist.tarball.replace(/.*\/blobs\/get\//, '')
1110 var deps, depsWithOptionalDeps, bundledDeps, shasum, license
1111
1112 // Combine metadata from mentions of the blob, to construct most accurate
1113 // metadata, including dependencies info. If needed, fall back to fetching
1114 // metadata from the tarball blob.
1115
1116 pull(
1117 self.getMentionLinks(blobId),
1118 pull.drain(function (link) {
1119 if (link.dependencies) deps = link.dependencies
1120 // if optionalDependencies is present, let the dependencies value
1121 // override that from other links. because old versions that didn't
1122 // publish optionalDependencies are missing optionalDependencies from
1123 // their dependencies.
1124 if (link.optionalDependencies) depsWithOptionalDeps = {
1125 deps: link.dependencies,
1126 optionalDeps: link.optionalDependencies
1127 }
1128 if (link.shasum) shasum = link.shasum
1129 if (link.license) license = link.license
1130 bundledDeps = link.bundledDependencies || link.bundleDependencies || bundledDeps
1131 // how to handle multiple assignments of dependencies to a package?
1132 }, function (err) {
1133 if (err) return cb(new Error(err.stack || err))
1134 if (deps && (shasum || !self.needShasum)) {
1135 // assume that the dependencies in the links to the blob are
1136 // correct.
1137 if (depsWithOptionalDeps) {
1138 obj.dependencies = depsWithOptionalDeps.deps || deps
1139 obj.optionalDependencies = depsWithOptionalDeps.optionalDeps
1140 } else {
1141 obj.dependencies = deps
1142 }
1143 obj.bundledDependencies = bundledDeps
1144 obj.license = license
1145 if (shasum) obj.dist.shasum = obj._shasum = shasum
1146 next(obj)
1147 } else {
1148 // get dependencies from the tarball
1149 getPackageJsonFromTarballBlob(self.server.sbot, blobId, function (err, pkg) {
1150 if (err) return cb(err)
1151 pkg.dist = obj.dist
1152 pkg.dist.shasum = pkg._shasum
1153 pkg.author = pkg.author || obj.author
1154 pkg.version = pkg.version || obj.version
1155 pkg.name = pkg.name || obj.name
1156 pkg.license = pkg.license || obj.license
1157 next(pkg)
1158 })
1159 }
1160 })
1161 )
1162 function next(pkg) {
1163 var feedId = obj.author.url
1164 self.server.getFeedName(feedId, function (err, name) {
1165 if (err) console.error('[npm-registry]', err.stack || err), name = ''
1166 pkg._npmUser = {
1167 email: feedId,
1168 name: name
1169 }
1170 cb(null, pkg)
1171 })
1172 }
1173}
1174
1175function getPackageJsonFromTarballBlob(sbot, id, cb) {
1176 var self = this
1177 getBlob(sbot, id, function (err, readBlob) {
1178 if (err) return cb(err)
1179 cb = once(cb)
1180 var extract = tar.extract()
1181 var pkg, shasum
1182 extract.on('entry', function (header, stream, next) {
1183 if (/^[^\/]*\/package\.json$/.test(header.name)) {
1184 pull(toPull.source(stream), pull.collect(function (err, bufs) {
1185 if (err) return cb(err)
1186 try { pkg = JSON.parse(Buffer.concat(bufs)) }
1187 catch(e) { return cb(e) }
1188 next()
1189 }))
1190 } else {
1191 stream.on('end', next)
1192 stream.resume()
1193 }
1194 })
1195 extract.on('finish', function () {
1196 pkg._shasum = shasum
1197 cb(null, pkg)
1198 })
1199 pull(
1200 readBlob,
1201 hash('sha1', 'hex', function (err, sum) {
1202 if (err) return cb(err)
1203 shasum = sum
1204 }),
1205 toPull(zlib.createGunzip()),
1206 toPull(extract)
1207 )
1208 })
1209}
1210

Built with git-ssb-web