force cloudflare

This commit is contained in:
fuomag9
2025-11-03 18:48:37 +01:00
parent a6cb399622
commit 89950226cd
3 changed files with 212 additions and 31 deletions
+2 -1
View File
@@ -161,7 +161,7 @@ docker compose up -d
## UI Features
- **Proxy Hosts:** HTTP(S) reverse proxies with HSTS, access lists, optional custom certificates, and WebSocket support.
- **Proxy Hosts:** HTTP(S) reverse proxies with HSTS, access lists, optional custom certificates, and WebSocket support. Hosts stay offline until a certificate (imported or managed with Cloudflare automation) is linked.
- **Redirects:** 301/302 responses with optional path/query preservation.
- **Dead Hosts:** Branded responses for offline services.
- **Access Lists:** Bcrypt-backed basic auth credentials, assignable to proxy hosts.
@@ -184,6 +184,7 @@ docker compose up -d
3. **Use HTTPS in production**: Configure BASE_URL with `https://` protocol
4. **Restrict network access**: Ensure port 3000 is only accessible via reverse proxy
5. **Keep updated**: Regularly update dependencies and Docker images
6. **Provide Cloudflare credentials before using managed certificates**: The built-in automation only issues TLS certificates when valid Cloudflare API credentials are configured; otherwise, services (including `PRIMARY_DOMAIN`) remain on HTTP or require imported certificates.
## License
+210 -29
View File
@@ -3,7 +3,7 @@ import { join } from "node:path";
import crypto from "node:crypto";
import prisma, { nowIso } from "./db";
import { config } from "./config";
import { getCloudflareSettings, setSetting } from "./settings";
import { getCloudflareSettings, getGeneralSettings, setSetting } from "./settings";
const CERTS_DIR = process.env.CERTS_DIRECTORY || join(process.cwd(), "data", "certs");
mkdirSync(CERTS_DIR, { recursive: true });
@@ -106,6 +106,11 @@ type CertificateRow = {
type CaddyHttpRoute = Record<string, unknown>;
type CertificateUsage = {
certificate: CertificateRow;
domains: Set<string>;
};
function isPlainObject(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
@@ -173,10 +178,45 @@ function writeCertificateFiles(cert: CertificateRow) {
return { certificate_file: certPath, key_file: keyPath };
}
function collectCertificateUsage(rows: ProxyHostRow[], certificates: Map<number, CertificateRow>) {
const usage = new Map<number, CertificateUsage>();
for (const row of rows) {
if (!row.enabled || !row.certificate_id) {
continue;
}
const cert = certificates.get(row.certificate_id);
if (!cert) {
continue;
}
const domains = parseJson<string[]>(row.domains, []).map((domain) => domain?.trim().toLowerCase());
const filteredDomains = domains.filter((domain): domain is string => Boolean(domain));
if (filteredDomains.length === 0) {
continue;
}
if (!usage.has(cert.id)) {
usage.set(cert.id, {
certificate: cert,
domains: new Set()
});
}
const entry = usage.get(cert.id)!;
for (const domain of filteredDomains) {
entry.domains.add(domain);
}
}
return usage;
}
function buildProxyRoutes(
rows: ProxyHostRow[],
certificates: Map<number, CertificateRow>,
accessAccounts: Map<number, AccessListEntryRow[]>
accessAccounts: Map<number, AccessListEntryRow[]>,
tlsReadyCertificates: Set<number>
): CaddyHttpRoute[] {
const routes: CaddyHttpRoute[] = [];
@@ -185,6 +225,10 @@ function buildProxyRoutes(
continue;
}
if (!row.certificate_id || !tlsReadyCertificates.has(row.certificate_id)) {
continue;
}
const domains = parseJson<string[]>(row.domains, []);
if (domains.length === 0) {
continue;
@@ -211,6 +255,27 @@ function buildProxyRoutes(
});
}
if (row.ssl_forced) {
hostRoutes.push({
match: [
{
host: domains,
expression: '{http.request.scheme} == "http"'
}
],
handle: [
{
handler: "static_response",
status_code: 308,
headers: {
Location: ["https://{http.request.host}{http.request.uri}"]
}
}
],
terminal: true
});
}
if (row.access_list_id) {
const accounts = accessAccounts.get(row.access_list_id) ?? [];
if (accounts.length > 0) {
@@ -325,23 +390,6 @@ function buildProxyRoutes(
terminal: true
};
if (row.certificate_id) {
const cert = certificates.get(row.certificate_id);
if (cert) {
const files = writeCertificateFiles(cert);
if (files) {
(route as Record<string, unknown>).tls = {
certificates: [
{
certificate_file: files.certificate_file,
key_file: files.key_file
}
]
};
}
}
}
hostRoutes.push(route);
routes.push(...hostRoutes);
}
@@ -392,10 +440,129 @@ function buildDeadRoutes(rows: DeadHostRow[]): CaddyHttpRoute[] {
}));
}
function buildTlsAutomation(certificates: Map<number, CertificateRow>) {
// TODO: This function needs to be migrated to async to use getCloudflareSettings()
// For now, Cloudflare DNS challenges are disabled until migration is complete
return undefined;
function buildTlsConnectionPolicies(
usage: Map<number, CertificateUsage>,
managedCertificatesWithAutomation: Set<number>
) {
const policies: Record<string, unknown>[] = [];
const readyCertificates = new Set<number>();
for (const [id, entry] of usage.entries()) {
const domains = Array.from(entry.domains);
if (domains.length === 0) {
continue;
}
if (entry.certificate.type === "imported") {
const files = writeCertificateFiles(entry.certificate);
if (!files) {
continue;
}
policies.push({
match: {
sni: domains
},
certificates: [files]
});
readyCertificates.add(id);
continue;
}
if (entry.certificate.type === "managed") {
if (!managedCertificatesWithAutomation.has(id)) {
continue;
}
policies.push({
match: {
sni: domains
}
});
readyCertificates.add(id);
}
}
return {
policies,
readyCertificates
};
}
async function buildTlsAutomation(
usage: Map<number, CertificateUsage>,
options: { acmeEmail?: string }
) {
const managedEntries = Array.from(usage.values()).filter(
(entry) => entry.certificate.type === "managed" && Boolean(entry.certificate.auto_renew)
);
if (managedEntries.length === 0) {
return {
managedCertificateIds: new Set<number>()
};
}
const cloudflare = await getCloudflareSettings();
if (!cloudflare || !cloudflare.apiToken) {
return {
managedCertificateIds: new Set<number>()
};
}
const providerBase: Record<string, string> = {
name: "cloudflare",
api_token: cloudflare.apiToken
};
if (cloudflare.zoneId) {
providerBase.zone_id = cloudflare.zoneId;
}
if (cloudflare.accountId) {
providerBase.account_id = cloudflare.accountId;
}
const managedCertificateIds = new Set<number>();
const policies: Record<string, unknown>[] = [];
for (const entry of managedEntries) {
const subjects = Array.from(entry.domains);
if (subjects.length === 0) {
continue;
}
managedCertificateIds.add(entry.certificate.id);
const providerConfig = { ...providerBase };
const issuer: Record<string, unknown> = {
module: "acme",
challenges: {
dns: {
provider: providerConfig
}
}
};
if (options.acmeEmail) {
issuer.email = options.acmeEmail;
}
policies.push({
subjects,
issuers: [issuer]
});
}
if (policies.length === 0) {
return {
managedCertificateIds
};
}
return {
tlsApp: {
automation: {
policies
}
},
managedCertificateIds
};
}
async function buildCaddyDocument() {
@@ -523,13 +690,23 @@ async function buildCaddyDocument() {
return map;
}, new Map());
const certificateUsage = collectCertificateUsage(proxyHostRows, certificateMap);
const generalSettings = await getGeneralSettings();
const { tlsApp, managedCertificateIds } = await buildTlsAutomation(certificateUsage, {
acmeEmail: generalSettings?.acmeEmail
});
const { policies: tlsConnectionPolicies, readyCertificates } = buildTlsConnectionPolicies(
certificateUsage,
managedCertificateIds
);
const httpRoutes: CaddyHttpRoute[] = [
...buildProxyRoutes(proxyHostRows, certificateMap, accessMap),
...buildProxyRoutes(proxyHostRows, accessMap, readyCertificates),
...buildRedirectRoutes(redirectHostRows),
...buildDeadRoutes(deadHostRows)
];
const tlsSection = buildTlsAutomation(certificateMap);
const hasTls = tlsConnectionPolicies.length > 0;
const httpApp =
httpRoutes.length > 0
@@ -537,8 +714,12 @@ async function buildCaddyDocument() {
http: {
servers: {
cpm: {
listen: [":80", ":443"],
routes: httpRoutes
listen: hasTls ? [":80", ":443"] : [":80"],
routes: httpRoutes,
automatic_https: {
disable: true
},
...(hasTls ? { tls_connection_policies: tlsConnectionPolicies } : {})
}
}
}
@@ -548,7 +729,7 @@ async function buildCaddyDocument() {
return {
apps: {
...httpApp,
...(tlsSection ? { tls: tlsSection } : {})
...(tlsApp ? { tls: tlsApp } : {})
}
};
}
File diff suppressed because one or more lines are too long