feat: add Bind Host IP and Allowed Hosts / Reverse Proxy Domain settings
This commit is contained in:
@@ -370,6 +370,9 @@ export const api = {
|
|||||||
getServerConfig: async (portHint?: number): Promise<{
|
getServerConfig: async (portHint?: number): Promise<{
|
||||||
online: boolean;
|
online: boolean;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
host?: string;
|
||||||
|
activeHost?: string;
|
||||||
|
allowedHosts?: string;
|
||||||
port: number;
|
port: number;
|
||||||
activePort?: number;
|
activePort?: number;
|
||||||
webPort?: number;
|
webPort?: number;
|
||||||
@@ -388,6 +391,9 @@ export const api = {
|
|||||||
if (res.ok && ct.includes("application/json")) {
|
if (res.ok && ct.includes("application/json")) {
|
||||||
const data = (await res.json()) as {
|
const data = (await res.json()) as {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
host?: string;
|
||||||
|
activeHost?: string;
|
||||||
|
allowedHosts?: string;
|
||||||
port: number;
|
port: number;
|
||||||
activePort?: number;
|
activePort?: number;
|
||||||
webPort?: number;
|
webPort?: number;
|
||||||
@@ -398,6 +404,9 @@ export const api = {
|
|||||||
return {
|
return {
|
||||||
online: true,
|
online: true,
|
||||||
enabled: data.enabled,
|
enabled: data.enabled,
|
||||||
|
...(data.host ? { host: data.host } : {}),
|
||||||
|
...(data.activeHost ? { activeHost: data.activeHost } : {}),
|
||||||
|
...(data.allowedHosts ? { allowedHosts: data.allowedHosts } : {}),
|
||||||
port: data.port,
|
port: data.port,
|
||||||
...(data.activePort !== undefined ? { activePort: data.activePort } : {}),
|
...(data.activePort !== undefined ? { activePort: data.activePort } : {}),
|
||||||
...(data.webPort !== undefined ? { webPort: data.webPort } : {}),
|
...(data.webPort !== undefined ? { webPort: data.webPort } : {}),
|
||||||
@@ -413,6 +422,8 @@ export const api = {
|
|||||||
return {
|
return {
|
||||||
online: false,
|
online: false,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
host: "127.0.0.1",
|
||||||
|
allowedHosts: "localhost, 127.0.0.1",
|
||||||
port: portHint ?? 8081,
|
port: portHint ?? 8081,
|
||||||
webPort: 5173,
|
webPort: 5173,
|
||||||
startOnBoot: false,
|
startOnBoot: false,
|
||||||
@@ -422,6 +433,8 @@ export const api = {
|
|||||||
updateServerConfig: async (
|
updateServerConfig: async (
|
||||||
patch: {
|
patch: {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
host?: string;
|
||||||
|
allowedHosts?: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
webPort?: number;
|
webPort?: number;
|
||||||
startOnBoot?: boolean;
|
startOnBoot?: boolean;
|
||||||
@@ -431,6 +444,9 @@ export const api = {
|
|||||||
): Promise<{
|
): Promise<{
|
||||||
online: boolean;
|
online: boolean;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
host?: string;
|
||||||
|
activeHost?: string;
|
||||||
|
allowedHosts?: string;
|
||||||
port: number;
|
port: number;
|
||||||
activePort?: number;
|
activePort?: number;
|
||||||
webPort?: number;
|
webPort?: number;
|
||||||
@@ -454,6 +470,9 @@ export const api = {
|
|||||||
if (res.ok && ct.includes("application/json")) {
|
if (res.ok && ct.includes("application/json")) {
|
||||||
const data = (await res.json()) as {
|
const data = (await res.json()) as {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
host?: string;
|
||||||
|
activeHost?: string;
|
||||||
|
allowedHosts?: string;
|
||||||
port: number;
|
port: number;
|
||||||
activePort?: number;
|
activePort?: number;
|
||||||
webPort?: number;
|
webPort?: number;
|
||||||
@@ -464,6 +483,9 @@ export const api = {
|
|||||||
return {
|
return {
|
||||||
online: true,
|
online: true,
|
||||||
enabled: data.enabled,
|
enabled: data.enabled,
|
||||||
|
...(data.host ? { host: data.host } : {}),
|
||||||
|
...(data.activeHost ? { activeHost: data.activeHost } : {}),
|
||||||
|
...(data.allowedHosts ? { allowedHosts: data.allowedHosts } : {}),
|
||||||
port: data.port,
|
port: data.port,
|
||||||
...(data.activePort !== undefined ? { activePort: data.activePort } : {}),
|
...(data.activePort !== undefined ? { activePort: data.activePort } : {}),
|
||||||
...(data.webPort !== undefined ? { webPort: data.webPort } : {}),
|
...(data.webPort !== undefined ? { webPort: data.webPort } : {}),
|
||||||
@@ -479,6 +501,8 @@ export const api = {
|
|||||||
return {
|
return {
|
||||||
online: false,
|
online: false,
|
||||||
enabled: patch.enabled ?? true,
|
enabled: patch.enabled ?? true,
|
||||||
|
...(patch.host ? { host: patch.host } : {}),
|
||||||
|
...(patch.allowedHosts ? { allowedHosts: patch.allowedHosts } : {}),
|
||||||
port: patch.port ?? portHint ?? 8081,
|
port: patch.port ?? portHint ?? 8081,
|
||||||
...(patch.webPort !== undefined ? { webPort: patch.webPort } : {}),
|
...(patch.webPort !== undefined ? { webPort: patch.webPort } : {}),
|
||||||
startOnBoot: patch.startOnBoot ?? false,
|
startOnBoot: patch.startOnBoot ?? false,
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ export function Settings() {
|
|||||||
const [probing, setProbing] = useState(false);
|
const [probing, setProbing] = useState(false);
|
||||||
|
|
||||||
// Server & Project Directory state
|
// Server & Project Directory state
|
||||||
|
const [hostDraft, setHostDraft] = useState<string>(s.serverHost || "127.0.0.1");
|
||||||
|
const [allowedHostsDraft, setAllowedHostsDraft] = useState<string>(
|
||||||
|
s.allowedHosts || "localhost, 127.0.0.1",
|
||||||
|
);
|
||||||
const [webPortDraft, setWebPortDraft] = useState<string>(String(s.webPort || 5173));
|
const [webPortDraft, setWebPortDraft] = useState<string>(String(s.webPort || 5173));
|
||||||
const [portDraft, setPortDraft] = useState<string>(String(s.serverPort));
|
const [portDraft, setPortDraft] = useState<string>(String(s.serverPort));
|
||||||
const [projectDirDraft, setProjectDirDraft] = useState<string>(
|
const [projectDirDraft, setProjectDirDraft] = useState<string>(
|
||||||
@@ -74,6 +78,7 @@ export function Settings() {
|
|||||||
);
|
);
|
||||||
const [browseProjectDirOpen, setBrowseProjectDirOpen] = useState<boolean>(false);
|
const [browseProjectDirOpen, setBrowseProjectDirOpen] = useState<boolean>(false);
|
||||||
const [serverOnline, setServerOnline] = useState<boolean>(false);
|
const [serverOnline, setServerOnline] = useState<boolean>(false);
|
||||||
|
const [activeRunningHost, setActiveRunningHost] = useState<string>(s.serverHost || "127.0.0.1");
|
||||||
const [activeRunningPort, setActiveRunningPort] = useState<number | null>(null);
|
const [activeRunningPort, setActiveRunningPort] = useState<number | null>(null);
|
||||||
const [activeRunningWebPort, setActiveRunningWebPort] = useState<number | null>(null);
|
const [activeRunningWebPort, setActiveRunningWebPort] = useState<number | null>(null);
|
||||||
const [savingServerCfg, setSavingServerCfg] = useState<boolean>(false);
|
const [savingServerCfg, setSavingServerCfg] = useState<boolean>(false);
|
||||||
@@ -83,9 +88,12 @@ export function Settings() {
|
|||||||
void api.getServerConfig(s.serverPort).then((res) => {
|
void api.getServerConfig(s.serverPort).then((res) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setServerOnline(res.online);
|
setServerOnline(res.online);
|
||||||
|
setActiveRunningHost(res.activeHost ?? res.host ?? s.serverHost);
|
||||||
setActiveRunningPort(res.activePort ?? res.port);
|
setActiveRunningPort(res.activePort ?? res.port);
|
||||||
setActiveRunningWebPort(res.activeWebPort ?? res.webPort ?? s.webPort);
|
setActiveRunningWebPort(res.activeWebPort ?? res.webPort ?? s.webPort);
|
||||||
if (res.online) {
|
if (res.online) {
|
||||||
|
if (res.host) setHostDraft(res.host);
|
||||||
|
if (res.allowedHosts) setAllowedHostsDraft(res.allowedHosts);
|
||||||
setPortDraft(String(res.port));
|
setPortDraft(String(res.port));
|
||||||
if (res.webPort) {
|
if (res.webPort) {
|
||||||
setWebPortDraft(String(res.webPort));
|
setWebPortDraft(String(res.webPort));
|
||||||
@@ -95,6 +103,8 @@ export function Settings() {
|
|||||||
}
|
}
|
||||||
set({
|
set({
|
||||||
serverEnabled: res.enabled,
|
serverEnabled: res.enabled,
|
||||||
|
...(res.host ? { serverHost: res.host } : {}),
|
||||||
|
...(res.allowedHosts ? { allowedHosts: res.allowedHosts } : {}),
|
||||||
serverPort: res.port,
|
serverPort: res.port,
|
||||||
...(res.webPort ? { webPort: res.webPort } : {}),
|
...(res.webPort ? { webPort: res.webPort } : {}),
|
||||||
serverStartOnBoot: res.startOnBoot,
|
serverStartOnBoot: res.startOnBoot,
|
||||||
@@ -146,6 +156,26 @@ export function Settings() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleApplyHostAndAllowed = async (overrideHost?: string, overrideAllowed?: string) => {
|
||||||
|
const nextHost = (overrideHost ?? hostDraft).trim() || "127.0.0.1";
|
||||||
|
const nextAllowed = (overrideAllowed ?? allowedHostsDraft).trim() || "localhost, 127.0.0.1";
|
||||||
|
setHostDraft(nextHost);
|
||||||
|
setAllowedHostsDraft(nextAllowed);
|
||||||
|
set({ serverHost: nextHost, allowedHosts: nextAllowed });
|
||||||
|
setSavingServerCfg(true);
|
||||||
|
try {
|
||||||
|
const res = await api.updateServerConfig(
|
||||||
|
{ host: nextHost, allowedHosts: nextAllowed },
|
||||||
|
s.serverPort,
|
||||||
|
);
|
||||||
|
setServerOnline(res.online);
|
||||||
|
setActiveRunningHost(res.activeHost ?? nextHost);
|
||||||
|
toast.success(`Host (${nextHost}) & Allowed Hosts updated`);
|
||||||
|
} finally {
|
||||||
|
setSavingServerCfg(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleApplyWebPort = async () => {
|
const handleApplyWebPort = async () => {
|
||||||
const parsed = parseInt(webPortDraft.trim(), 10);
|
const parsed = parseInt(webPortDraft.trim(), 10);
|
||||||
if (Number.isNaN(parsed) || parsed < 1024 || parsed > 65535) {
|
if (Number.isNaN(parsed) || parsed < 1024 || parsed > 65535) {
|
||||||
@@ -1027,12 +1057,113 @@ export function Settings() {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="ps-settings-row" style={{ flexDirection: "column", alignItems: "stretch", gap: 10 }}>
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", flexWrap: "wrap", gap: 8 }}>
|
||||||
|
<div>
|
||||||
|
<div className="ps-settings-label" style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
|
<span>Bind Host IP</span>
|
||||||
|
<Badge variant={!s.serverEnabled ? "neutral" : "accent"}>
|
||||||
|
{`${activeRunningHost || s.serverHost}:${activeRunningWebPort ?? s.webPort}`}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="ps-settings-desc">
|
||||||
|
IP address the server listens on. Default is <code>127.0.0.1</code> (localhost only). Set to <code>0.0.0.0</code> to expose on all network interfaces for a reverse proxy or LAN.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" }}>
|
||||||
|
<Button
|
||||||
|
variant={hostDraft === "127.0.0.1" ? "primary" : "secondary"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => void handleApplyHostAndAllowed("127.0.0.1", allowedHostsDraft)}
|
||||||
|
>
|
||||||
|
127.0.0.1 (Localhost)
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={hostDraft === "0.0.0.0" ? "primary" : "secondary"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
void handleApplyHostAndAllowed(
|
||||||
|
"0.0.0.0",
|
||||||
|
allowedHostsDraft.includes("0.0.0.0")
|
||||||
|
? allowedHostsDraft
|
||||||
|
: `${allowedHostsDraft}, 0.0.0.0`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
0.0.0.0 (All Interfaces)
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="ps-input"
|
||||||
|
style={{ width: 200, fontFamily: "var(--ps-font-mono)", fontSize: 12.5 }}
|
||||||
|
value={hostDraft}
|
||||||
|
onChange={(e) => setHostDraft(e.target.value)}
|
||||||
|
placeholder="127.0.0.1 or 0.0.0.0"
|
||||||
|
aria-label="Bind host IP"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
disabled={savingServerCfg}
|
||||||
|
onClick={() => void handleApplyHostAndAllowed()}
|
||||||
|
>
|
||||||
|
Apply Host IP
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ps-settings-row" style={{ flexDirection: "column", alignItems: "stretch", gap: 10 }}>
|
||||||
|
<div>
|
||||||
|
<div className="ps-settings-label">Allowed Hosts / Reverse Proxy Domain URLs</div>
|
||||||
|
<div className="ps-settings-desc">
|
||||||
|
Comma-separated list of allowable hostnames, IPs, or domain URLs (e.g. <code>localhost, 127.0.0.1, https://pubscript.mydomain.com, *.mydomain.com, 0.0.0.0</code>).
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="ps-input"
|
||||||
|
style={{ flex: 1, minWidth: 260, fontFamily: "var(--ps-font-mono)", fontSize: 12.5 }}
|
||||||
|
value={allowedHostsDraft}
|
||||||
|
onChange={(e) => setAllowedHostsDraft(e.target.value)}
|
||||||
|
placeholder="localhost, 127.0.0.1, https://write.example.com"
|
||||||
|
aria-label="Allowed hosts and domains"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
disabled={savingServerCfg}
|
||||||
|
onClick={() =>
|
||||||
|
void handleApplyHostAndAllowed(
|
||||||
|
hostDraft,
|
||||||
|
allowedHostsDraft.includes("*")
|
||||||
|
? allowedHostsDraft
|
||||||
|
: `${allowedHostsDraft}, 0.0.0.0, *`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
+ Allow All (0.0.0.0 / *)
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
disabled={savingServerCfg}
|
||||||
|
onClick={() => void handleApplyHostAndAllowed()}
|
||||||
|
>
|
||||||
|
Save Allowed Hosts
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="ps-settings-row">
|
<div className="ps-settings-row">
|
||||||
<div>
|
<div>
|
||||||
<div className="ps-settings-label" style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
<div className="ps-settings-label" style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
<span>Web App Port</span>
|
<span>Web App Port</span>
|
||||||
<Badge variant={!s.serverEnabled ? "neutral" : "accent"}>
|
<Badge variant={!s.serverEnabled ? "neutral" : "accent"}>
|
||||||
{`http://localhost:${activeRunningWebPort ?? s.webPort}`}
|
{`http://${activeRunningHost || s.serverHost}:${activeRunningWebPort ?? s.webPort}`}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="ps-settings-desc">
|
<div className="ps-settings-desc">
|
||||||
|
|||||||
@@ -274,6 +274,8 @@ export interface AppSettings {
|
|||||||
activeAgentId: string;
|
activeAgentId: string;
|
||||||
// Local / Self-Hosted Server & Startup Settings
|
// Local / Self-Hosted Server & Startup Settings
|
||||||
serverEnabled: boolean;
|
serverEnabled: boolean;
|
||||||
|
serverHost: string;
|
||||||
|
allowedHosts: string;
|
||||||
webPort: number;
|
webPort: number;
|
||||||
serverPort: number;
|
serverPort: number;
|
||||||
serverStartOnBoot: boolean;
|
serverStartOnBoot: boolean;
|
||||||
@@ -310,6 +312,8 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
|||||||
activeAgentId: DEFAULT_AI_AGENTS[0]!.id,
|
activeAgentId: DEFAULT_AI_AGENTS[0]!.id,
|
||||||
// Local / Self-Hosted Server & Startup Settings
|
// Local / Self-Hosted Server & Startup Settings
|
||||||
serverEnabled: true,
|
serverEnabled: true,
|
||||||
|
serverHost: "127.0.0.1",
|
||||||
|
allowedHosts: "localhost, 127.0.0.1",
|
||||||
webPort: 5173,
|
webPort: 5173,
|
||||||
serverPort: 8081,
|
serverPort: 8081,
|
||||||
serverStartOnBoot: false,
|
serverStartOnBoot: false,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export default defineConfig({
|
|||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
port: 5173,
|
port: 5173,
|
||||||
strictPort: false,
|
strictPort: false,
|
||||||
|
allowedHosts: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": {
|
"/api": {
|
||||||
target: process.env.VITE_API_ORIGIN ?? "http://127.0.0.1:8081",
|
target: process.env.VITE_API_ORIGIN ?? "http://127.0.0.1:8081",
|
||||||
@@ -30,5 +31,6 @@ export default defineConfig({
|
|||||||
preview: {
|
preview: {
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
port: 4173,
|
port: 4173,
|
||||||
|
allowedHosts: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+91
-5
@@ -46,7 +46,7 @@ function makeHttpError(status: number, payload: object): HttpError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function buildApp() {
|
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(cors, { origin: true, credentials: true });
|
||||||
await app.register(cookie);
|
await app.register(cookie);
|
||||||
await app.register(websocket);
|
await app.register(websocket);
|
||||||
@@ -101,6 +101,8 @@ export async function buildApp() {
|
|||||||
|
|
||||||
interface PersistedServerSysConfig {
|
interface PersistedServerSysConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
host: string;
|
||||||
|
allowedHosts: string;
|
||||||
port: number;
|
port: number;
|
||||||
webPort: number;
|
webPort: number;
|
||||||
startOnBoot: boolean;
|
startOnBoot: boolean;
|
||||||
@@ -109,6 +111,48 @@ export async function buildApp() {
|
|||||||
|
|
||||||
let secondaryWebServer: HttpServer | null = null;
|
let secondaryWebServer: HttpServer | null = null;
|
||||||
let activeWebPort: number = 5173;
|
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 {
|
function loadPersistedServerSysConfig(): PersistedServerSysConfig {
|
||||||
try {
|
try {
|
||||||
@@ -116,6 +160,14 @@ export async function buildApp() {
|
|||||||
const raw = JSON.parse(readFileSync(sysConfigPath, "utf8")) as Partial<PersistedServerSysConfig>;
|
const raw = JSON.parse(readFileSync(sysConfigPath, "utf8")) as Partial<PersistedServerSysConfig>;
|
||||||
return {
|
return {
|
||||||
enabled: typeof raw.enabled === "boolean" ? raw.enabled : true,
|
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:
|
port:
|
||||||
typeof raw.port === "number" && raw.port >= 1024 && raw.port <= 65535
|
typeof raw.port === "number" && raw.port >= 1024 && raw.port <= 65535
|
||||||
? raw.port
|
? raw.port
|
||||||
@@ -139,6 +191,8 @@ export async function buildApp() {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
host: "127.0.0.1",
|
||||||
|
allowedHosts: "localhost, 127.0.0.1",
|
||||||
port: config.port,
|
port: config.port,
|
||||||
webPort: 5173,
|
webPort: 5173,
|
||||||
startOnBoot: existsSync(autostartDesktopPath) || existsSync(systemdServicePath),
|
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;
|
activeWebPort = targetWebPort;
|
||||||
|
activeBindHost = targetHost || "127.0.0.1";
|
||||||
if (secondaryWebServer) {
|
if (secondaryWebServer) {
|
||||||
await new Promise<void>((res) => secondaryWebServer!.close(() => res()));
|
await new Promise<void>((res) => secondaryWebServer!.close(() => res()));
|
||||||
secondaryWebServer = null;
|
secondaryWebServer = null;
|
||||||
@@ -163,7 +233,7 @@ export async function buildApp() {
|
|||||||
// Port may already be served by Vite dev server (e.g. :5173); keep activeWebPort
|
// Port may already be served by Vite dev server (e.g. :5173); keep activeWebPort
|
||||||
res(targetWebPort);
|
res(targetWebPort);
|
||||||
});
|
});
|
||||||
srv.listen(targetWebPort, "0.0.0.0", () => {
|
srv.listen(targetWebPort, activeBindHost, () => {
|
||||||
secondaryWebServer = srv;
|
secondaryWebServer = srv;
|
||||||
res(targetWebPort);
|
res(targetWebPort);
|
||||||
});
|
});
|
||||||
@@ -257,6 +327,9 @@ export async function buildApp() {
|
|||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
enabled: persisted.enabled,
|
enabled: persisted.enabled,
|
||||||
|
host: persisted.host,
|
||||||
|
activeHost: activeBindHost || persisted.host,
|
||||||
|
allowedHosts: persisted.allowedHosts,
|
||||||
port: persisted.port,
|
port: persisted.port,
|
||||||
activePort: config.port,
|
activePort: config.port,
|
||||||
webPort: persisted.webPort,
|
webPort: persisted.webPort,
|
||||||
@@ -281,12 +354,22 @@ export async function buildApp() {
|
|||||||
if (!Number.isFinite(nextWebPort) || nextWebPort < 1024 || nextWebPort > 65535) {
|
if (!Number.isFinite(nextWebPort) || nextWebPort < 1024 || nextWebPort > 65535) {
|
||||||
return reply.code(400).send({ error: "invalid_web_port" });
|
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 =
|
const nextProjectDir =
|
||||||
typeof body.projectDirectory === "string" && body.projectDirectory.trim()
|
typeof body.projectDirectory === "string" && body.projectDirectory.trim()
|
||||||
? body.projectDirectory.trim()
|
? body.projectDirectory.trim()
|
||||||
: current.projectDirectory;
|
: current.projectDirectory;
|
||||||
const next: PersistedServerSysConfig = {
|
const next: PersistedServerSysConfig = {
|
||||||
enabled: typeof body.enabled === "boolean" ? body.enabled : current.enabled,
|
enabled: typeof body.enabled === "boolean" ? body.enabled : current.enabled,
|
||||||
|
host: nextHost,
|
||||||
|
allowedHosts: nextAllowedHosts,
|
||||||
port: nextPort,
|
port: nextPort,
|
||||||
webPort: nextWebPort,
|
webPort: nextWebPort,
|
||||||
startOnBoot:
|
startOnBoot:
|
||||||
@@ -294,10 +377,13 @@ export async function buildApp() {
|
|||||||
projectDirectory: nextProjectDir,
|
projectDirectory: nextProjectDir,
|
||||||
};
|
};
|
||||||
const configured = applySystemStartupFiles(next);
|
const configured = applySystemStartupFiles(next);
|
||||||
const boundWebPort = await bindSecondaryWebPort(next.webPort, next.enabled);
|
const boundWebPort = await bindSecondaryWebPort(next.webPort, next.host, next.enabled);
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
enabled: next.enabled,
|
enabled: next.enabled,
|
||||||
|
host: next.host,
|
||||||
|
activeHost: activeBindHost,
|
||||||
|
allowedHosts: next.allowedHosts,
|
||||||
port: next.port,
|
port: next.port,
|
||||||
activePort: config.port,
|
activePort: config.port,
|
||||||
webPort: next.webPort,
|
webPort: next.webPort,
|
||||||
@@ -412,7 +498,7 @@ export async function buildApp() {
|
|||||||
fileURLToPath(import.meta.url) === process.argv[1]
|
fileURLToPath(import.meta.url) === process.argv[1]
|
||||||
) {
|
) {
|
||||||
const persisted = loadPersistedServerSysConfig();
|
const persisted = loadPersistedServerSysConfig();
|
||||||
void bindSecondaryWebPort(persisted.webPort, persisted.enabled);
|
void bindSecondaryWebPort(persisted.webPort, persisted.host, persisted.enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
|
|||||||
Reference in New Issue
Block a user