@studiometa/trafic-agent 0.1.30 → 0.1.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +4 -4
- package/dist/index.js +2 -2
- package/dist/{server-zVWzPuj2.js → server-BgOOBUhR.js} +423 -19
- package/dist/server-BgOOBUhR.js.map +1 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -2
- package/dist/setup/agent.d.ts.map +1 -1
- package/dist/setup/ddev.d.ts +17 -0
- package/dist/setup/ddev.d.ts.map +1 -1
- package/dist/setup/migrations/0009__scale_to_zero_fixes.d.ts +24 -0
- package/dist/setup/migrations/0009__scale_to_zero_fixes.d.ts.map +1 -0
- package/dist/setup/migrations/index.d.ts.map +1 -1
- package/dist/{setup-DXihIjv9.js → setup-vg_pqnPs.js} +51 -252
- package/dist/setup-vg_pqnPs.js.map +1 -0
- package/dist/utils/ddev.d.ts +14 -0
- package/dist/utils/ddev.d.ts.map +1 -1
- package/dist/utils/traefik.d.ts +25 -0
- package/dist/utils/traefik.d.ts.map +1 -0
- package/package.json +1 -1
- package/dist/server-zVWzPuj2.js.map +0 -1
- package/dist/setup-DXihIjv9.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { d as validateConfig, i as runPendingMigrations, l as loadConfig, n as setup, r as listMigrations, s as startIdleScheduler, t as audit } from "./setup-vg_pqnPs.js";
|
|
3
|
+
import { _ as step, d as exec, m as isRoot, n as startServer, p as info, v as success, w as closeDb, y as warn } from "./server-BgOOBUhR.js";
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
import { execSync, spawnSync } from "node:child_process";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
@@ -108,7 +108,7 @@ function runUpgrade(dryRun = false, reExecArgs) {
|
|
|
108
108
|
}
|
|
109
109
|
const alreadyReExeced = process.env["TRAFIC_UPGRADE_REEXEC"] === "1";
|
|
110
110
|
step("Check for updates");
|
|
111
|
-
const current = "0.1.
|
|
111
|
+
const current = "0.1.32";
|
|
112
112
|
info(`Current version: ${current}`);
|
|
113
113
|
const latest = fetchLatestVersion();
|
|
114
114
|
if (!latest) warn("Could not reach npm registry — skipping version check");
|
|
@@ -191,7 +191,7 @@ function printHelp() {
|
|
|
191
191
|
console.log(HELP.trim());
|
|
192
192
|
}
|
|
193
193
|
function printVersion() {
|
|
194
|
-
console.log("0.1.
|
|
194
|
+
console.log("0.1.32");
|
|
195
195
|
}
|
|
196
196
|
async function runStart(values) {
|
|
197
197
|
const config = loadConfig(values.config);
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { a as printAuditResults, c as stopIdleProjects, d as validateConfig, l as loadConfig, n as setup, o as runAudit, s as startIdleScheduler, t as audit, u as parseDuration } from "./setup-vg_pqnPs.js";
|
|
2
|
+
import { A as logAccess, B as watchProjectList, C as cleanOldLogs, D as getIdleProjects, E as getDb, F as getProjectInfo, G as parseBearerToken, H as matchHostname, I as listProjects, L as loadProjectList, M as updateProjectAccess, N as buildHostnameIndex, O as getProject, P as getProjectFromHostname, R as startProject, S as shouldNeverStop, T as getAccessLogs, U as matchIp, V as checkAuth, W as parseBasicAuth, b as getIdleTimeoutMs, j as setProjectStatus, k as initDb, n as startServer, w as closeDb, x as loadProjectConfig, z as stopProject } from "./server-BgOOBUhR.js";
|
|
3
3
|
export { audit, buildHostnameIndex, checkAuth, cleanOldLogs, closeDb, getAccessLogs, getDb, getIdleProjects, getIdleTimeoutMs, getProject, getProjectFromHostname, getProjectInfo, initDb, listProjects, loadConfig, loadProjectConfig, loadProjectList, logAccess, matchHostname, matchIp, parseBasicAuth, parseBearerToken, parseDuration, printAuditResults, runAudit, setProjectStatus, setup, shouldNeverStop, startIdleScheduler, startProject, startServer, stopIdleProjects, stopProject, updateProjectAccess, validateConfig, watchProjectList };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync, watch } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, watch, writeFileSync } from "node:fs";
|
|
2
2
|
import { execSync } from "node:child_process";
|
|
3
3
|
import { DatabaseSync } from "node:sqlite";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -224,29 +224,49 @@ function applyDefaultPolicy(config, ip, authorization) {
|
|
|
224
224
|
}
|
|
225
225
|
//#endregion
|
|
226
226
|
//#region src/utils/ddev.ts
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const trimmed = line.trim();
|
|
232
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
233
|
-
const colonIndex = trimmed.indexOf(":");
|
|
234
|
-
if (colonIndex === -1) continue;
|
|
235
|
-
const key = trimmed.slice(0, colonIndex).trim();
|
|
236
|
-
let value = trimmed.slice(colonIndex + 1).trim();
|
|
237
|
-
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
238
|
-
result[key] = value;
|
|
239
|
-
}
|
|
240
|
-
return result;
|
|
227
|
+
/** Strip a single pair of surrounding quotes. */
|
|
228
|
+
function unquote(value) {
|
|
229
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
|
230
|
+
return value;
|
|
241
231
|
}
|
|
232
|
+
/** A project name at the start of a line, e.g. `my-project:` */
|
|
233
|
+
var PROJECT_LINE = /^(\S[^:]*):\s*$/;
|
|
234
|
+
/** An indented `approot: /path` belonging to the project above it */
|
|
235
|
+
var APPROOT_LINE = /^\s+approot:\s*(.+?)\s*$/;
|
|
242
236
|
/**
|
|
243
237
|
* Load projects from DDEV's project_list.yaml
|
|
244
238
|
* Returns a map of project name -> app root path
|
|
239
|
+
*
|
|
240
|
+
* The file nests the path under the project name:
|
|
241
|
+
*
|
|
242
|
+
* scalian:
|
|
243
|
+
* approot: /home/ddev/www/scalian
|
|
244
|
+
*
|
|
245
|
+
* Indentation therefore has to be read. A flat key/value pass produces one
|
|
246
|
+
* entry per line instead: the project mapped to an empty string, plus a
|
|
247
|
+
* phantom project called `approot`. Both did damage — the empty path meant
|
|
248
|
+
* `loadProjectConfig` looked for `.ddev/config.trafic.yaml` relative to the
|
|
249
|
+
* working directory and never found it, so per-project settings were silently
|
|
250
|
+
* ignored, and the phantom entered the hostname index, where the TLS ask
|
|
251
|
+
* endpoint would vouch for `approot.<tld>` and have a certificate issued for
|
|
252
|
+
* a project that does not exist.
|
|
245
253
|
*/
|
|
246
254
|
function loadProjectList(projectListPath) {
|
|
247
255
|
if (!existsSync(projectListPath)) return /* @__PURE__ */ new Map();
|
|
248
|
-
const
|
|
249
|
-
|
|
256
|
+
const projects = /* @__PURE__ */ new Map();
|
|
257
|
+
let current;
|
|
258
|
+
for (const line of readFileSync(projectListPath, "utf-8").split("\n")) {
|
|
259
|
+
if (!line.trim() || line.trimStart().startsWith("#")) continue;
|
|
260
|
+
const project = PROJECT_LINE.exec(line);
|
|
261
|
+
if (project) {
|
|
262
|
+
current = unquote(project[1].trim());
|
|
263
|
+
projects.set(current, "");
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const approot = APPROOT_LINE.exec(line);
|
|
267
|
+
if (approot && current) projects.set(current, unquote(approot[1]));
|
|
268
|
+
}
|
|
269
|
+
return projects;
|
|
250
270
|
}
|
|
251
271
|
/**
|
|
252
272
|
* Build a hostname -> project name index
|
|
@@ -536,6 +556,363 @@ function getIdleTimeoutMs(config) {
|
|
|
536
556
|
return ms > 0 ? ms : void 0;
|
|
537
557
|
}
|
|
538
558
|
//#endregion
|
|
559
|
+
//#region src/setup/steps.ts
|
|
560
|
+
var stepNumber = 0;
|
|
561
|
+
var dryRun = false;
|
|
562
|
+
/**
|
|
563
|
+
* Set dry-run mode
|
|
564
|
+
*/
|
|
565
|
+
function setDryRun(value) {
|
|
566
|
+
dryRun = value;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Reset step counter
|
|
570
|
+
*/
|
|
571
|
+
function resetSteps() {
|
|
572
|
+
stepNumber = 0;
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Log a step
|
|
576
|
+
*/
|
|
577
|
+
function step(message) {
|
|
578
|
+
stepNumber++;
|
|
579
|
+
console.log(`\n\x1b[36m[${stepNumber}]\x1b[0m ${message}`);
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Log success
|
|
583
|
+
*/
|
|
584
|
+
function success(message) {
|
|
585
|
+
console.log(` \x1b[32m✓\x1b[0m ${message}`);
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Log warning
|
|
589
|
+
*/
|
|
590
|
+
function warn(message) {
|
|
591
|
+
console.log(` \x1b[33m⚠\x1b[0m ${message}`);
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Log error
|
|
595
|
+
*/
|
|
596
|
+
function error(message) {
|
|
597
|
+
console.log(` \x1b[31m✗\x1b[0m ${message}`);
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Log info
|
|
601
|
+
*/
|
|
602
|
+
function info(message) {
|
|
603
|
+
console.log(` ${message}`);
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Execute a command (or show in dry-run mode)
|
|
607
|
+
*/
|
|
608
|
+
function exec(command, options) {
|
|
609
|
+
if (dryRun) {
|
|
610
|
+
if (!options?.silent) console.log(` \x1b[90m$ ${command}\x1b[0m`);
|
|
611
|
+
return "";
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
return execSync(command, {
|
|
615
|
+
encoding: "utf-8",
|
|
616
|
+
stdio: options?.silent ? "pipe" : "inherit"
|
|
617
|
+
});
|
|
618
|
+
} catch (err) {
|
|
619
|
+
throw new Error(`Command failed: ${command}\n${err.stderr ?? ""}`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Execute a command and return output (silent)
|
|
624
|
+
*/
|
|
625
|
+
function execSilent(command) {
|
|
626
|
+
if (dryRun) return "";
|
|
627
|
+
try {
|
|
628
|
+
return execSync(command, {
|
|
629
|
+
encoding: "utf-8",
|
|
630
|
+
stdio: "pipe"
|
|
631
|
+
}).trim();
|
|
632
|
+
} catch {
|
|
633
|
+
return "";
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Check if a command exists
|
|
638
|
+
*/
|
|
639
|
+
function commandExists(command) {
|
|
640
|
+
try {
|
|
641
|
+
execSync(`which ${command}`, { stdio: "pipe" });
|
|
642
|
+
return true;
|
|
643
|
+
} catch {
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Check if running as root
|
|
649
|
+
*/
|
|
650
|
+
function isRoot() {
|
|
651
|
+
return process.getuid?.() === 0;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Write a file (or show in dry-run mode)
|
|
655
|
+
*/
|
|
656
|
+
function writeFile(path, content) {
|
|
657
|
+
if (dryRun) {
|
|
658
|
+
console.log(` \x1b[90mWrite ${path}:\x1b[0m`);
|
|
659
|
+
const lines = content.split("\n").slice(0, 5);
|
|
660
|
+
for (const line of lines) console.log(` \x1b[90m ${line}\x1b[0m`);
|
|
661
|
+
if (content.split("\n").length > 5) console.log(` \x1b[90m ...\x1b[0m`);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
writeFileSync(path, content);
|
|
665
|
+
}
|
|
666
|
+
//#endregion
|
|
667
|
+
//#region src/setup/io.ts
|
|
668
|
+
/**
|
|
669
|
+
* The real implementation, used when no io is injected.
|
|
670
|
+
*
|
|
671
|
+
* `writeFile` comes from steps.js rather than `node:fs` so writes are
|
|
672
|
+
* skipped in dry-run mode. Reads stay direct: they change nothing.
|
|
673
|
+
*/
|
|
674
|
+
var nodeIo = {
|
|
675
|
+
exec,
|
|
676
|
+
execSilent,
|
|
677
|
+
commandExists,
|
|
678
|
+
writeFile,
|
|
679
|
+
fileExists: existsSync,
|
|
680
|
+
readFile: (path) => readFileSync(path, "utf-8"),
|
|
681
|
+
env: process.env
|
|
682
|
+
};
|
|
683
|
+
//#endregion
|
|
684
|
+
//#region src/setup/ddev.ts
|
|
685
|
+
/**
|
|
686
|
+
* Install system dependencies required by Trafic
|
|
687
|
+
*/
|
|
688
|
+
function installSystemDeps(io = nodeIo) {
|
|
689
|
+
step("Install system dependencies");
|
|
690
|
+
io.exec("apt-get update -qq", { silent: true });
|
|
691
|
+
io.exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y jq curl rsync gnupg ca-certificates", { silent: true });
|
|
692
|
+
success("System dependencies installed: jq, curl, rsync, gnupg, ca-certificates");
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Add the official DDEV apt repository and GPG key.
|
|
696
|
+
* Idempotent: safe to call even if the repository is already configured.
|
|
697
|
+
*/
|
|
698
|
+
function addDdevAptRepo(io = nodeIo) {
|
|
699
|
+
io.exec("install -m 0755 -d /etc/apt/keyrings", { silent: true });
|
|
700
|
+
io.exec("curl -fsSL https://pkg.ddev.com/apt/gpg.key | gpg --dearmor | tee /etc/apt/keyrings/ddev.gpg > /dev/null", { silent: true });
|
|
701
|
+
io.exec("chmod a+r /etc/apt/keyrings/ddev.gpg", { silent: true });
|
|
702
|
+
io.exec("echo \"deb [signed-by=/etc/apt/keyrings/ddev.gpg] https://pkg.ddev.com/apt/ * *\" | tee /etc/apt/sources.list.d/ddev.list > /dev/null", { silent: true });
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Install DDEV
|
|
706
|
+
*/
|
|
707
|
+
function installDdev(io = nodeIo) {
|
|
708
|
+
step("Install DDEV");
|
|
709
|
+
if (io.commandExists("ddev")) {
|
|
710
|
+
info(`DDEV already installed: ${io.exec("ddev --version 2>/dev/null | head -1", { silent: true })?.trim() ?? "unknown version"}`);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
info("Adding DDEV apt repository...");
|
|
714
|
+
addDdevAptRepo(io);
|
|
715
|
+
info("Installing DDEV...");
|
|
716
|
+
io.exec("apt-get update -qq", { silent: true });
|
|
717
|
+
io.exec("DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y ddev", { silent: true });
|
|
718
|
+
io.exec("HOME=/home/ddev mkcert -install", { silent: true });
|
|
719
|
+
io.exec("chown -R ddev:ddev /home/ddev/.local/share/mkcert", { silent: true });
|
|
720
|
+
success(`DDEV installed: ${io.exec("ddev --version 2>/dev/null | head -1", { silent: true })?.trim() ?? "unknown version"}`);
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Configure DDEV global settings for production
|
|
724
|
+
*
|
|
725
|
+
* Note on DNS and /etc/hosts:
|
|
726
|
+
* When using a custom TLD (not ddev.site), DDEV may try to edit /etc/hosts
|
|
727
|
+
* if it can't resolve project hostnames via DNS. To avoid this:
|
|
728
|
+
* 1. Configure DNS so *.{tld} resolves to the server's IP (recommended)
|
|
729
|
+
* 2. Or install dnsmasq for local wildcard DNS resolution
|
|
730
|
+
*
|
|
731
|
+
* With proper DNS, DDEV won't need sudo for hostname management.
|
|
732
|
+
*/
|
|
733
|
+
function configureDdev(tld, email, io = nodeIo) {
|
|
734
|
+
step("Configure DDEV");
|
|
735
|
+
const ddevCmd = (cmd) => `su - ddev -c '${cmd}'`;
|
|
736
|
+
io.exec(ddevCmd(`ddev config global --project-tld=${tld} --router-http-port=80 --router-https-port=443 --router-bind-all-interfaces=true --use-letsencrypt=${email ? "true" : "false"} ${email ? `--letsencrypt-email=${email}` : ""} --instrumentation-opt-in=false`), { silent: true });
|
|
737
|
+
success(`DDEV configured with TLD: ${tld}`);
|
|
738
|
+
info(`Ensure DNS is configured: *.${tld} → server IP`);
|
|
739
|
+
if (email) success(`Let's Encrypt enabled with email: ${email}`);
|
|
740
|
+
success("DDEV global settings configured");
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Get the Docker gateway IP reachable from inside DDEV containers on Linux.
|
|
744
|
+
* DDEV runs containers on the `ddev_default` network — we inspect that first.
|
|
745
|
+
* Falls back to the default `bridge` network, then to 172.17.0.1.
|
|
746
|
+
* `host.docker.internal` is not available on Linux without extra configuration.
|
|
747
|
+
*/
|
|
748
|
+
function getDockerGatewayIp(io = nodeIo) {
|
|
749
|
+
const ddevIp = io.execSilent("docker network inspect ddev_default --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}'");
|
|
750
|
+
if (ddevIp) return ddevIp;
|
|
751
|
+
return io.execSilent("docker network inspect bridge --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}'") || "172.17.0.1";
|
|
752
|
+
}
|
|
753
|
+
/** DDEV tool ports, used when the global config does not say otherwise */
|
|
754
|
+
var DEFAULT_TOOL_PORTS = {
|
|
755
|
+
"mailpit-http-port": "8025",
|
|
756
|
+
"mailpit-https-port": "8026",
|
|
757
|
+
"xhgui-http-port": "8143",
|
|
758
|
+
"xhgui-https-port": "8142"
|
|
759
|
+
};
|
|
760
|
+
/**
|
|
761
|
+
* Read the ports DDEV publishes its tools on.
|
|
762
|
+
*
|
|
763
|
+
* ddev-router publishes these on 0.0.0.0, and Docker bypasses UFW for
|
|
764
|
+
* published ports, so a firewall rule cannot protect them — the forward auth
|
|
765
|
+
* middleware is the only thing that can. Read them rather than hardcode: they
|
|
766
|
+
* are configurable, and an entry point Traefik does not know about is ignored.
|
|
767
|
+
*/
|
|
768
|
+
function readToolPorts(io = nodeIo) {
|
|
769
|
+
const output = io.execSilent("su - ddev -c 'ddev config global'");
|
|
770
|
+
const ports = [];
|
|
771
|
+
for (const [key, fallback] of Object.entries(DEFAULT_TOOL_PORTS)) {
|
|
772
|
+
const match = new RegExp(`^${key}=(\\d+)$`, "m").exec(output);
|
|
773
|
+
ports.push(match?.[1] ?? fallback);
|
|
774
|
+
}
|
|
775
|
+
return [...new Set(ports)];
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Build the static config attaching forward auth to every entry point.
|
|
779
|
+
*
|
|
780
|
+
* The error page middleware stays on the web entry points only: a 502 from a
|
|
781
|
+
* stopped project should show the waiting page, while a tool port has nothing
|
|
782
|
+
* to wait for.
|
|
783
|
+
*/
|
|
784
|
+
function buildStaticConfig(toolPorts) {
|
|
785
|
+
const webEntryPoint = (name) => ` ${name}:
|
|
786
|
+
http:
|
|
787
|
+
middlewares:
|
|
788
|
+
- trafic-auth@file
|
|
789
|
+
- trafic-errors@file
|
|
790
|
+
`;
|
|
791
|
+
const toolEntryPoint = (port) => ` http-${port}:
|
|
792
|
+
http:
|
|
793
|
+
middlewares:
|
|
794
|
+
- trafic-auth@file
|
|
795
|
+
`;
|
|
796
|
+
return `entryPoints:
|
|
797
|
+
${webEntryPoint("http-80")}${webEntryPoint("http-443")}${toolPorts.map(toolEntryPoint).join("")}`;
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Configure Traefik for forward auth
|
|
801
|
+
*/
|
|
802
|
+
function configureTraefik(io = nodeIo) {
|
|
803
|
+
step("Configure Traefik for forward auth");
|
|
804
|
+
io.exec("mkdir -p /home/ddev/.ddev/traefik/custom-global-config", { silent: true });
|
|
805
|
+
io.exec("chown -R ddev:ddev /home/ddev/.ddev", { silent: true });
|
|
806
|
+
const gatewayIp = getDockerGatewayIp(io);
|
|
807
|
+
const dynamicConfig = `# Trafic: Forward auth middleware definition
|
|
808
|
+
http:
|
|
809
|
+
middlewares:
|
|
810
|
+
trafic-auth:
|
|
811
|
+
forwardAuth:
|
|
812
|
+
address: "http://${gatewayIp}:9876/__auth__"
|
|
813
|
+
authResponseHeaders:
|
|
814
|
+
- "X-Trafic-Project"
|
|
815
|
+
|
|
816
|
+
trafic-errors:
|
|
817
|
+
errors:
|
|
818
|
+
status:
|
|
819
|
+
- "502"
|
|
820
|
+
- "503"
|
|
821
|
+
service: trafic-service
|
|
822
|
+
query: "/"
|
|
823
|
+
|
|
824
|
+
routers:
|
|
825
|
+
# Lowest priority, so a DDEV project router always wins. It only matches
|
|
826
|
+
# when none does - which is what a stopped project produces, because DDEV
|
|
827
|
+
# removes that project's router and Traefik would otherwise answer 404.
|
|
828
|
+
# Without this the waiting page never appears and scale-to-zero never
|
|
829
|
+
# restarts anything. Auth is unaffected: it is attached at the entry
|
|
830
|
+
# point, not here, so this cannot reintroduce the bypass that #27 fixed.
|
|
831
|
+
#
|
|
832
|
+
# Two of them, because a router carrying tls only matches HTTPS entry
|
|
833
|
+
# points: one alone would leave plain HTTP answering 404.
|
|
834
|
+
trafic-catchall:
|
|
835
|
+
rule: "PathPrefix(\`/\`)"
|
|
836
|
+
priority: 1
|
|
837
|
+
service: trafic-service
|
|
838
|
+
|
|
839
|
+
trafic-catchall-tls:
|
|
840
|
+
rule: "PathPrefix(\`/\`)"
|
|
841
|
+
priority: 1
|
|
842
|
+
service: trafic-service
|
|
843
|
+
tls: {}
|
|
844
|
+
|
|
845
|
+
services:
|
|
846
|
+
trafic-service:
|
|
847
|
+
loadBalancer:
|
|
848
|
+
servers:
|
|
849
|
+
- url: "http://${gatewayIp}:9876"
|
|
850
|
+
`;
|
|
851
|
+
io.writeFile("/home/ddev/.ddev/traefik/trafic.yaml", dynamicConfig);
|
|
852
|
+
io.exec("chown ddev:ddev /home/ddev/.ddev/traefik/trafic.yaml", { silent: true });
|
|
853
|
+
io.writeFile("/home/ddev/.ddev/traefik/custom-global-config/trafic.yaml", dynamicConfig);
|
|
854
|
+
io.exec("chown ddev:ddev /home/ddev/.ddev/traefik/custom-global-config/trafic.yaml", { silent: true });
|
|
855
|
+
const staticConfig = buildStaticConfig(readToolPorts(io));
|
|
856
|
+
io.writeFile("/home/ddev/.ddev/traefik/static_config.trafic.yaml", staticConfig);
|
|
857
|
+
io.exec("chown ddev:ddev /home/ddev/.ddev/traefik/static_config.trafic.yaml", { silent: true });
|
|
858
|
+
success("Traefik configured with Trafic middleware");
|
|
859
|
+
}
|
|
860
|
+
//#endregion
|
|
861
|
+
//#region src/utils/traefik.ts
|
|
862
|
+
/**
|
|
863
|
+
* Files DDEV reads the dynamic Traefik configuration from, relative to the
|
|
864
|
+
* .ddev directory. Both are written by setup: DDEV 1.25+ picks up
|
|
865
|
+
* custom-global-config, older versions watch the traefik root.
|
|
866
|
+
*/
|
|
867
|
+
var DYNAMIC_CONFIGS = ["traefik/trafic.yaml", "traefik/custom-global-config/trafic.yaml"];
|
|
868
|
+
/** Matches the host in an agent URL, e.g. http://172.17.0.1:9876/__auth__ */
|
|
869
|
+
var AGENT_URL = /http:\/\/(\d+\.\d+\.\d+\.\d+):(\d+)/g;
|
|
870
|
+
/**
|
|
871
|
+
* The address the forward-auth middleware currently points at.
|
|
872
|
+
*/
|
|
873
|
+
function readForwardAuthAddress(content) {
|
|
874
|
+
return /http:\/\/(\d+\.\d+\.\d+\.\d+):\d+/.exec(content)?.[1];
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Point every agent URL in a dynamic config at a different host, keeping
|
|
878
|
+
* each port as it was.
|
|
879
|
+
*/
|
|
880
|
+
function rewriteForwardAuthAddress(content, gatewayIp) {
|
|
881
|
+
return content.replace(AGENT_URL, `http://${gatewayIp}:$2`);
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Correct the forward-auth address if the Docker gateway has moved.
|
|
885
|
+
*
|
|
886
|
+
* setup runs `configureTraefik` before any DDEV project exists, so the
|
|
887
|
+
* `ddev_default` network is absent and the address falls back to the default
|
|
888
|
+
* bridge. Once a project starts, that address is stale. Migration 0006 exists
|
|
889
|
+
* to fix it on servers provisioned earlier, but `markAllMigrationsApplied()`
|
|
890
|
+
* marks it applied on fresh ones, so nothing ever recomputed it.
|
|
891
|
+
*
|
|
892
|
+
* Traefik watches the dynamic config, so rewriting the file is enough — no
|
|
893
|
+
* restart. Any failure is logged and ignored: a stale address is survivable,
|
|
894
|
+
* an agent that will not start is not.
|
|
895
|
+
*/
|
|
896
|
+
function syncForwardAuthAddress(projectListPath, io = nodeIo) {
|
|
897
|
+
try {
|
|
898
|
+
const ddevDir = dirname(projectListPath);
|
|
899
|
+
const gatewayIp = getDockerGatewayIp(io);
|
|
900
|
+
const changed = [];
|
|
901
|
+
for (const relative of DYNAMIC_CONFIGS) {
|
|
902
|
+
const path = join(ddevDir, relative);
|
|
903
|
+
if (!io.fileExists(path)) continue;
|
|
904
|
+
const content = io.readFile(path);
|
|
905
|
+
const current = readForwardAuthAddress(content);
|
|
906
|
+
if (!current || current === gatewayIp) continue;
|
|
907
|
+
io.writeFile(path, rewriteForwardAuthAddress(content, gatewayIp));
|
|
908
|
+
changed.push(path);
|
|
909
|
+
}
|
|
910
|
+
if (changed.length > 0) console.log(`Traefik forward auth: corrected address to ${gatewayIp} in ${changed.length} file(s)`);
|
|
911
|
+
} catch (error) {
|
|
912
|
+
console.warn(`Could not check the Traefik forward auth address: ${error instanceof Error ? error.message : error}`);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
//#endregion
|
|
539
916
|
//#region src/server.ts
|
|
540
917
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
541
918
|
var config;
|
|
@@ -636,6 +1013,31 @@ async function handleErrors(req, res) {
|
|
|
636
1013
|
}, 100);
|
|
637
1014
|
}
|
|
638
1015
|
/**
|
|
1016
|
+
* Decide whether Caddy may obtain a certificate for a hostname.
|
|
1017
|
+
*
|
|
1018
|
+
* Caddy's on-demand TLS asks before issuing for a name it has not seen.
|
|
1019
|
+
* Without a check, anyone pointing a DNS record at this server would make it
|
|
1020
|
+
* request a certificate, and Let's Encrypt quota is per registered domain —
|
|
1021
|
+
* a handful of strangers could exhaust it for every preview.
|
|
1022
|
+
*/
|
|
1023
|
+
function allowTlsFor(domain, index) {
|
|
1024
|
+
return index.has(domain.toLowerCase());
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Handle Caddy's on-demand TLS ask. 2xx allows issuance, anything else denies.
|
|
1028
|
+
*/
|
|
1029
|
+
function handleTlsAsk(req, res) {
|
|
1030
|
+
const domain = new URL(req.url ?? "/", `http://${req.headers.host}`).searchParams.get("domain") ?? "";
|
|
1031
|
+
if (allowTlsFor(domain, hostnameIndex)) {
|
|
1032
|
+
res.writeHead(200);
|
|
1033
|
+
res.end("ok");
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
console.log(`TLS ask denied for unknown hostname: ${domain || "(none)"}`);
|
|
1037
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
1038
|
+
res.end("Unknown project");
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
639
1041
|
* Handle status polling requests
|
|
640
1042
|
*/
|
|
641
1043
|
function handleStatus(req, res) {
|
|
@@ -662,6 +1064,7 @@ async function handleRequest(req, res) {
|
|
|
662
1064
|
try {
|
|
663
1065
|
if (url === "/__auth__" || url.startsWith("/__auth__/")) handleAuth(req, res);
|
|
664
1066
|
else if (url === "/__status__" || url.startsWith("/__status__/")) handleStatus(req, res);
|
|
1067
|
+
else if (url === "/__tls__" || url.startsWith("/__tls__?")) handleTlsAsk(req, res);
|
|
665
1068
|
else if (url === "/__health__") {
|
|
666
1069
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
667
1070
|
res.end(JSON.stringify({
|
|
@@ -697,6 +1100,7 @@ function reloadProjects() {
|
|
|
697
1100
|
function startServer(agentConfig) {
|
|
698
1101
|
config = agentConfig;
|
|
699
1102
|
initDb(config.dbPath);
|
|
1103
|
+
syncForwardAuthAddress(config.projectListPath);
|
|
700
1104
|
reloadProjects();
|
|
701
1105
|
watchProjectList(config.projectListPath, () => {
|
|
702
1106
|
console.log("Project list changed, reloading...");
|
|
@@ -717,6 +1121,6 @@ function startServer(agentConfig) {
|
|
|
717
1121
|
});
|
|
718
1122
|
}
|
|
719
1123
|
//#endregion
|
|
720
|
-
export {
|
|
1124
|
+
export { logAccess as A, watchProjectList as B, cleanOldLogs as C, getIdleProjects as D, getDb as E, getProjectInfo as F, parseBearerToken as G, matchHostname as H, listProjects as I, loadProjectList as L, updateProjectAccess as M, buildHostnameIndex as N, getProject as O, getProjectFromHostname as P, startProject as R, shouldNeverStop as S, getAccessLogs as T, matchIp as U, checkAuth as V, parseBasicAuth as W, step as _, configureTraefik as a, getIdleTimeoutMs as b, installSystemDeps as c, exec as d, execSilent as f, setDryRun as g, resetSteps as h, configureDdev as i, setProjectStatus as j, initDb as k, nodeIo as l, isRoot as m, startServer as n, getDockerGatewayIp as o, info as p, addDdevAptRepo as r, installDdev as s, allowTlsFor as t, error as u, success as v, closeDb as w, loadProjectConfig as x, warn as y, stopProject as z };
|
|
721
1125
|
|
|
722
|
-
//# sourceMappingURL=server-
|
|
1126
|
+
//# sourceMappingURL=server-BgOOBUhR.js.map
|