new notification system
This commit is contained in:
@@ -5,7 +5,7 @@ export const CORS_HEADERS = {
|
||||
};
|
||||
|
||||
export const SECURITY_HEADERS = {
|
||||
"Content-Security-Policy": "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://unpkg.com; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://fonts.googleapis.com; img-src 'self' data: blob: https://cdn.jsdelivr.net https://a.tile.openstreetmap.org https://b.tile.openstreetmap.org https://c.tile.openstreetmap.org https://tile.openstreetmap.org https://a.basemaps.cartocdn.com https://b.basemaps.cartocdn.com https://c.basemaps.cartocdn.com https://d.basemaps.cartocdn.com; font-src 'self' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://fonts.gstatic.com; connect-src 'self' wss: ws: https://cdn.jsdelivr.net",
|
||||
"Content-Security-Policy": "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://unpkg.com; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://fonts.googleapis.com; img-src 'self' data: blob: https://cdn.jsdelivr.net https://a.tile.openstreetmap.org https://b.tile.openstreetmap.org https://c.tile.openstreetmap.org https://tile.openstreetmap.org https://a.basemaps.cartocdn.com https://b.basemaps.cartocdn.com https://c.basemaps.cartocdn.com https://d.basemaps.cartocdn.com; font-src 'self' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://fonts.gstatic.com; connect-src 'self' wss: ws: https://cdn.jsdelivr.net",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"X-Frame-Options": "DENY",
|
||||
"X-XSS-Protection": "1; mode=block",
|
||||
|
||||
@@ -45,7 +45,11 @@ export async function handleAssetsRoutes(
|
||||
|
||||
const file = Bun.file(resolvedPath);
|
||||
if (await file.exists()) {
|
||||
return new Response(file, { headers: deps.secureHeaders(deps.mimeType(url.pathname)) });
|
||||
const headers: Record<string, string> = { ...deps.secureHeaders(deps.mimeType(url.pathname)) };
|
||||
if (relativePath === "notification-sw.js") {
|
||||
headers["Service-Worker-Allowed"] = "/";
|
||||
}
|
||||
return new Response(file, { headers });
|
||||
}
|
||||
return new Response("Not found", { status: 404 });
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ type MiscRouteDeps = {
|
||||
getRdSessionCount: () => number;
|
||||
getFileBrowserSessionCount: () => number;
|
||||
getProcessSessionCount: () => number;
|
||||
tlsCertPath?: string;
|
||||
tlsSource?: "certbot" | "configured" | "self-signed";
|
||||
};
|
||||
|
||||
export async function handleMiscRoutes(
|
||||
@@ -388,5 +390,28 @@ export async function handleMiscRoutes(
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method === "GET" && url.pathname === "/api/cert/info") {
|
||||
return Response.json(
|
||||
{ source: deps.tlsSource || "unknown" },
|
||||
{ headers: { "Content-Type": "application/json", ...deps.CORS_HEADERS } },
|
||||
);
|
||||
}
|
||||
|
||||
if (req.method === "GET" && url.pathname === "/api/cert/download" && deps.tlsCertPath) {
|
||||
try {
|
||||
const file = Bun.file(deps.tlsCertPath);
|
||||
if (await file.exists()) {
|
||||
return new Response(file, {
|
||||
headers: {
|
||||
"Content-Type": "application/x-pem-file",
|
||||
"Content-Disposition": 'attachment; filename="overlord-ca.crt"',
|
||||
...deps.CORS_HEADERS,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch { }
|
||||
return new Response("Certificate not available", { status: 404 });
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -86,6 +86,10 @@ type WsLifecycleDeps = {
|
||||
notifyRemoteDesktopStatus: (clientId: string, status: string, reason?: string) => void;
|
||||
handleBuildTagConnection: (clientId: string, buildTag: string) => void;
|
||||
notifyDashboard: () => void;
|
||||
broadcastClientEvent: (
|
||||
event: "client_online" | "client_offline" | "client_purgatory",
|
||||
info: { id: string; host?: string; user?: string; os?: string; ip?: string; country?: string },
|
||||
) => void;
|
||||
};
|
||||
|
||||
const ENROLLMENT_TIMEOUT_MS = 30_000;
|
||||
@@ -296,6 +300,14 @@ export async function handleWebSocketMessage(
|
||||
logger.info(`[purgatory] client ${resolvedId} is pending approval`);
|
||||
ws.send(encodeMessage({ type: "enrollment_status", status: "pending" }));
|
||||
deps.notifyDashboard();
|
||||
deps.broadcastClientEvent("client_purgatory", {
|
||||
id: resolvedId,
|
||||
host: (payload as any).host || undefined,
|
||||
user: (payload as any).user || undefined,
|
||||
os: (payload as any).os || undefined,
|
||||
ip: ip || undefined,
|
||||
country,
|
||||
});
|
||||
try { ws.close(4001, "pending"); } catch {}
|
||||
return;
|
||||
}
|
||||
@@ -349,8 +361,14 @@ export async function handleWebSocketMessage(
|
||||
clientManager.addClient(infoObj.id, infoObj);
|
||||
|
||||
deps.dispatchAutoScriptsForConnection(infoObj, ws);
|
||||
deps.notifyDashboard();
|
||||
|
||||
deps.notifyDashboard(); deps.broadcastClientEvent("client_online", {
|
||||
id: infoObj.id,
|
||||
host: infoObj.host,
|
||||
user: infoObj.user,
|
||||
os: infoObj.os,
|
||||
ip: infoObj.ip,
|
||||
country: infoObj.country,
|
||||
});
|
||||
if (infoObj.role === "client") {
|
||||
deps.notifyRemoteDesktopStatus(resolvedId, "online");
|
||||
metrics.recordConnection();
|
||||
@@ -642,6 +660,17 @@ export function handleWebSocketClose(
|
||||
return;
|
||||
}
|
||||
|
||||
if (role === "client" && currentClient) {
|
||||
deps.broadcastClientEvent("client_offline", {
|
||||
id: clientId,
|
||||
host: currentClient.host,
|
||||
user: currentClient.user,
|
||||
os: currentClient.os,
|
||||
ip: currentClient.ip,
|
||||
country: currentClient.country,
|
||||
});
|
||||
}
|
||||
|
||||
clientManager.deleteClient(clientId);
|
||||
stopAllProxiesForClient(clientId);
|
||||
clearClientSyncState(clientId);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { certificatesExist, generateSelfSignedCert, getLocalIPs, isOpenSSLAvailable } from "../certGenerator";
|
||||
import { logger } from "../logger";
|
||||
import { X509Certificate } from "crypto";
|
||||
import path from "path";
|
||||
|
||||
type TlsBootstrapParams = {
|
||||
@@ -119,6 +120,15 @@ export async function prepareTlsOptions(params: TlsBootstrapParams): Promise<Tls
|
||||
}
|
||||
}
|
||||
|
||||
if (source === "configured" && tlsOptions.cert) {
|
||||
try {
|
||||
const x509 = new X509Certificate(tlsOptions.cert);
|
||||
if (x509.issuer === x509.subject) {
|
||||
source = "self-signed";
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
return {
|
||||
tlsOptions,
|
||||
certPathUsed: params.certPath,
|
||||
|
||||
@@ -304,6 +304,27 @@ export function createNotificationPluginHandlers(deps: CreateDeps) {
|
||||
}
|
||||
},
|
||||
|
||||
broadcastClientLifecycleEvent(
|
||||
event: "client_online" | "client_offline" | "client_purgatory",
|
||||
info: { id: string; host?: string; user?: string; os?: string; ip?: string; country?: string },
|
||||
) {
|
||||
const item = { type: "client_event", event, clientId: info.id, host: info.host, user: info.user, os: info.os, ip: info.ip, country: info.country, ts: Date.now() };
|
||||
for (const session of sessionManager.getAllNotificationSessions().values()) {
|
||||
const sRole = session.userRole ?? session.viewer.data.userRole ?? "";
|
||||
const sUserId = session.userId ?? session.viewer.data.userId;
|
||||
if (event === "client_purgatory") {
|
||||
if (sRole !== "admin" && sRole !== "operator") continue;
|
||||
} else {
|
||||
if (sRole !== "admin") {
|
||||
if (sUserId === undefined || !deps.canUserAccessClient(sUserId, sRole, info.id)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
safeSendViewer(session.viewer, item);
|
||||
}
|
||||
},
|
||||
|
||||
markPluginLoaded,
|
||||
|
||||
isPluginLoaded(clientId: string, pluginId: string): boolean {
|
||||
|
||||
Reference in New Issue
Block a user