git ssb

3+

cel / ssb-npm-registry



Tree: 9bc172fc9883a9eb2fa1ceee027cc37daadcc6e6

Files: 9bc172fc9883a9eb2fa1ceee027cc37daadcc6e6 / index.js

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

Built with git-ssb-web