new notification system

This commit is contained in:
KingKDot
2026-03-22 00:36:02 -05:00
parent b98613ae51
commit 92530044c9
15 changed files with 580 additions and 5 deletions
@@ -0,0 +1,124 @@
const DISMISS_KEY = "overlord_cert_banner_dismissed";
export async function showCertBannerIfNeeded(anchor) {
if (sessionStorage.getItem(DISMISS_KEY) === "1") return;
if (localStorage.getItem(DISMISS_KEY) === "1") return;
let source;
try {
const res = await fetch("/api/cert/info");
if (!res.ok) return;
const data = await res.json();
source = data.source;
} catch {
return;
}
if (source !== "self-signed") return;
if ("serviceWorker" in navigator && window.isSecureContext) {
try {
const reg = await navigator.serviceWorker.getRegistration("/");
if (reg && reg.active) {
return;
}
} catch { }
}
const os = detectOS();
const banner = document.createElement("div");
banner.id = "cert-trust-banner";
banner.className =
"mx-4 mt-3 mb-1 flex items-start gap-3 rounded-lg border border-amber-500/30 " +
"bg-amber-950/60 px-4 py-3 text-sm text-amber-200 shadow-lg";
banner.innerHTML = `
<span class="mt-0.5 text-lg select-none">&#x26A0;&#xFE0F;</span>
<div class="flex-1 min-w-0">
<p class="font-semibold text-amber-100 mb-1">Untrusted self-signed certificate</p>
<p class="mb-2 text-amber-200/80 leading-relaxed">
This server is using a self-signed TLS certificate. Your browser doesn't
trust it, which may block some features (desktop notifications, service workers).
Download the certificate and add it to your OS / browser trust store to fix this.
</p>
<div class="flex flex-wrap items-center gap-2">
<a href="/api/cert/download"
download="overlord-ca.crt"
class="inline-flex items-center gap-1.5 rounded bg-amber-600 hover:bg-amber-500
px-3 py-1 text-xs font-medium text-white transition-colors">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2"
viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round"
d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M7 10l5 5m0 0l5-5m-5 5V3"/></svg>
Download Certificate
</a>
<button id="cert-banner-how" type="button"
class="text-xs text-amber-300 underline underline-offset-2 hover:text-amber-100 transition-colors">
How to install (${os.label})
</button>
</div>
<div id="cert-banner-instructions" class="hidden mt-2 rounded bg-slate-900/80
border border-slate-700/50 p-3 text-xs text-slate-300 leading-relaxed whitespace-pre-line">${os.instructions}</div>
</div>
<button id="cert-banner-dismiss" type="button"
class="ml-2 mt-0.5 text-amber-400 hover:text-amber-200 transition-colors"
aria-label="Dismiss">&times;</button>
`;
anchor.insertAdjacentElement("afterend", banner);
document.getElementById("cert-banner-dismiss")?.addEventListener("click", () => {
banner.remove();
localStorage.setItem(DISMISS_KEY, "1");
sessionStorage.setItem(DISMISS_KEY, "1");
});
document.getElementById("cert-banner-how")?.addEventListener("click", () => {
const el = document.getElementById("cert-banner-instructions");
if (el) el.classList.toggle("hidden");
});
}
function detectOS() {
const ua = navigator.userAgent.toLowerCase();
if (ua.includes("win")) {
return {
label: "Windows",
instructions:
`1. Double-click the downloaded <strong>overlord-ca.crt</strong> file.
2. Click <strong>Install Certificate...</strong>
3. Choose <strong>Current User</strong> or <strong>Local Machine</strong>.
4. Select <strong>"Place all certificates in the following store"</strong>.
5. Click <strong>Browse</strong> → choose <strong>Trusted Root Certification Authorities</strong>.
6. Click <strong>Next</strong> → <strong>Finish</strong>.
7. <strong>Restart your browser</strong> for the change to take effect.`,
};
}
if (ua.includes("mac")) {
return {
label: "macOS",
instructions:
`1. Double-click the downloaded <strong>overlord-ca.crt</strong> file — it opens in Keychain Access.
2. If prompted, add it to the <strong>login</strong> or <strong>System</strong> keychain.
3. Find the certificate in the list, double-click it.
4. Expand <strong>Trust</strong> and set <strong>"When using this certificate"</strong> to <strong>Always Trust</strong>.
5. Close the dialog (you'll be prompted for your password).
6. <strong>Restart your browser</strong>.`,
};
}
return {
label: "Linux",
instructions:
`<strong>Debian / Ubuntu:</strong>
sudo cp overlord-ca.crt /usr/local/share/ca-certificates/overlord-ca.crt
sudo update-ca-certificates
<strong>Fedora / RHEL:</strong>
sudo cp overlord-ca.crt /etc/pki/ca-trust/source/anchors/overlord-ca.crt
sudo update-ca-trust
Then <strong>restart your browser</strong>.
For Chrome/Chromium you may also need: Settings → Privacy → Manage Certificates → Authorities → Import.`,
};
}
+2
View File
@@ -8,10 +8,12 @@ import {
import { mountNav } from "./nav/template.js";
import { createAdaptiveNavController } from "./nav/layout.js";
import { applyUserRoleUI } from "./nav/role-ui.js";
import { showCertBannerIfNeeded } from "./cert-banner.js";
const host = document.getElementById("top-nav");
if (host) {
const refs = mountNav(host);
showCertBannerIfNeeded(host);
const { applyAdaptiveNavLayout } = createAdaptiveNavController(host, refs);
const path = window.location.pathname;
@@ -0,0 +1,48 @@
self.addEventListener("install", () => self.skipWaiting());
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim());
});
self.addEventListener("notificationclick", (event) => {
event.notification.close();
const targetUrl = (event.notification.data && event.notification.data.url)
? event.notification.data.url
: "/notifications";
event.waitUntil(
self.clients
.matchAll({ type: "window", includeUncontrolled: true })
.then((clientList) => {
for (const client of clientList) {
try {
const clientUrl = new URL(client.url);
if (clientUrl.origin === self.location.origin) {
if (clientUrl.pathname !== "/notifications") {
client.navigate(targetUrl);
}
return client.focus();
}
} catch {}
}
return self.clients.openWindow(targetUrl);
}),
);
});
self.addEventListener("message", (event) => {
const data = event.data;
if (!data || data.type !== "show_notification") return;
const title = String(data.title || "Overlord Notification");
const options = {
body: String(data.body || ""),
icon: data.icon || "/assets/overlord.png",
badge: "/assets/overlord.png",
tag: data.tag || `overlord-${Date.now()}`,
data: { url: "/notifications" },
requireInteraction: false,
};
event.waitUntil(self.registration.showNotification(title, options));
});
@@ -1,9 +1,12 @@
import {
startNotificationClient,
subscribeNotifications,
subscribeClientEvents,
subscribeReady,
subscribeStatus,
markAllNotificationsRead,
getClientEventNotificationEnabled,
setClientEventNotificationEnabled,
} from "./notify-client.js";
// ── DOM refs ──────────────────────────────────────────────────────────────────
@@ -42,6 +45,11 @@ const resetTelegramTemplateBtn = document.getElementById("reset-telegram-templat
const previewTelegramTemplateBtn = document.getElementById("preview-telegram-template");
const telegramTemplatePreview = document.getElementById("telegram-template-preview");
const eventNotifOnlineInput = document.getElementById("event-notif-online");
const eventNotifOfflineInput = document.getElementById("event-notif-offline");
const eventNotifPurgatoryInput = document.getElementById("event-notif-purgatory");
const saveEventNotifsBtn = document.getElementById("save-event-notifs");
// Preview modal
const panel = document.getElementById("notification-panel");
const previewModal = document.getElementById("notification-preview-modal");
@@ -541,6 +549,35 @@ function connect() {
});
subscribeNotifications((item) => renderRow(item, true));
subscribeClientEvents((item) => renderClientEventRow(item));
}
const CLIENT_EVENT_BADGE = {
client_online: `<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-emerald-900/60 text-emerald-300 border border-emerald-700/50"><i class="fa-solid fa-circle text-xs"></i> online</span>`,
client_offline: `<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-rose-900/60 text-rose-300 border border-rose-700/50"><i class="fa-solid fa-circle text-xs"></i> offline</span>`,
client_purgatory: `<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-amber-900/60 text-amber-300 border border-amber-700/50"><i class="fa-solid fa-hourglass-half text-xs"></i> purgatory</span>`,
};
function renderClientEventRow(item) {
if (!listEl) return;
const badge = CLIENT_EVENT_BADGE[item.event] ||
`<span class="text-xs text-slate-400">${escapeHtml(item.event)}</span>`;
const row = document.createElement("tr");
row.className = "border-t border-slate-800/60";
row.innerHTML = `
<td class="py-2 pr-4 whitespace-nowrap text-slate-400">${formatTime(item.ts)}</td>
<td class="py-2 pr-4 whitespace-nowrap">${escapeHtml(item.clientId || "-")}</td>
<td class="py-2 pr-4 whitespace-nowrap">${escapeHtml(item.user || "-")}</td>
<td class="py-2 pr-4 max-w-xl truncate italic text-slate-400">client event</td>
<td class="py-2 pr-4 whitespace-nowrap">${escapeHtml(item.os || "-")}</td>
<td class="py-2 pr-4 whitespace-nowrap">-</td>
<td class="py-2 pr-4 whitespace-nowrap">${badge}</td>
<td class="py-2 pr-4"></td>
`;
listEl.prepend(row);
const rows = listEl.querySelectorAll("tr");
if (rows.length > MAX_ROWS) rows[rows.length - 1].remove();
if (emptyState) emptyState.classList.add("hidden");
}
// ── Preview modal ─────────────────────────────────────────────────────────────
@@ -564,11 +601,30 @@ document.addEventListener("visibilitychange", clearIfActive);
window.addEventListener("focus", clearIfActive);
clearIfActive();
// ── Event notification toggles ───────────────────────────────────────────────
function loadEventNotifPrefs() {
if (eventNotifOnlineInput) eventNotifOnlineInput.checked = getClientEventNotificationEnabled("client_online");
if (eventNotifOfflineInput) eventNotifOfflineInput.checked = getClientEventNotificationEnabled("client_offline");
if (eventNotifPurgatoryInput) eventNotifPurgatoryInput.checked = getClientEventNotificationEnabled("client_purgatory");
}
function wireEventNotifSave() {
if (!saveEventNotifsBtn) return;
saveEventNotifsBtn.addEventListener("click", () => {
if (eventNotifOnlineInput) setClientEventNotificationEnabled("client_online", eventNotifOnlineInput.checked);
if (eventNotifOfflineInput) setClientEventNotificationEnabled("client_offline", eventNotifOfflineInput.checked);
if (eventNotifPurgatoryInput) setClientEventNotificationEnabled("client_purgatory", eventNotifPurgatoryInput.checked);
window.showToast?.("Event notification preferences saved", "success", 3000);
});
}
// ── Bootstrap ─────────────────────────────────────────────────────────────────
initWebhookTemplateEditor();
wireKeywordSave();
wireWebhookSave();
wireTelegramSave();
loadMySettings();
loadEventNotifPrefs();
wireEventNotifSave();
initRoleUi();
connect();
@@ -2,6 +2,13 @@ import { decodeMsgpack } from "./msgpack-helpers.js";
const STORAGE_KEY = "overlord_notifications_enabled";
const UNREAD_KEY = "overlord_notifications_unread";
const DESKTOP_NOTIF_KEY = "overlord_desktop_notifications_enabled";
const CLIENT_EVENT_KEYS = {
client_online: "overlord_desktop_notify_client_online",
client_offline: "overlord_desktop_notify_client_offline",
client_purgatory: "overlord_desktop_notify_client_purgatory",
};
let enabled = localStorage.getItem(STORAGE_KEY);
if (enabled === null) {
enabled = "1";
@@ -10,12 +17,21 @@ if (enabled === null) {
if (localStorage.getItem(UNREAD_KEY) === null) {
localStorage.setItem(UNREAD_KEY, "0");
}
if (
localStorage.getItem(DESKTOP_NOTIF_KEY) === null &&
typeof Notification !== "undefined" &&
Notification.permission === "granted"
) {
localStorage.setItem(DESKTOP_NOTIF_KEY, "1");
}
let ws = null;
let started = false;
let readyHandlers = new Set();
let notificationHandlers = new Set();
let statusHandlers = new Set();
let unreadHandlers = new Set();
let clientEventHandlers = new Set();
let lastHistory = [];
function emitStatus(status) {
@@ -43,6 +59,14 @@ function emitNotification(item) {
}
}
function emitClientEvent(item) {
for (const handler of clientEventHandlers) {
try {
handler(item);
} catch {}
}
}
function shouldNotify() {
return localStorage.getItem(STORAGE_KEY) === "1";
}
@@ -65,6 +89,125 @@ function incrementUnread() {
setUnreadCount(getUnreadCount() + 1);
}
export function getDesktopNotificationsEnabled() {
const stored = localStorage.getItem(DESKTOP_NOTIF_KEY);
if (stored === null) {
return typeof Notification !== "undefined" && Notification.permission === "granted";
}
return stored === "1";
}
export function setDesktopNotificationsEnabled(value) {
localStorage.setItem(DESKTOP_NOTIF_KEY, value ? "1" : "0");
}
export function getClientEventNotificationEnabled(eventType) {
const key = CLIENT_EVENT_KEYS[eventType];
if (!key) return false;
const stored = localStorage.getItem(key);
return stored === null ? true : stored === "1";
}
export function setClientEventNotificationEnabled(eventType, value) {
const key = CLIENT_EVENT_KEYS[eventType];
if (!key) return;
localStorage.setItem(key, value ? "1" : "0");
}
export async function requestDesktopNotificationPermission() {
if (!("Notification" in window)) return "denied";
if (Notification.permission !== "default") return Notification.permission;
try {
return await Notification.requestPermission();
} catch {
return "denied";
}
}
function fireNotification(title, body, tag, clickUrl) {
if ("serviceWorker" in navigator && navigator.serviceWorker.controller) {
navigator.serviceWorker.ready
.then((reg) =>
reg.showNotification(title, {
body,
icon: "/assets/overlord.png",
tag,
data: { url: clickUrl || "/notifications" },
requireInteraction: false,
}),
)
.catch(() => fireNotificationFallback(title, body, tag, clickUrl));
return;
}
fireNotificationFallback(title, body, tag, clickUrl);
}
function fireNotificationFallback(title, body, tag, clickUrl) {
try {
const n = new Notification(title, {
body,
icon: "/assets/overlord.png",
tag,
silent: false,
});
n.onclick = () => {
window.focus();
n.close();
if (clickUrl && window.location.pathname !== clickUrl) {
window.location.href = clickUrl;
}
};
} catch (err) {
console.warn("[notifications] desktop notification failed", err);
}
}
function showDesktopNotification(item) {
if (!getDesktopNotificationsEnabled()) return;
if (!("Notification" in window)) return;
if (Notification.permission !== "granted") return;
const title = item.keyword
? `Overlord \u2014 ${item.keyword}`
: "Overlord \u2014 Notification";
const lines = [item.title];
if (item.user) lines.push(`User: ${item.user}`);
if (item.host) lines.push(`Host: ${item.host}`);
if (item.process) lines.push(`Process: ${item.process}`);
const body = lines.filter(Boolean).join("\n");
fireNotification(title, body, `overlord-${item.id || Date.now()}`, "/notifications");
}
const CLIENT_EVENT_LABELS = {
client_online: "\u{1F7E2} Client Online",
client_offline: "\u{1F534} Client Offline",
client_purgatory: "\u{1F7E1} Client Awaiting Approval",
};
function showClientEventDesktopNotification(item) {
if (!getDesktopNotificationsEnabled()) return;
if (!getClientEventNotificationEnabled(item.event)) return;
if (!("Notification" in window)) return;
if (Notification.permission !== "granted") return;
const title = CLIENT_EVENT_LABELS[item.event] || "Overlord \u2014 Client Event";
const lines = [];
if (item.host) lines.push(`Host: ${item.host}`);
if (item.user) lines.push(`User: ${item.user}`);
if (item.os) lines.push(`OS: ${item.os}`);
if (item.clientId) lines.push(`ID: ${item.clientId}`);
const body = lines.join("\n") || item.clientId || "";
const dest = item.event === "client_purgatory" ? "/purgatory" : "/";
fireNotification(
title,
body,
`overlord-client-${item.event}-${item.clientId || Date.now()}`,
dest,
);
}
function handleMessage(payload) {
if (!payload || typeof payload.type !== "string") return;
if (payload.type === "ready") {
@@ -77,6 +220,12 @@ function handleMessage(payload) {
if (shouldNotify()) {
incrementUnread();
}
showDesktopNotification(payload.item);
}
if (payload.type === "client_event" && payload.event) {
console.log("[notifications] client event", payload.event, payload.clientId);
emitClientEvent(payload);
showClientEventDesktopNotification(payload);
}
}
@@ -159,6 +308,26 @@ export async function startNotificationClient() {
} catch (err) {
console.warn("[notifications] failed to load msgpackr", err);
}
if ("serviceWorker" in navigator && window.isSecureContext) {
fetch("/assets/notification-sw.js")
.then((res) => {
if (!res.ok) return;
navigator.serviceWorker
.register("/assets/notification-sw.js", { scope: "/" })
.catch(() => { });
})
.catch(() => {/* SW script unreachable (e.g. self-signed cert) — skip quietly */});
}
if (getDesktopNotificationsEnabled()) {
requestDesktopNotificationPermission().then((perm) => {
if (perm !== "granted") {
setDesktopNotificationsEnabled(false);
}
});
}
connect();
}
@@ -167,6 +336,11 @@ export function subscribeNotifications(handler) {
return () => notificationHandlers.delete(handler);
}
export function subscribeClientEvents(handler) {
clientEventHandlers.add(handler);
return () => clientEventHandlers.delete(handler);
}
export function subscribeUnread(handler) {
unreadHandlers.add(handler);
try {
+30 -1
View File
@@ -1,6 +1,9 @@
import {
getNotificationsEnabled,
setNotificationsEnabled,
getDesktopNotificationsEnabled,
setDesktopNotificationsEnabled,
requestDesktopNotificationPermission,
} from "./notify-client.js";
const PREF_REFRESH_KEY = "overlord_refresh_interval_seconds";
@@ -17,6 +20,8 @@ const passwordPolicyHint = document.getElementById("password-policy-hint");
const prefsForm = document.getElementById("prefs-form");
const prefNotificationsInput = document.getElementById("pref-notifications");
const prefDesktopNotificationsInput = document.getElementById("pref-desktop-notifications");
const prefDesktopNotificationsHint = document.getElementById("pref-desktop-notifications-hint");
const prefRefreshSecondsInput = document.getElementById("pref-refresh-seconds");
const myTelegramChatIdInput = document.getElementById("my-telegram-chat-id");
@@ -259,6 +264,9 @@ async function loadTlsSettings() {
function loadPrefs() {
prefNotificationsInput.checked = getNotificationsEnabled();
if (prefDesktopNotificationsInput) {
prefDesktopNotificationsInput.checked = getDesktopNotificationsEnabled();
}
const refreshSeconds = Number(localStorage.getItem(PREF_REFRESH_KEY) || 8);
prefRefreshSecondsInput.value = String(Math.min(120, Math.max(3, refreshSeconds)));
}
@@ -325,7 +333,28 @@ function savePrefs(event) {
localStorage.setItem(PREF_REFRESH_KEY, String(refreshSeconds));
prefRefreshSecondsInput.value = String(refreshSeconds);
showMessage("Preferences saved.");
const wantsDesktop = prefDesktopNotificationsInput
? prefDesktopNotificationsInput.checked
: false;
if (wantsDesktop) {
requestDesktopNotificationPermission().then((perm) => {
if (perm === "granted") {
setDesktopNotificationsEnabled(true);
if (prefDesktopNotificationsHint) prefDesktopNotificationsHint.classList.add("hidden");
showMessage("Preferences saved. Desktop notifications enabled.");
} else {
setDesktopNotificationsEnabled(false);
if (prefDesktopNotificationsInput) prefDesktopNotificationsInput.checked = false;
if (prefDesktopNotificationsHint) prefDesktopNotificationsHint.classList.remove("hidden");
showMessage("Desktop notifications require browser permission — not granted.", "error");
}
});
} else {
setDesktopNotificationsEnabled(false);
if (prefDesktopNotificationsHint) prefDesktopNotificationsHint.classList.add("hidden");
showMessage("Preferences saved.");
}
}
async function saveSecurityPolicy(event) {
+42
View File
@@ -251,6 +251,48 @@
</div>
</div>
<!-- Desktop Event Notifications -->
<div class="bg-slate-900/60 border border-slate-800 rounded-xl p-4">
<div class="flex items-center justify-between flex-wrap gap-3 mb-4">
<div>
<h2 class="text-lg font-semibold">Desktop Event Notifications</h2>
<p class="text-sm text-slate-400">
Choose which client events trigger a desktop (OS) notification. Desktop notifications must be enabled in <a href="/settings" class="text-sky-400 hover:underline">Settings</a> first.
</p>
</div>
<button
id="save-event-notifs"
class="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-emerald-600 hover:bg-emerald-700 text-white"
>
<i class="fa-solid fa-save"></i>
<span>Save</span>
</button>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3 text-sm">
<label class="flex items-center gap-3 rounded-lg bg-slate-950 border border-slate-800 px-4 py-3 cursor-pointer hover:border-slate-600 transition-colors">
<input id="event-notif-online" type="checkbox" class="h-4 w-4 accent-emerald-500 shrink-0" />
<span class="flex flex-col gap-0.5">
<span class="text-slate-200 font-medium">&#x1F7E2; Client Online</span>
<span class="text-slate-500 text-xs">Fire when a client connects</span>
</span>
</label>
<label class="flex items-center gap-3 rounded-lg bg-slate-950 border border-slate-800 px-4 py-3 cursor-pointer hover:border-slate-600 transition-colors">
<input id="event-notif-offline" type="checkbox" class="h-4 w-4 accent-red-500 shrink-0" />
<span class="flex flex-col gap-0.5">
<span class="text-slate-200 font-medium">&#x1F534; Client Offline</span>
<span class="text-slate-500 text-xs">Fire when a client disconnects</span>
</span>
</label>
<label class="flex items-center gap-3 rounded-lg bg-slate-950 border border-slate-800 px-4 py-3 cursor-pointer hover:border-slate-600 transition-colors">
<input id="event-notif-purgatory" type="checkbox" class="h-4 w-4 accent-yellow-500 shrink-0" />
<span class="flex flex-col gap-0.5">
<span class="text-slate-200 font-medium">&#x1F7E1; Client in Purgatory</span>
<span class="text-slate-500 text-xs">Fire when a client awaits approval</span>
</span>
</label>
</div>
</div>
<!-- Notification feed -->
<div
id="notification-panel"
+8
View File
@@ -86,6 +86,14 @@
<input id="pref-notifications" type="checkbox" class="h-4 w-4" />
<span>Enable notification badge updates</span>
</label>
<label class="flex items-center gap-2">
<input id="pref-desktop-notifications" type="checkbox" class="h-4 w-4 accent-emerald-500" />
<span>Enable desktop (OS) notifications</span>
</label>
<p id="pref-desktop-notifications-hint" class="hidden text-xs text-amber-400">
<i class="fa-solid fa-triangle-exclamation mr-1"></i>
Browser permission was denied. Reset notification permissions for this site in your browser settings, then try again.
</p>
<label class="block">
<span class="text-sm text-slate-300">Client refresh interval (seconds)</span>
<input
+3
View File
@@ -358,6 +358,8 @@ async function startServer() {
getRdSessionCount: sessionManager.getRdSessionCount,
getFileBrowserSessionCount: sessionManager.getFileBrowserSessionCount,
getProcessSessionCount: sessionManager.getProcessSessionCount,
tlsCertPath: tls?.certPathUsed,
tlsSource: tls?.source,
},
assets: {
PUBLIC_ROOT,
@@ -471,6 +473,7 @@ async function startServer() {
notifyRemoteDesktopStatus,
handleBuildTagConnection,
notifyDashboard: sessionManager.notifyDashboardViewers,
broadcastClientEvent: notificationPluginHandlers.broadcastClientLifecycleEvent,
};
const server = Bun.serve<SocketData>({
+1 -1
View File
@@ -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 {