From e97ba51a6ca3ff76f0f6e41449f4812a49068b50 Mon Sep 17 00:00:00 2001 From: Louis Paul Date: Thu, 26 Mar 2026 12:57:27 +0000 Subject: [PATCH] Initial infra: Traefik, Oktopus, Hub, Apps, network, security --- apps/docker-compose.yml | 91 +++++++++++ oktopus/docker-compose.yml | 106 +++++++++++++ setup.sh | 47 ++++++ system/10-lan.network | 12 ++ system/10-netplan-all-en.network | 0 system/10-netplan-all-eth.network | 0 system/20-wan.network | 9 ++ system/jail.local | 11 ++ traefik-hub/Dockerfile | 7 + traefik-hub/docker-compose.yml | 31 ++++ traefik-hub/index.html | 148 +++++++++++++++++ traefik-hub/login.html | 36 +++++ traefik-hub/package.json | 6 + traefik-hub/server.js | 254 ++++++++++++++++++++++++++++++ traefik/docker-compose.yml | 38 +++++ traefik/dynamic/routes.yml | 1 + 16 files changed, 797 insertions(+) create mode 100644 apps/docker-compose.yml create mode 100644 oktopus/docker-compose.yml create mode 100755 setup.sh create mode 100644 system/10-lan.network create mode 100644 system/10-netplan-all-en.network create mode 100644 system/10-netplan-all-eth.network create mode 100644 system/20-wan.network create mode 100644 system/jail.local create mode 100644 traefik-hub/Dockerfile create mode 100644 traefik-hub/docker-compose.yml create mode 100644 traefik-hub/index.html create mode 100644 traefik-hub/login.html create mode 100644 traefik-hub/package.json create mode 100644 traefik-hub/server.js create mode 100644 traefik/docker-compose.yml create mode 100644 traefik/dynamic/routes.yml diff --git a/apps/docker-compose.yml b/apps/docker-compose.yml new file mode 100644 index 0000000..7225a95 --- /dev/null +++ b/apps/docker-compose.yml @@ -0,0 +1,91 @@ +services: + + # ── Gitea ──────────────────────────────────────────────────── + gitea: + image: gitea/gitea:latest + environment: + - GITEA__database__DB_TYPE=postgres + - GITEA__database__HOST=gitea-db:5432 + - GITEA__database__NAME=gitea + - GITEA__database__USER=gitea + - GITEA__database__PASSWD=gitea + volumes: + - gitea-data:/data + networks: + - proxy + - internal + labels: + - "traefik.enable=true" + - "traefik.http.routers.git.rule=Host(`git.gigafibre.ca`)" + - "traefik.http.routers.git.entrypoints=websecure" + - "traefik.http.routers.git.tls.certresolver=letsencrypt" + - "traefik.http.services.git.loadbalancer.server.port=3000" + restart: unless-stopped + + gitea-db: + image: postgres:14-alpine + environment: + - POSTGRES_DB=gitea + - POSTGRES_USER=gitea + - POSTGRES_PASSWORD=gitea + volumes: + - gitea-db-data:/var/lib/postgresql/data + networks: + - internal + restart: unless-stopped + + # ── Targo Timesheet Frontend ───────────────────────────────── + targo-frontend: + image: nginx:alpine + volumes: + - targo-frontend-data:/usr/share/nginx/html + networks: + - proxy + labels: + - "traefik.enable=true" + - "traefik.http.routers.targo-app.rule=Host(`timesheet.gigafibre.ca`)" + - "traefik.http.routers.targo-app.entrypoints=websecure" + - "traefik.http.routers.targo-app.tls.certresolver=letsencrypt" + - "traefik.http.services.targo-app.loadbalancer.server.port=80" + restart: unless-stopped + + # ── Dispatch App Frontend ──────────────────────────────────── + dispatch-frontend: + image: nginx:alpine + volumes: + - dispatch-frontend-data:/usr/share/nginx/html + networks: + - proxy + labels: + - "traefik.enable=true" + - "traefik.http.routers.dispatch.rule=Host(`dispatch.gigafibre.ca`)" + - "traefik.http.routers.dispatch.entrypoints=websecure" + - "traefik.http.routers.dispatch.tls.certresolver=letsencrypt" + - "traefik.http.services.dispatch.loadbalancer.server.port=80" + restart: unless-stopped + + # ── Targo Backend + DB ─────────────────────────────────────── + targo-db: + image: postgres:14-alpine + environment: + - POSTGRES_DB=targo + - POSTGRES_USER=targo + - POSTGRES_PASSWORD=targo + volumes: + - targo-db-data:/var/lib/postgresql/data + networks: + - internal + restart: unless-stopped + +networks: + proxy: + external: true + internal: + driver: bridge + +volumes: + gitea-data: + gitea-db-data: + targo-frontend-data: + dispatch-frontend-data: + targo-db-data: diff --git a/oktopus/docker-compose.yml b/oktopus/docker-compose.yml new file mode 100644 index 0000000..4eea514 --- /dev/null +++ b/oktopus/docker-compose.yml @@ -0,0 +1,106 @@ +services: + frontend: + image: oktopusp/frontend-ce:latest + environment: + - NEXT_PUBLIC_REST_ENDPOINT= + networks: + - proxy + - oktopus + labels: + - "traefik.enable=true" + - "traefik.http.routers.oss.rule=Host(`oss.gigafibre.ca`)" + - "traefik.http.routers.oss.entrypoints=web,websecure" + - "traefik.http.routers.oss.tls.certresolver=letsencrypt" + - "traefik.http.services.oss.loadbalancer.server.port=3000" + - "traefik.docker.network=proxy" + restart: unless-stopped + + controller: + image: oktopusp/controller:latest + environment: + - MONGO_URI=mongodb://mongo:27017 + - NATS_URL=nats://nats:4222 + networks: + - proxy + - oktopus + labels: + - "traefik.enable=true" + - "traefik.http.routers.oss-api.rule=Host(`oss.gigafibre.ca`) && PathPrefix(`/api`)" + - "traefik.http.routers.oss-api.entrypoints=web,websecure" + - "traefik.http.routers.oss-api.tls.certresolver=letsencrypt" + - "traefik.http.services.oss-api.loadbalancer.server.port=8000" + - "traefik.docker.network=proxy" + restart: unless-stopped + + adapter: + image: oktopusp/adapter:latest + environment: + - MONGO_URI=mongodb://mongo:27017 + - NATS_URL=nats://nats:4222 + networks: + - oktopus + restart: unless-stopped + + mongo: + image: mongo:7 + volumes: + - oktopus-mongo:/data/db + networks: + - oktopus + restart: unless-stopped + + nats: + image: nats:2-alpine + command: ["--jetstream"] + volumes: + - oktopus-nats:/data + networks: + - oktopus + restart: unless-stopped + + mqtt: + image: oktopusp/mqtt:latest + environment: + - NATS_URL=nats://nats:4222 + ports: + - "1883:1883" + networks: + - oktopus + restart: unless-stopped + + mqtt-adapter: + image: oktopusp/mqtt-adapter:latest + environment: + - NATS_URL=nats://nats:4222 + - MQTT_URL=tcp://mqtt:1883 + networks: + - oktopus + restart: unless-stopped + + acs: + image: oktopusp/acs:latest + environment: + - NATS_URL=nats://nats:4222 + ports: + - "9292:9292" + networks: + - oktopus + restart: unless-stopped + + socketio: + image: oktopusp/socketio:latest + environment: + - NATS_URL=nats://nats:4222 + networks: + - oktopus + restart: unless-stopped + +networks: + proxy: + external: true + oktopus: + driver: bridge + +volumes: + oktopus-mongo: + oktopus-nats: diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..e75c6e3 --- /dev/null +++ b/setup.sh @@ -0,0 +1,47 @@ +#!/bin/bash +set -e +echo "=== Gigafibre Infrastructure Setup ===" + +# 1. Docker +if ! command -v docker &>/dev/null; then + echo "Installing Docker..." + apt-get update && apt-get install -y ca-certificates curl gnupg + install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian bookworm stable" > /etc/apt/sources.list.d/docker.list + apt-get update && apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin +fi + +# 2. Security +apt-get install -y ufw fail2ban +cp system/jail.local /etc/fail2ban/jail.local 2>/dev/null +systemctl enable --now fail2ban +ufw default deny incoming && ufw default allow outgoing +ufw allow 22/tcp && ufw allow 80/tcp && ufw allow 443/tcp +ufw allow 1883/tcp && ufw allow 9292/tcp +echo "y" | ufw enable + +# 3. Network +cp system/*.network /etc/systemd/network/ +systemctl restart systemd-networkd + +# 4. Docker network +docker network create proxy 2>/dev/null || true + +# 5. Traefik +cd /opt/infra && docker compose -f traefik/docker-compose.yml up -d + +# 6. Apps +docker compose -f apps/docker-compose.yml up -d + +# 7. Oktopus +docker compose -f oktopus/docker-compose.yml up -d + +# 8. Traefik Hub +cd traefik-hub && docker build -t traefik-hub:latest . && cd .. +docker compose -f traefik-hub/docker-compose.yml up -d + +echo "=== Setup complete ===" +echo "Hub: https://hub.gigafibre.ca" +echo "OSS: https://oss.gigafibre.ca" +echo "Git: https://git.gigafibre.ca" diff --git a/system/10-lan.network b/system/10-lan.network new file mode 100644 index 0000000..c75daee --- /dev/null +++ b/system/10-lan.network @@ -0,0 +1,12 @@ +[Match] +Name=ens18 + +[Network] +Address=10.100.5.61/24 +DNS=8.8.8.8 +DNS=1.1.1.1 + +[Route] +Destination=10.0.0.0/8 +Gateway=10.100.5.254 +Metric=200 diff --git a/system/10-netplan-all-en.network b/system/10-netplan-all-en.network new file mode 100644 index 0000000..e69de29 diff --git a/system/10-netplan-all-eth.network b/system/10-netplan-all-eth.network new file mode 100644 index 0000000..e69de29 diff --git a/system/20-wan.network b/system/20-wan.network new file mode 100644 index 0000000..84d9d0e --- /dev/null +++ b/system/20-wan.network @@ -0,0 +1,9 @@ +[Match] +Name=ens19 + +[Network] +Address=96.125.196.67/26 + +[Route] +Gateway=96.125.196.65 +Metric=100 diff --git a/system/jail.local b/system/jail.local new file mode 100644 index 0000000..5eb2d27 --- /dev/null +++ b/system/jail.local @@ -0,0 +1,11 @@ +[DEFAULT] +bantime = 3600 +findtime = 600 +maxretry = 3 +banaction = ufw + +[sshd] +enabled = true +port = 22 +maxretry = 3 +backend = systemd diff --git a/traefik-hub/Dockerfile b/traefik-hub/Dockerfile new file mode 100644 index 0000000..9a791f6 --- /dev/null +++ b/traefik-hub/Dockerfile @@ -0,0 +1,7 @@ +FROM node:20-slim +WORKDIR /app +COPY package.json . +RUN npm install --production +COPY server.js login.html index.html . +EXPOSE 3080 +CMD ["node", "server.js"] diff --git a/traefik-hub/docker-compose.yml b/traefik-hub/docker-compose.yml new file mode 100644 index 0000000..dab3eae --- /dev/null +++ b/traefik-hub/docker-compose.yml @@ -0,0 +1,31 @@ +services: + hub: + image: traefik-hub:latest + environment: + - TRAEFIK_API=http://traefik-traefik-1:8080/api + - ADMIN_USER=admin + - ADMIN_PASS=targo2026 + - SESSION_SECRET=Hub2026SecretKey + - OPENSRS_USER=targo + - OPENSRS_KEY=7ab6b976c403ad4a310c326aac646df5202eb7e7498542f933055a3fa4e6615ff85f93e873d922b89751a7cf59843da5875dd457343b4934 + - OPENSRS_DOMAIN=gigafibre.ca + - SERVER_IP=96.125.196.67 + - ROUTES_FILE=/dynamic/routes.yml + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - /opt/traefik/dynamic:/dynamic + networks: + - proxy + labels: + - "traefik.enable=true" + - "traefik.http.routers.hub.rule=Host(`hub.gigafibre.ca`)" + - "traefik.http.routers.hub.entrypoints=websecure" + - "traefik.http.routers.hub.tls.certresolver=letsencrypt" + - "traefik.http.services.hub.loadbalancer.server.port=3080" + ports: + - "3080:3080" + restart: unless-stopped + +networks: + proxy: + external: true diff --git a/traefik-hub/index.html b/traefik-hub/index.html new file mode 100644 index 0000000..ca6b1f2 --- /dev/null +++ b/traefik-hub/index.html @@ -0,0 +1,148 @@ + + + +Traefik Hub + +

Traefik Hub

+
+ +
Deploying...
+
+ diff --git a/traefik-hub/login.html b/traefik-hub/login.html new file mode 100644 index 0000000..7fb5c11 --- /dev/null +++ b/traefik-hub/login.html @@ -0,0 +1,36 @@ + + + +Traefik Hub — Login + +
+

Traefik Hub

+

Infrastructure Management Dashboard

+
Invalid credentials
+ + + + + +
+ + diff --git a/traefik-hub/package.json b/traefik-hub/package.json new file mode 100644 index 0000000..3e46ed8 --- /dev/null +++ b/traefik-hub/package.json @@ -0,0 +1,6 @@ +{ + "name": "traefik-hub", + "version": "1.0.0", + "scripts": { "start": "node server.js" }, + "dependencies": { "dockerode": "^4.0.4" } +} diff --git a/traefik-hub/server.js b/traefik-hub/server.js new file mode 100644 index 0000000..6401495 --- /dev/null +++ b/traefik-hub/server.js @@ -0,0 +1,254 @@ +const http = require("http"); +const fs = require("fs"); +const Docker = require("dockerode"); +const crypto = require("crypto"); + +const PORT = 3080; +const TRAEFIK_API = process.env.TRAEFIK_API || "http://127.0.0.1:8080/api"; +const SESSION_SECRET = process.env.SESSION_SECRET || crypto.randomBytes(32).toString("hex"); +const ADMIN_USER = process.env.ADMIN_USER || "admin"; +const ADMIN_PASS = process.env.ADMIN_PASS || "targo2026"; +const OPENSRS_USER = process.env.OPENSRS_USER || "targo"; +const OPENSRS_KEY = process.env.OPENSRS_KEY || ""; +const OPENSRS_DOMAIN = process.env.OPENSRS_DOMAIN || "gigafibre.ca"; +const SERVER_IP = process.env.SERVER_IP || "96.125.196.67"; +const ROUTES_FILE = process.env.ROUTES_FILE || "/dynamic/routes.yml"; + +const docker = new Docker({ socketPath: "/var/run/docker.sock" }); +const sessions = new Map(); + +// --- Auth --- +function createSession(user) { const t = crypto.randomBytes(48).toString("hex"); sessions.set(t, { user, created: Date.now() }); return t; } +function getSession(req) { + const c = (req.headers.cookie || "").split(";").find(c => c.trim().startsWith("hub_session=")); + if (!c) return null; const t = c.split("=")[1]?.trim(); const s = sessions.get(t); + if (!s || Date.now() - s.created > 86400000) { sessions.delete(t); return null; } return s; +} + +// --- Traefik API --- +function fetchTraefik(path) { + return new Promise(r => { + http.get(TRAEFIK_API + path, res => { let d = ""; res.on("data", c => d += c); res.on("end", () => { try { r(JSON.parse(d)); } catch { r(null); } }); }).on("error", () => r(null)); + }); +} + +// --- Docker helpers --- +async function listContainers() { + return (await docker.listContainers({ all: true })).map(c => ({ + id: c.Id.slice(0, 12), name: c.Names[0]?.replace("/", ""), image: c.Image, + state: c.State, status: c.Status, + ports: c.Ports.map(p => p.PublicPort ? p.PublicPort + ":" + p.PrivatePort : "" + p.PrivatePort).filter(Boolean), + labels: c.Labels, networks: Object.keys(c.NetworkSettings?.Networks || {}) + })); +} +async function containerAction(id, action) { + const c = docker.getContainer(id); + if (action === "start") await c.start(); else if (action === "stop") await c.stop(); + else if (action === "restart") await c.restart(); else if (action === "remove") { try { await c.stop(); } catch {} await c.remove({ force: true }); } + else if (action === "logs") return (await c.logs({ stdout: true, stderr: true, tail: 150, timestamps: true })).toString("utf8"); + return { ok: true }; +} +async function getSystemInfo() { + const [info, ver] = await Promise.all([docker.info(), docker.version()]); + return { containers: info.Containers, running: info.ContainersRunning, stopped: info.ContainersStopped, + images: info.Images, memTotal: info.MemTotal, cpus: info.NCPU, os: info.OperatingSystem, + dockerVersion: ver.Version, swarm: info.Swarm?.LocalNodeState || "inactive" }; +} +async function getNetworks() { + return (await docker.listNetworks()).filter(n => !["none","host"].includes(n.Name)) + .map(n => ({ id: n.Id.slice(0, 12), name: n.Name, driver: n.Driver, scope: n.Scope })); +} +async function getVolumes() { + const { Volumes } = await docker.listVolumes(); + return (Volumes || []).map(v => ({ name: v.Name, driver: v.Driver, mountpoint: v.Mountpoint })); +} + +// --- Deploy new service --- +async function deployService(opts) { + const { image, subdomain, port, envVars, volumes, networks: nets, name } = opts; + const containerName = name || subdomain + "-svc"; + const domain = subdomain + "." + OPENSRS_DOMAIN; + + // 1. Pull image + await new Promise((resolve, reject) => { + docker.pull(image, (err, stream) => { + if (err) return reject(err); + docker.modem.followProgress(stream, (err) => err ? reject(err) : resolve(), () => {}); + }); + }); + + // 2. Prepare labels + const labels = { + "traefik.enable": "true", + ["traefik.http.routers." + subdomain + ".rule"]: "Host(`" + domain + "`)", + ["traefik.http.routers." + subdomain + ".entrypoints"]: "websecure", + ["traefik.http.routers." + subdomain + ".tls.certresolver"]: "letsencrypt", + ["traefik.http.services." + subdomain + ".loadbalancer.server.port"]: String(port), + "hub.managed": "true", + "hub.subdomain": subdomain, + "hub.domain": domain + }; + + // 3. Parse env vars + const Env = (envVars || []).filter(e => e.key && e.value).map(e => e.key + "=" + e.value); + + // 4. Parse volumes + const Binds = (volumes || []).filter(v => v.host && v.container).map(v => v.host + ":" + v.container); + + // 5. Create container + const container = await docker.createContainer({ + Image: image, name: containerName, Labels: labels, Env, + HostConfig: { Binds, RestartPolicy: { Name: "unless-stopped" }, NetworkMode: "proxy" }, + ExposedPorts: { [port + "/tcp"]: {} } + }); + + // 6. Connect to additional networks + for (const net of (nets || []).filter(n => n !== "proxy")) { + try { const network = docker.getNetwork(net); await network.connect({ Container: container.id }); } catch {} + } + + // 7. Start + await container.start(); + + // 8. Create DNS record + try { + const dnsRecs = await getDnsRecords(); + if (!dnsRecs.find(r => r.subdomain === subdomain)) { + dnsRecs.push({ subdomain, ip: SERVER_IP }); + await setDnsRecords(dnsRecs); + } + } catch (e) { console.error("DNS error:", e.message); } + + return { ok: true, container: containerName, url: "https://" + domain }; +} + +// --- Routes (file provider) --- +function parseRoutesYaml(yaml) { + const routes = {}; if (!yaml) return routes; + const blocks = yaml.split(/\n(?= \S)/).filter(b => b.includes("-route:")); + for (const block of blocks) { + const nm = block.match(/([\w-]+)-route:/)?.[1]; + const rule = block.match(/Host\(`([\w.-]+)`\)/)?.[1]; + if (nm && rule) routes[nm] = { subdomain: rule.replace("." + OPENSRS_DOMAIN, ""), target: "", port: 0 }; + } + const svcBlocks = yaml.split(/\n(?= \S)/).filter(b => b.includes("-svc:")); + for (const block of svcBlocks) { + const nm = block.match(/([\w-]+)-svc:/)?.[1]; + const url = block.match(/url:\s*"http:\/\/([^:]+):(\d+)"/); + if (nm && url && routes[nm]) { routes[nm].target = url[1]; routes[nm].port = parseInt(url[2]); } + } + return routes; +} +function buildRoutesYaml(routes) { + const entries = Object.entries(routes); + if (!entries.length) return "http:\n routers: {}\n services: {}"; + let routers = "", services = ""; + for (const [name, r] of entries) { + const domain = r.subdomain.includes(".") ? r.subdomain : r.subdomain + "." + OPENSRS_DOMAIN; + routers += " " + name + "-route:\n rule: \"Host(`" + domain + "`)\"\n entryPoints:\n - websecure\n service: " + name + "-svc\n tls:\n certResolver: letsencrypt\n"; + services += " " + name + "-svc:\n loadBalancer:\n servers:\n - url: \"http://" + r.target + ":" + r.port + "\"\n"; + } + return "http:\n routers:\n" + routers + " services:\n" + services; +} +function getRoutes() { try { return parseRoutesYaml(fs.readFileSync(ROUTES_FILE, "utf8")); } catch { return {}; } } +function saveRoute(sub, target, port) { + const routes = getRoutes(); routes[sub.replace(/[^a-z0-9]/gi, "-")] = { subdomain: sub, target, port: parseInt(port) }; + fs.writeFileSync(ROUTES_FILE, buildRoutesYaml(routes)); return { ok: true }; +} +function deleteRoute(name) { const routes = getRoutes(); delete routes[name]; fs.writeFileSync(ROUTES_FILE, buildRoutesYaml(routes)); return { ok: true }; } + +// --- OpenSRS DNS --- +function opensrsSign(xml) { const md5 = s => crypto.createHash("md5").update(s).digest("hex"); return md5(md5(xml + OPENSRS_KEY) + OPENSRS_KEY); } +function opensrsReq(xml) { + return new Promise((resolve, reject) => { + const sig = opensrsSign(xml); + const req = require("https").request({ hostname: "rr-n1-tor.opensrs.net", port: 55443, path: "/", method: "POST", + headers: { "Content-Type": "text/xml", "X-Username": OPENSRS_USER, "X-Signature": sig } + }, res => { let d = ""; res.on("data", c => d += c); res.on("end", () => resolve(d)); }); + req.on("error", reject); req.write(xml); req.end(); + }); +} +async function getDnsRecords() { + const xml = '
0.9
XCPGET_DNS_ZONEDOMAIN' + OPENSRS_DOMAIN + '
'; + const resp = await opensrsReq(xml); + const records = [], seen = new Set(); + const items = resp.match(/([\s\S]*?)<\/item>/g) || []; + for (const item of items) { + const sub = item.match(/(.*?)<\/item>/)?.[1] || ""; + const ip = item.match(/(.*?)<\/item>/)?.[1] || ""; + const key = sub + "|" + ip; + if (ip && !seen.has(key)) { seen.add(key); records.push({ subdomain: sub, ip, domain: sub ? sub + "." + OPENSRS_DOMAIN : OPENSRS_DOMAIN }); } + } + return records; +} +async function setDnsRecords(records) { + // Deduplicate + const seen = new Set(); const unique = []; + for (const r of records) { const k = r.subdomain + "|" + r.ip; if (!seen.has(k)) { seen.add(k); unique.push(r); } } + let items = ""; + unique.forEach((r, i) => { items += '' + r.subdomain + '' + r.ip + ''; }); + const xml = '
0.9
XCPSET_DNS_ZONEDOMAIN' + OPENSRS_DOMAIN + '' + items + '
'; + return (await opensrsReq(xml)).includes('"is_success">1'); +} + +// --- Body parser --- +function parseBody(req) { return new Promise(r => { let b = ""; req.on("data", c => b += c); req.on("end", () => { try { r(JSON.parse(b)); } catch { r({}); } }); }); } + +// --- Server --- +const server = http.createServer(async (req, res) => { + const path = new URL(req.url, "http://localhost").pathname; + + if (path === "/" || path === "/index.html") { + if (!getSession(req)) { res.writeHead(200, { "Content-Type": "text/html" }); res.end(fs.readFileSync("/app/login.html", "utf8")); return; } + res.writeHead(200, { "Content-Type": "text/html" }); res.end(fs.readFileSync("/app/index.html", "utf8")); return; + } + if (path === "/api/login" && req.method === "POST") { + const { user, pass } = await parseBody(req); + if (user === ADMIN_USER && pass === ADMIN_PASS) { const t = createSession(user); res.writeHead(200, { "Content-Type": "application/json", "Set-Cookie": "hub_session=" + t + "; HttpOnly; Path=/; SameSite=Strict; Max-Age=86400" }); res.end(JSON.stringify({ ok: true })); } + else { res.writeHead(401, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Invalid" })); } return; + } + if (path === "/api/logout") { res.writeHead(200, { "Content-Type": "application/json", "Set-Cookie": "hub_session=; HttpOnly; Path=/; Max-Age=0" }); res.end(JSON.stringify({ ok: true })); return; } + if (path.startsWith("/api/") && !getSession(req)) { res.writeHead(401); res.end(JSON.stringify({ error: "Unauthorized" })); return; } + + try { + let data; + switch (true) { + case path === "/api/overview": + const [routers, services, entrypoints, system, containers] = await Promise.all([fetchTraefik("/http/routers"), fetchTraefik("/http/services"), fetchTraefik("/entrypoints"), getSystemInfo(), listContainers()]); + data = { routers, services, entrypoints, system, containers }; break; + case path === "/api/routers": data = await fetchTraefik("/http/routers"); break; + case path === "/api/services": data = await fetchTraefik("/http/services"); break; + case path === "/api/entrypoints": data = await fetchTraefik("/entrypoints"); break; + case path === "/api/middlewares": data = await fetchTraefik("/http/middlewares"); break; + case path === "/api/containers": data = await listContainers(); break; + case path === "/api/system": data = await getSystemInfo(); break; + case path === "/api/networks": data = await getNetworks(); break; + case path === "/api/volumes": data = await getVolumes(); break; + case path === "/api/certificates": + data = ((await fetchTraefik("/http/routers")) || []).filter(r => r.tls).map(r => ({ + domain: r.rule?.match(/Host\(`([^`]+)`\)/)?.[1] || "?", status: r.tls?.certResolver ? "managed" : "manual", resolver: r.tls?.certResolver, router: r.name + })); break; + case path === "/api/routes" && req.method === "GET": data = { routes: getRoutes(), domain: OPENSRS_DOMAIN, serverIp: SERVER_IP }; break; + case path === "/api/routes" && req.method === "POST": const rd = await parseBody(req); data = saveRoute(rd.subdomain, rd.target, rd.port); break; + case path.startsWith("/api/routes/") && req.method === "DELETE": data = deleteRoute(path.split("/")[3]); break; + case path === "/api/dns" && req.method === "GET": data = { records: await getDnsRecords(), domain: OPENSRS_DOMAIN, serverIp: SERVER_IP }; break; + case path === "/api/dns" && req.method === "POST": + const dd = await parseBody(req); const cur = await getDnsRecords(); + if (!cur.find(r => r.subdomain === dd.subdomain && r.ip === (dd.ip || SERVER_IP))) { cur.push({ subdomain: dd.subdomain, ip: dd.ip || SERVER_IP }); } + data = { ok: await setDnsRecords(cur) }; break; + case path === "/api/dns" && req.method === "DELETE": + const del = await parseBody(req); const recs = await getDnsRecords(); + data = { ok: await setDnsRecords(recs.filter(r => !(r.subdomain === del.subdomain && r.ip === del.ip))) }; break; + case path === "/api/deploy" && req.method === "POST": data = await deployService(await parseBody(req)); break; + case path.startsWith("/api/container/") && path.endsWith("/logs"): + data = await containerAction(path.split("/")[3], "logs"); + res.writeHead(200, { "Content-Type": "text/plain" }); res.end(typeof data === "string" ? data : JSON.stringify(data)); return; + case path.startsWith("/api/container/") && req.method === "POST": + data = await containerAction(path.split("/")[3], path.split("/")[4]); break; + default: res.writeHead(404); res.end("Not found"); return; + } + res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(data)); + } catch (e) { console.error(e); res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: e.message })); } +}); + +server.listen(PORT, "0.0.0.0", () => console.log("Traefik Hub on :" + PORT)); diff --git a/traefik/docker-compose.yml b/traefik/docker-compose.yml new file mode 100644 index 0000000..a293ec0 --- /dev/null +++ b/traefik/docker-compose.yml @@ -0,0 +1,38 @@ +services: + traefik: + image: traefik:v2.11 + command: + - "--api.dashboard=true" + - "--api.insecure=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--providers.file.directory=/dynamic" + - "--providers.file.watch=true" + - "--entrypoints.web.address=:80" + - "--entrypoints.websecure.address=:443" + - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true" + - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web" + - "--certificatesresolvers.letsencrypt.acme.email=louispaul@targointernet.com" + - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" + - "--log.level=DEBUG" + ports: + - "80:80" + - "443:443" + - "8080:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - traefik-certs:/letsencrypt + - /opt/traefik/dynamic:/dynamic:ro + networks: + - proxy + restart: unless-stopped + +networks: + proxy: + name: proxy + driver: bridge + +volumes: + traefik-certs: + external: true + name: traefik_traefik-certs diff --git a/traefik/dynamic/routes.yml b/traefik/dynamic/routes.yml new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/traefik/dynamic/routes.yml @@ -0,0 +1 @@ +