add recording, fix solpush, persistence tests and tiny improvements

This commit is contained in:
KingKDot
2026-06-02 17:18:46 -05:00
parent 7f0a7b1d91
commit 146626ec0d
23 changed files with 1508 additions and 57 deletions
@@ -0,0 +1,69 @@
import { authenticateRequest } from "../../auth";
import { requireClientAccess, requireFeatureAccess } from "../../rbac";
import {
getRemoteDesktopRecordingFile,
getRemoteDesktopRecordingStatus,
listRemoteDesktopRecordings,
} from "../rd-recording";
type RdRecordingRouteDeps = {
secureHeaders: (contentType?: string) => Record<string, string>;
};
function forbiddenResponse(error: unknown): Response {
if (error instanceof Response) return error;
return new Response("Forbidden", { status: 403 });
}
async function authorize(req: Request, clientId: string): Promise<Response | null> {
const user = await authenticateRequest(req);
if (!user) return new Response("Unauthorized", { status: 401 });
try {
requireClientAccess(user, clientId);
requireFeatureAccess(user, "remote_desktop");
} catch (error) {
return forbiddenResponse(error);
}
return null;
}
export async function handleRemoteDesktopRecordingRoutes(
req: Request,
url: URL,
deps: RdRecordingRouteDeps,
): Promise<Response | null> {
const listMatch = url.pathname.match(/^\/api\/clients\/([^/]+)\/rd\/recordings$/);
if (listMatch && req.method === "GET") {
const clientId = decodeURIComponent(listMatch[1]);
const denied = await authorize(req, clientId);
if (denied) return denied;
return Response.json({
active: getRemoteDesktopRecordingStatus(clientId),
items: listRemoteDesktopRecordings(clientId),
});
}
const fileMatch = url.pathname.match(/^\/api\/clients\/([^/]+)\/rd\/recordings\/([^/]+)\/([^/]+)$/);
if (fileMatch && req.method === "GET") {
const clientId = decodeURIComponent(fileMatch[1]);
const recordingId = decodeURIComponent(fileMatch[2]);
const fileName = decodeURIComponent(fileMatch[3]);
const denied = await authorize(req, clientId);
if (denied) return denied;
const file = getRemoteDesktopRecordingFile(clientId, recordingId, fileName);
if (!file) return new Response("Not found", { status: 404 });
const contentType = fileName.toLowerCase().endsWith(".mp4") ? "video/mp4" : "video/webm";
return new Response(Bun.file(file.path), {
headers: {
...deps.secureHeaders(contentType),
"Content-Length": String(file.size),
"Content-Disposition": `attachment; filename="${fileName.replace(/"/g, "")}"`,
"Cache-Control": "no-store, private",
},
});
}
return null;
}
@@ -22,6 +22,7 @@ import { getMaxPayloadLimit, getMessageByteLength, isAllowedClientMessageType }
import { stopAllProxiesForClient } from "../socks5-proxy-manager";
import { verifyBuildToken, isBuildBanned } from "../build-signing";
import { clearThumbnail } from "../../thumbnails";
import { stopRemoteDesktopRecording } from "../rd-recording";
const OFFLINE_GRACE_MS = (() => {
const raw = process.env.OVERLORD_OFFLINE_GRACE_MS;
@@ -991,6 +992,7 @@ export function handleWebSocketClose(
deps.rdStreamingState.delete(clientId);
deps.hvncStreamingState.delete(clientId);
deps.webcamStreamingState.delete(clientId);
stopRemoteDesktopRecording(clientId, "client disconnected");
logger.info(`[close] ${clientId} code=${code} reason=${reason} disconnect_reason=${storedDisconnectReason || "unknown"}`);
if (role === "client" && currentClient) {
@@ -60,6 +60,7 @@ function checkOperatorAccess(
type AuthenticatedUser = {
userId: number;
username: string;
role: UserRole;
};
@@ -187,6 +188,7 @@ async function tryClientViewerUpgrade(
ip: getRequestIp(req, server),
userRole: user.role,
userId: user.userId,
username: user.username,
};
if (route.sessionId) {
data.sessionId = route.sessionId();
@@ -219,6 +221,7 @@ async function tryGlobalViewerUpgrade(
ip: getRequestIp(req, server),
userRole: user.role,
userId: user.userId,
username: user.username,
});
}