merge yatcm & nest cli

This commit is contained in:
David M 2024-09-06 11:21:23 -04:00
parent 85458fdf1c
commit 5b551f8a69
7 changed files with 392 additions and 16 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules
.env
*.sqlite

2
cli/.whitelist Normal file
View file

@ -0,0 +1,2 @@
*.$USERNAME.hackclub.app
$USERNAME.hackclub.app

View file

@ -4,6 +4,13 @@ const program = new Command();
const { execSync, spawn } = require('child_process');
const net = require('net');
const fs = require('fs');
const utils = require("./utils")
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
const os = require("os")
var username = os.userInfo().username
const isAdmin = !process.getuid() || os.userInfo().username == "nest-internal"
const validator = require('validator');
function run(command) {
try {
@ -29,21 +36,7 @@ program
server.close();
});
});
program
.command('caddy [cmd] [args...]')
.description('Manages caddy')
.action((cmd, args) => {
const yatcmArgs = cmd ? [cmd, ...args] : args;
const yatcm = spawn('yatcm', yatcmArgs);
yatcm.stdout.on('data', (data) => {
console.log(data.toString());
});
yatcm.stderr.on('data', (data) => {
console.error(data.toString());
});
});
program
.command('resources')
.description('See your Nest resource usage and limits')
@ -89,5 +82,79 @@ db
run(`sudo -u postgres /usr/local/nest/cli/helpers/create_db.sh ${name}`);
});
const caddy = program.command("caddy")
caddy
.command('list')
.description('lists all domains you have configured in caddy')
.option('--user', 'allows you to add a domain on behalf of a user (requires sudo)')
.action(async (options) => {
if (options?.user && isAdmin) username = options.user
var domains = await utils.getDomains(username)
domains = domains.map(domain => `- ${domain.domain} (${domain.proxy})`).join("\n")
console.log(domains)
});
caddy
.command('add <domain>')
.description('adds a domain to caddy')
.option('--proxy', 'changes where the domain should be proxied to (advanced)')
.option('--user', 'allows you to add a domain on behalf of a user (requires sudo)')
.action(async (domain, options) => {
if (options?.user && isAdmin) username = options.user
if (!validator.isFQDN(domain)) {
console.error("This domain is not a valid domain name. Please choose a valid domain name.")
process.exit(1)
}
if (await utils.domainExists(domain)) {
console.error("This domain already has already been taken by you or someone else. Pick another one!")
process.exit(1)
}
if (utils.checkWhitelist(domain, username)) {
await prisma.domain.create({
data: {
domain, username, proxy: options?.proxy || `unix//home/${username}/.${domain}.webserver.sock`
}
})
await utils.reloadCaddy()
return console.log(`${domain} added. (${options?.proxy || `unix//home/${username}/.${domain}.webserver.sock`})`)
}
// Proceed as a regular domain
if (!await utils.checkVerification(domain, username)) {
console.error(`Please set the TXT record for domain-verification to your username (${username}). You can remove it after it is added.`)
process.exit(1)
}
await prisma.domain.create({
data: {
domain, username, proxy: options?.proxy || `unix//home/${username}/.${domain}.webserver.sock`
}
})
await utils.reloadCaddy()
return console.log(`${domain} added. (${options?.proxy || `unix//home/${username}/.${domain}.webserver.sock`})`)
});
caddy
.command('rm <domain>')
.description('removes a domain from caddy')
.option('--user', 'allows you to add a domain on behalf of a user (requires sudo)')
.action(async (domain, options) => {
if (options?.user && isAdmin) username = options.user
if (!validator.isFQDN(domain)) {
console.error("This domain is not a valid domain name. Please choose a valid domain name.")
process.exit(1)
}
if (!await utils.domainExists(domain)) {
console.error("This domain is not in Caddy.")
process.exit(1)
}
if (!await utils.domainOwnership(domain, username)) {
console.error("You do not own the domain, so you cannot remove it.")
process.exit(1)
}
await prisma.domain.delete({
where: {
domain, username
}
})
await utils.reloadCaddy()
console.log(`${domain} removed.`)
});
program.parse(process.argv);

View file

@ -7,6 +7,11 @@
"nest": "./index.js"
},
"dependencies": {
"commander": "^12.1.0"
"@prisma/client": "5.18.0",
"commander": "12.1.0",
"dotenv": "16.4.5",
"minimatch": "10.0.1",
"prisma": "5.18.0",
"validator": "13.12.0"
}
}

16
cli/prisma/schema.prisma Normal file
View file

@ -0,0 +1,16 @@
generator client {
provider = "prisma-client-js"
binaryTargets = ["native"]
}
datasource db {
provider = "sqlite"
url = "file:./database.sqlite"
}
model Domain {
domain String @id @unique
createdAt DateTime @default(now())
username String
proxy String
}

206
cli/utils.js Normal file
View file

@ -0,0 +1,206 @@
const fs = require("node:fs")
const { minimatch } = require('minimatch')
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
const dns = require('node:dns').promises;
const os = require("os")
const isAdmin = !process.getuid() || os.userInfo().username == "nest-internal"
module.exports = {
checkWhitelist: function (domain, username) {
if (!fs.existsSync(".whitelist")) return
const whitelist = fs.readFileSync(".whitelist", "utf8").split("\n")
whitelist
var i = 0
while (i < whitelist.length) {
if (minimatch(domain, whitelist[i].replace("$USERNAME", username))) return true
i++
}
return false
},
async getDomains(username) {
const domains = await prisma.domain.findMany({
where: {
username
}
})
return domains
},
async domainExists(domain) {
const d = await prisma.domain.findFirst({
where: {
domain
}
})
if (!d) return false
else return true
},
async domainOwnership(domain, username) {
if (isAdmin) return true // If sudo, skip.
const d = await prisma.domain.findFirst({
where: {
domain,
username
}
})
if (!d) return false
else return true
},
async checkVerification(domain, username) {
if (isAdmin) return true // If sudo, skip.
try {
const records = await dns.resolveTxt(domain);
for (const record of records) {
for (const entry of record) {
if (entry.includes('domain-verification=' + username)) {
return true;
}
}
}
return false;
} catch (err) {
console.error('Error:', err);
return false;
}
},
async reloadCaddy() {
const domains = await prisma.domain.findMany({
where: {
}
})
var caddy = {
apps: {
http: {
servers: {
srv0: {
listen: [":443", ":80"],
routes: [], // normal routes
errors: {
routes: [] // error routing
}
}
}
},
tls: {
automation: {
policies: []
}
}
}
}
// dn42 certification support
const dn42Domains = domains.filter(domain => domain.domain.endsWith(".dn42")).map(domain => domain.domain)
if (dn42Domains.length > 1) {
caddy.apps.tls.automation.policies.push({
"subjects": dn42Domains,
"issuers": [
{
"ca": "https://acme.burble.dn42/v1/dn42/acme/directory",
"module": "acme"
}
]
})
}
domains.forEach(domain => {
caddy.apps.http.servers.srv0.routes.push({
"match": [
{
"host": [
domain.domain
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"health_checks": {
"active": {
"expect_status": 2,
"interval": 5000000000,
"timeout": 60000000000
}
},
"upstreams": [
{
"dial": domain.proxy
}
]
}
]
}
]
}
],
"terminal": true
})
caddy.apps.http.servers.srv0.errors.routes.push({
"match": [
{
"host": [
domain.domain
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "This site is either down or does not exist.\nIf this site really does exist, please make sure your Caddy is running. Try systemctl --user start caddy. It is also possible you have a >\n",
"handler": "static_response",
"status_code": 502
}
]
}
]
}
],
"match": [
{
"expression": "{err.status_code} == 502"
}
]
},
{
"handle": [
{
"body": "{err.status_code} | {err.status_text} (on {http.regexp.host.1})",
"close": true,
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
})
})
await fetch(process.env.CADDY_ADMIN_PATH || 'http://localhost:2019/load', {
unix: process.env.CADDY_SOCKET_PATH,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(caddy)
});
}
}

View file

@ -2,7 +2,84 @@
# yarn lockfile v1
commander@^12.1.0:
"@prisma/client@5.18.0":
version "5.18.0"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.18.0.tgz#526e4281a448f214c0ff81d65c39243608c98294"
integrity sha512-BWivkLh+af1kqC89zCJYkHsRcyWsM8/JHpsDMM76DjP3ZdEquJhXa4IeX+HkWPnwJ5FanxEJFZZDTWiDs/Kvyw==
"@prisma/debug@5.18.0":
version "5.18.0"
resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.18.0.tgz#527799e044d2903a35945e61ac2d8916e4b61ead"
integrity sha512-f+ZvpTLidSo3LMJxQPVgAxdAjzv5OpzAo/eF8qZqbwvgi2F5cTOI9XCpdRzJYA0iGfajjwjOKKrVq64vkxEfUw==
"@prisma/engines-version@5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169":
version "5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169.tgz#203426ebf4ec4e1acce7da4a59ec8f0df92b29e7"
integrity sha512-a/+LpJj8vYU3nmtkg+N3X51ddbt35yYrRe8wqHTJtYQt7l1f8kjIBcCs6sHJvodW/EK5XGvboOiwm47fmNrbgg==
"@prisma/engines@5.18.0":
version "5.18.0"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.18.0.tgz#26ea46e26498be622407cf95663d7fb4c39c895b"
integrity sha512-ofmpGLeJ2q2P0wa/XaEgTnX/IsLnvSp/gZts0zjgLNdBhfuj2lowOOPmDcfKljLQUXMvAek3lw5T01kHmCG8rg==
dependencies:
"@prisma/debug" "5.18.0"
"@prisma/engines-version" "5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169"
"@prisma/fetch-engine" "5.18.0"
"@prisma/get-platform" "5.18.0"
"@prisma/fetch-engine@5.18.0":
version "5.18.0"
resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.18.0.tgz#5b343e2b36b27e2713901ddd032ddd6932b3d55f"
integrity sha512-I/3u0x2n31rGaAuBRx2YK4eB7R/1zCuayo2DGwSpGyrJWsZesrV7QVw7ND0/Suxeo/vLkJ5OwuBqHoCxvTHpOg==
dependencies:
"@prisma/debug" "5.18.0"
"@prisma/engines-version" "5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169"
"@prisma/get-platform" "5.18.0"
"@prisma/get-platform@5.18.0":
version "5.18.0"
resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.18.0.tgz#0dc4c82fe9a4971f4519a57cb2dd69d8e0df4b71"
integrity sha512-Tk+m7+uhqcKDgnMnFN0lRiH7Ewea0OEsZZs9pqXa7i3+7svS3FSCqDBCaM9x5fmhhkufiG0BtunJVDka+46DlA==
dependencies:
"@prisma/debug" "5.18.0"
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
brace-expansion@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
dependencies:
balanced-match "^1.0.0"
commander@12.1.0:
version "12.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3"
integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==
dotenv@16.4.5:
version "16.4.5"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
minimatch@10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.1.tgz#ce0521856b453c86e25f2c4c0d03e6ff7ddc440b"
integrity sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==
dependencies:
brace-expansion "^2.0.1"
prisma@5.18.0:
version "5.18.0"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.18.0.tgz#5ef69c802a075b7596231ea57003496873610b9e"
integrity sha512-+TrSIxZsh64OPOmaSgVPH7ALL9dfU0jceYaMJXsNrTkFHO7/3RANi5K2ZiPB1De9+KDxCWn7jvRq8y8pvk+o9g==
dependencies:
"@prisma/engines" "5.18.0"
validator@13.12.0:
version "13.12.0"
resolved "https://registry.yarnpkg.com/validator/-/validator-13.12.0.tgz#7d78e76ba85504da3fee4fd1922b385914d4b35f"
integrity sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==