fix: use node:http for Caddy admin API calls to avoid Sec-Fetch-Mode CORS triggering

This commit is contained in:
fuomag9
2026-02-23 23:49:05 +01:00
parent 4fac5e4d50
commit 9254d8e910
3 changed files with 56 additions and 19 deletions

View File

@@ -49,4 +49,4 @@ ENV XDG_DATA_HOME=/data
# Run as non-root user (fully rootless)
USER caddy
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
CMD ["caddy", "run", "--resume", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]

View File

@@ -3,6 +3,8 @@
* Monitors Caddy for restarts/crashes and automatically reapplies configuration
*/
import http from "node:http";
import https from "node:https";
import { config } from "./config";
import { applyCaddyConfig } from "./caddy";
import { getSetting, setSetting } from "./settings";
@@ -34,24 +36,34 @@ let isMonitoring = false;
*/
async function getCaddyConfigId(): Promise<string | null> {
try {
const response = await fetch(`${config.caddyApiUrl}/config/`, {
method: "GET",
headers: { "Origin": config.caddyApiUrl },
signal: AbortSignal.timeout(5000)
const response = await new Promise<{ status: number; text: string; etag: string | null }>((resolve, reject) => {
const parsed = new URL(`${config.caddyApiUrl}/config/`);
const lib = parsed.protocol === "https:" ? https : http;
const req = lib.request(
{ hostname: parsed.hostname, port: parsed.port, path: parsed.pathname, method: "GET" },
(res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => resolve({ status: res.statusCode ?? 0, text: data, etag: res.headers.etag ?? null }));
}
);
req.setTimeout(5000, () => { req.destroy(); reject(new Error("timeout")); });
req.on("error", reject);
req.end();
});
if (!response.ok) {
if (response.status < 200 || response.status >= 300) {
return null;
}
// Use ETag or compute a simple hash from the response
const etag = response.headers.get("etag");
const etag = response.etag;
if (etag) {
return etag;
}
// Fallback: use the config object's structure
const configData = await response.json();
const configData = JSON.parse(response.text);
// Check if config is essentially empty (default state after restart)
const isEmpty = !configData.apps || Object.keys(configData.apps).length === 0;
return isEmpty ? "empty" : "configured";

View File

@@ -3,6 +3,8 @@ import { Resolver } from "node:dns/promises";
import { join } from "node:path";
import { isIP } from "node:net";
import crypto from "node:crypto";
import http from "node:http";
import https from "node:https";
import db, { nowIso } from "./db";
import { config } from "./config";
import {
@@ -1591,6 +1593,37 @@ async function buildCaddyDocument() {
};
}
/**
* Plain HTTP/HTTPS request to the Caddy admin API using node:http.
* Avoids browser-security headers (Sec-Fetch-*) that native fetch sends,
* which would trigger Caddy's CORS origin enforcement.
*/
function caddyRequest(url: string, method: string, body?: string): Promise<{ status: number; text: string }> {
return new Promise((resolve, reject) => {
const parsed = new URL(url);
const lib = parsed.protocol === "https:" ? https : http;
const req = lib.request(
{
hostname: parsed.hostname,
port: parsed.port,
path: parsed.pathname + parsed.search,
method,
headers: {
...(body ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } : {})
}
},
(res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => resolve({ status: res.statusCode ?? 0, text: data }));
}
);
req.on("error", reject);
if (body) req.write(body);
req.end();
});
}
export async function applyCaddyConfig() {
const document = await buildCaddyDocument();
const payload = JSON.stringify(document);
@@ -1598,18 +1631,10 @@ export async function applyCaddyConfig() {
setSetting("caddy_config_hash", { hash, updated_at: nowIso() });
try {
const response = await fetch(`${config.caddyApiUrl}/load`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Origin": config.caddyApiUrl
},
body: payload
});
const response = await caddyRequest(`${config.caddyApiUrl}/load`, "POST", payload);
if (!response.ok) {
const text = await response.text();
throw new Error(`Caddy config load failed: ${response.status} ${text}`);
if (response.status < 200 || response.status >= 300) {
throw new Error(`Caddy config load failed: ${response.status} ${response.text}`);
}
await syncInstances();