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')
}