@studiometa/trafic-agent 0.1.45 → 0.1.47
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/README.md +129 -20
- package/dist/cli.js +49 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{server-BAjHxgqh.js → server-EPU5dtPx.js} +355 -21
- package/dist/server-EPU5dtPx.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -1
- package/dist/setup/agent.d.ts +2 -0
- package/dist/setup/agent.d.ts.map +1 -1
- package/dist/setup/audit.d.ts +39 -1
- package/dist/setup/audit.d.ts.map +1 -1
- package/dist/setup/ddev.d.ts +102 -3
- package/dist/setup/ddev.d.ts.map +1 -1
- package/dist/setup/docker.d.ts +23 -0
- package/dist/setup/docker.d.ts.map +1 -1
- package/dist/setup/index.d.ts.map +1 -1
- package/dist/setup/migrations/0015__catchall_priority.d.ts +40 -0
- package/dist/setup/migrations/0015__catchall_priority.d.ts.map +1 -0
- package/dist/setup/migrations/0016__wildcard_dns_challenge.d.ts +63 -0
- package/dist/setup/migrations/0016__wildcard_dns_challenge.d.ts.map +1 -0
- package/dist/setup/migrations/0018__docker_address_pools.d.ts +29 -0
- package/dist/setup/migrations/0018__docker_address_pools.d.ts.map +1 -0
- package/dist/setup/migrations/index.d.ts.map +1 -1
- package/dist/setup/types.d.ts +11 -0
- package/dist/setup/types.d.ts.map +1 -1
- package/dist/{setup-BrMbM8w4.js → setup-4zlNJqDe.js} +373 -173
- package/dist/setup-4zlNJqDe.js.map +1 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/tls.d.ts +10 -0
- package/dist/utils/tls.d.ts.map +1 -0
- package/package.json +1 -1
- package/templates/wait.html +1 -0
- package/dist/server-BAjHxgqh.js.map +0 -1
- package/dist/setup-BrMbM8w4.js.map +0 -1
|
@@ -1,9 +1,147 @@
|
|
|
1
1
|
import { existsSync, readFileSync, watch, writeFileSync } from "node:fs";
|
|
2
|
+
import { parse } from "smol-toml";
|
|
2
3
|
import { execFile, execSync } from "node:child_process";
|
|
3
4
|
import { DatabaseSync } from "node:sqlite";
|
|
4
5
|
import { dirname, join, resolve } from "node:path";
|
|
5
6
|
import { createServer } from "node:http";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
//#region src/utils/config.ts
|
|
9
|
+
var DEFAULT_CONFIG_PATHS = [
|
|
10
|
+
"/etc/trafic/config.toml",
|
|
11
|
+
"./config.toml",
|
|
12
|
+
"./trafic.toml"
|
|
13
|
+
];
|
|
14
|
+
var DEFAULT_CONFIG = {
|
|
15
|
+
tld: "",
|
|
16
|
+
port: 9876,
|
|
17
|
+
dbPath: "/var/lib/trafic/db.sqlite",
|
|
18
|
+
projectListPath: "/home/ddev/.ddev/project_list.yaml",
|
|
19
|
+
projectsDir: "/home/ddev/www",
|
|
20
|
+
idleTimeout: "30m",
|
|
21
|
+
idleCheckInterval: "5m",
|
|
22
|
+
auth: {
|
|
23
|
+
defaultPolicy: "basic",
|
|
24
|
+
allowedIps: [],
|
|
25
|
+
tokens: [],
|
|
26
|
+
basicAuth: [],
|
|
27
|
+
rules: [],
|
|
28
|
+
trustedProxyHops: 1
|
|
29
|
+
},
|
|
30
|
+
tls: { dnsEnv: {} }
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Parse a duration string (e.g., "30m", "1h", "2h30m") to milliseconds
|
|
34
|
+
*/
|
|
35
|
+
function parseDuration(duration) {
|
|
36
|
+
const match = duration.match(/(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/);
|
|
37
|
+
if (!match) return 0;
|
|
38
|
+
const hours = parseInt(match[1] ?? "0", 10);
|
|
39
|
+
const minutes = parseInt(match[2] ?? "0", 10);
|
|
40
|
+
const seconds = parseInt(match[3] ?? "0", 10);
|
|
41
|
+
return (hours * 3600 + minutes * 60 + seconds) * 1e3;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Load configuration from TOML file
|
|
45
|
+
*/
|
|
46
|
+
function loadConfig(configPath) {
|
|
47
|
+
let path = configPath;
|
|
48
|
+
if (!path) path = DEFAULT_CONFIG_PATHS.find((p) => existsSync(p));
|
|
49
|
+
if (!path || !existsSync(path)) {
|
|
50
|
+
console.warn("No config file found, using defaults");
|
|
51
|
+
return DEFAULT_CONFIG;
|
|
52
|
+
}
|
|
53
|
+
const content = readFileSync(path, "utf-8");
|
|
54
|
+
const parsed = parse(content);
|
|
55
|
+
const auth = mergeAuthConfig(parsed.auth);
|
|
56
|
+
const tls = mergeTlsConfig(parsed.tls);
|
|
57
|
+
return {
|
|
58
|
+
tld: parsed.tld ?? DEFAULT_CONFIG.tld,
|
|
59
|
+
port: parsed.port ?? DEFAULT_CONFIG.port,
|
|
60
|
+
dbPath: parsed.db_path ?? DEFAULT_CONFIG.dbPath,
|
|
61
|
+
projectListPath: parsed.project_list_path ?? DEFAULT_CONFIG.projectListPath,
|
|
62
|
+
projectsDir: parsed.projects_dir ?? DEFAULT_CONFIG.projectsDir,
|
|
63
|
+
idleTimeout: parsed.idle_timeout ?? DEFAULT_CONFIG.idleTimeout,
|
|
64
|
+
idleCheckInterval: parsed.idle_check_interval ?? DEFAULT_CONFIG.idleCheckInterval,
|
|
65
|
+
auth,
|
|
66
|
+
tls
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Read the [tls] section.
|
|
71
|
+
*
|
|
72
|
+
* `dns_env` holds provider credentials, so its values are taken as-is and
|
|
73
|
+
* only checked for being strings — validateConfig reports the rest.
|
|
74
|
+
*/
|
|
75
|
+
function mergeTlsConfig(raw) {
|
|
76
|
+
if (!raw) return DEFAULT_CONFIG.tls;
|
|
77
|
+
const env = raw.dns_env ?? {};
|
|
78
|
+
const dnsEnv = {};
|
|
79
|
+
for (const [key, value] of Object.entries(env)) dnsEnv[key] = value;
|
|
80
|
+
return {
|
|
81
|
+
dnsProvider: raw.dns_provider || void 0,
|
|
82
|
+
caServer: raw.ca_server || void 0,
|
|
83
|
+
dnsEnv
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Parse basic_auth entries from TOML.
|
|
88
|
+
* Accepts either plain strings ("user:pass") or objects ({username, password}).
|
|
89
|
+
*/
|
|
90
|
+
function parseBasicAuthEntries(raw) {
|
|
91
|
+
if (!Array.isArray(raw)) return null;
|
|
92
|
+
return raw.map((entry) => {
|
|
93
|
+
if (typeof entry === "string") return entry;
|
|
94
|
+
if (typeof entry === "object" && entry !== null) {
|
|
95
|
+
const { username, password } = entry;
|
|
96
|
+
return `${username}:${password}`;
|
|
97
|
+
}
|
|
98
|
+
return String(entry);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function mergeAuthConfig(raw) {
|
|
102
|
+
if (!raw) return DEFAULT_CONFIG.auth;
|
|
103
|
+
const rules = raw.rules ?? [];
|
|
104
|
+
return {
|
|
105
|
+
defaultPolicy: raw.default_policy ?? DEFAULT_CONFIG.auth.defaultPolicy,
|
|
106
|
+
allowedIps: raw.allowed_ips ?? DEFAULT_CONFIG.auth.allowedIps,
|
|
107
|
+
tokens: raw.tokens ?? DEFAULT_CONFIG.auth.tokens,
|
|
108
|
+
basicAuth: parseBasicAuthEntries(raw.basic_auth) ?? DEFAULT_CONFIG.auth.basicAuth,
|
|
109
|
+
rules: rules.map((r) => ({
|
|
110
|
+
match: r.match,
|
|
111
|
+
policy: r.policy,
|
|
112
|
+
tokens: r.tokens,
|
|
113
|
+
allowedIps: r.allowed_ips
|
|
114
|
+
})),
|
|
115
|
+
trustedProxyHops: parseTrustedProxyHops(raw.trusted_proxy_hops)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Parse trusted_proxy_hops, ignoring values that cannot be a hop count.
|
|
120
|
+
*
|
|
121
|
+
* A bad value falls back to the default rather than 0, because 0 would read
|
|
122
|
+
* the socket peer — always the proxy — and quietly stop the IP allowlist and
|
|
123
|
+
* per-hostname IP rules from ever matching.
|
|
124
|
+
*/
|
|
125
|
+
function parseTrustedProxyHops(raw) {
|
|
126
|
+
const hops = typeof raw === "number" ? raw : Number(raw);
|
|
127
|
+
if (!Number.isInteger(hops) || hops < 1) return DEFAULT_CONFIG.auth.trustedProxyHops;
|
|
128
|
+
return hops;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Validate configuration
|
|
132
|
+
*/
|
|
133
|
+
function validateConfig(config) {
|
|
134
|
+
const errors = [];
|
|
135
|
+
if (!config.tld) errors.push("tld is required");
|
|
136
|
+
if (config.port < 1 || config.port > 65535) errors.push("port must be between 1 and 65535");
|
|
137
|
+
if (config.auth.defaultPolicy !== "allow" && config.auth.defaultPolicy !== "deny" && config.auth.defaultPolicy !== "basic" && config.auth.defaultPolicy !== "token") errors.push("auth.default_policy must be \"allow\", \"deny\", \"basic\", or \"token\"");
|
|
138
|
+
for (const [key, value] of Object.entries(config.tls.dnsEnv)) {
|
|
139
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) errors.push(`tls.dns_env key "${key}" is not a valid environment variable name`);
|
|
140
|
+
if (typeof value !== "string") errors.push(`tls.dns_env.${key} must be a string`);
|
|
141
|
+
}
|
|
142
|
+
return errors;
|
|
143
|
+
}
|
|
144
|
+
//#endregion
|
|
7
145
|
//#region src/utils/auth.ts
|
|
8
146
|
/**
|
|
9
147
|
* Check if an IP matches a pattern (supports CIDR notation)
|
|
@@ -832,14 +970,42 @@ function readRouterPorts(io = nodeIo) {
|
|
|
832
970
|
https: read("router-https-port", DEFAULT_ROUTER_PORTS.https)
|
|
833
971
|
};
|
|
834
972
|
}
|
|
973
|
+
/** Where the agent writes the Traefik files it owns. */
|
|
974
|
+
var TRAEFIK_DIR = "/home/ddev/.ddev/traefik";
|
|
975
|
+
/** Static config the agent owns, merged by DDEV into .static_config.yaml. */
|
|
976
|
+
var STATIC_CONFIG = `${TRAEFIK_DIR}/static_config.trafic.yaml`;
|
|
977
|
+
/** Wildcard TLS store, read before DDEV's own default_config.yaml. */
|
|
978
|
+
var TLS_STORE_CONFIG = `${TRAEFIK_DIR}/custom-global-config/0-trafic-tls.yaml`;
|
|
979
|
+
/** Router compose override carrying the DNS provider credentials. */
|
|
980
|
+
var ROUTER_COMPOSE_OVERRIDE = "/home/ddev/.ddev/router-compose.trafic.yaml";
|
|
981
|
+
/** Name of the DNS-01 resolver the agent adds to Traefik. */
|
|
982
|
+
var DNS_RESOLVER = "acme-dns";
|
|
835
983
|
/**
|
|
836
984
|
* Build the static config attaching forward auth to every entry point.
|
|
837
985
|
*
|
|
838
986
|
* The error page middleware stays on the web entry points only: a 502 from a
|
|
839
987
|
* stopped project should show the waiting page, while a tool port has nothing
|
|
840
988
|
* to wait for.
|
|
841
|
-
|
|
842
|
-
|
|
989
|
+
*
|
|
990
|
+
* With `tls`, a DNS-01 certificate resolver is added. It is only ever used by
|
|
991
|
+
* the default TLS store (see buildTlsStoreConfig): DDEV keeps writing
|
|
992
|
+
* `certResolver: acme-tlsChallenge` on every project router, and Traefik
|
|
993
|
+
* skips those requests once the default store holds a certificate matching
|
|
994
|
+
* the host with wildcard semantics.
|
|
995
|
+
*
|
|
996
|
+
* The propagation check is disabled on purpose. lego sets two TXT values at
|
|
997
|
+
* the same name, one for `<tld>` and one for `*.<tld>`, then asks public
|
|
998
|
+
* resolvers for each. A resolver that answered between the two writes caches
|
|
999
|
+
* the partial answer for the record TTL — 120s on Cloudflare, longer than
|
|
1000
|
+
* lego's check window — and the order fails with "NS 9.9.9.9:53 did not
|
|
1001
|
+
* return the expected TXT record" although both records were correct. That
|
|
1002
|
+
* was seen on a live server; the staging run had passed by timing luck.
|
|
1003
|
+
* Let's Encrypt resolves authoritatively itself, so the check adds a failure
|
|
1004
|
+
* mode without adding safety. `PropagationWait(delay, skipCheck)` sleeps the
|
|
1005
|
+
* delay and then skips the check, so the 30s covers propagation to the
|
|
1006
|
+
* provider's edges before the CA looks.
|
|
1007
|
+
*/
|
|
1008
|
+
function buildStaticConfig(toolPorts, routerPorts = DEFAULT_ROUTER_PORTS, tls) {
|
|
843
1009
|
const webEntryPoint = (name) => ` ${name}:
|
|
844
1010
|
http:
|
|
845
1011
|
middlewares:
|
|
@@ -853,8 +1019,66 @@ function buildStaticConfig(toolPorts, routerPorts = DEFAULT_ROUTER_PORTS) {
|
|
|
853
1019
|
`;
|
|
854
1020
|
const web = [routerPorts.http, routerPorts.https];
|
|
855
1021
|
const tools = toolPorts.filter((port) => !web.includes(port));
|
|
856
|
-
|
|
1022
|
+
const entryPoints = `entryPoints:
|
|
857
1023
|
${web.map((port) => webEntryPoint(`http-${port}`)).join("")}${tools.map(toolEntryPoint).join("")}`;
|
|
1024
|
+
if (!tls) return entryPoints;
|
|
1025
|
+
return `${entryPoints}
|
|
1026
|
+
certificatesResolvers:
|
|
1027
|
+
${DNS_RESOLVER}:
|
|
1028
|
+
acme:
|
|
1029
|
+
email: "${escapeYamlDouble(tls.email)}"
|
|
1030
|
+
storage: /mnt/ddev-global-cache/traefik/${DNS_RESOLVER}.json
|
|
1031
|
+
${tls.caServer ? ` caServer: "${escapeYamlDouble(tls.caServer)}"\n` : ""} dnsChallenge:
|
|
1032
|
+
provider: "${escapeYamlDouble(tls.provider)}"
|
|
1033
|
+
propagation:
|
|
1034
|
+
delayBeforeChecks: 30s
|
|
1035
|
+
disableChecks: true
|
|
1036
|
+
`;
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Build the default TLS store holding the wildcard certificate.
|
|
1040
|
+
*
|
|
1041
|
+
* Traefik's file provider keeps the first `tls.stores.default` it reads in
|
|
1042
|
+
* directory order and logs "TLS store default already configured, skipping"
|
|
1043
|
+
* for every later file. DDEV writes an empty one in `default_config.yaml`,
|
|
1044
|
+
* so this has to be read first — hence the `0-` prefix on the file name.
|
|
1045
|
+
*/
|
|
1046
|
+
function buildTlsStoreConfig(tld) {
|
|
1047
|
+
return `# Trafic: wildcard certificate served to every host without its own
|
|
1048
|
+
#
|
|
1049
|
+
# Read before DDEV's default_config.yaml — Traefik keeps the first default
|
|
1050
|
+
# store it sees, so the file name has to sort first.
|
|
1051
|
+
tls:
|
|
1052
|
+
stores:
|
|
1053
|
+
default:
|
|
1054
|
+
defaultGeneratedCert:
|
|
1055
|
+
resolver: ${DNS_RESOLVER}
|
|
1056
|
+
domain:
|
|
1057
|
+
main: "${escapeYamlDouble(tld)}"
|
|
1058
|
+
sans:
|
|
1059
|
+
- "*.${escapeYamlDouble(tld)}"
|
|
1060
|
+
`;
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Build the router compose override carrying the provider credentials.
|
|
1064
|
+
*
|
|
1065
|
+
* lego reads them from the router container's environment, so they cannot be
|
|
1066
|
+
* passed through a config file. DDEV merges `~/.ddev/router-compose.*.yaml`
|
|
1067
|
+
* into the router compose and recreates the container when it changes, on the
|
|
1068
|
+
* next `ddev start`.
|
|
1069
|
+
*/
|
|
1070
|
+
function buildRouterComposeOverride(env) {
|
|
1071
|
+
return `# Trafic: DNS-01 provider credentials for the Traefik router
|
|
1072
|
+
# Contains secrets — this file is mode 600.
|
|
1073
|
+
services:
|
|
1074
|
+
ddev-router:
|
|
1075
|
+
environment:
|
|
1076
|
+
${Object.entries(env).map(([key, value]) => ` - "${key}=${escapeYamlDouble(value)}"`).join("\n")}
|
|
1077
|
+
`;
|
|
1078
|
+
}
|
|
1079
|
+
/** Escape a value for a double-quoted YAML scalar. */
|
|
1080
|
+
function escapeYamlDouble(value) {
|
|
1081
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
858
1082
|
}
|
|
859
1083
|
/**
|
|
860
1084
|
* Build the dynamic config: the middlewares, the agent service, and the
|
|
@@ -887,12 +1111,20 @@ http:
|
|
|
887
1111
|
query: "/"
|
|
888
1112
|
|
|
889
1113
|
routers:
|
|
890
|
-
#
|
|
891
|
-
#
|
|
892
|
-
#
|
|
893
|
-
#
|
|
894
|
-
#
|
|
895
|
-
#
|
|
1114
|
+
# The lowest priority above DDEV's own fallback routers, which sit at
|
|
1115
|
+
# priority 1 since DDEV 1.25.4, and still far below any project router —
|
|
1116
|
+
# so a DDEV project router always wins. These only match when none does —
|
|
1117
|
+
# which is what a stopped project produces, because DDEV removes that
|
|
1118
|
+
# project's router and Traefik would otherwise answer 404. Without them
|
|
1119
|
+
# the waiting page never appears and scale-to-zero never restarts
|
|
1120
|
+
# anything. Auth is unaffected: it is attached at the entry point, not
|
|
1121
|
+
# here, so this cannot reintroduce the bypass that #27 fixed.
|
|
1122
|
+
#
|
|
1123
|
+
# Priority 1 is not enough any more: DDEV 1.25.4 ships
|
|
1124
|
+
# ddev-router-fallback-http and ddev-router-fallback-https with the same
|
|
1125
|
+
# PathPrefix(\`/\`) rule at priority 1, pointing at a 404 responder. With
|
|
1126
|
+
# equal priority and identical rules Traefik picked DDEV's, so a stopped
|
|
1127
|
+
# project answered DDEV's 404 page instead of the waiting page.
|
|
896
1128
|
#
|
|
897
1129
|
# Each names exactly one entry point. Leaving entryPoints out attaches a
|
|
898
1130
|
# router to all of them, which breaks DDEV's router health check — it
|
|
@@ -901,7 +1133,7 @@ http:
|
|
|
901
1133
|
rule: "PathPrefix(\`/\`)"
|
|
902
1134
|
entryPoints:
|
|
903
1135
|
- http-${routerPorts.http}
|
|
904
|
-
priority:
|
|
1136
|
+
priority: 2
|
|
905
1137
|
service: trafic-service
|
|
906
1138
|
|
|
907
1139
|
# Carries tls, so it only matches the https entry point. The plain one
|
|
@@ -910,7 +1142,7 @@ http:
|
|
|
910
1142
|
rule: "PathPrefix(\`/\`)"
|
|
911
1143
|
entryPoints:
|
|
912
1144
|
- http-${routerPorts.https}
|
|
913
|
-
priority:
|
|
1145
|
+
priority: 2
|
|
914
1146
|
service: trafic-service
|
|
915
1147
|
tls: {}
|
|
916
1148
|
|
|
@@ -922,23 +1154,108 @@ http:
|
|
|
922
1154
|
`;
|
|
923
1155
|
}
|
|
924
1156
|
/**
|
|
925
|
-
*
|
|
1157
|
+
* Read DDEV's Let's Encrypt settings.
|
|
1158
|
+
*
|
|
1159
|
+
* DDEV owns them — `ddev config global --use-letsencrypt --letsencrypt-email`
|
|
1160
|
+
* — so they are the single source of truth for the ACME account, wildcard
|
|
1161
|
+
* mode included.
|
|
926
1162
|
*/
|
|
927
|
-
function
|
|
1163
|
+
function readLetsEncryptSettings(io = nodeIo) {
|
|
1164
|
+
const output = io.execSilent("su - ddev -c 'ddev config global'");
|
|
1165
|
+
return {
|
|
1166
|
+
enabled: /^use-letsencrypt=true$/m.test(output),
|
|
1167
|
+
email: /^letsencrypt-email=(.+)$/m.exec(output)?.[1]?.trim() || void 0
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
/** Read the project TLD from DDEV's global config. */
|
|
1171
|
+
function readProjectTld(io = nodeIo) {
|
|
1172
|
+
const output = io.execSilent("su - ddev -c 'ddev config global'");
|
|
1173
|
+
return /^project-tld=(.+)$/m.exec(output)?.[1]?.trim() ?? "";
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Configure Traefik for forward auth, and for the wildcard certificate when
|
|
1177
|
+
* a DNS provider is configured.
|
|
1178
|
+
*
|
|
1179
|
+
* Everything the agent owns in Traefik's configuration is written here, so
|
|
1180
|
+
* setup and the migrations share one code path and cannot drift apart.
|
|
1181
|
+
*/
|
|
1182
|
+
function configureTraefik(options = {}, io = nodeIo) {
|
|
928
1183
|
step("Configure Traefik for forward auth");
|
|
929
|
-
io.exec(
|
|
1184
|
+
io.exec(`mkdir -p ${TRAEFIK_DIR}/custom-global-config`, { silent: true });
|
|
930
1185
|
io.exec("chown -R ddev:ddev /home/ddev/.ddev", { silent: true });
|
|
931
1186
|
const gatewayIp = getDockerGatewayIp(io);
|
|
932
1187
|
const routerPorts = readRouterPorts(io);
|
|
933
1188
|
const dynamicConfig = buildDynamicConfig(gatewayIp, routerPorts);
|
|
934
|
-
io.writeFile(
|
|
935
|
-
io.exec(
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
1189
|
+
io.writeFile(`${TRAEFIK_DIR}/custom-global-config/trafic.yaml`, dynamicConfig);
|
|
1190
|
+
io.exec(`chown ddev:ddev ${TRAEFIK_DIR}/custom-global-config/trafic.yaml`, { silent: true });
|
|
1191
|
+
const tls = options.tls ?? loadConfig().tls;
|
|
1192
|
+
const resolver = tls.dnsProvider ? buildTlsResolverOptions(tls, io) : void 0;
|
|
1193
|
+
const tld = resolver ? readWildcardTld(options.tld, io) : "";
|
|
1194
|
+
const staticConfig = buildStaticConfig(readToolPorts(io), routerPorts, resolver);
|
|
1195
|
+
io.writeFile(STATIC_CONFIG, staticConfig);
|
|
1196
|
+
io.exec(`chown ddev:ddev ${STATIC_CONFIG}`, { silent: true });
|
|
939
1197
|
info(`Entry points: http-${routerPorts.http}, http-${routerPorts.https} (web) plus tool ports`);
|
|
1198
|
+
if (resolver) writeWildcardFiles(tls, tld, io);
|
|
1199
|
+
else removeWildcardFiles(io);
|
|
940
1200
|
success("Traefik configured with Trafic middleware");
|
|
941
1201
|
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Name of one running DDEV project, or null when none runs.
|
|
1204
|
+
*
|
|
1205
|
+
* `ddev list -j` is the only source that knows: `project_list.yaml`, which the
|
|
1206
|
+
* agent watches for discovery, records every project DDEV has ever seen but
|
|
1207
|
+
* carries no live status — a project stopped an hour ago still sits there.
|
|
1208
|
+
* `ddev list -j` asks Docker, and wraps the entries in a `raw` array.
|
|
1209
|
+
*
|
|
1210
|
+
* Used to push a changed global Traefik config into the running router: DDEV
|
|
1211
|
+
* copies `custom-global-config/*.yaml` into it on any `ddev start`, so
|
|
1212
|
+
* starting a project that is already running is enough, and nothing that is
|
|
1213
|
+
* serving traffic has to be stopped.
|
|
1214
|
+
*/
|
|
1215
|
+
function findRunningProject(io = nodeIo) {
|
|
1216
|
+
const output = io.execSilent("su - ddev -c 'ddev list -j'");
|
|
1217
|
+
if (!output) return null;
|
|
1218
|
+
try {
|
|
1219
|
+
return (JSON.parse(output).raw ?? []).find((project) => project.status === "running")?.name ?? null;
|
|
1220
|
+
} catch {
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Turn the agent's TLS config into what the static config needs.
|
|
1226
|
+
*
|
|
1227
|
+
* The ACME account comes from DDEV: a resolver without an email is refused by
|
|
1228
|
+
* Let's Encrypt, and `use_letsencrypt=false` means DDEV is also stripping the
|
|
1229
|
+
* certificate resolver from the project routers.
|
|
1230
|
+
*/
|
|
1231
|
+
function buildTlsResolverOptions(tls, io) {
|
|
1232
|
+
const letsEncrypt = readLetsEncryptSettings(io);
|
|
1233
|
+
if (!letsEncrypt.enabled || !letsEncrypt.email) throw new Error("tls.dns_provider needs Let's Encrypt enabled in DDEV with an account email.\n Run setup again with --email=<address>, or unset tls.dns_provider.");
|
|
1234
|
+
return {
|
|
1235
|
+
provider: tls.dnsProvider,
|
|
1236
|
+
email: letsEncrypt.email,
|
|
1237
|
+
caServer: tls.caServer
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
/** Read the TLD the wildcard certificate covers, or fail before any write. */
|
|
1241
|
+
function readWildcardTld(configured, io) {
|
|
1242
|
+
const tld = configured || readProjectTld(io);
|
|
1243
|
+
if (!tld) throw new Error("Cannot configure the wildcard certificate: no project TLD is set in DDEV");
|
|
1244
|
+
return tld;
|
|
1245
|
+
}
|
|
1246
|
+
/** Write the wildcard TLS store and the router credentials. */
|
|
1247
|
+
function writeWildcardFiles(tls, tld, io) {
|
|
1248
|
+
io.writeFile(TLS_STORE_CONFIG, buildTlsStoreConfig(tld));
|
|
1249
|
+
io.exec(`chown ddev:ddev ${TLS_STORE_CONFIG}`, { silent: true });
|
|
1250
|
+
io.exec(`install -m 600 -o ddev -g ddev /dev/null ${ROUTER_COMPOSE_OVERRIDE}`, { silent: true });
|
|
1251
|
+
io.writeFile(ROUTER_COMPOSE_OVERRIDE, buildRouterComposeOverride(tls.dnsEnv));
|
|
1252
|
+
success(`Wildcard certificate: *.${tld} via the ${tls.dnsProvider} DNS-01 provider`);
|
|
1253
|
+
}
|
|
1254
|
+
/** Remove the wildcard files where no DNS provider is configured. */
|
|
1255
|
+
function removeWildcardFiles(io) {
|
|
1256
|
+
if (io.fileExists(TLS_STORE_CONFIG)) io.exec(`rm -f ${TLS_STORE_CONFIG}`, { silent: true });
|
|
1257
|
+
if (io.fileExists("/home/ddev/.ddev/router-compose.trafic.yaml")) io.exec(`rm -f ${ROUTER_COMPOSE_OVERRIDE}`, { silent: true });
|
|
1258
|
+
}
|
|
942
1259
|
//#endregion
|
|
943
1260
|
//#region src/utils/traefik.ts
|
|
944
1261
|
/**
|
|
@@ -999,6 +1316,22 @@ function syncForwardAuthAddress(projectListPath, io = nodeIo) {
|
|
|
999
1316
|
}
|
|
1000
1317
|
}
|
|
1001
1318
|
//#endregion
|
|
1319
|
+
//#region src/utils/tls.ts
|
|
1320
|
+
/**
|
|
1321
|
+
* Warn when the config asks for a wildcard certificate that Traefik never got.
|
|
1322
|
+
*
|
|
1323
|
+
* The files are written by setup and by migration 0016, not by the running
|
|
1324
|
+
* agent: writing them needs root, and applying them needs a project start.
|
|
1325
|
+
* So this only reports, and says which command fixes it.
|
|
1326
|
+
*/
|
|
1327
|
+
function warnIfWildcardNotApplied(config, staticConfigPath = STATIC_CONFIG) {
|
|
1328
|
+
if (!config.tls.dnsProvider) return false;
|
|
1329
|
+
if (existsSync(staticConfigPath) && readFileSync(staticConfigPath, "utf-8").includes(`acme-dns:`)) return false;
|
|
1330
|
+
console.warn(`tls.dns_provider is set to "${config.tls.dnsProvider}" but Traefik has no ${DNS_RESOLVER} resolver.`);
|
|
1331
|
+
console.warn(" The wildcard certificate is not being requested. Run `sudo trafic-agent upgrade`, or re-run setup.");
|
|
1332
|
+
return true;
|
|
1333
|
+
}
|
|
1334
|
+
//#endregion
|
|
1002
1335
|
//#region src/server.ts
|
|
1003
1336
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
1004
1337
|
var config;
|
|
@@ -1180,6 +1513,7 @@ function startServer(agentConfig) {
|
|
|
1180
1513
|
config = agentConfig;
|
|
1181
1514
|
initDb(config.dbPath);
|
|
1182
1515
|
syncForwardAuthAddress(config.projectListPath);
|
|
1516
|
+
warnIfWildcardNotApplied(config);
|
|
1183
1517
|
reloadProjects();
|
|
1184
1518
|
watchProjectList(config.projectListPath, () => {
|
|
1185
1519
|
console.log("Project list changed, reloading...");
|
|
@@ -1212,6 +1546,6 @@ function startServer(agentConfig) {
|
|
|
1212
1546
|
});
|
|
1213
1547
|
}
|
|
1214
1548
|
//#endregion
|
|
1215
|
-
export {
|
|
1549
|
+
export { checkAuth as $, initDb as A, resetSteps as B, shouldNeverStop as C, getDb as D, getAccessLogs as E, error as F, buildHostnameIndex as G, step as H, exec as I, listProjects as J, getProjectFromHostname as K, execSilent as L, setProjectStatus as M, updateProjectAccess as N, getIdleProjects as O, nodeIo as P, watchProjectList as Q, info as R, loadProjectConfig as S, closeDb as T, success as U, setDryRun as V, warn as W, startProject as X, loadProjectList as Y, stopProject as Z, installDdev as _, handleStatus as a, parseDuration as at, readToolPorts as b, DNS_RESOLVER as c, TLS_STORE_CONFIG as d, matchHostname as et, addDdevAptRepo as f, getDockerGatewayIp as g, findRunningProject as h, handleRequest as i, loadConfig as it, logAccess as j, getProject as k, ROUTER_COMPOSE_OVERRIDE as l, configureTraefik as m, handleAuth as n, parseBasicAuth as nt, routePath as o, validateConfig as ot, configureDdev as p, getProjectInfo as q, handleErrors as r, parseBearerToken as rt, startServer as s, getEffectiveAuthConfig as t, matchIp as tt, STATIC_CONFIG as u, installSystemDeps as v, cleanOldLogs as w, getIdleTimeoutMs as x, purgeTraefikAcmeStorage as y, isRoot as z };
|
|
1216
1550
|
|
|
1217
|
-
//# sourceMappingURL=server-
|
|
1551
|
+
//# sourceMappingURL=server-EPU5dtPx.js.map
|