http{/,s} git server
git clone http://git.nthia.dev/qwgit
const https = require('https')
const { Writable } = require('stream')
const path = require('path')
module.exports = API
function API(remote, opts) {
if (!(this instanceof API)) return new API(remote, opts)
if (!opts) opts = {}
this.remote = remote.replace(/\/?$/,'')
this._tls = {}
if (opts.cert) this._tls.cert = opts.cert
if (opts.key) this._tls.key = opts.key
if (opts.ca) this._tls.ca = opts.ca
this._verify = opts.verify ?? function (hostname, fp, cb) { cb(null, false) }
let u = new URL(this.remote)
this._hostname = u.host
}
API.prototype._request = function (u, opts, cb) {
cb = once(cb)
if (typeof opts === 'function') {
cb = opts
opts = {}
}
let r = https.request(this.remote + '/api/v1/' + u, Object.assign({
rejectUnauthorized: false,
}, opts, this._tls))
let res = null, trusted = false
r.once('error', cb)
r.once('response', onresponse)
r.once('socket', c => {
c.once('secure', () => {
let cert = c.getPeerCertificate(true)
let fp = cert?.fingerprint512.toLowerCase().replace(/:/g,'')
this._verify(this._hostname, fp, (err,ok) => {
if (err) {
c.destroy()
cb(err)
} else if (!ok) {
c.destroy()
err = new Error('untrusted fingerprint for ' + this._hostname)
err.code = 'UNTRUSTED_FINGERPRINT'
err.certificate = cert
err.hostname = this._hostname
err.fingerprint512 = fp
cb(err)
} else {
trusted = true
if (res) onresponse(res)
}
})
})
})
return r
function onresponse(res_) {
res = res_
if (!trusted) return
let buffers = []
res.once('error', cb)
res.pipe(new Writable({
write(buf,enc,next) {
buffers.push(buf)
next()
},
final(next) {
cb(null, res, Buffer.concat(buffers).toString())
next()
}
}))
}
}
API.prototype.listRepos = function (cb) {
let r = this._request('repo/list', {}, (err,res,body) => {
if (err) return cb(err)
else handleJSON(res, body, cb)
})
r.end()
}
API.prototype.createRepo = function (opts, cb) {
let params = new URLSearchParams(Object.keys(opts).flatMap(key => {
if (Array.isArray(opts[key])) return opts[key].map(v => [key,v])
else return [[key,opts[key]]]
}))
let ropts = {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
}
let r = this._request('repo/create', ropts, (err,res,body) => {
if (err) return cb(err)
else handleJSON(res, body, cb)
})
r.end(params.toString())
}
API.prototype.updateRepo = function (rid, opts, cb) {
let params = new URLSearchParams(Object.keys(opts).flatMap(key => {
if (Array.isArray(opts[key])) return opts[key].map(v => [key,v])
else return [[key,opts[key]]]
}))
let ropts = {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
}
let r = this._request('repo/'+rid+'/update', ropts, (err,res,body) => {
if (err) return cb(err)
else handleJSON(res, body, cb)
})
r.end(params.toString())
}
API.prototype.updateRepoFromPath = function (rpath, opts, cb) {
rpath = String(rpath).replace(/^\//,'')
let params = new URLSearchParams(Object.keys(opts).flatMap(key => {
if (Array.isArray(opts[key])) return opts[key].map(v => [key,v])
else return [[key,opts[key]]]
}))
let ropts = {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
}
let r = this._request('repo-from-path/'+rpath+'/update', ropts, (err,res,body) => {
if (err) return cb(err)
else handleJSON(res, body, cb)
})
r.end(params.toString())
}
API.prototype.removeRepo = function (id, cb) {
let ropts = {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
}
let r = this._request('repo/'+id+'/remove', ropts, (err,res,body) => {
if (err) return cb(err)
else handleJSON(res, body, cb)
})
r.end()
}
API.prototype.removeRepoFromPath = function (rpath, cb) {
let ropts = {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
}
let r = this._request('repo-from-path/'+rpath+'/remove', ropts, (err,res,body) => {
if (err) return cb(err)
else handleJSON(res, body, cb)
})
r.end()
}
API.prototype.getRepo = function (rid, cb) {
let r = this._request(`repo/${rid}/info`, {}, (err,res,body) => {
if (err) return cb(err)
else handleJSON(res, body, cb)
})
r.end()
}
API.prototype.getRepoFromPath = function (rpath, cb) {
let r = this._request(`repo-from-path/${rpath}/info`, {}, (err,res,body) => {
if (err) return cb(err)
else handleJSON(res, body, cb)
})
r.end()
}
API.prototype.reloadConfig = function (cb) {
let opts = {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
}
let r = this._request('config/reload', opts, (err,res,body) => {
if (err) return cb(err)
else handleJSON(res, body, cb)
})
r.end('')
}
function once(f) {
return function () {
let g = f
f = null
if (g) g.apply(null, arguments)
}
}
function handleJSON(res, body, cb) {
if (res.statusCode !== 200) return cb(new Error(body))
if (!/(application|text)\/json\s*($|;)/.test(res.headers['content-type'])) {
return cb(new Error(body))
}
let data = null
try { data = JSON.parse(body) }
catch (err) { return cb(err) }
cb(null, data)
}