git ssb

3+

cel / ssb-npm-registry



Tree: 49058f5f9a406656f03018486b4535f0bcb5250b

Files: 49058f5f9a406656f03018486b4535f0bcb5250b / index.js

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

Built with git-ssb-web