feat: add Bind Host IP and Allowed Hosts / Reverse Proxy Domain settings

This commit is contained in:
Schwitzz
2026-10-30 14:59:09 -05:00
parent 29dbc27c41
commit 723fafdef8
5 changed files with 253 additions and 6 deletions
+91 -5
View File
@@ -46,7 +46,7 @@ function makeHttpError(status: number, payload: object): HttpError {
}
export async function buildApp() {
const app = Fastify({ logger: true });
const app = Fastify({ logger: true, trustProxy: true });
await app.register(cors, { origin: true, credentials: true });
await app.register(cookie);
await app.register(websocket);
@@ -101,6 +101,8 @@ export async function buildApp() {
interface PersistedServerSysConfig {
enabled: boolean;
host: string;
allowedHosts: string;
port: number;
webPort: number;
startOnBoot: boolean;
@@ -109,6 +111,48 @@ export async function buildApp() {
let secondaryWebServer: HttpServer | null = null;
let activeWebPort: number = 5173;
let activeBindHost: string = "127.0.0.1";
function extractHostname(entry: string): string {
let s = entry.trim().toLowerCase();
if (!s) return "";
s = s.replace(/^[a-z]+:\/\//, "");
s = s.split("/")[0] ?? "";
if (!s.startsWith("[")) {
s = s.split(":")[0] ?? "";
}
return s.trim();
}
function isRequestHostAllowed(rawHostHeader: string | undefined, allowedHostsRaw: string): boolean {
if (!rawHostHeader) return true;
const firstForwarded = rawHostHeader.split(",")[0]?.trim() ?? "";
const reqHost = extractHostname(firstForwarded);
if (!reqHost || reqHost === "localhost" || reqHost === "127.0.0.1" || reqHost === "::1" || reqHost === "[::1]") {
return true;
}
const tokens = allowedHostsRaw
.split(/[,\s]+/)
.map((t) => extractHostname(t))
.filter(Boolean);
if (tokens.length === 0) return true;
for (const token of tokens) {
if (token === "*" || token === "0.0.0.0" || token === reqHost) {
return true;
}
if (token.startsWith("*.")) {
const suffix = token.slice(1); // ".domain.com"
if (reqHost === token.slice(2) || reqHost.endsWith(suffix)) {
return true;
}
} else if (token.startsWith(".")) {
if (reqHost === token.slice(1) || reqHost.endsWith(token)) {
return true;
}
}
}
return false;
}
function loadPersistedServerSysConfig(): PersistedServerSysConfig {
try {
@@ -116,6 +160,14 @@ export async function buildApp() {
const raw = JSON.parse(readFileSync(sysConfigPath, "utf8")) as Partial<PersistedServerSysConfig>;
return {
enabled: typeof raw.enabled === "boolean" ? raw.enabled : true,
host:
typeof raw.host === "string" && raw.host.trim()
? raw.host.trim()
: "127.0.0.1",
allowedHosts:
typeof raw.allowedHosts === "string" && raw.allowedHosts.trim()
? raw.allowedHosts.trim()
: "localhost, 127.0.0.1",
port:
typeof raw.port === "number" && raw.port >= 1024 && raw.port <= 65535
? raw.port
@@ -139,6 +191,8 @@ export async function buildApp() {
}
return {
enabled: true,
host: "127.0.0.1",
allowedHosts: "localhost, 127.0.0.1",
port: config.port,
webPort: 5173,
startOnBoot: existsSync(autostartDesktopPath) || existsSync(systemdServicePath),
@@ -146,8 +200,24 @@ export async function buildApp() {
};
}
async function bindSecondaryWebPort(targetWebPort: number, enabled: boolean): Promise<number> {
app.addHook("onRequest", async (req, reply) => {
const cfg = loadPersistedServerSysConfig();
const forwardedHost = (req.headers["x-forwarded-host"] as string | undefined) || req.headers.host;
if (!isRequestHostAllowed(forwardedHost, cfg.allowedHosts)) {
return reply.code(403).send({
error: "host_not_allowed",
message: `Host "${forwardedHost ?? ""}" is not in Allowed Hosts. Add your domain or 0.0.0.0 in Settings → Server & System.`,
});
}
});
async function bindSecondaryWebPort(
targetWebPort: number,
targetHost: string,
enabled: boolean,
): Promise<number> {
activeWebPort = targetWebPort;
activeBindHost = targetHost || "127.0.0.1";
if (secondaryWebServer) {
await new Promise<void>((res) => secondaryWebServer!.close(() => res()));
secondaryWebServer = null;
@@ -163,7 +233,7 @@ export async function buildApp() {
// Port may already be served by Vite dev server (e.g. :5173); keep activeWebPort
res(targetWebPort);
});
srv.listen(targetWebPort, "0.0.0.0", () => {
srv.listen(targetWebPort, activeBindHost, () => {
secondaryWebServer = srv;
res(targetWebPort);
});
@@ -257,6 +327,9 @@ export async function buildApp() {
return {
ok: true,
enabled: persisted.enabled,
host: persisted.host,
activeHost: activeBindHost || persisted.host,
allowedHosts: persisted.allowedHosts,
port: persisted.port,
activePort: config.port,
webPort: persisted.webPort,
@@ -281,12 +354,22 @@ export async function buildApp() {
if (!Number.isFinite(nextWebPort) || nextWebPort < 1024 || nextWebPort > 65535) {
return reply.code(400).send({ error: "invalid_web_port" });
}
const nextHost =
typeof body.host === "string" && body.host.trim()
? body.host.trim()
: current.host;
const nextAllowedHosts =
typeof body.allowedHosts === "string" && body.allowedHosts.trim()
? body.allowedHosts.trim()
: current.allowedHosts;
const nextProjectDir =
typeof body.projectDirectory === "string" && body.projectDirectory.trim()
? body.projectDirectory.trim()
: current.projectDirectory;
const next: PersistedServerSysConfig = {
enabled: typeof body.enabled === "boolean" ? body.enabled : current.enabled,
host: nextHost,
allowedHosts: nextAllowedHosts,
port: nextPort,
webPort: nextWebPort,
startOnBoot:
@@ -294,10 +377,13 @@ export async function buildApp() {
projectDirectory: nextProjectDir,
};
const configured = applySystemStartupFiles(next);
const boundWebPort = await bindSecondaryWebPort(next.webPort, next.enabled);
const boundWebPort = await bindSecondaryWebPort(next.webPort, next.host, next.enabled);
return {
ok: true,
enabled: next.enabled,
host: next.host,
activeHost: activeBindHost,
allowedHosts: next.allowedHosts,
port: next.port,
activePort: config.port,
webPort: next.webPort,
@@ -412,7 +498,7 @@ export async function buildApp() {
fileURLToPath(import.meta.url) === process.argv[1]
) {
const persisted = loadPersistedServerSysConfig();
void bindSecondaryWebPort(persisted.webPort, persisted.enabled);
void bindSecondaryWebPort(persisted.webPort, persisted.host, persisted.enabled);
}
return app;