var http = require('http') var os = require('os') var path = require('path') var fs = require('fs') var crypto = require('crypto') var pkg = require('./package') var semver = require('semver') var toPull = require('stream-to-pull-stream') var pull = require('pull-stream') var tar = require('tar-stream') var zlib = require('zlib') var cat = require('pull-cat') var hash = require('pull-hash') var multicb = require('multicb') var memo = require('asyncmemo') var lru = require('hashlru') var chunkedBlobs = require('./largeblobs') function escapeHTML(str) { return String(str) .replace(//g, '>') } function idToHex(id) { var b64 = String(id).replace(/^[%#&]|\.[a-z0-9]*$/g, '') return Buffer.from(b64, 'base64').toString('hex') } function onceify(fn, self) { var cbs = [], err, data return function (cb) { if (fn) { cbs.push(cb) fn.call(self, function (_err, _data) { err = _err, data = _data var _cbs = cbs cbs = null while (_cbs.length) _cbs.shift()(err, data) }) fn = null } else if (cbs) { cbs.push(cb) } else { cb(err, data) } } } function once(cb) { var done return function (err, result) { if (done) { if (err) console.trace(err) } else { done = true cb(err, result) } } } function npmLogin(registryAddress, cb) { var tokenLine = registryAddress.replace(/^http:/, '') + ':_authToken=1' var filename = path.join(os.homedir(), '.npmrc') fs.readFile(filename, 'utf8', function (err, data) { if (err && err.code === 'ENOENT') data = '' else if (err) return cb(new Error(err.stack)) var lines = data ? data.split('\n') : [] if (lines.indexOf(tokenLine) > -1) return cb() var trailingNewline = (lines.length === 0 || lines[lines.length-1] === '') var line = trailingNewline ? tokenLine + '\n' : '\n' + tokenLine fs.appendFile(filename, line, cb) }) } function formatHost(host) { if (host === '::1') return 'localhost' host = host.replace(/^::ffff:/, '') return host[0] !== '[' && /:.*:/.test(host) ? '[' + host + ']' : host } var wantWarnTime = Number(process.env.WANT_WARN_TIME) || 60e3 exports.name = 'npm-registry' exports.version = '1.0.0' exports.manifest = { getAddress: 'async' } exports.init = function (sbot, config) { var conf = config.npm || {} var port = conf.port || 8043 var host = conf.host || 'localhost' var autoAuth = conf.autoAuth !== false var listenUrl var server = http.createServer(exports.respond(sbot, config)) var getAddress = onceify(function (cb) { server.on('error', cb) server.listen(port, host, function () { server.removeListener('error', cb) var addr = this.address() var listenHost = addr.address var regHost = listenHost === '::' ? '::1' : listenHost === '0.0.0.0' ? '127.0.0.1' : listenHost var regUrl = 'http://' + formatHost(regHost) + ':' + addr.port listenUrl = 'http://' + formatHost(listenHost) + ':' + addr.port if (autoAuth) npmLogin(regUrl, next) else next() function next(err) { cb(err, regUrl) } }) sbot.on('close', function () { server.close() }) }) /* getAddress called by local RPC is used to discover the local * ssb-npm-registry address that can be given to local npm clients. However, * when running the server we output the address the server is listening on, * to avoid misleading situations like saying listening on localhost but * actually listening on a wildcard address. */ getAddress(function (err) { if (err) return console.error('[npm-registry]', err.stack || err) console.log('[npm-registry] Listening on ' + listenUrl) }) return { getAddress: getAddress } } exports.respond = function (sbot, config) { var reg = new SsbNpmRegistryServer(sbot, config) return function (req, res) { new Req(reg, req, res).serve() } } function publishMsg(sbot, value, cb) { var gotExpectedPrevious = false sbot.publish(value, function next(err, msg) { if (err && /^expected previous:/.test(err.message)) { // retry once on this error if (gotExpectedPrevious) return cb(err) gotExpectedPrevious = true return sbot.publish(value, next) } cb(err, msg) }) } function getDependencyBranches(sbot, id, cb) { // get ids of heads of tree of dependencyBranch message links to include all // the dependencies of the given tarball id. var getPackageJsonCached = memo(getPackageJsonFromTarballBlob, sbot) var msgs = {} var branched = {} var blobs = {} function addPkgById(id, cb) { if (blobs[id]) return cb() blobs[id] = true getPackageJsonCached(id, function (err, pkg) { if (err) return cb(err) var done = multicb() for (var name in pkg.dependencies || {}) { addPkgBySpec(name, pkg.dependencies[name], done()) } for (var name in pkg.optionalDependencies || {}) { addPkgBySpec(name, pkg.optionalDependencies[name], done()) } done(cb) }) } function addPkgBySpec(name, spec, cb) { var versions = {} var distTags = {} pull( getMentions(sbot.links2, {$prefix: 'npm:' + name + ':'}), pull.filter(mentionMatches(null, name, spec)), pull.map(function (mention) { // query to get the messages since links2 does not include the msg id return sbot.links({ source: mention.author, dest: mention.link, rel: 'mentions', values: true, }) }), pull.flatten(), pull.drain(function (msg) { var c = msg && msg.value && msg.value.content if (!c || !Array.isArray(c.mentions)) return c.mentions.forEach(function (link) { var data = link && link.name && decodeName(link.name) if (!data || data.name !== name) return versions[data.version] = {msg: msg, mention: link, mentionData: data} if (data.distTag) distTags[data.distTag] = data.version }) }, function (err) { if (err) return cb(err) var version = distTags[spec] || semver.maxSatisfying(Object.keys(versions), spec) var item = versions[version] if (!item) return cb(new Error('Dependency version not found: ' + name + '@' + spec)) msgs[item.msg.key] = item.msg.value var c = item.msg.value.content if (Array.isArray(c.dependencyBranch)) { for (var k = 0; k < c.dependencyBranch.length; k++) { branched[c.dependencyBranch[k]] = true } } // console.log('add', item.msg.key, item.mentionData.name, item.mentionData.version) addPkgById(item.mention.link, cb) }) ) } addPkgById(id, function (err) { if (err) return cb(err) var ids = [] for (var key in msgs) { if (!branched[key]) ids.push(key) } cb(null, ids) }) } function packageLinks(sbot, feed, id, name, spec) { var matches = mentionMatches(id, name, spec) return pull( sbot.links({ source: feed, dest: id, rel: 'mentions', values: true, }), pull.filter(function (msg) { var c = msg && msg.value && msg.value.content return c && Array.isArray(c.mentions) && c.mentions.some(matches) }) ) } function getVersionBranches(sbot, link, cb) { var data = decodeName(link.name) var msgs = {}, branched = {} pull( getMentions(sbot.links2, {$prefix: 'npm:' + data.name + ':'}), pull.map(function (mention) { return packageLinks(sbot, mention.author, mention.link, data.name) }), pull.flatten(), pull.drain(function (msg) { var c = msg && msg.value && msg.value.content if (!c) return msgs[msg.key] = msg.value if (Array.isArray(c.versionBranch)) { for (var k = 0; k < c.versionBranch.length; k++) { branched[c.versionBranch[k]] = true } } }, function (err) { if (err) return cb(err) var ids = [] for (var key in msgs) { if (!branched[key]) ids.push(key) } cb(null, ids) }) ) } // For each dependency that is not a bundledDependency, get message ids for // that dependency name + version. function publishSingleMention(sbot, mention, cb) { if (!sbot.links2) return cb(new Error('ssb-links plugin is required to publish ssb-npm packages')) // Calculate dependencyBranch and versionBranch message ids. var value = { type: 'npm-packages', mentions: [mention] } var done = multicb({pluck: 1, spread: true}) getDependencyBranches(sbot, mention.link, done()) getVersionBranches(sbot, mention, done()) done(function (err, dependencyBranches, versionBranches) { if (err) return cb(err) value.dependencyBranch = dependencyBranches || undefined value.versionBranch = versionBranches || undefined publishMsg(sbot, value, cb) }) } function publishMentions(sbot, mentions, cb) { // console.error("publishing %s mentions", mentions.length) if (mentions.length === 0) return cb(new Error('Empty mentions list')) // if it is just one mention, fetch and add useful metadata if (mentions.length === 1) return publishSingleMention(sbot, mentions[0], cb) publishMsg(sbot, { type: 'npm-packages', mentions: mentions, }, cb) } exports.publishPkgMentions = function (sbot, mentions, cb) { // try to fit the mentions into as few messages as possible, // while fitting under the message size limit. var msgs = [] ;(function next(i, chunks) { if (i >= mentions.length) return cb(null, msgs) var chunkLen = Math.ceil(mentions.length / chunks) publishMentions(sbot, mentions.slice(i, i + chunkLen), function (err, msg) { if (err && /must not be large/.test(err.message)) return next(i, chunks + 1) if (err && msgs.length) return onPartialPublish(err) if (err) return cb(err) msgs.push(msg) next(i + chunkLen, chunks) }) })(0, 1) function onPartialPublish(err) { var remaining = mentions.length - i return cb(new Error('Published messages ' + msgs.map(function (msg) { return msg.key }).join(', ') + ' ' + 'but failed to publish remaining ' + remaining + ': ' + (err.stack || err))) } } exports.expandPkgMentions = function (sbot, mentions, props, cb) { cb = once(cb) var waiting = 0 var expandedMentions = mentions.map(function (link) { var id = link && link.link if (!id) return link waiting++ var newLink = {} for (var k in link) newLink[k] = link[k] getPackageJsonFromTarballBlob(sbot, id, function (err, pkg) { if (err) return cb(err) for (var k in props) newLink[k] = pkg[k] if (props.shasum && !pkg.shasum) newLink.shasum = pkg._shasum if (!--waiting) next() }) return newLink }) if (!waiting) next() function next() { cb(null, expandedMentions) } } function SsbNpmRegistryServer(sbot, config) { this.sbot = sbot this.config = config this.npmConfig = config.npm || {} this.host = this.npmConfig.host || 'localhost' this.fetchAll = this.npmConfig.fetchAll this.needShasum = this.npmConfig.needShasum this.getMsg = memo({cache: lru(100)}, this.getMsg) this.getFeedName = memo({cache: lru(100)}, this.getFeedName) if (sbot.ws) { var wsPort = config.ws && Number(config.ws.port) || '8989' this.wsUrl = 'http://localhost:' + wsPort } } SsbNpmRegistryServer.prototype = Object.create(http.Server.prototype) SsbNpmRegistryServer.prototype.constructor = SsbNpmRegistryServer SsbNpmRegistryServer.prototype.pushBlobs = function (ids, cb) { var self = this if (!self.sbot.blobs.push) return cb(new Error('missing blobs.push')) ;(function next(i) { if (i >= ids.length) return cb() self.sbot.blobs.push(ids[i], function (err) { if (err) return cb(err) next(i+1) }) })(0) } function getBlob(sbot, id, cb) { var blobs = sbot.blobs blobs.size(id, function (err, size) { if (err && err.code !== 'ENOENT') return cb(err) if (typeof size === 'number') return cb(null, blobs.get(id), size) var chunks = chunkedBlobs[id] if (chunks) return getLargeBlob(sbot, chunks, cb) var timeout = wantWarnTime > 0 && setTimeout(function () { console.error('Blob taking a long time to fetch:', id) }, wantWarnTime) blobs.want(id, function (err, got) { if (timeout) clearTimeout(timeout) if (err) cb(err) else if (!got) cb('missing blob ' + id) else blobs.size(id, function (err, size) { if (err) return cb(err) cb(null, blobs.get(id), size) }) }) }) } function getLargeBlob(sbot, ids, cb) { var done = multicb({pluck: 1}) var totalSize = 0 ids.forEach(function (id) { var cb = done() getBlob(sbot, id, function (err, source, size) { if (err) return cb(err) totalSize += size cb(null, source) }) }) done(function (err, sources) { if (err) return cb(err) cb(null, cat(sources), totalSize) }) } function getMentions(links2, name) { return links2.read({ query: [ {$filter: {rel: ['mentions', name, {$gt: true}]}}, {$filter: {dest: {$prefix: '&'}}}, {$map: { name: ['rel', 1], size: ['rel', 2], link: 'dest', author: 'source', ts: 'ts' }} ] }) } function mentionMatches(id, name, spec) { return function (mention) { var data = mention && (!id || id === mention.link) && mention.name && decodeName(mention.name) return data && data.name === name && (spec ? semver.satisfies(data.version, spec) : true) } } SsbNpmRegistryServer.prototype.getMentions = function (name) { if (!this.sbot.links2) return pull.empty() return getMentions(this.sbot.links2, name) } SsbNpmRegistryServer.prototype.getMsg = function (id, cb) { if (this.sbot.ooo) { var timeout = wantWarnTime > 0 && setTimeout(function () { console.error('Message taking a long time to fetch:', id) }, wantWarnTime) return this.sbot.ooo.get({id: id, timeout: 0}, timeout ? function (err, msg) { clearTimeout(timeout) cb(err, msg) } : cb) } else this.sbot.get(id, function (err, value) { if (err) console.error('Unable to get message:', id) if (err) return cb(err) cb(null, {key: id, value: value}) }) } SsbNpmRegistryServer.prototype.getFeedName = function (id, cb) { var self = this if (self.sbot.names && self.sbot.names.getSignifier) { self.sbot.names.getSignifier(id, function (err, name) { if (err || !name) tryLinks() else cb(null, name) }) } else { tryLinks() } function tryLinks() { if (!self.sbot.links) return nope() pull( self.sbot.links({ source: id, dest: id, rel: 'about', values: true, limit: 1, reverse: true, keys: false, meta: false }), pull.map(function (value) { return value && value.content && value.content.name }), pull.filter(Boolean), pull.take(1), pull.collect(function (err, names) { if (err || !names.length) return nope() cb(null, names[0]) }) ) } function nope() { cb(null, '') } } SsbNpmRegistryServer.prototype.streamTree = function (heads, prop) { var self = this var stack = heads.slice() var seen = {} return function (abort, cb) { if (abort) return cb(abort) for (var id; stack.length && seen[id = stack.pop()];); if (!id) return cb(true) seen[id] = true // TODO: use DFS self.getMsg(id, function (err, msg) { if (err) return cb(new Error(err.stack || err)) var c = msg && msg.value && msg.value.content var links = c && c[prop] if (Array.isArray(links)) { stack.push.apply(stack, links) } else if (links) { stack.push(links) } cb(null, msg) }) } } function Req(server, req, res) { this.server = server this.req = req this.res = res this.blobsToPush = [] this.fetchAll = server.fetchAll != null ? server.fetchAll : true var ua = this.req.headers['user-agent'] var m = /\bnpm\/([0-9]*)/.exec(ua) var npmVersion = m && m[1] this.needShasum = server.needShasum != null ? server.needShasum : (npmVersion && npmVersion < 5) this.baseUrl = this.server.npmConfig.baseUrl if (this.baseUrl) { this.baseUrl = this.baseUrl.replace(/\/+$/, '') } else { var hostname = req.headers.host || (formatHost(req.socket.localAddress) + ':' + req.socket.localPort) this.baseUrl = 'http://' + hostname } this.blobBaseUrl = this.baseUrl + '/-/blobs/get/' } Req.prototype.serve = function () { if (process.env.DEBUG) { console.log(this.req.method, this.req.url, formatHost(this.req.socket.remoteAddress)) } this.res.setTimeout(0) var pathname = this.req.url.replace(/\?.*/, '') var m if ((m = /^\/(\^|%5[Ee])?(%25.*sha256)+(\/.*)$/.exec(pathname))) { try { this.headMsgIds = decodeURIComponent(m[2]).split(',') this.headMsgPlus = !!m[1] // ^ means also include packages published after the head message id } catch(e) { return this.respondError(400, e.stack || e) } pathname = m[3] } if (pathname === '/') return this.serveHome() if (pathname === '/robots.txt') return this.serveRobots() if (pathname === '/-/bootstrap') return this.serveBootstrap() if (pathname === '/-/whoami') return this.serveWhoami() if (pathname === '/-/ping') return this.respond(200, true) if (pathname === '/-/user/org.couchdb.user:1') return this.serveUser1() if (pathname.startsWith('/-/blobs/get/')) return this.serveBlob(pathname.substr(13)) if (pathname.startsWith('/-/msg/')) return this.serveMsg(pathname.substr(7)) if ((m = /^\/-\/prebuild\/(.*)$/.exec(pathname))) return this.servePrebuild(m[1]) if (!/^\/-\//.test(pathname)) return this.servePkg(pathname.substr(1)) return this.respond(404, {error: 'Not found'}) } Req.prototype.respond = function (status, message) { this.res.writeHead(status, {'content-type': 'application/json'}) this.res.end(message && JSON.stringify(message, 0, 2)) } Req.prototype.respondError = function (status, message) { this.respond(status, {error: message}) } Req.prototype.respondErrorStr = function (status, err) { this.res.writeHead(status, {'content-type': 'text/plain'}) this.res.end(err.stack || err) } Req.prototype.respondRaw = function (status, body) { this.res.writeHead(status) this.res.end(body) } Req.prototype.serveHome = function () { var self = this self.res.writeHead(200, {'content-type': 'text/html'}) self.res.end('' + '' + escapeHTML(pkg.name) + '' + '

' + escapeHTML(pkg.name) + '

\n' + '

Bootstrap

\n' + '') } Req.prototype.blobDist = function (id) { var m = /^&([^.]+)\.([a-z0-9]+)$/.exec(id) if (!m) throw new Error('bad blob id: ' + id) return { integrity: m[2] + '-' + m[1], tarball: this.blobBaseUrl + id } } Req.prototype.getMsgIdForBlobMention = function (blobId, feedId, cb) { var self = this pull( self.server.sbot.links({ source: feedId, dest: blobId, rel: 'mentions', values: true, }), pull.filter(function (msg) { var c = msg && msg.value && msg.value.content return c.type === 'npm-packages' }), pull.collect(function (err, msgs) { if (err) return cb(err) if (msgs.length === 0) return cb(new Error('Unable to find message id for mention ' + blobId + ' ' + feedId)) if (msgs.length > 1) console.warn('Warning: multiple messages mentioning blob id ' + blobId + ' ' + feedId) // TODO: make a smarter decision about which message id to use cb(null, msgs.pop().key) }) ) } Req.prototype.resolvePkg = function (pkgSpec, cb) { var m = /(.[^@]*)(?:@(.*))?/.exec(pkgSpec) if (!m) return cb(new Error('unable to parse spec: \'' + pkgSpec + '\'')) var self = this var pkgName = m[1] var spec = m[2] || '*' var versions = {} var distTags = {} pull( self.getMentions({$prefix: 'npm:' + pkgName + ':'}), pull.drain(function (mention) { var data = decodeName(mention.name) if (!data.version) return if (data.distTag) { distTags[data.distTag] = data.version } versions[data.version] = { author: mention.author, name: pkgName, version: data.version, blobId: mention.link } }, function (err) { if (err) return cb(err) var version = distTags[spec] || semver.maxSatisfying(Object.keys(versions), spec) var item = versions[version] if (!item) return cb(new Error('Version not found: ' + pkgName + '@' + spec)) self.getMsgIdForBlobMention(item.blobId, item.author, function (err, id) { if (err) return cb(err) item.msgId = id cb(null, item) }) }) ) } Req.prototype.resolvePkgs = function (specs, cb) { var done = multicb({pluck: 1}) var self = this specs.forEach(function (spec) { self.resolvePkg(spec, done()) }) done(cb) } Req.prototype.serveBootstrap = function () { var self = this var pkgs = self.server.npmConfig.defaultPkgs || ['ssb-npm'] var postInstallCmd = self.server.npmConfig.postInstallCmd if (postInstallCmd == null) postInstallCmd = 'sbot server' var ssbNpmRegistryName = require('./package.json').name var ssbNpmRegistryVersion = require('./package.json').version var ssbNpmRegistrySpec = ssbNpmRegistryName + '@^' + ssbNpmRegistryVersion var done = multicb({pluck: 1, spread: true}) self.resolvePkg(ssbNpmRegistrySpec, done()) self.resolvePkgs(pkgs, done()) done(function (err, ssbNpmRegistryPkgInfo, pkgsInfo) { if (err) return self.respondErrorStr(500, err.stack || err) if (!ssbNpmRegistryPkgInfo) return self.respondErrorStr(500, 'Missing ssb-npm-registry package') var ssbNpmRegistryBlobId = ssbNpmRegistryPkgInfo.blobId var ssbNpmRegistryBlobHex = idToHex(ssbNpmRegistryBlobId) var ssbNpmRegistryNameVersion = ssbNpmRegistryPkgInfo.name + '-' + ssbNpmRegistryPkgInfo.version var pkgMsgs = pkgsInfo.map(function (info) { return info.msgId }) var globalPkgs = pkgsInfo.map(function (info) { return info.name + '@' + info.version }).join(' ') var npmCmd = 'install -g ' + globalPkgs var tmpDir = '/tmp/' + encodeURIComponent(ssbNpmRegistryNameVersion) var wsUrl = self.baseUrl + '/-' var tarballLink = wsUrl + '/blobs/get/' + ssbNpmRegistryBlobId var script = 'mkdir -p ' + tmpDir + ' && cd ' + tmpDir + ' &&\n' + 'wget -q \'' + tarballLink + '\' -O package.tgz &&\n' + 'echo \'' + ssbNpmRegistryBlobHex + ' package.tgz\' | sha256sum -c &&\n' + 'tar xzf package.tgz &&\n' + './*/bootstrap/bin.js --ws-url ' + wsUrl + ' \\\n' + pkgMsgs.map(function (id) { return ' --branch ' + id + ' \\\n' }).join('') + ' -- ' + npmCmd + (postInstallCmd ? ' &&\n' +postInstallCmd : '') + '\n' self.res.writeHead(200, {'Content-type': 'text/plain'}) self.res.end(script) }) } Req.prototype.serveRobots = function () { this.res.writeHead(200, {'Content-type': 'text/plain'}) this.res.end('User-agent: *\nDisallow: /\n') } Req.prototype.serveWhoami = function () { var self = this self.server.sbot.whoami(function (err, feed) { if (err) return self.respondError(500, err.stack || err) self.respond(200, {username: feed.id}) }) } Req.prototype.serveUser1 = function () { this.respond(this.req.method === 'PUT' ? 201 : 200, {token: '1'}) } Req.prototype.serveBlob = function (id) { var self = this if (self.req.headers['if-none-match'] === id) return self.respondRaw(304) getBlob(self.server.sbot, id, function (err, readBlob, size) { if (err) { if (/^invalid/.test(err.message)) return self.respondErrorStr(400, err.message) else return self.respondErrorStr(500, err.message || err) } self.res.writeHead(200, { 'Cache-Control': 'public, max-age=315360000', 'Content-Length': size, 'etag': id }) pull( readBlob, toPull(self.res, function (err) { if (err) console.error('[npm-registry]', err) }) ) }) } Req.prototype.serveMsg = function (id) { var self = this try { id = decodeURIComponent(id) } catch (e) {} if (self.req.headers['if-none-match'] === id) return self.respondRaw(304) self.server.getMsg(id, function (err, msg) { if (err) return self.respondError(500, err.message || err) var out = Buffer.from(JSON.stringify(msg, null, 2), 'utf8') self.res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=315360000', 'Content-Length': out.length, 'etag': id }) self.res.end(out) }) } function decodeName(name) { var parts = String(name).replace(/\.tgz$/, '').split(':') return { name: parts[1], version: parts[2], distTag: parts[3], } } Req.prototype.getMsgMentions = function (name) { return pull( this.server.streamTree(this.headMsgIds, 'dependencyBranch'), // decryption could be done here pull.map(function (msg) { var c = msg.value && msg.value.content if (!c.mentions || !Array.isArray(c.mentions)) return [] return c.mentions.map(function (mention) { return { name: mention.name, size: mention.size, link: mention.link, author: msg.value.author, ts: msg.value.timestamp, } }) }), pull.flatten(), pull.filter(typeof name === 'string' ? function (link) { return link.name === name } : name && name.$prefix ? function (link) { return link.name.substr(0, name.$prefix.length) === name.$prefix } : function () { throw new TypeError('unsupported name filter') }) ) } Req.prototype.getMentions = function (name) { var useMsgMentions = this.headMsgIds var useServerMentions = !this.headMsgIds || this.headMsgPlus if (useServerMentions && !this.server.sbot.links2) { return this.headMsgPlus ? pull.error(new Error('ssb-links plugin is needed for ^msgid queries')) : pull.error(new Error('ssb-links plugin is needed for non-msgid queries')) } return cat([ useMsgMentions ? this.getMsgMentions(name) : pull.empty(), useServerMentions ? this.server.getMentions(name) : pull.empty() ]) } Req.prototype.getMentionLinks = function (blobId) { return pull( this.headMsgIds ? this.server.streamTree(this.headMsgIds, 'dependencyBranch') : this.server.sbot.links({ dest: blobId, rel: 'mentions', values: true, }), // decryption could be done here pull.map(function (msg) { var c = msg.value && msg.value.content return c && Array.isArray(c.mentions) && c.mentions || [] }), pull.flatten(), pull.filter(function (link) { return link && link.link === blobId }) ) } Req.prototype.servePkg = function (pathname) { var self = this var parts = pathname.split('/') var pkgName = parts.shift().replace(/%2f/i, '/') if (parts[0] === '-rev') return this.respondError(501, 'Unpublish is not supported') var spec = parts.shift() if (spec) try { spec = decodeURIComponent(spec) } finally {} if (parts.length > 0) return this.respondError(404) if (self.req.method === 'PUT') return self.publishPkg(pkgName) var obj = { _id: pkgName, name: pkgName, 'dist-tags': {}, versions: {}, time: {} } var distTags = {/* : {version, ts}*/} pull( self.getMentions({$prefix: 'npm:' + pkgName + ':'}), pull.drain(function (mention) { var data = decodeName(mention.name) if (!data.version) return if (data.distTag) { var tag = distTags[data.distTag] if (!tag || mention.ts > tag.ts) { /* TODO: sort by causal order instead of only by timestamps */ distTags[data.distTag] = {ts: mention.ts, version: data.version} } } obj.versions[data.version] = { author: { url: mention.author }, name: pkgName, version: data.version, dist: self.blobDist(mention.link) } var ts = new Date(mention.ts) if (ts > obj.time.updated || !obj.time.updated) obj.time.updated = ts if (ts < obj.time.created || !obj.time.created) obj.time.created = ts obj.time[data.version] = ts.toISOString() }, function (err) { if (err) return self.respondError(500, err.stack || err) for (var tag in distTags) { obj['dist-tags'][tag] = distTags[tag].version } if (spec) resolveSpec() else if (self.fetchAll) resolveAll() else resolved() }) ) function resolveSpec() { var version = obj['dist-tags'][spec] || semver.maxSatisfying(Object.keys(obj.versions), spec) obj = obj.versions[version] if (!obj) return self.respondError(404, 'version not found: ' + spec) self.populatePackageJson(obj, function (err, pkg) { if (err) return resolved(err) obj = pkg || obj resolved() }) } function resolveVersion(version, cb) { self.populatePackageJson(obj.versions[version], function (err, pkg) { if (err) return cb(err) if (pkg) obj.versions[version] = pkg if (pkg && pkg.license && !obj.license) obj.license = pkg.license cb() }) } function resolveAll() { var done = multicb() for (var version in obj.versions) { resolveVersion(version, done()) } done(resolved) } function resolved(err) { if (err) return self.respondError(500, err.stack || err) self.respond(200, obj) } } Req.prototype.servePrebuild = function (name) { var self = this var getMention = self.getMentions('prebuild:' + name) var blobsByAuthor = {/* : BlobId */} pull(getMention, pull.drain(function (link) { blobsByAuthor[link.author] = link.link }, function (err) { if (err) return self.respondError(500, err.stack || err) var authorsByLink = {/* : [FeedId...] */} var blobId for (var feed in blobsByAuthor) { var blob = blobId = blobsByAuthor[feed] var feeds = authorsByLink[blob] || (authorsByLink[blob] = []) feeds.push(feed) } switch (Object.keys(authorsByLink).length) { case 0: return self.respondError(404, 'Not Found') case 1: return self.serveBlob(blobId) default: return self.respond(300, {choices: authorsByLink}) } })) } var localhosts = { '::1': true, '127.0.0.1': true, '::ffff:127.0.0.1': true, } Req.prototype.publishPkg = function (pkgName) { var self = this var remoteAddress = self.req.socket.remoteAddress if (!(remoteAddress in localhosts)) { return self.respondError(403, 'You may not publish as this user.') } var chunks = [] self.req.on('data', function (data) { chunks.push(data) }) self.req.on('end', function () { var data try { data = JSON.parse(Buffer.concat(chunks)) } catch(e) { return self.respondError(400, e.stack) } return self.publishPkg2(pkgName, data || {}) }) } Req.prototype.publishPkg2 = function (name, data) { var self = this if (data.users) console.trace('[npm-registry] users property is not supported') var attachments = data._attachments || {} var links = {/* -.tgz: {link: , size: number} */} var done = multicb() function addAttachmentAsBlob(filename, cb) { var data = attachments[filename].data var tarball = Buffer.from(data, 'base64') var length = attachments[filename].length if (length && length !== tarball.length) return self.respondError(400, 'Length mismatch for attachment \'' + filename + '\'') self.server.sbot.blobs.add(function (err, id) { if (err) return cb(err) self.blobsToPush.push(id) var shasum = crypto.createHash('sha1').update(tarball).digest('hex') links[filename] = {link: id, size: tarball.length, shasum: shasum} cb() })(pull.once(tarball)) } for (var filename in attachments) { addAttachmentAsBlob(filename, done()) } done(function (err) { if (err) return self.respondError(500, err.stack || err) try { self.publishPkg3(name, data, links) } catch(e) { self.respondError(500, e.stack || e) } }) } Req.prototype.publishPkg3 = function (name, data, links) { var self = this var versions = data.versions || {} var linksByVersion = {/* : link */} // associate tarball blobs with versions for (var version in versions) { var pkg = versions[version] if (!pkg) return self.respondError(400, 'Bad package object') if (!pkg.dist) return self.respondError(400, 'Missing package dist property') if (!pkg.dist.tarball) return self.respondError(400, 'Missing dist.tarball property') if (pkg.deprecated) return self.respondError(501, 'Deprecation is not supported') var m = /\/-\/([^\/]+|@[^\/]+\/[^\/]+)$/.exec(pkg.dist.tarball) if (!m) return self.respondError(400, 'Bad tarball URL \'' + pkg.dist.tarball + '\'') var filename = m[1] var link = links[filename] if (!link) return self.respondError(501, 'Unable to find attachment \'' + filename + '\'') // TODO?: try to find missing tarball mentioned in other messages if (pkg.version && pkg.version !== version) return self.respondError(400, 'Mismatched package version: ' + [pkg.version, version]) linksByVersion[version] = link link.version = version link.license = pkg.license link.dependencies = pkg.dependencies || {} link.optionalDependencies = pkg.optionalDependencies link.bundledDependencies = pkg.bundledDependencies || pkg.bundleDependencies } // associate blobs with dist-tags var tags = data['dist-tags'] || {} for (var tag in tags) { var version = tags[tag] var link = linksByVersion[version] if (!link) return self.respondError(501, 'Setting a dist-tag for a version not being published is not supported.') // TODO?: support setting dist-tag without version, // by looking up a tarball blob for the version link.tag = tag } // compute blob links to publish var mentions = [] for (var filename in links) { var link = links[filename] || {} if (!link.version) return self.respondError(400, 'Attachment ' + filename + ' was not linked to in the package metadata') mentions.push({ name: 'npm:' + name + ':' + link.version + (link.tag ? ':' + link.tag : ''), link: link.link, size: link.size, shasum: link.shasum, license: link.license, dependencies: link.dependencies, optionalDependencies: link.optionalDependencies, bundledDependencies: link.bundledDependencies, }) } return self.publishPkgs(mentions) } Req.prototype.publishPkgs = function (mentions) { var self = this exports.publishPkgMentions(self.server.sbot, mentions, function (err, msgs) { if (err) return self.respondError(500, err.stack || err) self.server.pushBlobs(self.blobsToPush, function (err) { if (err) console.error('[npm-registry] Failed to push blob ' + id + ': ' + (err.stack || err)) self.respond(201) console.log(msgs.map(function (msg) { return msg.key }).join('\n')) }) }) } Req.prototype.populatePackageJson = function (obj, cb) { var self = this var blobId = obj.dist.tarball.replace(/.*\/blobs\/get\//, '') var deps, depsWithOptionalDeps, bundledDeps, shasum, license // Combine metadata from mentions of the blob, to construct most accurate // metadata, including dependencies info. If needed, fall back to fetching // metadata from the tarball blob. pull( self.getMentionLinks(blobId), pull.drain(function (link) { if (link.dependencies) deps = link.dependencies // if optionalDependencies is present, let the dependencies value // override that from other links. because old versions that didn't // publish optionalDependencies are missing optionalDependencies from // their dependencies. if (link.optionalDependencies) depsWithOptionalDeps = { deps: link.dependencies, optionalDeps: link.optionalDependencies } if (link.shasum) shasum = link.shasum if (link.license) license = link.license bundledDeps = link.bundledDependencies || link.bundleDependencies || bundledDeps // how to handle multiple assignments of dependencies to a package? }, function (err) { if (err) return cb(new Error(err.stack || err)) if (deps && (shasum || !self.needShasum)) { // assume that the dependencies in the links to the blob are // correct. if (depsWithOptionalDeps) { obj.dependencies = depsWithOptionalDeps.deps || deps obj.optionalDependencies = depsWithOptionalDeps.optionalDeps } else { obj.dependencies = deps } obj.bundledDependencies = bundledDeps obj.license = license if (shasum) obj.dist.shasum = obj._shasum = shasum next(obj) } else { // get dependencies from the tarball getPackageJsonFromTarballBlob(self.server.sbot, blobId, function (err, pkg) { if (err) return cb(err) pkg.dist = obj.dist pkg.dist.shasum = pkg._shasum pkg.author = pkg.author || obj.author pkg.version = pkg.version || obj.version pkg.name = pkg.name || obj.name pkg.license = pkg.license || obj.license next(pkg) }) } }) ) function next(pkg) { var feedId = obj.author.url self.server.getFeedName(feedId, function (err, name) { if (err) console.error('[npm-registry]', err.stack || err), name = '' pkg._npmUser = { email: feedId, name: name } cb(null, pkg) }) } } function getPackageJsonFromTarballBlob(sbot, id, cb) { var self = this getBlob(sbot, id, function (err, readBlob) { if (err) return cb(err) cb = once(cb) var extract = tar.extract() var pkg, shasum extract.on('entry', function (header, stream, next) { if (/^[^\/]*\/package\.json$/.test(header.name)) { pull(toPull.source(stream), pull.collect(function (err, bufs) { if (err) return cb(err) try { pkg = JSON.parse(Buffer.concat(bufs)) } catch(e) { return cb(e) } next() })) } else { stream.on('end', next) stream.resume() } }) extract.on('finish', function () { pkg._shasum = shasum cb(null, pkg) }) pull( readBlob, hash('sha1', 'hex', function (err, sum) { if (err) return cb(err) shasum = sum }), toPull(zlib.createGunzip()), toPull(extract) ) }) }