http{/,s} git server
git clone http://git.nthia.dev/qwgit
#!/usr/bin/env node
const path = require('path')
const fs = require('fs')
const { spawn } = require('child_process')
const { homedir } = require('os')
const [args,argv] = require('./lib/args.js')(process.argv.slice(2), {
boolean: ['h','help','q','quiet']
})
const defaultPort = { ssl: 8765, clear: 8764 }
if (argv.get('help') ?? argv.get('h') !== undefined) return usage(0)
if (args.length === 0) return usage(1)
let quiet = (argv.get('quiet') ?? argv.get('q') ?? [])[0] ?? false
let datadir = (argv.get('datadir') ?? argv.get('d') ?? [])[0]
?? path.join(homedir(), '.local/share/qwgit')
let repodir = path.join(datadir, 'repos')
let configDir = argv.get('configdir') ?? path.join(homedir(), '.config/qwgit')
let certdir = (argv.get('certdir') ?? [configDir])[0]
let lcertdir = (argv.get('local-cert-dir') ?? [])[0] ?? path.join(certdir, 'local-certs')
let rcertdir = (argv.get('remote-cert-dir') ?? [])[0] ?? path.join(certdir, 'remote-certs')
let files = {
repo: (argv.get('repos') ?? []).concat(argv.get('repo') ?? [])[0]
?? path.join(configDir, 'repos.json'),
auth: (argv.get('auth') ?? [])[0] ?? path.join(configDir, 'auth.json'),
namespaces: (argv.get('namespaces') ?? [])[0] ?? path.join(configDir, 'namespaces.json'),
remoteCerts: (argv.get('remote-certs') ?? [])[0] ?? path.join(certdir,'remote-certs.json'),
}
let mkConfigDir = true
if (args[0] === 'server') {
const LocalCerts = require('./lib/local-certs.js')
let certs = new LocalCerts({ dir: lcertdir })
let certname = getCertName(argv) ?? 'server'
loadFiles(files, (err,data) => {
let qwgit = require('./')({
repodir,
repos: data.repo ?? [],
auth: data.auth ?? [],
namespaces: data.namespaces ?? [],
})
setListeners()
qwgit.on('reloadConfig', () => {
loadFiles(files, (err,data) => {
qwgit.setConfig(data)
setListeners()
})
})
let fds = argv.get('fd') ?? []
let listenArgs = args.length > 1 || fds.length > 0
? args.slice(1) : [ '+'+defaultPort.ssl, defaultPort.clear ]
listenArgs.forEach(arg => {
let opts = {}, ssl = false, cname = certname
let m = null
if (m = /^(\+)?(\d+)(?::([^:]+))?$/.exec(arg)) { // port:certname
ssl = m[1] === '+'
opts.port = m[2]
cname = m[3] ?? cname
} else if (m = /^([\d.]+):(\+)?(\d+)(?::([^:]+))?$/.exec(arg)) { // ipv4:port:certname
opts.host = m[1]
ssl = m[2] === '+'
opts.port = Number(m[3])
cname = m[3] ?? cname
} else if (m = /^\[([^\]+])\]:(\+)?(\d+)(?::([^:]+))?$/.exec(arg)) { // [ipv6]:port:certname
opts.host = m[1]
ssl = m[2] === '+'
opts.port = Number(m[3])
cname = m[4] ?? cname
} else {
return error(1, `unexpected formatting for port: ${arg}`)
}
createServer(ssl, cname, opts)
})
fds.forEach(arg => {
let m = /^(\+)?(\d+)(?::[^:]+)?$/.exec(arg)
if (!m) return error(1, `unexpected formatting for fd: ${arg}`)
let ssl = m[1] === '+'
let fd = Number(m[2])
let cname = m[3] ?? cname
createServer(ssl, cname, { fd })
})
function setListeners() {
qwgit.repos.on('create', repo => {
writeRepos(qwgit, err => { if (err) console.error(err) })
})
qwgit.repos.on('update', repo => {
writeRepos(qwgit, err => { if (err) console.error(err) })
})
qwgit.repos.on('remove', repo => {
fs.rm(path.join(repodir, repo.id), { recursive: true }, err => {
if (err) console.error(err)
})
writeRepos(qwgit, err => {
if (err) console.error(err)
})
})
}
function createServer(ssl, cname, opts) {
if (ssl) {
certs.getAllFiles(cname, (err,cdata) => {
if (err) return error(1, err)
let server = qwgit.createServer(cdata)
server.on('error', err => console.error(err))
server.listen(opts)
server.on('listening', () => {
console.error(`listening on ${opts.host ?? ''}:${String(opts.port).padEnd(5)} `
+ `https://${opts.host ?? '127.0.0.1'}:${opts.port}/`)
})
})
} else {
let server = qwgit.createServer()
server.on('error', err => console.error(err))
server.listen(opts)
server.on('listening', () => {
console.error(`listening on ${opts.host ?? ''}:${String(opts.port).padEnd(5)} `
+ `http://${opts.host ?? '127.0.0.1'}:${opts.port}/`)
})
}
}
})
} else if (args[0] === 'config' && /^(auth|repos?)$/.test(args[1]) && args[2] === 'file') {
let key = args[1]
if (key === 'repos') key = 'repo'
console.log(files[key])
} else if (args[0] === 'config' && /^(auth|repos?)$/.test(args[1]) && args[2] === 'show') {
let key = args[1]
if (key === 'repos') key = 'repo'
fs.readFile(files[key], 'utf8', (err,src) => {
if (err) error(1, err)
else console.log(src)
})
} else if (args[0] === 'config' && args[1] === 'reload') {
let remote = (argv.get('remote') ?? argv.get('r') ?? [])[0]
?? 'https://localhost:' + defaultPort.ssl
loadAPI([getCertName(argv),'client','default'], remote, (err,api) => {
if (err) return error(1, err)
api.reloadConfig(err => {
if (err) return error(1, err)
})
})
} else if (args[0] === 'certs' && args.length === 1
|| (args[0] === 'cert' && (args[1] === 'list' || args[1] === undefined))) {
const LocalCerts = require('./lib/local-certs.js')
let certs = new LocalCerts({ dir: lcertdir })
certs.list((err,names) => {
if (err) error(1, err)
else names.forEach(name => console.log(name))
})
} else if (args[0] === 'cert' && args[1] === 'new') {
const LocalCerts = require('./lib/local-certs.js')
let certs = new LocalCerts({ dir: lcertdir })
const { randomBytes, X509Certificate } = require('crypto')
let name = getCertName(argv) ?? args[2] ?? randomBytes(4).toString('hex')
certs.create(name, err => {
if (err) return error(1, err)
if (!quiet) {
certs.getFingerprint(name, 'cert.pem', (err,fp) => {
if (err) return error(1, err)
console.error(`created cert with name: ${name}`)
console.error(`created cert with fingerprint: ${fp}`)
})
}
})
} else if (args[0] === 'cert' && args[1] === 'rm') {
const LocalCerts = require('./lib/local-certs.js')
let certs = new LocalCerts({ dir: lcertdir })
let names = (argv.get('name') ?? []).concat(argv.get('n') ?? []).concat(args.slice(2))
;(function next(i) {
if (i >= names.length) return
certs.remove(names[i], err => {
if (err) error(1, err)
else next(i+1)
})
})(0)
} else if (args[0] === 'cert' && /^(rename|mv|move)$/.test(args[1])) {
const LocalCerts = require('./lib/local-certs.js')
let certs = new LocalCerts({ dir: lcertdir })
let oldName = args[2], newName = args[3]
if (oldName === undefined) return error(1, 'old name not given in cert rename')
if (newName === undefined) return error(1, 'new name not given in cert rename')
certs.rename(oldName, newName, err => {
if (err) error(1, err)
})
} else if (args[0] === 'cert' && (args[1] === 'import' || args[1] === 'link')) {
const LocalCerts = require('./lib/local-certs.js')
let certs = new LocalCerts({ dir: lcertdir })
const { randomBytes, X509Certificate } = require('crypto')
let name = (argv.get('name') ?? []).concat(argv.get('n') ?? [])[0]
?? randomBytes(4).toString('hex')
let files = args.slice(2)
if (files.length === 0) {
return error(1, `usage: ${getCmd()} cert ${args[1]} [CERT FILES OR DIRECTORY]`)
}
let link = args[1] === 'link'
certs.import({ name, files, link }, (err,imported) => {
if (err) return error(1, err)
let okCert = imported.includes('cert.pem')
let okKey = imported.includes('key.pem')
if (!okCert || !okKey) {
if (!okCert) console.error(`${args[1]} failed: cert not provided`)
if (!okKey) console.error(`${args[1]} failed: private key not provided`)
certs.remove(name, err => {
if (err) return error(1, err)
else process.exit(1)
})
} else if (!quiet) {
certs.getFingerprint(name, 'cert.pem', (err,fp) => {
if (err) return error(1, err)
console.error(`${args[1]}ed cert with name: ${name}`)
console.error(`${args[1]}ed cert with fingerprint: ${fp}`)
})
}
})
} else if (args[0] === 'cert' && /^(fp|fingerprint)$/.test(args[1])) {
const LocalCerts = require('./lib/local-certs.js')
let certs = new LocalCerts({ dir: lcertdir })
let names = null
let showName = false
let next = function next(i) {
if (i >= names.length) return
certs.getFingerprint(names[i], 'cert.pem', (err,fp) => {
if (err) return error(1, err)
if (showName) console.log(names[i],fp)
else console.log(fp)
next(i+1)
})
}
if (args.length === 2) {
showName = true
certs.list((err,list) => {
if (err) return error(1, err)
names = list
next(0)
})
} else {
names = args
next(2)
}
} else if (args[0] === 'cert' && args[1] === 'path') {
if (args.length !== 4) {
return error(1, `usage: ${getCmd()} cert file NAME FILE`)
}
console.log(path.join(lcertdir, args[2], args[3]+'.pem'))
} else if (args[0] === 'cert' && (args[1] === 'remotes' || (args[1] === 'remote' && args[2] === 'list'))) {
const loadRemoteCerts = require('./lib/remote-certs.js')
loadRemoteCerts(files.remoteCerts, rcertdir, (err,rcerts) => {
if (err) return error(1, err)
rcerts.list().forEach(row => console.log(row.hostname, row.fingerprint512 ?? row.fingerprint))
})
} else if (args[0] === 'cert' && args[1] === 'remote' && args[2] === 'add') {
if (args.length < 5) return error(1, `usage: ${getCmd()} cert remote add HOST FINGERPRINT`)
const loadRemoteCerts = require('./lib/remote-certs.js')
loadRemoteCerts(files.remoteCerts, rcertdir, (err,rcerts) => {
if (err) return error(1, err)
let hostname = args[3]
let fingerprint = args[4]
rcerts.add({ fingerprint, hostname })
rcerts.flush(err => {
if (err) return error(1, err)
})
})
} else if (args[0] === 'cert' && args[1] === 'remote' && /^(rm|remove)$/.test(args[2])) {
if (args.length < 4) return error(1, `usage: ${getCmd()} cert remote remove HOST FINGERPRINT`)
const loadRemoteCerts = require('./lib/remote-certs.js')
loadRemoteCerts(files.remoteCerts, rcertdir, (err,rcerts) => {
if (err) return error(1, err)
let hostname = args[3]
let fingerprint = args[4]
if (args.length === 5) {
rcerts.remove({ fingerprint, hostname })
} else {
rcerts.remove({ hostname })
}
rcerts.flush(err => {
if (err) return error(1, err)
})
})
} else if (args[0] === 'cert' && args[1] === 'remote' && /^fetch(-cert)?$/.test(args[2])) {
let remote = (argv.get('remote') ?? argv.get('r') ?? [])[0] ?? args[3]
let u = new URL(remote)
const getCert = require('./lib/get-cert.js')
getCert(remote, (err,cert) => {
if (err) return error(1, err)
if (!cert) return error(1, 'remote did not provide a tls certificate')
console.log(cert.fingerprint512?.toLowerCase().replace(/:/g,''))
})
} else if (args[0] === 'cert' && args[1] === 'remote' && args[2] === 'fetch-ca') {
let remote = (argv.get('remote') ?? argv.get('r') ?? [])[0] ?? args[3]
let u = new URL(remote)
const getCert = require('./lib/get-cert.js')
getCert(remote, (err,cert) => {
if (err) return error(1, err)
if (!cert) return error(1, 'remote did not provide a tls certificate')
console.log(rawToPEM(cert.raw))
})
} else if (args[0] === 'cert' && args[1] === 'remote' && args[2] === 'import') {
let remote = (argv.get('remote') ?? argv.get('r') ?? [])[0] ?? args[3]
let u = new URL(/^https?:/.test(remote) ? remote : 'https://' + remote)
const loadRemoteCerts = require('./lib/remote-certs.js')
const getCert = require('./lib/get-cert.js')
loadRemoteCerts(files.remoteCerts, rcertdir, (err,rcerts) => {
if (err) return error(1, err)
getCert(remote, (err,cert) => {
if (err) return error(1, err)
if (!cert) return error(1, 'remote did not provide a tls certificate')
let recs = rcerts.getFromFingerprint(cert.fingerprint512)
rcerts.add({
fingerprint: cert.fingerprint512,
hostname: hostname(u),
cert: rawToPEM(cert.raw),
})
rcerts.flush(err => {
if (err) return error(1, err)
})
})
})
} else if (args[0] === 'cert' && args[1] === 'remote' && args[2] === 'fetch-or-import') {
let remote = (argv.get('remote') ?? argv.get('r') ?? [])[0] ?? args[3]
let u = new URL(/^https?:/.test(remote) ? remote : 'https://' + remote)
const loadRemoteCerts = require('./lib/remote-certs.js')
const getCert = require('./lib/get-cert.js')
loadRemoteCerts(files.remoteCerts, rcertdir, (err,rcerts) => {
if (err) return error(1, err)
let hrecs = rcerts.getFromHostname(hostname(u))
if (hrecs.length > 0) return hrecs.forEach(r => {
console.log(r.cert)
})
getCert(remote, (err,cert) => {
if (err) return error(1, err)
if (!cert) return error(1, 'remote did not provide a tls certificate')
let recs = rcerts.getFromFingerprint(cert.fingerprint512)
let rec = {
fingerprint: cert.fingerprint512,
hostname: hostname(u),
cert: rawToPEM(cert.raw),
}
rcerts.add(rec)
rcerts.flush(err => {
if (err) return error(1, err)
console.log(rec.cert)
})
})
})
} else if (args[0] === 'install') {
const loadRemoteCerts = require('./lib/remote-certs.js')
findGitRoot((err,gitDir) => {
if (err) return error(1, err)
let remote = (argv.get('remote') ?? argv.get('r') ?? [])[0]
?? args[1]
?? 'https://localhost:' + defaultPort.ssl + '/' + path.basename(gitDir)
if (/^[\/]/.test(remote)) remote = 'https://localhost:' + defaultPort.ssl + remote
if (!/^https?:/.test(remote)) remote = 'https://' + remote
let rindex = (argv.get('index') ?? []).concat(argv.get('i') ?? [])[0] ?? 0
let u = new URL(remote)
let rhost = u.protocol + '//' + u.host
let remoteName = (argv.get('remote-name') ?? []).concat(argv.get('rn') ?? [])[0]
?? u.hostname
loadRemoteCerts(files.remoteCerts, rcertdir, (err,rcerts) => {
let recs = rcerts.getFromHostname(hostname(u))
if (recs.length === 0) return errImport()
if (recs[rindex] === undefined) return errImport()
if (recs[rindex].cert === undefined) return errImport()
let caFile = rcerts.certPath(recs[rindex].cert)
fs.stat(caFile, (err,s) => {
if (err && err.code === 'ENOENT') return errImport()
getLocalCertFiles([getCertName(argv),'client','default'], (err,opts,ldir) => {
let certFile = path.join(ldir,'cert.pem')
let keyFile = path.join(ldir,'key.pem')
mcmd([
['git',['config','--local',`http.${rhost}.sslCAinfo`,caFile], { stdio: 'inherit' }],
['git',['config','--local',`http.${rhost}.sslCert`,certFile], { stdio: 'inherit' }],
['git',['config','--local',`http.${rhost}.sslKey`,keyFile], { stdio: 'inherit' }],
['git',['remote','add',remoteName,remote], { stdio: 'inherit' }],
], err => {
if (err) return error(1, err)
})
})
})
})
})
} else if (args[0] === 'cert' && args[1] === 'remote' && args[2] === 'ca') {
let remote = (argv.get('remote') ?? argv.get('r') ?? [])[0] ?? args[3]
let u = new URL(/^https?:/.test(remote) ? remote : 'https://' + remote)
const loadRemoteCerts = require('./lib/remote-certs.js')
const getCert = require('./lib/get-cert.js')
loadRemoteCerts(files.remoteCerts, rcertdir, (err,rcerts) => {
if (err) return error(1, err)
let recs = rcerts.getFromHostname(hostname(u))
recs.forEach(rec => {
if (rec.cert) console.log(rec.cert)
})
})
} else if ((args[0] === 'repos' && args.length === 1) || (args[0] === 'repo' && args[1] === 'list')) {
let remote = (argv.get('remote') ?? argv.get('r') ?? [])[0]
?? 'https://localhost:' + defaultPort.ssl
loadAPI([getCertName(argv),'client','default'], remote, (err,api) => {
if (err) return error(1, err)
api.listRepos(onlist)
})
function onlist(err,repos) {
if (err && err.code === 'UNTRUSTED_FINGERPRINT') {
const loadRemoteCerts = require('./lib/remote-certs.js')
loadRemoteCerts(files.remoteCerts, rcertdir, (ierr,rcerts) => {
if (ierr) return error(1, ierr)
untrustedError(rcerts, err)
})
return
}
if (err) return error(1, err)
repos.forEach(repo => console.log(JSON.stringify(repo)))
}
} else if (args[0] === 'repo' && /^(create|new)$/.test(args[1])) {
let [args,argv] = require('./lib/args.js')(process.argv.slice(2), {
boolean: ['h','help','q','quiet','public','unlisted']
})
let remote = (argv.get('remote') ?? argv.get('r') ?? [])[0]
?? 'https://localhost:' + defaultPort.ssl
let paths = (argv.get('path') ?? []).concat(argv.get('paths') ?? []).concat(args.slice(2))
let r = {
path: paths,
list: (argv.get('list') ?? []).concat(argv.get('l') ?? []).concat(argv.get('x') ?? [])
.concat(argv.get('all') ?? []),
push: (argv.get('push') ?? []).concat(argv.get('write') ?? [])
.concat(argv.get('all') ?? []),
pull: (argv.get('pull') ?? []).concat(argv.get('read') ?? [])
.concat(argv.get('all') ?? []),
name: (argv.get('name') ?? []).concat(argv.get('n') ?? [])[0] ?? path.basename(paths[0] ?? ''),
description: (argv.get('description') ?? [])
.concat(argv.get('desc') ?? []).concat(argv.get('d') ?? []) ?? '',
}
let vcount = updateVisibility(r, argv)
r.list = r.list.concat(r['list:add'] ?? [])
let lrm = r['list:remove'] ?? []
r.list = r.list.filter(x => !lrm.includes(x))
delete r['list:add']
delete r['list:remove']
r.push = r.push.concat(r['push:add'] ?? [])
let pushrm = r['push:remove'] ?? []
r.push = r.push.filter(x => !pushrm.includes(x))
delete r['push:add']
delete r['push:remove']
r.pull = r.pull.concat(r['pull:add'] ?? [])
let pullrm = r['pull:remove'] ?? []
r.pull = r.pull.filter(x => !pullrm.includes(x))
delete r['pull:add']
delete r['pull:remove']
if (vcount > 1) return error(1, 'multiple visibility modifiers are in conflict')
loadAPI([getCertName(argv),'client','default'], remote, (err,api) => {
if (err) return error(1, err)
api.createRepo(r, (err,repo) => {
if (err && err.code === 'UNTRUSTED_FINGERPRINT') {
const loadRemoteCerts = require('./lib/remote-certs.js')
loadRemoteCerts(files.remoteCerts, rcertdir, (ierr,rcerts) => {
if (ierr) return error(1, ierr)
untrustedError(rcerts, err)
})
}
else if (err) error(1, err)
else console.log(repo.id)
})
})
} else if (args[0] === 'repo' && args[1] === 'update') {
let remote = (argv.get('remote') ?? argv.get('r') ?? [])[0]
?? 'https://localhost:' + defaultPort.ssl
let rid = args[2]
let r = {}
let id = (argv.get('id') ?? [])[0]
if (id !== undefined) r.id = id
let mods = [
['paths','path'],
['list','l','x','all'],
['push','write','all'],
['pull','read','all'],
['tags','tag'],
]
mods.forEach(ms => {
let xs = ms.flatMap(m => argv.get(m) ?? [])
if (xs.length > 0) r[ms[0]] = xs
let rs = ms.flatMap(r => (argv.get(r+':remove') ?? []).concat(argv.get(r+':rm') ?? []))
if (rs.length > 0) r[ms[0]+':remove'] = rs
let as = ms.flatMap(r => (argv.get(r+':add') ?? []).concat(argv.get(r+':push') ?? []))
if (as.length > 0) r[ms[0]+':add'] = as
})
let name = (argv.get('name') ?? []).concat(argv.get('n') ?? [])[0]
if (name !== undefined) r.name = name
let description = (argv.get('description') ?? [])
.concat(argv.get('desc') ?? []).concat(argv.get('d') ?? [])
if (description !== undefined) r.description = description
let vcount = updateVisibility(r, argv)
if (vcount > 1) return error(1, 'multiple visibility modifiers are in conflict')
loadAPI([getCertName(argv),'client','default'], remote, (err,api) => {
if (err) return error(1, err)
if (/^\//.test(rid)) {
api.updateRepoFromPath(rid, r, (err,repo) => {
if (err) error(1, err)
})
} else {
api.updateRepo(rid, r, (err,repo) => {
if (err) error(1, err)
})
}
})
} else if (args[0] === 'repo' && /^(rm|remove|del(?:ete)?)$/.test(args[1])) {
let remote = (argv.get('remote') ?? argv.get('r') ?? [])[0]
?? 'https://localhost:' + defaultPort.ssl
let id = (argv.get('id') ?? [])[0] ?? args[2]
loadAPI([getCertName(argv),'client','default'], remote, (err,api) => {
if (err) return error(1, err)
if (/^\//.test(id)) {
api.removeRepoFromPath(id, err => {
if (err) error(1, err)
})
} else {
api.removeRepo(id, err => {
if (err) error(1, err)
})
}
})
} else if (args[0] === 'clone') {
let remote = args[1]
if (/^[\/]/.test(remote)) remote = 'https://localhost:' + defaultPort.ssl + remote
if (!/^https?:/.test(remote)) remote = 'https://' + remote
let u = new URL(/^https?:/.test(remote) ? remote : 'https://' + remote)
let rindex = (argv.get('index') ?? []).concat(argv.get('i') ?? [])[0] ?? 0
const loadRemoteCerts = require('./lib/remote-certs.js')
const getCert = require('./lib/get-cert.js')
loadRemoteCerts(files.remoteCerts, rcertdir, (err,rcerts) => {
if (err) return error(1, err)
let recs = rcerts.getFromHostname(hostname(u))
if (recs.length === 0) return errImport()
if (recs[rindex] === undefined) return errImport()
if (recs[rindex].cert === undefined) return errImport()
let caFile = rcerts.certPath(recs[rindex].cert)
fs.stat(caFile, (err,s) => {
if (err && err.code === 'ENOENT') return errImport()
getLocalCertFiles([getCertName(argv),'client','default'], (err,opts,ldir) => {
let certFile = path.join(ldir,'cert.pem')
let keyFile = path.join(ldir,'key.pem')
spawn('git', [
'-c', 'http.sslCAinfo=' + caFile,
'-c', 'http.sslCert=' + certFile,
'-c', 'http.sslKey=' + keyFile,
'clone',
remote
].concat(args.slice(2)), { stdio: 'inherit' })
})
})
})
} else {
usage(1)
}
function getCertName(argv) {
return (argv.get('certname') ?? argv.get('cname') ?? argv.get('cn') ?? [])[0]
}
function loadAPI(certnames, remote, cb) {
const loadRemoteCerts = require('./lib/remote-certs.js')
const API = require('./lib/api.js')
getLocalCertFiles(certnames, (err,opts) => {
if (err) return cb(err)
loadRemoteCerts(files.remoteCerts, rcertdir, (err,rcerts) => {
if (err) return error(1, err)
let api = new API(remote, Object.assign({ verify }, opts))
cb(null, api)
function verify(hostname, fp, f) {
f(null, rcerts.verify({ hostname, fingerprint: fp }))
}
})
})
}
function getLocalCertFiles(certnames, cb) {
const LocalCerts = require('./lib/local-certs.js')
certnames = certnames.filter(Boolean)
if (certnames.length === 0) return cb(null, null)
;(function next(i) {
if (i >= certnames.length) {
return cb(new Error(`no certs found for cert list: ${certnames.join(', ')}`))
}
let lcerts = new LocalCerts({ dir: lcertdir })
lcerts.getAllFiles(certnames[i], (err,opts) => {
if (err) return error(1, err)
if (!opts) return next(i+1)
cb(null, opts, path.join(lcertdir, certnames[i]))
})
})(0)
}
function writeRepos(qwgit, cb) {
let repos = qwgit.repos.list()
let str = '[' + repos.map(r => JSON.stringify(r)).join(',\n') + ']\n'
if (mkConfigDir) {
fs.mkdir(path.dirname(files.repo), { recursive: true }, err => {
if (err) cb(err)
else fs.writeFile(files.repo, str, cb)
})
} else {
fs.writeFile(files.repo, str, cb)
}
mkConfigDir = false
}
function loadFiles(files, cb) {
let out = {}, pending = 1
Object.entries(files).forEach(([key,file]) => {
pending++
fs.readFile(file, (err,src) => {
if (err && err.code !== 'ENOENT') return cb(err)
if (src) {
try { out[key] = JSON.parse(src) }
catch (err) {
cb = noop
return cb(err)
}
}
if (--pending === 0) cb(null, out)
})
})
if (--pending === 0) cb(null, out)
}
function getCerts(dir, cb) {
let opts = {}, pending = 4
mread(dir, ['privkey.pem','key.pem'], (err, src) => {
if (src) opts.key = src
if (--pending === 0) cb(null, opts)
})
mread(dir, ['cert.pem'], (err, src) => {
if (src) opts.cert = src
if (--pending === 0) cb(null, opts)
})
mread(dir, ['fullchain.pem','ca.pem'], (err, src) => {
if (src) opts.ca = src
if (--pending === 0) cb(null, opts)
})
if (--pending === 0) cb(null, opts)
}
function mread(dir, files, i, cb) {
if (typeof i === 'function') {
cb = i
i = 0
}
if (i >= files.length) return cb(null, null)
fs.readFile(path.join(dir, files[i]), (err,src) => {
if (err && err.code === 'ENOENT') read(dir, files, i+1, cb)
else if (err) cb(err)
else cb(null, src)
})
}
function usage(code) {
let cmd = getCmd()
console.log(`usage: ${cmd} COMMAND ...
${cmd} server {OPTIONS}
Start a web server on ADDR:PORT, file descriptor FD, or an AF_UNIX FILE.
--http-port=PORT Port to listen on. (default: 9650)
--http-fd=FD File descriptor inherited from a parent process.
-a ADDR, --addr=ADDR (Default: "0.0.0.0")
-u FILE, --unix=FILE Listen on an AF_UNIX FILE.
--certname=CERT_NAME, --cname=CERT_NAME, --cn=CERT_NAME
Use the certificate profile by CERT_NAME. Default: "server"
${cmd} cert
${cmd} cert list
List the names of all local tls certificates known to qwgit,
printing one name per line.
${cmd} cert new CERT_NAME {OPTIONS}
Create a new ed25519 local tls certificate and private key.
If no CERT_NAME is given, a random hex string is used.
${cmd} cert rm [NAME...]
Remove one or more local tls certificates by NAME(s).
${cmd} cert rename OLD_NAME NEW_NAME
${cmd} cert move OLD_NAME NEW_NAME
${cmd} cert mv OLD_NAME NEW_NAME
Give a local tls certificate a new name.
${cmd} cert import {OPTIONS} [FILES OR DIRECTORY...]
Copy tls certificate FILES or a DIRECTORY of local certificate files into qwgit.
Each of the files should be related: private key, certificate file, certificate chain.
--certname=CERT_NAME, --cname=CERT_NAME, --cn=CERT_NAME
Assign the certificates a name.
${cmd} cert link [FILES OR DIRECTORY...]
Add local tls certificates to qwgit by symbolic links to FILES or DIRECTORY.
Each of the files should be related: private key, certificate file, certificate chain.
--certname=CERT_NAME, --cname=CERT_NAME, --cn=CERT_NAME
Assign the certificates a name.
${cmd} cert remotes
${cmd} cert remote list
List all remote host/fingerprint tls certificate associations known to qwgit.
${cmd} cert remote add HOST FINGERPRINT
Associate a remote HOST with a tls certificate hex string FINGERPRINT.
${cmd} cert remote rm HOST FINGERPRINT
${cmd} cert remote remove HOST FINGERPRINT
Unassociate a remote HOST with a tls certificate hex string FINGERPRINT.
${cmd} cert remote rm HOST
${cmd} cert remote remove HOST
Unassociate all tls certificate fingerprints from a remote HOST.
${cmd} repo list
${cmd} repo create REPO
--unlisted
--public
--private
--only=USERLIST
${cmd} repo update REPO
--unlisted
--public
--private
--only=USERLIST
${cmd} mirror
${cmd} push
`.trim().replace(/^ {4}/g,'') + '\n')
process.exit(code)
}
function error(code, err) {
console.error(err)
process.exit(code)
}
function noop() {}
function getCmd() { return path.basename(process.argv[1]) }
function untrustedError(rcerts, err) {
const checkCertChain = require('./lib/cert-chain.js')
let cmd = getCmd()
let recs = Array.from(rcerts.getFromHostname(err.hostname) ?? new Set)
let fp = err.fingerprint512
let tlsErr = checkCertChain(err.certificate)
if (recs.length === 0) {
error(1, `No fingerprints set for host "${err.hostname}".
${tlsErr
? `Error verifying the remote certificate: ${tlsErr.message}`
: `The TLS certificate provided by the remote is signed by a root certificate bundled with this runtime.`
}
If you want to associate the reported fingerprint with the given host,
run this command:
${cmd} cert remote add ${err.hostname} ${fp}
`.trim().replace(/^ {6}/mg,'') + '\n')
} else {
let s = recs.length > 1 ? '' : 's'
error(1, `!!! WARNING: fingerprint provided by remote does not match previously stored value${s}.
This fingerprint was provided by the remote:
${err.fingerprint512}
And these are the previously saved fingerprint${s}:
${recs.map(r => r.fingerprint512).join('\n ')}
${tlsErr
? `${tlsErr}`
: `This TLS certificate appears "trustworthy" according to the hierarchical CA issuer chain.`
}
This problem may be caused by one of several scenarios:
* the connection has been actively compromised
* the remote updated its certificate genuinely
* the fingerprint was set incorrectly for the given remote
If you think this is a compromised connection, you can do nothing or attempt to check the
fingerprint through other means.
If you think the provided fingerprint is correct, use a combination of
\`${cmd} cert remote add HOST FINGERPRINT\` and
\`${cmd} cert remote remove HOST FINGERPRINT\`
commands to remedy the situation.
`.trim().replace(/^ {4}/mg,'') + '\n')
}
}
function rawToPEM(raw) {
return [
'-----BEGIN CERTIFICATE-----',
raw.toString('base64').split(/(.{64})/g).filter(Boolean),
'-----END CERTIFICATE-----'
].flat().join('\n')
}
function mcmd(cs,cb) {
;(function next(i) {
if (i >= cs.length) return cb(null, 0)
let c = cs[i]
let ps = spawn(c[0],c[1],c[2])
ps.once('exit', code => {
if (code === 0) next(i+1)
else cb(new Error(`non-zero exit code (${code}) in command: ${c[0]} ${c[1].join(' ')}`))
})
})(0)
}
function hostname(u) { return u.host }
function findGitRoot(cb) {
let prev = null
;(function next(d) {
if (d === prev) return cb(null, null)
prev = d
fs.stat(path.join(d,'.git'), (err,s) => {
if (s) cb(null, d)
else next(path.dirname(d))
})
})(process.cwd())
}
function updateVisibility(r, argv) {
let vcount = 0
if (argv.has('public')) {
if (r['list:add'] === undefined) r['list:add'] = []
if (r['pull:add'] === undefined) r['pull:add'] = []
r['list:add'].push('*')
r['pull:add'].push('*')
vcount++
}
if (argv.has('unlisted')) {
if (r['list:remove'] === undefined) r['list:remove'] = []
if (r['pull:add'] === undefined) r['pull:add'] = []
r['list:remove'].push('*')
r['pull:add'].push('*')
vcount++
}
if (argv.has('private')) {
if (r['list:remove'] === undefined) r['list:remove'] = []
if (r['pull:remove'] === undefined) r['pull:remove'] = []
r['list:remove'].push('*')
r['pull:remove'].push('*')
vcount++
}
return vcount
}
function errImport() {
error(1, `no cert imported for remote: ${remote}
import a certificate for this remote with:
${getCmd()} cert remote import '${remote.replace(/\x27/g,'%27').replace(/\\/g,'%5c')}'
`.replace(/^ {8}/mg,'').trim() + '\n')
}