http{/,s} git server
git clone http://git.nthia.dev/qwgit
const EventEmitter = require('events')
const fs = require('fs')
const path = require('path')
const { randomBytes } = require('crypto')
const { nextTick } = process
module.exports = function (file, rdir, cb) {
cb = once(cb ?? noop)
let src = null
let pending = 3
fs.mkdir(rdir, { recursive: true }, err => {
if (err) cb(err)
else if (--pending === 0) ready()
})
fs.readFile(file, 'utf8', (err,src_) => {
src = src_
if (err && err.code !== 'ENOENT') cb(err)
else if (--pending === 0) ready()
})
if (--pending === 0) ready()
function ready() {
let data = null
try { data = JSON.parse(src || '[]') }
catch (err) { return cb(`error parsing json in ${file}: ${err}`) }
if (!Array.isArray(data)) data = []
cb(null, new RemoteCerts(file, rdir, data))
}
}
function RemoteCerts(file, rdir, data) {
if (!(this instanceof RemoteCerts)) return new RemoteCerts(file, data)
this._file = file
this._dir = rdir
this._byFingerprint = new Map
this._byHostname = new Map
this._fsQueue = []
data.forEach(row => this._add(row))
}
RemoteCerts.prototype = Object.create(EventEmitter.prototype)
RemoteCerts.prototype._simplifyQueue = function () {
for (let i = 0; i < this._fsQueue.length; i++) {
let a = this._fsQueue[i]
for (let j = i+1; j < this._fsQueue.length; j++) {
let b = this._fsQueue[j]
if (a.file === b.file) a._remove = true
}
}
this._fsQueue = this._fsQueue.filter(x => !x._remove)
}
RemoteCerts.prototype.flush = function (cb) {
cb = once(cb ?? noop)
let data = this.list()
let pending = 1 + this._fsQueue.length
this._simplifyQueue()
fs.writeFile(this._file, JSON.stringify(data,null,2)+'\n', done)
for (let i = 0; i < this._fsQueue.length; i++) {
let q = this._fsQueue[i]
if (q.type === 'write') {
fs.writeFile(q.file, q.data, done)
} else if (q.type === 'delete') {
fs.unlink(q.file, done)
} else {
throw new Error('unexpected fs queue type: ' + q.type)
}
}
done()
function done(err) {
if (err) cb(err)
else if (--pending === 0) cb(null)
}
}
RemoteCerts.prototype.add = function (row) {
let fp = normfp(row.fingerprint512 ?? row.fingerprint)
let host = row.hostname
if (/[\s'"]/.test(host)) throw new Error('invalid characters in host')
if (/[\s'"]/.test(fp)) throw new Error('invalid characters in fingerprint')
let rec = { fingerprint512: fp, hostname: host }
if (row.cert) {
rec.cert = randomBytes(6).toString('hex') + '.pem'
this._fsQueue.push({
type: 'write',
file: path.join(this._dir, rec.cert),
data: row.cert
})
}
let frecs = this._byFingerprint.get(fp) ?? new Set
let fcert = null
for (let x of frecs) {
if (x.cert && x.hostname === host && x.fingerprint512 === fp) {
fcert = x.cert
break
}
}
let hrecs = this._byHostname.get(host) ?? new Set
let hcert = null
for (let x of hrecs) {
if (x.cert && x.hostname === host && x.fingerprint512 === fp) {
hcert = x.cert
break
}
}
if (fcert && !rec.cert) rec.cert = fcert
else if (hcert && !rec.cert) rec.cert = hcert
this._add(rec)
}
RemoteCerts.prototype._add = function (row) {
let fp = normfp(row.fingerprint512 ?? row.fingerprint)
let host = row.hostname
let frecs = this._byFingerprint.get(fp) ?? new Set
filterDrainSet(frecs, x => {
if (x.hostname === host && x.fingerprint512 === fp) {
if (x.cert) fcert = x.cert
return true
}
return false
})
let hrecs = this._byHostname.get(host) ?? new Set
filterDrainSet(hrecs, x => {
if (x.hostname === host && x.fingerprint512 === fp) {
if (x.cert) fcert = x.cert
return true
}
return false
})
frecs.add(row)
hrecs.add(row)
this._byFingerprint.set(fp, frecs)
this._byHostname.set(host, hrecs)
}
RemoteCerts.prototype.remove = function (row) {
let fp = normfp(row.fingerprint512 ?? row.fingerprint)
let host = row.hostname
if (fp !== undefined && host !== undefined) {
let hrecs = this._byFingerprint.get(fp) ?? new Set
for (let rec of hrecs) {
if (rec.hostname === host && rec.cert !== undefined) {
this._fsQueue.push({
type: 'delete',
file: path.join(this._dir, rec.cert),
})
}
}
filterDrainSet(hrecs, x => x.hostname === host)
if (hrecs.size === 0) this._byFingerprint.delete(fp)
else this._byFingerprint.set(fp, hrecs)
} else if (fp !== undefined) {
this._byFingerprint.delete(fp)
}
if (host !== undefined && fp !== undefined) {
let hrecs = this._byHostname.get(host) ?? new Set
for (let rec of hrecs) {
if (rec.fingerprint512 === fp && rec.cert !== undefined) {
this._fsQueue.push({
type: 'delete',
file: path.join(this._dir, rec.cert),
})
}
}
filterDrainSet(hrecs, x => x.fingerprint512 === fp)
if (hrecs.size === 0) this._byHostname.delete(host)
else this._byHostname.set(host, hrecs)
} else if (host !== undefined) {
let frecs = Array.from(this._byHostname.get(host) ?? new Set)
frecs.forEach(rec => {
let hrecs = this._byFingerprint.get(rec.fingerprint512) ?? new Set
for (let x of hrecs) {
if (x.hostname === host && x.cert !== undefined) {
this._fsQueue.push({
type: 'delete',
file: path.join(this._dir, x.cert),
})
}
}
filterDrainSet(hrecs, x => x.hostname === host)
this._byFingerprint.set(fp, hrecs)
})
this._byHostname.delete(host)
}
}
RemoteCerts.prototype.getFromFingerprint = function (fp) {
return Array.from(this._byFingerprint.get(normfp(fp)) ?? new Set)
}
RemoteCerts.prototype.getFromHostname = function (host) {
return Array.from(this._byHostname.get(host) ?? new Set)
}
RemoteCerts.prototype.readCert = function (file, cb) {
if (/^\.|\.\.|:/.test(file)) nextTick(cb, new Error('invalid cert filename'))
if (!/\.pem$/.test(file)) nextTick(cb, new Error('cert not a pem file'))
fs.readFile(this.certPath(file), cb)
}
RemoteCerts.prototype.certPath = function (file) {
return path.join(this._dir,file)
}
RemoteCerts.prototype.list = function () {
let rows = []
for (let [fp,recs] of this._byFingerprint) {
rows = rows.concat(Array.from(recs))
}
return rows
}
RemoteCerts.prototype.verify = function (opts) {
let host = opts.hostname
let fp = normfp(opts.fingerprint512 ?? opts.fingerprint)
let hrecs = this._byFingerprint.get(fp)
return hrecs ? someSet(hrecs, x => x.hostname === host) : false
}
function noop() {}
function normfp(fp) { return fp?.toLowerCase().replace(/:/g,'') }
function someSet(s, f) {
for (let x of s) {
if (f(x)) return true
}
return false
}
function filterDrainSet(s, f) {
for (let x of s) {
if (f(x)) s.delete(x)
}
}
function once(f) {
return function () {
let g = f
f = null
if (g) g.apply(null, arguments)
}
}