custom-server.ts
Rama main · réplica de lectura del repositorio original.
/**
* DBackup Custom Server - HTTPS by default, HTTP optional.
*
* This file replaces the Next.js standalone server.js as the Docker entry point.
* It creates an HTTPS server (with auto-generated self-signed cert if needed)
* and delegates request handling to the Next.js application.
*
* Environment variables:
* DISABLE_HTTPS - "true" to use plain HTTP (default: false - HTTPS)
* PORT - Listen port (default: 3000)
* HOSTNAME - Bind address (default: "0.0.0.0")
* CERTS_DIR - Directory for tls.crt/tls.key (default: "/data/certs")
* TRUSTED_PROXIES - The reverse proxies whose X-Forwarded-For counts, see server/client-address.ts
*/
import { createServer as createHttpsServer } from "node:https";
import { createServer as createHttpServer, type Server } from "node:http";
import { execSync } from "node:child_process";
import type { BlockList } from "node:net";
import path from "node:path";
import fs from "node:fs";
import type { IncomingMessage, ServerResponse } from "node:http";
import next from "next";
import { CLIENT_ADDRESS_HEADER, clientAddressOf, trustedProxiesFrom } from "./server/client-address";
// ── Configuration ──────────────────────────────────────────────
(process.env as Record<string, string | undefined>).NODE_ENV = "production";
process.chdir(__dirname);
const port = parseInt(process.env.PORT || "3000", 10);
const hostname = process.env.HOSTNAME || "0.0.0.0";
const disableHttps = process.env.DISABLE_HTTPS === "true";
const certsDir = process.env.CERTS_DIR || "/data/certs";
const certPath = path.join(certsDir, "tls.crt");
const keyPath = path.join(certsDir, "tls.key");
// Checked before anything starts, so an entry with a typo stops the server instead of trusting the wrong proxies.
let trustedProxies: BlockList;
try {
trustedProxies = trustedProxiesFrom(process.env.TRUSTED_PROXIES);
console.log(`[Server] X-Forwarded-For counts from: ${process.env.TRUSTED_PROXIES?.trim() || "private (the default)"}`);
} catch (err: unknown) {
console.error(`[Server] ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
}
// ── Next.js Setup ────────────────────────────────────────────
// Set standalone config env BEFORE importing next (matches generated server.js)
const nextConfig = JSON.parse(
fs.readFileSync(path.join(__dirname, ".next/required-server-files.json"), "utf8")
).config;
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig);
const app = next({ dev: false, hostname, port, dir: __dirname, conf: nextConfig });
const handler = app.getRequestHandler();
// ── TLS Certificate Management ────────────────────────────────
function isSelfSigned(): boolean {
try {
const info = execSync(`openssl x509 -in "${certPath}" -noout -issuer -subject`, {
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"],
timeout: 5000,
});
const issuer = info.match(/issuer\s*=\s*(.*)/i)?.[1]?.trim() || "";
const subject = info.match(/subject\s*=\s*(.*)/i)?.[1]?.trim() || "";
return issuer === subject;
} catch {
return false;
}
}
function isExpired(): boolean {
try {
execSync(`openssl x509 -in "${certPath}" -noout -checkend 0`, {
stdio: "pipe",
timeout: 5000,
});
return false; // checkend 0 exits 0 if NOT expired
} catch {
return true; // exits 1 if expired
}
}
/**
* Extracts additional SAN entries from BETTER_AUTH_URL.
* Returns a comma-prefixed string like ",DNS:myhost.example.com" or ",IP:192.168.1.1",
* or an empty string if no extra SAN is needed.
*/
function getExtraSansFromAuthUrl(): string {
const authUrl = process.env.BETTER_AUTH_URL || "";
if (!authUrl) return "";
try {
const hostname = new URL(authUrl).hostname;
if (!hostname || hostname === "localhost" || hostname === "127.0.0.1") return "";
const isIp = /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname);
return isIp ? `,IP:${hostname}` : `,DNS:${hostname}`;
} catch {
return "";
}
}
function generateSelfSignedCert(): void {
const extraSans = getExtraSansFromAuthUrl();
execSync(
`openssl req -x509 -newkey rsa:2048 -keyout "${keyPath}" -out "${certPath}" ` +
`-days 365 -nodes -subj "/CN=DBackup/O=DBackup Self-Signed" ` +
`-addext "subjectAltName=DNS:localhost,IP:127.0.0.1${extraSans}"`,
{ stdio: "pipe", timeout: 30000 }
);
execSync(`chmod 600 "${keyPath}"`, { stdio: "pipe" });
execSync(`chmod 644 "${certPath}"`, { stdio: "pipe" });
}
/**
* Checks whether the existing self-signed cert covers the hostname from BETTER_AUTH_URL.
* Returns true if the cert needs to be regenerated.
*/
function selfSignedCertMissesHostname(): boolean {
const authUrl = process.env.BETTER_AUTH_URL || "";
if (!authUrl) return false;
let hostname: string;
try {
hostname = new URL(authUrl).hostname;
} catch {
return false;
}
if (!hostname || hostname === "localhost" || hostname === "127.0.0.1") return false;
try {
const sans = execSync(
`openssl x509 -in "${certPath}" -noout -ext subjectAltName`,
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }
);
return !sans.includes(hostname);
} catch {
return false;
}
}
function ensureCertificate(): boolean {
if (!fs.existsSync(certsDir)) {
fs.mkdirSync(certsDir, { recursive: true, mode: 0o700 });
}
if (fs.existsSync(certPath) && fs.existsSync(keyPath)) {
// Check if existing cert is expired
if (isExpired()) {
if (isSelfSigned()) {
console.log("[TLS] Self-signed certificate expired - regenerating...");
try {
generateSelfSignedCert();
console.log("[TLS] Self-signed certificate renewed successfully");
return true;
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
console.error("[TLS] Failed to renew certificate:", message);
return false;
}
} else {
console.warn(
"[TLS] WARNING: Custom certificate has expired! " +
"Upload a new certificate via Settings - Certificate or replace files in the configured CERTS_DIR"
);
}
} else if (isSelfSigned() && selfSignedCertMissesHostname()) {
console.log(
"[TLS] Self-signed certificate does not cover the configured hostname - regenerating..."
);
try {
generateSelfSignedCert();
console.log("[TLS] Self-signed certificate regenerated with updated hostname");
return true;
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
console.error("[TLS] Failed to regenerate certificate:", message);
return false;
}
} else {
console.log("[TLS] Using existing certificate from configured CERTS_DIR");
}
return true;
}
console.log("[TLS] No certificate found. Generating self-signed certificate...");
try {
generateSelfSignedCert();
console.log("[TLS] Self-signed certificate generated successfully");
return true;
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
console.error("[TLS] Failed to generate certificate:", message);
console.error(
"[TLS] Falling back to HTTP. Install openssl or provide custom certificates."
);
return false;
}
}
// ── Server Start ──────────────────────────────────────────────
app.prepare().then(() => {
let server: Server;
let protocol: string;
const requestHandler = async (req: IncomingMessage, res: ServerResponse) => {
try {
// The app reads the address of the client from here only. A value the client sent is replaced.
req.headers[CLIENT_ADDRESS_HEADER] = clientAddressOf(
req.socket.remoteAddress,
req.headers["x-forwarded-for"],
req.headers["x-real-ip"],
trustedProxies
);
await handler(req, res);
} catch (err) {
console.error("[Server] Request error:", err);
res.statusCode = 500;
res.end("Internal Server Error");
}
};
if (disableHttps) {
protocol = "http";
server = createHttpServer(requestHandler);
console.log("[Server] HTTPS disabled via DISABLE_HTTPS=true - using HTTP");
} else {
const certReady = ensureCertificate();
if (certReady && fs.existsSync(certPath) && fs.existsSync(keyPath)) {
protocol = "https";
const tlsOptions = {
cert: fs.readFileSync(certPath),
key: fs.readFileSync(keyPath),
};
server = createHttpsServer(tlsOptions, requestHandler);
console.log("[Server] HTTPS enabled with certificate from configured CERTS_DIR");
} else {
protocol = "http";
server = createHttpServer(requestHandler);
console.warn(
"[Server] WARNING: Falling back to HTTP - certificate generation failed"
);
}
}
server.listen(port, hostname, () => {
console.log(
`[Server] DBackup ready on ${protocol}://${hostname}:${port}`
);
});
// Graceful shutdown
const shutdown = (signal: string) => {
console.log(`[Server] ${signal} received - shutting down...`);
server.close(() => {
console.log("[Server] Closed");
process.exit(0);
});
setTimeout(() => process.exit(1), 10000);
};
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));
});