fix: use node:http for Caddy admin API calls to avoid Sec-Fetch-Mode CORS triggering
This commit is contained in:
@@ -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"]
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user