qwgit / index.js
http{/,s} git server
git clone http://git.nthia.dev/qwgit

const { pipeline, Duplex, Writable } = require('stream')
const { createGunzip } = require('zlib')
const { spawn } = require('child_process')
const https = require('https')
const http = require('http')
const backend = require('git-http-backend')
const EventEmitter = require('events')
const fs = require('fs')
const path = require('path')
const webview = require('./lib/webview.js')
const webapi = require('./lib/webapi.js')
const auth = require('./lib/auth.js')
const Repos = require('./lib/repos.js')

module.exports = Qwgit

function Qwgit(opts) {
  if (!(this instanceof Qwgit)) return new Qwgit(opts)
  EventEmitter.call(this)
  this.on('error', () => {})
  this._repodir = opts.repodir
  this._fs = opts.fs ?? fs
  this.setConfig(opts)
  this._fs.mkdir(this._repodir, { recursive: true }, err => this.emit('error', err))
}
Qwgit.prototype = Object.create(EventEmitter.prototype)

Qwgit.prototype.setConfig = function (opts) {
  this.repos = new Repos({
    repodir: this._repodir,
    namespaces: opts.namespaces,
    fs: this._fs,
    repos: opts.repos,
  })
  this._auth = auth(opts.auth)
  this._webview = webview({ repos: this.repos, repodir: this._repodir })
}

Qwgit.prototype.createServer = function (opts) {
  let self = this
  opts = Object.assign({
    requestCert: true,
    rejectUnauthorized: false,
  }, opts ?? {})
  let server = (opts.key ? https : http).createServer(opts, handle)
  server.on('error', err => this.emit('error',err))
  return server

  function handle(req, res) {
    console.log(req.method, req.url)
    let creds = {}
    let fp = req.socket?.getPeerCertificate
      ? req.socket.getPeerCertificate()?.fingerprint512
      : undefined
    if (fp !== undefined) creds.fingerprint512 = fp
    let qs = new URLSearchParams(req.url.replace(/^[^?]*\??/,''))
    let infoRefs = /([^?]*)\/info\/refs(?:\?|$)/.exec(req.url)
    if (req.url.startsWith('/api/v1/')) {
      self._auth.find(creds, (err,ids) => {
        if (err) error(500, res, err)
        else webapi(self, req, res, ids)
      })
    } else if (req.method === 'GET' && infoRefs) {
      let service = qs.get('service')
      let repo = self.repos.getFromPath(decodeURIComponent(infoRefs[1]))
      if (!repo) {
        res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' })
        return res.end('repository not found\n')
      }
      let rdir = path.join(self._repodir, repo.id)
      if (service === undefined) { // unsmart
        self._fs.readFile(path.join(rdir,'info/refs'), 'utf8', (err,src) => {
          if (err) return error(500, res, err)
          res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' })
          res.end(src)
        })
      } else if (service === 'git-upload-pack' || service === 'git-receive-pack') {
        let ps = spawn(service, [rdir])
        res.writeHead(200, {
          'content-type': `application/x-${service}-advertisement`,
          'cache-control': 'no-cache',
        })
        res.write(`${(service.length+15).toString(16).padStart(4,'0')}# service=${service}\n0000`)
        ps.stdout.pipe(res)
        ps.stderr.resume()
        ps.stdin.end()
      } else {
        res.writeHead(403, { 'content-type': 'text/plain' })
        res.end('service not supported')
      }
    } else if (req.method === 'GET') {
      self._auth.find(creds, (err,ids) => {
        if (err) error(500, res, err)
        else self._webview(self, req, res, ids)
      })
    } else if (String(req.headers['content-encoding']).toLowerCase() === 'gzip') {
      pipeline(req, createGunzip(), backend(req.url, onservice), res, done)
    } else {
      pipeline(req, backend(req.url, onservice), res, done)
    }
    function onservice(err, service) {
      if (err) return
      let repo = self.repos.getFromPath(decodeURIComponent(req.url.replace(/(?:\/[^\/]+|\?.*)$/,'')))
      if (!repo) {
        res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' })
        return res.end('repository not found\n')
      }
      let rdir = path.join(self._repodir, repo.id)
      res.setHeader('content-type', service.type)
      console.log(service.action, service.cmd, service.fields, service.args)
      self._auth.find(creds, (err,ids) => {
        if (err) return error(500, res, err)
        let ok = self.repos.isRepoAuthorized(service.action, repo, ids)
        if (!ok) {
          res.writeHead(401, { 'content-type': 'text/plain; charset=utf-8' })
          return res.end('authorization required\n')
        }
        let ps = spawn(service.cmd, service.args.concat(rdir))
        ps.stderr.pipe(process.stderr, { end: false })
        pipeline(ps.stdout, service.createStream(), ps.stdin, done)
      })
    }
  }
  function done(err) { if (err) self.emit('error', err) }
}

function error(code, res, err) {
  res.writeHead(code, { 'content-type': 'text/plain; charset=utf-8' })
  return res.end(String(err.message ?? err) + '\n')
}