feat: add Bind Host IP and Allowed Hosts / Reverse Proxy Domain settings
This commit is contained in:
+91
-5
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user