mirror of
https://github.com/hackclub/nest.git
synced 2025-01-02 20:26:40 +00:00
merge yatcm & nest cli
This commit is contained in:
parent
85458fdf1c
commit
5b551f8a69
7 changed files with 392 additions and 16 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
.env
|
||||
*.sqlite
|
2
cli/.whitelist
Normal file
2
cli/.whitelist
Normal file
|
@ -0,0 +1,2 @@
|
|||
*.$USERNAME.hackclub.app
|
||||
$USERNAME.hackclub.app
|
95
cli/index.js
95
cli/index.js
|
@ -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);
|
|
@ -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
16
cli/prisma/schema.prisma
Normal 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
206
cli/utils.js
Normal 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)
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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==
|
||||
|
|
Loading…
Reference in a new issue