fix: preserve PK01 Caddy managed blocks
This commit is contained in:
+10
-48
@@ -4,6 +4,7 @@ import { join } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { rootPath, type UniDeskConfig } from "./config";
|
||||
import type { RenderedCliResult } from "./output";
|
||||
import { applyLocalCaddyManagedSite } from "./pk01-caddy";
|
||||
import { runSshCommandCapture, type SshCaptureResult } from "./ssh";
|
||||
import { runRemoteSshCommandCapture } from "./remote";
|
||||
import { startJob } from "./jobs";
|
||||
@@ -2506,37 +2507,20 @@ function applyAgentRunMasterCaddySite(exposure: AgentRunPublicExposure): Record<
|
||||
if (!caddy.enabled) return { ok: true, action: "disabled-by-yaml", valuesPrinted: false };
|
||||
const pathValue = caddy.configPath;
|
||||
if (!existsSync(pathValue)) return { ok: false, error: "master-caddy-config-missing", path: pathValue, valuesPrinted: false };
|
||||
const before = readFileSync(pathValue, "utf8");
|
||||
const desiredBlock = renderAgentRunCaddySiteBlock(caddy.domain, caddy.upstreamBaseUrl, caddy.responseHeaderTimeoutSeconds);
|
||||
const existing = caddySiteBlock(before, caddy.domain);
|
||||
const alreadyConfigured = existing === desiredBlock;
|
||||
let backupPath: string | null = null;
|
||||
let action = "kept-existing";
|
||||
if (!alreadyConfigured) {
|
||||
const stamp = new Date().toISOString().replace(/[-:]/gu, "").replace(/\..*$/u, "Z");
|
||||
backupPath = `${pathValue}.bak-agentrun-https-${stamp}`;
|
||||
copyFileSync(pathValue, backupPath);
|
||||
const next = existing === null ? `${before.replace(/\s*$/u, "")}\n\n${desiredBlock}\n` : before.replace(existing, desiredBlock);
|
||||
writeFileSync(pathValue, next, "utf8");
|
||||
chmodSync(pathValue, statSync(backupPath).mode & 0o777);
|
||||
action = existing === null ? "added-site" : "updated-site";
|
||||
}
|
||||
const validate = spawnSync("caddy", ["validate", "--config", pathValue, "--adapter", "caddyfile"], { encoding: "utf8" });
|
||||
const reload = validate.status === 0 && !alreadyConfigured ? spawnSync("systemctl", ["reload", caddy.serviceName], { encoding: "utf8" }) : null;
|
||||
const active = spawnSync("systemctl", ["is-active", caddy.serviceName], { encoding: "utf8" });
|
||||
const ok = validate.status === 0 && (reload === null || reload.status === 0) && active.status === 0 && String(active.stdout).trim() === "active";
|
||||
const applied = applyLocalCaddyManagedSite({
|
||||
serviceId: "agentrun",
|
||||
markerName: "agentrun",
|
||||
configPath: pathValue,
|
||||
serviceName: caddy.serviceName,
|
||||
siteBlock: desiredBlock,
|
||||
legacySiteDomain: caddy.domain,
|
||||
});
|
||||
return {
|
||||
ok,
|
||||
action,
|
||||
path: pathValue,
|
||||
backupPath,
|
||||
...applied,
|
||||
domain: caddy.domain,
|
||||
upstreamBaseUrl: caddy.upstreamBaseUrl,
|
||||
serviceName: caddy.serviceName,
|
||||
responseHeaderTimeoutSeconds: caddy.responseHeaderTimeoutSeconds,
|
||||
validate: { exitCode: validate.status, stdoutTail: String(validate.stdout).slice(-1000), stderrTail: String(validate.stderr).slice(-2000) },
|
||||
reload: reload === null ? null : { exitCode: reload.status, stdoutTail: String(reload.stdout).slice(-1000), stderrTail: String(reload.stderr).slice(-1000) },
|
||||
active: { exitCode: active.status, stdoutTail: String(active.stdout).slice(-1000), stderrTail: String(active.stderr).slice(-1000) },
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
@@ -2557,24 +2541,6 @@ function renderAgentRunCaddySiteBlock(domain: string, upstreamBaseUrl: string, r
|
||||
}`;
|
||||
}
|
||||
|
||||
function caddySiteBlock(text: string, domain: string): string | null {
|
||||
const startMatch = new RegExp(`(^|\\n)${escapeRegExp(domain)}\\s*\\{`, "u").exec(text);
|
||||
if (startMatch === null) return null;
|
||||
const start = startMatch.index + (startMatch[1] === "\n" ? 1 : 0);
|
||||
const open = text.indexOf("{", start);
|
||||
if (open < 0) return null;
|
||||
let depth = 0;
|
||||
for (let index = open; index < text.length; index += 1) {
|
||||
const ch = text[index];
|
||||
if (ch === "{") depth += 1;
|
||||
if (ch === "}") {
|
||||
depth -= 1;
|
||||
if (depth === 0) return text.slice(start, index + 1);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function frpsAllowPortExists(toml: string, port: number): boolean {
|
||||
const sections = toml.split(/(?=\[\[allowPorts\]\])/u);
|
||||
return sections.some((section) => {
|
||||
@@ -2585,10 +2551,6 @@ function frpsAllowPortExists(toml: string, port: number): boolean {
|
||||
});
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
||||
}
|
||||
|
||||
async function triggerCurrent(config: UniDeskConfig, options: TriggerOptions): Promise<Record<string, unknown>> {
|
||||
if (options.node !== null || options.lane !== null) {
|
||||
const target = resolveAgentRunLaneTarget(options);
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
import { chmodSync, copyFileSync, existsSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
||||
import { spawnSync, type SpawnSyncReturns } from "node:child_process";
|
||||
import { Buffer } from "node:buffer";
|
||||
import type { UniDeskConfig } from "./config";
|
||||
import { runSshCommandCapture, type SshCaptureResult } from "./ssh";
|
||||
|
||||
export interface Pk01CaddyBlockExposure {
|
||||
enabled: boolean;
|
||||
dns: { hostname: string };
|
||||
frpc: { remotePort: number };
|
||||
pk01: {
|
||||
route: string;
|
||||
caddyConfigPath: string;
|
||||
caddyServiceName: string;
|
||||
responseHeaderTimeoutSeconds: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Pk01CaddyManagedBlockMarkers {
|
||||
start: string;
|
||||
end: string;
|
||||
}
|
||||
|
||||
export interface LocalCaddyManagedSiteOptions {
|
||||
serviceId: string;
|
||||
markerName: string;
|
||||
configPath: string;
|
||||
serviceName: string;
|
||||
siteBlock: string;
|
||||
legacySiteDomain?: string;
|
||||
}
|
||||
|
||||
export function caddyManagedBlockMarkers(markerName: string): Pk01CaddyManagedBlockMarkers {
|
||||
return {
|
||||
start: `# BEGIN unidesk managed ${markerName}`,
|
||||
end: `# END unidesk managed ${markerName}`,
|
||||
};
|
||||
}
|
||||
|
||||
export function renderCaddyManagedBlock(markerName: string, siteBlock: string): string {
|
||||
const markers = caddyManagedBlockMarkers(markerName);
|
||||
return `${markers.start}
|
||||
${siteBlock.trim()}
|
||||
${markers.end}
|
||||
`;
|
||||
}
|
||||
|
||||
export function renderSimpleReverseProxyCaddySiteBlock(params: {
|
||||
hostname: string;
|
||||
upstream: string;
|
||||
responseHeaderTimeoutSeconds: number;
|
||||
}): string {
|
||||
return `${params.hostname} {
|
||||
reverse_proxy ${params.upstream} {
|
||||
transport http {
|
||||
response_header_timeout ${params.responseHeaderTimeoutSeconds}s
|
||||
}
|
||||
}
|
||||
}`;
|
||||
}
|
||||
|
||||
export async function applyPk01CaddyManagedBlock(
|
||||
config: UniDeskConfig,
|
||||
serviceId: string,
|
||||
exposure: Pk01CaddyBlockExposure,
|
||||
markers: Pk01CaddyManagedBlockMarkers,
|
||||
): Promise<Record<string, unknown>> {
|
||||
if (!exposure.enabled) return { ok: true, action: "not-enabled" };
|
||||
const block = renderCaddyManagedBlock(
|
||||
markerNameFromMarkers(markers),
|
||||
renderSimpleReverseProxyCaddySiteBlock({
|
||||
hostname: exposure.dns.hostname,
|
||||
upstream: `127.0.0.1:${exposure.frpc.remotePort}`,
|
||||
responseHeaderTimeoutSeconds: exposure.pk01.responseHeaderTimeoutSeconds,
|
||||
}),
|
||||
);
|
||||
const blockB64 = Buffer.from(block, "utf8").toString("base64");
|
||||
const script = `
|
||||
set -u
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
block="$tmp/${serviceId}.caddy"
|
||||
next="$tmp/Caddyfile.next"
|
||||
printf '%s' '${blockB64}' | base64 -d >"$block"
|
||||
sudo python3 - ${shQuote(exposure.pk01.caddyConfigPath)} "$block" "$next" >"$tmp/update.out" 2>"$tmp/update.err" <<'PY'
|
||||
${pk01CaddyApplyManagedBlockPython()}
|
||||
PY
|
||||
update_rc=$?
|
||||
if [ "$update_rc" -eq 0 ]; then
|
||||
sudo caddy fmt --overwrite "$next" >"$tmp/fmt.out" 2>"$tmp/fmt.err"
|
||||
fmt_rc=$?
|
||||
else
|
||||
: >"$tmp/fmt.out"; : >"$tmp/fmt.err"; fmt_rc=1
|
||||
fi
|
||||
if [ "$fmt_rc" -eq 0 ]; then
|
||||
sudo caddy validate --config "$next" >"$tmp/validate.out" 2>"$tmp/validate.err"
|
||||
validate_rc=$?
|
||||
else
|
||||
: >"$tmp/validate.out"; : >"$tmp/validate.err"; validate_rc=1
|
||||
fi
|
||||
if [ "$validate_rc" -eq 0 ]; then
|
||||
sudo install -m 0644 "$next" ${shQuote(exposure.pk01.caddyConfigPath)} >"$tmp/install.out" 2>"$tmp/install.err"
|
||||
install_rc=$?
|
||||
else
|
||||
: >"$tmp/install.out"; : >"$tmp/install.err"; install_rc=1
|
||||
fi
|
||||
if [ "$install_rc" -eq 0 ]; then
|
||||
sudo systemctl reload ${shQuote(exposure.pk01.caddyServiceName)} >"$tmp/reload.out" 2>"$tmp/reload.err" || sudo systemctl restart ${shQuote(exposure.pk01.caddyServiceName)} >>"$tmp/reload.out" 2>>"$tmp/reload.err"
|
||||
reload_rc=$?
|
||||
else
|
||||
: >"$tmp/reload.out"; : >"$tmp/reload.err"; reload_rc=1
|
||||
fi
|
||||
python3 - "$update_rc" "$fmt_rc" "$validate_rc" "$install_rc" "$reload_rc" "$tmp/update.out" "$tmp/update.err" "$tmp/fmt.out" "$tmp/fmt.err" "$tmp/validate.out" "$tmp/validate.err" "$tmp/install.out" "$tmp/install.err" "$tmp/reload.out" "$tmp/reload.err" <<'PY'
|
||||
import json, sys
|
||||
rcs = [int(value) for value in sys.argv[1:6]]
|
||||
def text(path):
|
||||
try:
|
||||
return open(path, encoding="utf-8", errors="replace").read()[-3000:]
|
||||
except FileNotFoundError:
|
||||
return ""
|
||||
payload = {
|
||||
"ok": all(rc == 0 for rc in rcs),
|
||||
"serviceId": "${serviceId}",
|
||||
"hostname": "${exposure.dns.hostname}",
|
||||
"remotePort": ${exposure.frpc.remotePort},
|
||||
"caddyConfigPath": "${exposure.pk01.caddyConfigPath}",
|
||||
"service": "${exposure.pk01.caddyServiceName}",
|
||||
"managedBlock": {"start": "${markers.start}", "end": "${markers.end}"},
|
||||
"steps": {
|
||||
"update": {"exitCode": rcs[0], "stdout": text(sys.argv[6]), "stderr": text(sys.argv[7])},
|
||||
"fmt": {"exitCode": rcs[1], "stdout": text(sys.argv[8]), "stderr": text(sys.argv[9])},
|
||||
"validate": {"exitCode": rcs[2], "stdout": text(sys.argv[10]), "stderr": text(sys.argv[11])},
|
||||
"install": {"exitCode": rcs[3], "stdout": text(sys.argv[12]), "stderr": text(sys.argv[13])},
|
||||
"reload": {"exitCode": rcs[4], "stdout": text(sys.argv[14]), "stderr": text(sys.argv[15])},
|
||||
},
|
||||
}
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
sys.exit(0 if payload["ok"] else 1)
|
||||
PY
|
||||
`;
|
||||
const result = await runSshCommandCapture(config, exposure.pk01.route, ["script"], script);
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return parsed ?? { ok: false, capture: compactCapture(result, { full: true }) };
|
||||
}
|
||||
|
||||
export function mergeDesiredCaddyfilePreservingManagedBlocksScript(params: {
|
||||
desiredPath: string;
|
||||
currentPath: string;
|
||||
mergedPath: string;
|
||||
outputPath: string;
|
||||
errorPath: string;
|
||||
legacySiteDomains?: string[];
|
||||
}): string {
|
||||
const domains = params.legacySiteDomains ?? [];
|
||||
return `sudo python3 - ${shQuote(params.desiredPath)} ${shQuote(params.currentPath)} ${shQuote(params.mergedPath)} ${domains.map(shQuote).join(" ")} >${shQuote(params.outputPath)} 2>${shQuote(params.errorPath)} <<'PY'
|
||||
${pk01CaddyMergeManagedBlocksPython()}
|
||||
PY`;
|
||||
}
|
||||
|
||||
export function applyLocalCaddyManagedSite(options: LocalCaddyManagedSiteOptions): Record<string, unknown> {
|
||||
const path = options.configPath;
|
||||
if (!existsSync(path)) return { ok: false, error: "caddy-config-missing", path, valuesPrinted: false };
|
||||
const before = readFileSync(path, "utf8");
|
||||
const managedBlock = renderCaddyManagedBlock(options.markerName, options.siteBlock);
|
||||
const migrated = options.legacySiteDomain === undefined ? before : removeLegacySiteBlocksOutsideManagedBlocks(before, options.legacySiteDomain);
|
||||
const next = upsertManagedBlock(migrated, managedBlock);
|
||||
const alreadyConfigured = next === before;
|
||||
let backupPath: string | null = null;
|
||||
let action = "kept-existing";
|
||||
const tmpPath = `${path}.unidesk-${safeFilePart(options.serviceId)}.tmp`;
|
||||
let validate: SpawnSyncReturns<string>;
|
||||
let reload: SpawnSyncReturns<string> | null = null;
|
||||
if (!alreadyConfigured) {
|
||||
writeFileSync(tmpPath, next, "utf8");
|
||||
chmodSync(tmpPath, statSync(path).mode & 0o777);
|
||||
validate = spawnSync("caddy", ["validate", "--config", tmpPath, "--adapter", "caddyfile"], { encoding: "utf8" });
|
||||
if (validate.status === 0) {
|
||||
const stamp = new Date().toISOString().replace(/[-:]/gu, "").replace(/\..*$/u, "Z");
|
||||
backupPath = `${path}.bak-${options.serviceId}-https-${stamp}`;
|
||||
copyFileSync(path, backupPath);
|
||||
writeFileSync(path, next, "utf8");
|
||||
chmodSync(path, statSync(backupPath).mode & 0o777);
|
||||
action = before.includes(caddyManagedBlockMarkers(options.markerName).start) ? "updated-managed-block" : "added-managed-block";
|
||||
reload = spawnSync("systemctl", ["reload", options.serviceName], { encoding: "utf8" });
|
||||
} else {
|
||||
action = "validate-failed-no-mutation";
|
||||
}
|
||||
try {
|
||||
unlinkSync(tmpPath);
|
||||
} catch {
|
||||
// best-effort cleanup only
|
||||
}
|
||||
} else {
|
||||
validate = spawnSync("caddy", ["validate", "--config", path, "--adapter", "caddyfile"], { encoding: "utf8" });
|
||||
}
|
||||
const active = spawnSync("systemctl", ["is-active", options.serviceName], { encoding: "utf8" });
|
||||
const ok = validate.status === 0 && (reload === null || reload.status === 0) && active.status === 0 && String(active.stdout).trim() === "active";
|
||||
return {
|
||||
ok,
|
||||
action,
|
||||
path,
|
||||
backupPath,
|
||||
serviceId: options.serviceId,
|
||||
markerName: options.markerName,
|
||||
serviceName: options.serviceName,
|
||||
alreadyConfigured,
|
||||
validate: spawnSummary(validate, 1000, 2000),
|
||||
reload: reload === null ? null : spawnSummary(reload, 1000, 1000),
|
||||
active: spawnSummary(active, 1000, 1000),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function upsertManagedBlock(text: string, block: string): string {
|
||||
const marker = parseManagedBlockMarker(block);
|
||||
if (marker === null) throw new Error("managed Caddy block is missing UniDesk BEGIN/END markers");
|
||||
const pattern = managedBlockPattern();
|
||||
let matched = false;
|
||||
const replaced = text.replace(pattern, (matchedText: string, name: string) => {
|
||||
if (name !== marker) return matchedText;
|
||||
matched = true;
|
||||
return `${block.trim()}\n\n`;
|
||||
}).replace(/\s+$/u, "\n");
|
||||
if (matched) return replaced;
|
||||
return `${text.trimEnd()}\n\n${block.trim()}\n`;
|
||||
}
|
||||
|
||||
export function caddySiteBlock(text: string, domain: string): string | null {
|
||||
const startMatch = new RegExp(`(^|\\n)${escapeRegExp(domain)}\\s*\\{`, "u").exec(text);
|
||||
if (startMatch === null) return null;
|
||||
const start = startMatch.index + (startMatch[1] === "\n" ? 1 : 0);
|
||||
const open = text.indexOf("{", start);
|
||||
if (open < 0) return null;
|
||||
let depth = 0;
|
||||
for (let index = open; index < text.length; index += 1) {
|
||||
const ch = text[index];
|
||||
if (ch === "{") depth += 1;
|
||||
if (ch === "}") {
|
||||
depth -= 1;
|
||||
if (depth === 0) return text.slice(start, index + 1);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function removeLegacySiteBlocksOutsideManagedBlocks(text: string, domain: string): string {
|
||||
const pattern = managedBlockPattern();
|
||||
let cursor = 0;
|
||||
let next = "";
|
||||
for (const match of text.matchAll(pattern)) {
|
||||
const index = match.index ?? 0;
|
||||
next += removeCaddySiteBlocks(text.slice(cursor, index), domain);
|
||||
next += match[0];
|
||||
cursor = index + match[0].length;
|
||||
}
|
||||
next += removeCaddySiteBlocks(text.slice(cursor), domain);
|
||||
return next;
|
||||
}
|
||||
|
||||
export function shQuote(value: string): string {
|
||||
return `'${value.replaceAll("'", "'\"'\"'")}'`;
|
||||
}
|
||||
|
||||
export function parseJsonOutput(stdout: string): Record<string, unknown> | null {
|
||||
const trimmed = stdout.trim();
|
||||
if (!trimmed) return null;
|
||||
const candidates = [trimmed, trimmed.slice(trimmed.indexOf("{"))].filter((item) => item.startsWith("{"));
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const parsed = JSON.parse(candidate) as unknown;
|
||||
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed as Record<string, unknown>;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function compactCapture(result: SshCaptureResult, options: { full?: boolean } = {}): Record<string, unknown> {
|
||||
const full = options.full === true;
|
||||
return {
|
||||
exitCode: result.exitCode,
|
||||
timedOut: result.timedOut,
|
||||
stdoutTail: full || result.exitCode !== 0 ? result.stdout.slice(-8000) : "",
|
||||
stderrTail: full || result.exitCode !== 0 ? result.stderr.slice(-4000) : "",
|
||||
};
|
||||
}
|
||||
|
||||
function pk01CaddyApplyManagedBlockPython(): string {
|
||||
return String.raw`import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
config_path = pathlib.Path(sys.argv[1])
|
||||
block_path = pathlib.Path(sys.argv[2])
|
||||
next_path = pathlib.Path(sys.argv[3])
|
||||
text = config_path.read_text(encoding="utf-8") if config_path.exists() else ""
|
||||
block = block_path.read_text(encoding="utf-8").strip() + "\n"
|
||||
|
||||
marker_match = re.match(r"(?s)^# BEGIN unidesk managed (?P<name>[^\n]+)\n.*\n# END unidesk managed (?P=name)\n?$", block)
|
||||
if marker_match is None:
|
||||
raise SystemExit("block missing matching UniDesk managed markers")
|
||||
marker_name = marker_match.group("name")
|
||||
pattern = re.compile(r"(?ms)^# BEGIN unidesk managed (?P<name>[^\n]+)\n(?P<body>.*?)\n# END unidesk managed (?P=name)\n*")
|
||||
matched = False
|
||||
|
||||
def replace(match):
|
||||
global matched
|
||||
if match.group("name") != marker_name:
|
||||
return match.group(0)
|
||||
matched = True
|
||||
return block.rstrip() + "\n\n"
|
||||
|
||||
next_text = pattern.sub(replace, text).rstrip() + "\n"
|
||||
if not matched:
|
||||
next_text = text.rstrip() + "\n\n" + block
|
||||
next_path.write_text(next_text, encoding="utf-8")
|
||||
print("update" if matched else "append")`;
|
||||
}
|
||||
|
||||
export function pk01CaddyMergeManagedBlocksPython(): string {
|
||||
return String.raw`import pathlib
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
desired_path = pathlib.Path(sys.argv[1])
|
||||
current_path = pathlib.Path(sys.argv[2])
|
||||
merged_path = pathlib.Path(sys.argv[3])
|
||||
legacy_domains = sys.argv[4:]
|
||||
desired = desired_path.read_text(encoding="utf-8")
|
||||
current = current_path.read_text(encoding="utf-8") if current_path.exists() else ""
|
||||
pattern = re.compile(r"(?ms)^# BEGIN unidesk managed (?P<name>[^\n]+)\n(?P<body>.*?)\n# END unidesk managed (?P=name)\n*")
|
||||
|
||||
def caddy_site_block(text, domain):
|
||||
match = re.search(r"(^|\n)" + re.escape(domain) + r"\s*\{", text)
|
||||
if match is None:
|
||||
return None
|
||||
start = match.start() + (1 if match.group(1) == "\n" else 0)
|
||||
open_index = text.find("{", start)
|
||||
if open_index < 0:
|
||||
return None
|
||||
depth = 0
|
||||
for index in range(open_index, len(text)):
|
||||
char = text[index]
|
||||
if char == "{":
|
||||
depth += 1
|
||||
elif char == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return text[start:index + 1]
|
||||
return None
|
||||
|
||||
def remove_site_blocks(text, domain):
|
||||
while True:
|
||||
block = caddy_site_block(text, domain)
|
||||
if block is None:
|
||||
return re.sub(r"\n{3,}", "\n\n", text)
|
||||
text = text.replace(block, "")
|
||||
|
||||
blocks = {}
|
||||
order = []
|
||||
for source in (current, desired):
|
||||
for match in pattern.finditer(source):
|
||||
name = match.group("name")
|
||||
if name not in blocks:
|
||||
order.append(name)
|
||||
blocks[name] = match.group(0).rstrip() + "\n"
|
||||
|
||||
base = pattern.sub("", desired)
|
||||
for domain in legacy_domains:
|
||||
base = remove_site_blocks(base, domain)
|
||||
base = base.rstrip()
|
||||
managed = "\n\n".join(blocks[name].rstrip() for name in order)
|
||||
next_text = base
|
||||
if managed:
|
||||
next_text = next_text + "\n\n" + managed
|
||||
merged_path.write_text(next_text.rstrip() + "\n", encoding="utf-8")
|
||||
print(json.dumps({"currentManagedBlocks": len(pattern.findall(current)), "desiredManagedBlocks": len(pattern.findall(desired)), "mergedManagedBlocks": len(blocks)}))`;
|
||||
}
|
||||
|
||||
function managedBlockPattern(): RegExp {
|
||||
return /^# BEGIN unidesk managed ([^\n]+)\n(?:(?!^# END unidesk managed \1$).*\n)*# END unidesk managed \1\n*/gmu;
|
||||
}
|
||||
|
||||
function parseManagedBlockMarker(block: string): string | null {
|
||||
const match = /^# BEGIN unidesk managed ([^\n]+)\n[\s\S]*\n# END unidesk managed \1\s*$/u.exec(block.trim());
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
function removeCaddySiteBlocks(text: string, domain: string): string {
|
||||
let next = text;
|
||||
while (true) {
|
||||
const block = caddySiteBlock(next, domain);
|
||||
if (block === null) return next;
|
||||
next = next.replace(block, "").replace(/\n{3,}/gu, "\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
function markerNameFromMarkers(markers: Pk01CaddyManagedBlockMarkers): string {
|
||||
const begin = "# BEGIN unidesk managed ";
|
||||
const end = "# END unidesk managed ";
|
||||
if (!markers.start.startsWith(begin) || !markers.end.startsWith(end)) throw new Error("invalid UniDesk managed Caddy markers");
|
||||
const startName = markers.start.slice(begin.length);
|
||||
const endName = markers.end.slice(end.length);
|
||||
if (startName !== endName) throw new Error("managed Caddy marker start/end mismatch");
|
||||
return startName;
|
||||
}
|
||||
|
||||
function safeFilePart(value: string): string {
|
||||
return value.replace(/[^A-Za-z0-9_.-]/gu, "_");
|
||||
}
|
||||
|
||||
function spawnSummary(result: SpawnSyncReturns<string>, stdoutLimit: number, stderrLimit: number): Record<string, unknown> {
|
||||
return {
|
||||
exitCode: result.status,
|
||||
stdoutTail: String(result.stdout).slice(-stdoutLimit),
|
||||
stderrTail: String(result.stderr).slice(-stderrLimit),
|
||||
};
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { isAbsolute, join } from "node:path";
|
||||
import type { UniDeskConfig } from "./config";
|
||||
import { rootPath } from "./config";
|
||||
import { startJob } from "./jobs";
|
||||
import { applyPk01CaddyBlock } from "./platform-infra-public-service";
|
||||
import { runSshCommandCapture, type SshCaptureResult } from "./ssh";
|
||||
|
||||
const configFile = rootPath("config", "platform-infra", "langbot.yaml");
|
||||
@@ -477,7 +478,7 @@ async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Reco
|
||||
const confirmedYaml = renderManifest(langbot, target, secretMaterial.fingerprint);
|
||||
const result = await capture(config, target.route, ["script"], applyScript(confirmedYaml, langbot, target, secretMaterial, frpcSecret));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
const caddy = await applyPk01Caddy(config, target);
|
||||
const caddy = await applyPk01CaddyBlock(config, serviceName, target.publicExposure, { start: caddyManagedStart, end: caddyManagedEnd });
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false) && caddy.ok === true,
|
||||
action: "platform-infra-langbot-apply",
|
||||
@@ -1183,99 +1184,6 @@ PY
|
||||
`;
|
||||
}
|
||||
|
||||
async function applyPk01Caddy(config: UniDeskConfig, target: LangBotTarget): Promise<Record<string, unknown>> {
|
||||
const exposure = target.publicExposure;
|
||||
if (!exposure.enabled) return { ok: true, action: "not-enabled" };
|
||||
const block = `${caddyManagedStart}
|
||||
${exposure.dns.hostname} {
|
||||
reverse_proxy 127.0.0.1:${exposure.frpc.remotePort} {
|
||||
transport http {
|
||||
response_header_timeout ${exposure.pk01.responseHeaderTimeoutSeconds}s
|
||||
}
|
||||
}
|
||||
}
|
||||
${caddyManagedEnd}
|
||||
`;
|
||||
const blockB64 = Buffer.from(block, "utf8").toString("base64");
|
||||
const script = `
|
||||
set -u
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
block="$tmp/langbot.caddy"
|
||||
printf '%s' '${blockB64}' | base64 -d >"$block"
|
||||
sudo python3 - ${shQuote(exposure.pk01.caddyConfigPath)} "$block" ${shQuote(caddyManagedStart)} ${shQuote(caddyManagedEnd)} >"$tmp/update.out" 2>"$tmp/update.err" <<'PY'
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
config_path = pathlib.Path(sys.argv[1])
|
||||
block_path = pathlib.Path(sys.argv[2])
|
||||
start = sys.argv[3]
|
||||
end = sys.argv[4]
|
||||
text = config_path.read_text(encoding="utf-8") if config_path.exists() else ""
|
||||
block = block_path.read_text(encoding="utf-8").strip() + "\\n"
|
||||
if start in text and end in text:
|
||||
before = text.split(start, 1)[0].rstrip()
|
||||
tail = text.split(end, 1)[1].lstrip()
|
||||
next_text = before + "\\n\\n" + block + "\\n" + tail
|
||||
action = "update"
|
||||
else:
|
||||
next_text = text.rstrip() + "\\n\\n" + block
|
||||
action = "append"
|
||||
tmp = config_path.with_suffix(config_path.suffix + ".unidesk-langbot.tmp")
|
||||
tmp.write_text(next_text, encoding="utf-8")
|
||||
tmp.replace(config_path)
|
||||
print(action)
|
||||
PY
|
||||
update_rc=$?
|
||||
if [ "$update_rc" -eq 0 ]; then
|
||||
sudo caddy fmt --overwrite ${shQuote(exposure.pk01.caddyConfigPath)} >"$tmp/fmt.out" 2>"$tmp/fmt.err"
|
||||
fmt_rc=$?
|
||||
else
|
||||
: >"$tmp/fmt.out"; : >"$tmp/fmt.err"; fmt_rc=1
|
||||
fi
|
||||
if [ "$fmt_rc" -eq 0 ]; then
|
||||
sudo caddy validate --config ${shQuote(exposure.pk01.caddyConfigPath)} >"$tmp/validate.out" 2>"$tmp/validate.err"
|
||||
validate_rc=$?
|
||||
else
|
||||
: >"$tmp/validate.out"; : >"$tmp/validate.err"; validate_rc=1
|
||||
fi
|
||||
if [ "$validate_rc" -eq 0 ]; then
|
||||
sudo systemctl reload ${shQuote(exposure.pk01.caddyServiceName)} >"$tmp/reload.out" 2>"$tmp/reload.err" || sudo systemctl restart ${shQuote(exposure.pk01.caddyServiceName)} >>"$tmp/reload.out" 2>>"$tmp/reload.err"
|
||||
reload_rc=$?
|
||||
else
|
||||
: >"$tmp/reload.out"; : >"$tmp/reload.err"; reload_rc=1
|
||||
fi
|
||||
python3 - "$update_rc" "$fmt_rc" "$validate_rc" "$reload_rc" "$tmp/update.out" "$tmp/update.err" "$tmp/fmt.out" "$tmp/fmt.err" "$tmp/validate.out" "$tmp/validate.err" "$tmp/reload.out" "$tmp/reload.err" <<'PY'
|
||||
import json, sys
|
||||
rcs = [int(value) for value in sys.argv[1:5]]
|
||||
def text(path):
|
||||
try:
|
||||
return open(path, encoding="utf-8", errors="replace").read()[-3000:]
|
||||
except FileNotFoundError:
|
||||
return ""
|
||||
payload = {
|
||||
"ok": all(rc == 0 for rc in rcs),
|
||||
"hostname": "${exposure.dns.hostname}",
|
||||
"remotePort": ${exposure.frpc.remotePort},
|
||||
"caddyConfigPath": "${exposure.pk01.caddyConfigPath}",
|
||||
"service": "${exposure.pk01.caddyServiceName}",
|
||||
"managedBlock": {"start": "${caddyManagedStart}", "end": "${caddyManagedEnd}"},
|
||||
"steps": {
|
||||
"update": {"exitCode": rcs[0], "stdout": text(sys.argv[5]), "stderr": text(sys.argv[6])},
|
||||
"fmt": {"exitCode": rcs[1], "stdout": text(sys.argv[7]), "stderr": text(sys.argv[8])},
|
||||
"validate": {"exitCode": rcs[2], "stdout": text(sys.argv[9]), "stderr": text(sys.argv[10])},
|
||||
"reload": {"exitCode": rcs[3], "stdout": text(sys.argv[11]), "stderr": text(sys.argv[12])},
|
||||
},
|
||||
}
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
sys.exit(0 if payload["ok"] else 1)
|
||||
PY
|
||||
`;
|
||||
const result = await capture(config, exposure.pk01.route, ["script"], script);
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return parsed ?? { ok: false, capture: compactCapture(result, { full: true }) };
|
||||
}
|
||||
|
||||
function statusScript(langbot: LangBotConfig, target: LangBotTarget): string {
|
||||
return `
|
||||
set -u
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { Buffer } from "node:buffer";
|
||||
import type { UniDeskConfig } from "./config";
|
||||
import { applyPk01CaddyManagedBlock } from "./pk01-caddy";
|
||||
import { runSshCommandCapture, type SshCaptureResult } from "./ssh";
|
||||
|
||||
export interface PublicServiceExposure {
|
||||
@@ -57,96 +58,7 @@ export async function applyPk01CaddyBlock(
|
||||
exposure: PublicServiceExposure,
|
||||
markers: { start: string; end: string },
|
||||
): Promise<Record<string, unknown>> {
|
||||
if (!exposure.enabled) return { ok: true, action: "not-enabled" };
|
||||
const block = `${markers.start}
|
||||
${exposure.dns.hostname} {
|
||||
reverse_proxy 127.0.0.1:${exposure.frpc.remotePort} {
|
||||
transport http {
|
||||
response_header_timeout ${exposure.pk01.responseHeaderTimeoutSeconds}s
|
||||
}
|
||||
}
|
||||
}
|
||||
${markers.end}
|
||||
`;
|
||||
const blockB64 = Buffer.from(block, "utf8").toString("base64");
|
||||
const script = `
|
||||
set -u
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
block="$tmp/${serviceId}.caddy"
|
||||
printf '%s' '${blockB64}' | base64 -d >"$block"
|
||||
sudo python3 - ${shQuote(exposure.pk01.caddyConfigPath)} "$block" ${shQuote(markers.start)} ${shQuote(markers.end)} >"$tmp/update.out" 2>"$tmp/update.err" <<'PY'
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
config_path = pathlib.Path(sys.argv[1])
|
||||
block_path = pathlib.Path(sys.argv[2])
|
||||
start = sys.argv[3]
|
||||
end = sys.argv[4]
|
||||
text = config_path.read_text(encoding="utf-8") if config_path.exists() else ""
|
||||
block = block_path.read_text(encoding="utf-8").strip() + "\\n"
|
||||
if start in text and end in text:
|
||||
before = text.split(start, 1)[0].rstrip()
|
||||
tail = text.split(end, 1)[1].lstrip()
|
||||
next_text = before + "\\n\\n" + block + "\\n" + tail
|
||||
action = "update"
|
||||
else:
|
||||
next_text = text.rstrip() + "\\n\\n" + block
|
||||
action = "append"
|
||||
tmp = config_path.with_suffix(config_path.suffix + ".unidesk-${serviceId}.tmp")
|
||||
tmp.write_text(next_text, encoding="utf-8")
|
||||
tmp.replace(config_path)
|
||||
print(action)
|
||||
PY
|
||||
update_rc=$?
|
||||
if [ "$update_rc" -eq 0 ]; then
|
||||
sudo caddy fmt --overwrite ${shQuote(exposure.pk01.caddyConfigPath)} >"$tmp/fmt.out" 2>"$tmp/fmt.err"
|
||||
fmt_rc=$?
|
||||
else
|
||||
: >"$tmp/fmt.out"; : >"$tmp/fmt.err"; fmt_rc=1
|
||||
fi
|
||||
if [ "$fmt_rc" -eq 0 ]; then
|
||||
sudo caddy validate --config ${shQuote(exposure.pk01.caddyConfigPath)} >"$tmp/validate.out" 2>"$tmp/validate.err"
|
||||
validate_rc=$?
|
||||
else
|
||||
: >"$tmp/validate.out"; : >"$tmp/validate.err"; validate_rc=1
|
||||
fi
|
||||
if [ "$validate_rc" -eq 0 ]; then
|
||||
sudo systemctl reload ${shQuote(exposure.pk01.caddyServiceName)} >"$tmp/reload.out" 2>"$tmp/reload.err" || sudo systemctl restart ${shQuote(exposure.pk01.caddyServiceName)} >>"$tmp/reload.out" 2>>"$tmp/reload.err"
|
||||
reload_rc=$?
|
||||
else
|
||||
: >"$tmp/reload.out"; : >"$tmp/reload.err"; reload_rc=1
|
||||
fi
|
||||
python3 - "$update_rc" "$fmt_rc" "$validate_rc" "$reload_rc" "$tmp/update.out" "$tmp/update.err" "$tmp/fmt.out" "$tmp/fmt.err" "$tmp/validate.out" "$tmp/validate.err" "$tmp/reload.out" "$tmp/reload.err" <<'PY'
|
||||
import json, sys
|
||||
rcs = [int(value) for value in sys.argv[1:5]]
|
||||
def text(path):
|
||||
try:
|
||||
return open(path, encoding="utf-8", errors="replace").read()[-3000:]
|
||||
except FileNotFoundError:
|
||||
return ""
|
||||
payload = {
|
||||
"ok": all(rc == 0 for rc in rcs),
|
||||
"serviceId": "${serviceId}",
|
||||
"hostname": "${exposure.dns.hostname}",
|
||||
"remotePort": ${exposure.frpc.remotePort},
|
||||
"caddyConfigPath": "${exposure.pk01.caddyConfigPath}",
|
||||
"service": "${exposure.pk01.caddyServiceName}",
|
||||
"managedBlock": {"start": "${markers.start}", "end": "${markers.end}"},
|
||||
"steps": {
|
||||
"update": {"exitCode": rcs[0], "stdout": text(sys.argv[5]), "stderr": text(sys.argv[6])},
|
||||
"fmt": {"exitCode": rcs[1], "stdout": text(sys.argv[7]), "stderr": text(sys.argv[8])},
|
||||
"validate": {"exitCode": rcs[2], "stdout": text(sys.argv[9]), "stderr": text(sys.argv[10])},
|
||||
"reload": {"exitCode": rcs[3], "stdout": text(sys.argv[11]), "stderr": text(sys.argv[12])},
|
||||
},
|
||||
}
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
sys.exit(0 if payload["ok"] else 1)
|
||||
PY
|
||||
`;
|
||||
const result = await capture(config, exposure.pk01.route, ["script"], script);
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return parsed ?? { ok: false, capture: compactCapture(result, { full: true }) };
|
||||
return await applyPk01CaddyManagedBlock(config, serviceId, exposure, markers);
|
||||
}
|
||||
|
||||
export function prepareFrpcSecret(params: {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { join } from "node:path";
|
||||
import type { UniDeskConfig } from "./config";
|
||||
import { rootPath } from "./config";
|
||||
import type { RenderedCliResult } from "./output";
|
||||
import { applyLocalCaddyManagedSite } from "./pk01-caddy";
|
||||
import {
|
||||
codexPoolSentinelSummary,
|
||||
codexPoolSentinelRuntimeImage,
|
||||
@@ -2872,55 +2873,21 @@ async function applyMasterCaddySite(pool: CodexPoolConfig): Promise<Record<strin
|
||||
if (!caddy.enabled) return { ok: true, action: "disabled-by-yaml", valuesPrinted: false };
|
||||
const path = caddy.configPath;
|
||||
if (!existsSync(path)) return { ok: false, error: "master-caddy-config-missing", path, valuesPrinted: false };
|
||||
const before = readFileSync(path, "utf8");
|
||||
const desiredBlock = renderCaddySiteBlock(caddy.domain, caddy.upstreamBaseUrl, caddy.responseHeaderTimeoutSeconds, caddy.edgeRetry);
|
||||
const existing = caddySiteBlock(before, caddy.domain);
|
||||
const alreadyConfigured = existing === desiredBlock;
|
||||
let backupPath: string | null = null;
|
||||
let action = "kept-existing";
|
||||
if (!alreadyConfigured) {
|
||||
const stamp = new Date().toISOString().replace(/[-:]/gu, "").replace(/\..*$/u, "Z");
|
||||
backupPath = `${path}.bak-sub2api-https-${stamp}`;
|
||||
copyFileSync(path, backupPath);
|
||||
const next = existing === null
|
||||
? `${before.replace(/\s*$/u, "")}\n\n${desiredBlock}\n`
|
||||
: before.replace(existing, desiredBlock);
|
||||
writeFileSync(path, next, "utf8");
|
||||
chmodSync(path, statSync(backupPath).mode & 0o777);
|
||||
action = existing === null ? "added-site" : "updated-site";
|
||||
}
|
||||
const validate = Bun.spawnSync(["caddy", "validate", "--config", path, "--adapter", "caddyfile"]);
|
||||
let reload: ReturnType<typeof Bun.spawnSync> | null = null;
|
||||
if (validate.exitCode === 0 && !alreadyConfigured) {
|
||||
reload = Bun.spawnSync(["systemctl", "reload", caddy.serviceName]);
|
||||
}
|
||||
const active = Bun.spawnSync(["systemctl", "is-active", caddy.serviceName]);
|
||||
const ok = validate.exitCode === 0 && (reload === null || reload.exitCode === 0) && active.exitCode === 0 && String(active.stdout).trim() === "active";
|
||||
const applied = applyLocalCaddyManagedSite({
|
||||
serviceId: "sub2api-codex-pool",
|
||||
markerName: "sub2api-codex-pool",
|
||||
configPath: path,
|
||||
serviceName: caddy.serviceName,
|
||||
siteBlock: desiredBlock,
|
||||
legacySiteDomain: caddy.domain,
|
||||
});
|
||||
return {
|
||||
ok,
|
||||
action,
|
||||
path,
|
||||
backupPath,
|
||||
...applied,
|
||||
domain: caddy.domain,
|
||||
upstreamBaseUrl: caddy.upstreamBaseUrl,
|
||||
serviceName: caddy.serviceName,
|
||||
responseHeaderTimeoutSeconds: caddy.responseHeaderTimeoutSeconds,
|
||||
edgeRetry: caddy.edgeRetry,
|
||||
validate: {
|
||||
exitCode: validate.exitCode,
|
||||
stdoutTail: Buffer.from(validate.stdout).toString("utf8").slice(-1000),
|
||||
stderrTail: Buffer.from(validate.stderr).toString("utf8").slice(-2000),
|
||||
},
|
||||
reload: reload === null ? null : {
|
||||
exitCode: reload.exitCode,
|
||||
stdoutTail: Buffer.from(reload.stdout).toString("utf8").slice(-1000),
|
||||
stderrTail: Buffer.from(reload.stderr).toString("utf8").slice(-1000),
|
||||
},
|
||||
active: {
|
||||
exitCode: active.exitCode,
|
||||
stdoutTail: Buffer.from(active.stdout).toString("utf8").slice(-1000),
|
||||
stderrTail: Buffer.from(active.stderr).toString("utf8").slice(-1000),
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
@@ -2961,24 +2928,6 @@ ${matchLines.join("\n")}
|
||||
`;
|
||||
}
|
||||
|
||||
function caddySiteBlock(text: string, domain: string): string | null {
|
||||
const startMatch = new RegExp(`(^|\\n)${escapeRegExp(domain)}\\s*\\{`, "u").exec(text);
|
||||
if (startMatch === null) return null;
|
||||
const start = startMatch.index + (startMatch[1] === "\n" ? 1 : 0);
|
||||
const open = text.indexOf("{", start);
|
||||
if (open < 0) return null;
|
||||
let depth = 0;
|
||||
for (let index = open; index < text.length; index += 1) {
|
||||
const ch = text[index];
|
||||
if (ch === "{") depth += 1;
|
||||
if (ch === "}") {
|
||||
depth -= 1;
|
||||
if (depth === 0) return text.slice(start, index + 1);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function frpsAllowPortExists(toml: string, port: number): boolean {
|
||||
const sections = toml.split(/(?=\[\[allowPorts\]\])/u);
|
||||
return sections.some((section) => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { UniDeskConfig } from "./config";
|
||||
import { rootPath } from "./config";
|
||||
import { startJob } from "./jobs";
|
||||
import type { RenderedCliResult } from "./output";
|
||||
import { pk01CaddyMergeManagedBlocksPython, renderCaddyManagedBlock, renderSimpleReverseProxyCaddySiteBlock } from "./pk01-caddy";
|
||||
import { runSshCommandCapture, type SshCaptureResult } from "./ssh";
|
||||
|
||||
const defaultTargetId = "D601";
|
||||
@@ -16,6 +17,7 @@ const configPath = rootPath("config", "platform-infra", "sub2api.yaml");
|
||||
const codexPoolConfigPath = rootPath("config", "platform-infra", "sub2api-codex-pool.yaml");
|
||||
const repoRoot = rootPath();
|
||||
const secretName = "sub2api-secrets";
|
||||
const sub2apiCaddyManagedMarker = "sub2api";
|
||||
const requiredSecretKeys = ["POSTGRES_PASSWORD", "ADMIN_PASSWORD", "JWT_SECRET", "TOTP_ENCRYPTION_KEY"] as const;
|
||||
|
||||
type DatabaseMode = "bundled" | "external-pending" | "external-active";
|
||||
@@ -2448,8 +2450,15 @@ fi
|
||||
install_rc=1
|
||||
if [ "$download_rc" -eq 0 ]; then
|
||||
sudo mkdir -p "$(dirname ${shQuote(exposure.pk01.caddyConfigPath)})" ${shQuote(exposure.pk01.caddyStorageDir)} /etc/systemd/system
|
||||
sudo install -m 0644 "$caddyfile" ${shQuote(exposure.pk01.caddyConfigPath)} >"$install_out" 2>"$install_err"
|
||||
merged_caddyfile="$tmp/Caddyfile.merged"
|
||||
sudo python3 - "$caddyfile" ${shQuote(exposure.pk01.caddyConfigPath)} "$merged_caddyfile" ${shQuote(exposure.dns.hostname)} >"$install_out" 2>"$install_err" <<'PY'
|
||||
${pk01CaddyMergeManagedBlocksPython()}
|
||||
PY
|
||||
install_rc=$?
|
||||
if [ "$install_rc" -eq 0 ]; then
|
||||
sudo install -m 0644 "$merged_caddyfile" ${shQuote(exposure.pk01.caddyConfigPath)} >>"$install_out" 2>>"$install_err"
|
||||
install_rc=$?
|
||||
fi
|
||||
if [ "$install_rc" -eq 0 ]; then
|
||||
sudo install -m 0644 "$service_unit" /etc/systemd/system/${exposure.pk01.caddyServiceName}.service >>"$install_out" 2>>"$install_err"
|
||||
install_rc=$?
|
||||
@@ -2582,6 +2591,14 @@ PY
|
||||
|
||||
function renderPk01Caddyfile(exposure: Sub2ApiPublicExposureConfig): string {
|
||||
const apexHost = baseDomain(exposure.dns.hostname);
|
||||
const apiBlock = renderCaddyManagedBlock(
|
||||
sub2apiCaddyManagedMarker,
|
||||
renderSimpleReverseProxyCaddySiteBlock({
|
||||
hostname: exposure.dns.hostname,
|
||||
upstream: `127.0.0.1:${exposure.frpc.remotePort}`,
|
||||
responseHeaderTimeoutSeconds: exposure.pk01.responseHeaderTimeoutSeconds,
|
||||
}),
|
||||
);
|
||||
return `{
|
||||
email ${exposure.pk01.caddyEmail}
|
||||
storage file_system {
|
||||
@@ -2593,13 +2610,7 @@ ${apexHost}, www.${apexHost} {
|
||||
reverse_proxy 127.0.0.1:${exposure.pk01.pikanodeHttpHostPort}
|
||||
}
|
||||
|
||||
${exposure.dns.hostname} {
|
||||
reverse_proxy 127.0.0.1:${exposure.frpc.remotePort} {
|
||||
transport http {
|
||||
response_header_timeout ${exposure.pk01.responseHeaderTimeoutSeconds}s
|
||||
}
|
||||
}
|
||||
}
|
||||
${apiBlock.trim()}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user