http{/,s} git server
git clone http://git.nthia.dev/qwgit
const fs = require('fs')
const path = require('path')
const EventEmitter = require('events')
const { nextTick } = process
const { X509Certificate } = require('crypto')
const mkcert = require('./openssl-mkcert.js')
module.exports = Certs
function Certs(opts) {
if (!(this instanceof Certs)) return new Certs(opts)
EventEmitter.call(this)
this._datadir = opts.dir
}
Certs.prototype = Object.create(EventEmitter.prototype)
Certs.prototype.get = function (name, file, cb) {
if (!validName(name)) return nextTick(cb, new Error('invalid cert name'))
if (/\.\.|[\/:]/.test(file)) return nextTick(cb, new Error('invalid cert file'))
fs.readFile(path.join(this._datadir, name, file), (err,data) => {
if (err && err.code === 'ENOENT') cb(null, null)
else if (err) cb(err)
else cb(null, data)
})
}
Certs.prototype.getAllFiles = function (name, cb) {
if (!validName(name)) return nextTick(cb, new Error('invalid cert name'))
let dir = path.join(this._datadir, name)
let out = {}
fs.readdir(dir, (err,files) => {
if (err && err.code === 'ENOENT') return cb(null, null)
if (err) return cb(err)
let pending = 1
files.forEach(file => {
if (!/\.pem$/.test(file)) return
if (/^\./.test(file)) return
pending++
let key = path.basename(file).replace(/\.pem$/,'')
fs.readFile(path.join(dir,file), (err,src) => {
if (err) {
let f = cb
cb = noop
return f(err)
}
out[key] = src
if (--pending === 0) cb(null, out)
})
})
if (--pending === 0) cb(null, out)
})
}
Certs.prototype.getFingerprint = function (name, file, cb) {
this.get(name, file, (err,src) => {
if (err) return cb(err)
let fp = new X509Certificate(src)?.fingerprint512
if (!fp) return cb(null, null)
else cb(null, fp.toLowerCase().replace(/:/g,''))
})
}
Certs.prototype.create = function (name, cb) {
if (!validName(name)) return nextTick(cb, new Error('cert name contains invalid characters'))
let dir = path.join(this._datadir, name)
fs.mkdir(dir, { recursive: true }, err => {
if (err) cb(err)
else mkcert(dir, cb)
})
}
Certs.prototype.rename = function (prevName, newName, cb) {
if (!validName(prevName)) return nextTick(cb, new Error('previous cert name contains invalid characters'))
if (!validName(newName)) return nextTick(cb, new Error('new cert name contains invalid characters'))
let src = path.join(this._datadir, prevName)
let dst = path.join(this._datadir, newName)
fs.rename(src, dst, cb)
}
Certs.prototype.remove = function (name, cb) {
if (!validName(name)) return nextTick(cb, new Error('cert name contains invalid characters'))
fs.rm(path.join(this._datadir,name), { recursive: true }, cb)
}
Certs.prototype.list = function (cb) {
let self = this
fs.readdir(this._datadir, (err,files) => {
if (err && err.code !== 'ENOENT') return cb(err)
files ??= []
let list = []
;(function next(i) {
if (i >= files.length) return cb(null, list)
fs.stat(path.join(self._datadir, files[i], 'cert.pem'), (err,s) => {
if (s) list.push(files[i])
next(i+1)
})
})(0)
})
}
Certs.prototype.import = function (opts, cb) {
let self = this
let name = opts.name
let files = opts.files
let link = opts.link ?? false
let dir = path.join(this._datadir, name)
let certSources = []
let next = function next(i) {
if (i >= files.length) return importFiles()
fs.stat(files[i], (err,s) => {
if (err) return cb(err)
if (s.isDirectory()) {
fs.readdir(files[i], (err,list) => {
if (err) return cb(err)
;(function nextFile(j) {
if (j >= list.length) return next(i+1)
if (/^\./.test(list[j])) return nextFile(j+1)
let file = path.join(files[i],list[j])
fs.readFile(file, 'utf8', (err,src) => {
if (err) return cb(err)
pushCert(certSources, file, src)
nextFile(j+1)
})
})(0)
})
} else {
fs.readFile(file, 'utf8', (err,src) => {
if (err) return cb(err)
pushCert(certSources, file, src)
next(i+1)
})
}
})
}
fs.mkdir(dir, { recursive: true }, err => {
if (err) cb(err)
else next(0)
})
function importFiles() {
let files = new Map
let bestChain = -1
for (let i = 0; i < certSources.length; i++) {
let c = certSources[i]
if (c.type === 'private key' && files.has('key.pem')) {
return cb(new Error('multiple private keys provided'))
} else if (c.type === 'private key') {
files.set('key.pem', c)
} else if (c.type === 'certificate' && c.certs.length === 1) {
files.set('cert.pem', c)
} else if (c.type === 'certificate' && c.certs.length > 1 && bestChain < c.certs.length) {
files.set('ca.pem', c)
bestChain = c.certs.length
}
}
let keys = Array.from(files.keys())
;(function next(i) {
if (i >= keys.length) return cb(null, keys)
let c = files.get(keys[i])
let dst = path.join(self._datadir, name, keys[i])
if (link) {
fs.symlink(c.file, dst, err => {
if (err) cb(err)
else next(i+1)
})
} else {
fs.writeFile(dst, c.source, err => {
if (err) cb(err)
else next(i+1)
})
}
})(0)
}
function pushCert(out, file, src) {
let type = parseCertType(src)
let c = {
file,
source: src,
type,
}
if (type === 'certificate') c.certs = parseCerts(src)
out.push(c)
}
}
function validName(name) {
return name.length > 0 && !/\.\.|[\/:\s\0]/.test(name) && name !== '.'
}
function parseCerts(src) {
if (!src) return null
let r = /(?<=^|\n)(-{5}BEGIN ([^-]+)-{5}\r?\n[^]*?\r?\n-{5}END \2-{5}\r?\n)/g
if (typeof src !== 'string') src = src.toString()
return src.match(r).map(c => new X509Certificate(c))
}
function parseCertType(src) {
let m = /^-{5}\s*BEGIN\s+(.*)\s*-{5}\s*\r?\n/.exec(src)
return m ? m[1].toLowerCase() : null
}
function noop() {}