'), this.renderTry(read), pull.once('
Built with git-ssb-web
var fs = require('fs') var http = require('http') var path = require('path') var url = require('url') var qs = require('querystring') var util = require('util') var ref = require('ssb-ref') var pull = require('pull-stream') var ssbGit = require('ssb-git-repo') var toPull = require('stream-to-pull-stream') var cat = require('pull-cat') var GitRepo = require('pull-git-repo') var u = require('./lib/util') var markdown = require('./lib/markdown') var paginate = require('./lib/paginate') var asyncMemo = require('asyncmemo') var multicb = require('multicb') var schemas = require('ssb-msg-schemas') var Issues = require('ssb-issues') var PullRequests = require('ssb-pull-requests') var paramap = require('pull-paramap') var Mentions = require('ssb-mentions') var many = require('pull-many') var ident = require('pull-identify-filetype') var mime = require('mime-types') var moment = require('moment') var LRUCache = require('lrucache') var hlCssPath = path.resolve(require.resolve('highlight.js'), '../../styles') var emojiPath = path.resolve(require.resolve('emoji-named-characters'), '../pngs') function ParamError(msg) { var err = Error.call(this, msg) err.name = ParamError.name return err } util.inherits(ParamError, Error) function parseAddr(str, def) { if (!str) return def var i = str.lastIndexOf(':') if (~i) return {host: str.substr(0, i), port: str.substr(i+1)} if (isNaN(str)) return {host: str, port: def.port} return {host: def.host, port: str} } function tryDecodeURIComponent(str) { if (!str || (str[0] == '%' && ref.isBlobId(str))) return str try { str = decodeURIComponent(str) } finally { return str } } function getContentType(filename) { var ext = u.getExtension(filename) return contentTypes[ext] || u.imgMimes[ext] || 'text/plain; charset=utf-8' } var contentTypes = { css: 'text/css' } function readReqForm(req, cb) { pull( toPull(req), pull.collect(function (err, bufs) { if (err) return cb(err) var data try { data = qs.parse(Buffer.concat(bufs).toString('ascii')) } catch(e) { return cb(e) } cb(null, data) }) ) } var msgTypes = { 'git-repo': true, 'git-update': true, 'issue': true, 'pull-request': true } var _httpServer module.exports = { name: 'git-ssb-web', version: require('./package').version, manifest: {}, init: function (ssb, config, reconnect) { // close existing server. when scuttlebot plugins get a deinit method, we // will close it in that instead it if (_httpServer) _httpServer.close() var web = new GitSSBWeb(ssb, config, reconnect) _httpSserver = web.httpServer return {} } } function GitSSBWeb(ssb, config, reconnect) { this.ssb = ssb this.config = config this.reconnect = reconnect if (config.logging && config.logging.level) this.logLevel = this.logLevels.indexOf(config.logging.level) this.ssbAppname = config.appname || 'ssb' this.isPublic = config.public this.getVotes = require('./lib/votes')(ssb) this.getMsg = asyncMemo({cache: new LRUCache(100)}, ssb.get) this.issues = Issues.init(ssb) this.pullReqs = PullRequests.init(ssb) this.getRepo = asyncMemo({ cache: new LRUCache(32) }, function (id, cb) { this.getMsg(id, function (err, msg) { if (err) return cb(err) ssbGit.getRepo(ssb, {key: id, value: msg}, {live: true}, cb) }) }) this.about = function (id, cb) { cb(null, {name: id}) } ssb.whoami(function (err, feed) { this.myId = feed.id this.about = require('./lib/about')(ssb, this.myId) }.bind(this)) this.i18n = require('./lib/i18n')(path.join(__dirname, 'locale'), 'en') this.users = require('./lib/users')(this) this.repos = require('./lib/repos')(this) var webConfig = config['git-ssb-web'] || {} var addr = parseAddr(config.listenAddr, { host: webConfig.host || 'localhost', port: webConfig.port || 7718 }) this.listen(addr.host, addr.port) } var G = GitSSBWeb.prototype G.logLevels = ['error', 'warning', 'notice', 'info'] G.logLevel = G.logLevels.indexOf('notice') G.log = function (level) { if (this.logLevels.indexOf(level) > this.logLevel) return console.log.apply(console, [].slice.call(arguments, 1)) } G.listen = function (host, port) { this.httpServer = http.createServer(G_onRequest.bind(this)) this.httpServer.listen(port, host, function () { var hostName = ~host.indexOf(':') ? '[' + host + ']' : host this.log('notice', 'Listening on http://' + hostName + ':' + port + '/') }.bind(this)) } G.getRepoName = function (ownerId, repoId, cb) { this.about.getName({ owner: ownerId, target: repoId }, cb) } G.getRepoFullName = function (author, repoId, cb) { var done = multicb({ pluck: 1, spread: true }) this.getRepoName(author, repoId, done()) this.about.getName(author, done()) done(cb) } G.addAuthorName = function () { var about = this.about return paramap(function (msg, cb) { var author = msg && msg.value && msg.value.author if (!author) return cb(null, msg) about.getName(author, function (err, authorName) { msg.authorName = authorName cb(err, msg) }) }, 8) } /* Serving a request */ function serve(req, res) { return pull( pull.filter(function (data) { if (Array.isArray(data)) { res.writeHead.apply(res, data) return false } return true }), toPull(res) ) } function G_onRequest(req, res) { this.log('info', req.method, req.url) req._u = url.parse(req.url, true) var locale = req._u.query.locale || (/locale=([^;]*)/.exec(req.headers.cookie) || [])[1] var reqLocales = req.headers['accept-language'] this.i18n.pickCatalog(reqLocales, locale, function (err, t) { if (err) return pull(this.serveError(req, err, 500), serve(req, res)) req._t = t req._locale = t.locale pull(this.handleRequest(req), serve(req, res)) }.bind(this)) } G.handleRequest = function (req) { var path = req._u.pathname.slice(1) var dirs = ref.isLink(path) ? [path] : path.split(/\/+/).map(tryDecodeURIComponent) var dir = dirs[0] if (req.method == 'POST') return this.handlePOST(req, dir) if (dir == '') return this.serveIndex(req) else if (dir == 'search') return this.serveSearch(req) else if (ref.isBlobId(dir)) return this.serveBlob(req, dir) else if (ref.isMsgId(dir)) return this.serveMessage(req, dir, dirs.slice(1)) else if (ref.isFeedId(dir)) return this.users.serveUserPage(req, dir, dirs.slice(1)) else if (dir == 'static') return this.serveFile(req, dirs) else if (dir == 'highlight') return this.serveFile(req, [hlCssPath].concat(dirs.slice(1)), true) else if (dir == 'emoji') return this.serveFile(req, [emojiPath].concat(dirs.slice(1)), true) else return this.serve404(req) } G.handlePOST = function (req, dir) { var self = this if (self.isPublic) return self.serveBuffer(405, req._t('error.POSTNotAllowed')) return u.readNext(function (cb) { readReqForm(req, function (err, data) { if (err) return cb(null, self.serveError(req, err, 400)) if (!data) return cb(null, self.serveError(req, new ParamError(req._t('error.MissingData')), 400)) switch (data.action) { case 'fork-prompt': return cb(null, self.serveRedirect(req, u.encodeLink([data.id, 'fork']))) case 'fork': if (!data.id) return cb(null, self.serveError(req, new ParamError(req._t('error.MissingId')), 400)) return ssbGit.createRepo(self.ssb, {upstream: data.id}, function (err, repo) { if (err) return cb(null, self.serveError(req, err)) cb(null, self.serveRedirect(req, u.encodeLink(repo.id))) }) case 'vote': var voteValue = +data.value || 0 if (!data.id) return cb(null, self.serveError(req, new ParamError(req._t('error.MissingId')), 400)) var msg = schemas.vote(data.id, voteValue) return self.ssb.publish(msg, function (err) { if (err) return cb(null, self.serveError(req, err)) cb(null, self.serveRedirect(req, req.url)) }) case 'repo-name': if (!data.id) return cb(null, self.serveError(req, new ParamError(req._t('error.MissingId')), 400)) if (!data.name) return cb(null, self.serveError(req, new ParamError(req._t('error.MissingName')), 400)) var msg = schemas.name(data.id, data.name) return self.ssb.publish(msg, function (err) { if (err) return cb(null, self.serveError(req, err)) cb(null, self.serveRedirect(req, req.url)) }) case 'comment': if (!data.id) return cb(null, self.serveError(req, new ParamError(req._t('error.MissingId')), 400)) var msg = schemas.post(data.text, data.id, data.branch || data.id) msg.issue = data.issue msg.repo = data.repo if (data.open != null) Issues.schemas.reopens(msg, data.id) if (data.close != null) Issues.schemas.closes(msg, data.id) var mentions = Mentions(data.text) if (mentions.length) msg.mentions = mentions return self.ssb.publish(msg, function (err) { if (err) return cb(null, self.serveError(req, err)) cb(null, self.serveRedirect(req, req.url)) }) case 'new-issue': var msg = Issues.schemas.new(dir, data.text) var mentions = Mentions(data.text) if (mentions.length) msg.mentions = mentions return self.ssb.publish(msg, function (err, msg) { if (err) return cb(null, self.serveError(req, err)) cb(null, self.serveRedirect(req, u.encodeLink(msg.key))) }) case 'new-pull': var msg = PullRequests.schemas.new(dir, data.branch, data.head_repo, data.head_branch, data.text) var mentions = Mentions(data.text) if (mentions.length) msg.mentions = mentions return self.ssb.publish(msg, function (err, msg) { if (err) return cb(null, self.serveError(req, err)) cb(null, self.serveRedirect(req, u.encodeLink(msg.key))) }) case 'markdown': return cb(null, self.serveMarkdown(data.text, {id: data.repo})) default: cb(null, self.serveBuffer(400, req._t('error.UnknownAction', data))) } }) }) } G.serveFile = function (req, dirs, outside) { var filename = path.resolve.apply(path, [__dirname].concat(dirs)) // prevent escaping base dir if (!outside && filename.indexOf('../') === 0) return this.serveBuffer(403, req._t("error.403Forbidden")) return u.readNext(function (cb) { fs.stat(filename, function (err, stats) { cb(null, err ? err.code == 'ENOENT' ? this.serve404(req) : this.serveBuffer(500, err.message) : u.ifModifiedSince(req, stats.mtime) ? pull.once([304]) : stats.isDirectory() ? this.serveBuffer(403, req._t('error.DirectoryNotListable')) : cat([ pull.once([200, { 'Content-Type': getContentType(filename), 'Content-Length': stats.size, 'Last-Modified': stats.mtime.toGMTString() }]), toPull(fs.createReadStream(filename)) ])) }.bind(this)) }.bind(this)) } G.serveBuffer = function (code, buf, contentType, headers) { headers = headers || {} headers['Content-Type'] = contentType || 'text/plain; charset=utf-8' headers['Content-Length'] = Buffer.byteLength(buf) return pull.values([ [code, headers], buf ]) } G.serve404 = function (req) { return this.serveBuffer(404, req._t("error.404NotFound")) } G.serveRedirect = function (req, path) { return this.serveBuffer(302, '
' + '' + u.escape(err.stack) + '' } G.renderTry = function (read) { var self = this var ended return function (end, cb) { if (ended) return cb(ended) read(end, function (err, data) { if (err === true) cb(true) else if (err) { ended = true cb(null, self.renderError(err)) } else cb(null, data) }) } } G.serveTemplate = function (req, title, code, read) { var self = this if (read === undefined) return this.serveTemplate.bind(this, req, title, code) var q = req._u.query.q && u.escape(req._u.query.q) || '' var app = 'git ssb' var appName = this.ssbAppname if (req._t) app = req._t(app) return cat([ pull.values([ [code || 200, { 'Content-Type': 'text/html' }], '', '
Built with git-ssb-web
' + '' + i + ' | ' + '' + line + ' |
' + markdown(c.text) + '' : '') + '
' + req._t('error.BlobNotFoundInRepo', { repo: u.link([repoId]) }) + '
' + '' + u.escape(err.stack) + '' )) } G.serveRaw = function (length, contentType) { var headers = { 'Content-Type': contentType || 'text/plain; charset=utf-8', 'Cache-Control': 'max-age=31536000' } if (length != null) headers['Content-Length'] = length return function (read) { return cat([pull.once([200, headers]), read]) } } G.getBlob = function (req, key, cb) { var blobs = this.ssb.blobs blobs.want(key, function (err, got) { if (err) cb(err) else if (!got) cb(new Error(req._t('error.MissingBlob', {key: key}))) else cb(null, blobs.get(key)) }) } G.serveBlob = function (req, key) { var self = this return u.readNext(function (cb) { self.getBlob(req, key, function (err, read) { if (err) cb(null, self.serveError(req, err)) else if (!read) cb(null, self.serve404(req)) else cb(null, identToResp(read)) }) }) } function identToResp(read) { var ended, type, queue var id = ident(function (_type) { type = _type && mime.lookup(_type) })(read) return function (end, cb) { if (ended) return cb(ended) if (end) id(end, function (end) { cb(end === true ? null : end) }) else if (queue) { var _queue = queue queue = null cb(null, _queue) } else if (!type) id(null, function (end, data) { if (ended = end) return cb(end) queue = data cb(null, [200, { 'Content-Type': type || 'text/plain; charset=utf-8', 'Cache-Control': 'max-age=31536000' }]) }) else id(null, cb) } }