diff --git a/.config/config-schema.json b/.config/config-schema.json index 8c340e4..b1a1ab9 100644 --- a/.config/config-schema.json +++ b/.config/config-schema.json @@ -127,10 +127,23 @@ "SessionConfig": { "additionalProperties": false, "properties": { + "dnsServers": { + "default": "system default", + "description": "Dns servers for the container to use", + "items": { + "type": "string" + }, + "type": "array" + }, "keepaliveDuration": { "default": 1440, "description": "The amount of time to keep an inactive session alive for, in minutes.", "type": "number" + }, + "maxStorage": { + "default": "10GB", + "description": "Max amount of storage a container can use, in gigabytes", + "type": "number" } }, "type": "object" diff --git a/package.json b/package.json index de0ddac..3123b21 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "type": "module", "scripts": { + "postinstall": "jq '.exports[\".\"].types = (\"./\" + .types)' ./node_modules/xpra-html5-client/package.json > tmp.json && mv tmp.json ./node_modules/xpra-html5-client/package.json", "dev": "CONFIG=$(cat .config/config.json) tsx server.ts", "build": "CONFIG=$(cat .config/config.json) next build", "start": "NODE_ENV=production CONFIG=$(cat .config/config.json) tsx server.ts", @@ -43,6 +44,7 @@ "dockerode": "^4.0.2", "drizzle-orm": "^0.32.0", "drizzle-zod": "^0.5.1", + "http-proxy-middleware": "^3.0.0", "input-otp": "^1.2.4", "lucide-react": "^0.358.0", "next": "15.0.0-rc.0", @@ -60,7 +62,6 @@ "tailwind-merge": "^2.2.2", "tailwindcss-animate": "^1.0.7", "tsx": "^4.9.1", - "ws": "^8.17.1", "xpra-html5-client": "^2.3.0", "zod": "^3.23.8" }, @@ -72,7 +73,6 @@ "@types/pg": "^8.11.4", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "@types/ws": "^8.5.10", "autoprefixer": "^10.4.19", "drizzle-kit": "^0.23.0", "postcss": "^8.4.38", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 913c7db..705a660 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -95,6 +95,9 @@ importers: drizzle-zod: specifier: ^0.5.1 version: 0.5.1(drizzle-orm@0.32.0(@types/pg@8.11.4)(@types/react@18.3.3)(postgres@3.4.4)(react@19.0.0-rc-f6cce072-20240723))(zod@3.23.8) + http-proxy-middleware: + specifier: ^3.0.0 + version: 3.0.0 input-otp: specifier: ^1.2.4 version: 1.2.4(react-dom@19.0.0-rc-f6cce072-20240723(react@19.0.0-rc-f6cce072-20240723))(react@19.0.0-rc-f6cce072-20240723) @@ -146,9 +149,6 @@ importers: tsx: specifier: ^4.9.1 version: 4.9.1 - ws: - specifier: ^8.17.1 - version: 8.17.1 xpra-html5-client: specifier: ^2.3.0 version: 2.3.0 @@ -177,9 +177,6 @@ importers: '@types/react-dom': specifier: ^18.3.0 version: 18.3.0 - '@types/ws': - specifier: ^8.5.10 - version: 8.5.10 autoprefixer: specifier: ^10.4.19 version: 10.4.19(postcss@8.4.38) @@ -1563,6 +1560,9 @@ packages: '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/http-proxy@1.17.15': + resolution: {integrity: sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==} + '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -1581,9 +1581,6 @@ packages: '@types/node@20.11.30': resolution: {integrity: sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==} - '@types/node@20.12.7': - resolution: {integrity: sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==} - '@types/node@20.14.14': resolution: {integrity: sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==} @@ -1605,9 +1602,6 @@ packages: '@types/ssh2@1.15.0': resolution: {integrity: sha512-YcT8jP5F8NzWeevWvcyrrLB3zcneVjzYY9ZDSMAMboI+2zR1qYWFhwsyOFVzT7Jorn67vqxC0FRiw8YyG9P1ww==} - '@types/ws@8.5.10': - resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} - '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -2118,6 +2112,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -2139,6 +2136,15 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + follow-redirects@1.15.6: + resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + foreground-child@3.1.1: resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} engines: {node: '>=14'} @@ -2205,6 +2211,14 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-proxy-middleware@3.0.0: + resolution: {integrity: sha512-36AV1fIaI2cWRzHo+rbcxhe3M3jUDCNzc4D5zRl57sEWRAxdXYtw7FSQKYY6PDKssiAKjLYypbssHk+xs/kMXw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -2246,6 +2260,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-obj@3.0.0: + resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} + engines: {node: '>=10'} + is-plain-object@5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} @@ -2699,6 +2717,9 @@ packages: regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -3066,18 +3087,6 @@ packages: utf-8-validate: optional: true - ws@8.17.1: - resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - xpra-html5-client@2.3.0: resolution: {integrity: sha512-Tj7cetiPZZbTK9x9sq7xrg0u9Rm6T1d5ukln+HDa7vx22UrAoeo9pG50OuwoBw4f+5sOGCAbYlYfe5IP5Og8Nw==} @@ -4195,6 +4204,10 @@ snapshots: '@types/estree@1.0.5': {} + '@types/http-proxy@1.17.15': + dependencies: + '@types/node': 20.14.14 + '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -4216,10 +4229,6 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@20.12.7': - dependencies: - undici-types: 5.26.5 - '@types/node@20.14.14': dependencies: undici-types: 5.26.5 @@ -4247,10 +4256,6 @@ snapshots: dependencies: '@types/node': 18.19.26 - '@types/ws@8.5.10': - dependencies: - '@types/node': 20.12.7 - '@types/yargs-parser@21.0.3': {} '@types/yargs@13.0.12': @@ -4745,6 +4750,8 @@ snapshots: estraverse@5.3.0: {} + eventemitter3@4.0.7: {} + events@3.3.0: {} fast-deep-equal@3.1.3: {} @@ -4767,6 +4774,10 @@ snapshots: dependencies: to-regex-range: 5.0.1 + follow-redirects@1.15.6(debug@4.3.5): + optionalDependencies: + debug: 4.3.5 + foreground-child@3.1.1: dependencies: cross-spawn: 7.0.3 @@ -4832,6 +4843,25 @@ snapshots: html-escaper@2.0.2: {} + http-proxy-middleware@3.0.0: + dependencies: + '@types/http-proxy': 1.17.15 + debug: 4.3.5 + http-proxy: 1.18.1(debug@4.3.5) + is-glob: 4.0.3 + is-plain-obj: 3.0.0 + micromatch: 4.0.5 + transitivePeerDependencies: + - supports-color + + http-proxy@1.18.1(debug@4.3.5): + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.15.6(debug@4.3.5) + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + ieee754@1.2.1: {} inherits@2.0.4: {} @@ -4865,6 +4895,8 @@ snapshots: is-number@7.0.0: {} + is-plain-obj@3.0.0: {} + is-plain-object@5.0.0: {} isexe@2.0.0: {} @@ -5241,6 +5273,8 @@ snapshots: regenerator-runtime@0.14.1: {} + requires-port@1.0.0: {} + resolve-pkg-maps@1.0.0: {} resolve@1.22.8: @@ -5682,8 +5716,6 @@ snapshots: ws@7.5.9: {} - ws@8.17.1: {} - xpra-html5-client@2.3.0: dependencies: brotli: 1.3.3 diff --git a/server.ts b/server.ts index c8a6f9f..a3ff1a5 100644 --- a/server.ts +++ b/server.ts @@ -1,13 +1,13 @@ import { createServer } from "node:http"; -import { Socket } from "node:net"; +import type { Socket } from "node:net"; import { getConfig } from "@/lib/config"; import docker from "@/lib/docker"; import { db, session } from "@/lib/drizzle/db"; import { consola } from "consola"; import { and, eq } from "drizzle-orm"; +import { createProxyMiddleware } from "http-proxy-middleware"; import next from "next"; import { getToken } from "next-auth/jwt"; -import { WebSocketServer } from "ws"; const config = getConfig(); const port = Number.parseInt(process.env.PORT as string) || 3000; const dev = process.env.NODE_ENV !== "production"; @@ -25,52 +25,14 @@ consola.start(`✨ Stardust: Starting ${dev ? "development" : "production"} serv await app.prepare(); const nextRequest = app.getRequestHandler(); const nextUpgrade = app.getUpgradeHandler(); -const websockify = new WebSocketServer({ noServer: true }); -websockify.on("connection", async (ws, req) => { - try { - const id = req.url?.split("/")[2]; - if (!id) { - ws.close(1008, "Missing ID"); - return; - } - const ip = (await docker.getContainer(id).inspect()).NetworkSettings.Networks[config.docker.network].IPAddress; - const socket = new Socket(); - socket.connect(5901, ip); - ws.on("message", (message: Uint8Array) => { - socket.write(message); - }); - ws.on("close", (code, reason) => { - consola.info( - `✨ Stardust: Connection closed with code ${code} and ${ - reason.toString() ? `reason ${reason.toString()}` : "no reason" - }`, - ); - socket.end(); - }); - - socket.on("data", (data) => { - ws.send(data); - }); - - socket.on("error", (err) => { - consola.warn(`✨ Stardust: ${err.message}`); - ws.close(); - }); - - socket.on("close", () => { - ws.close(); - }); - } catch (error) { - ws.close(1008, "Server error"); - consola.error(`✨ Stardust: ${(error as Error).message}`); - } -}); server.on("request", nextRequest); server.on("upgrade", async (req, socket, head) => { - if (req.url?.includes("websockify")) { - const cookie = req.headers.cookie?.includes("__Secure") ? "__Secure-authjs.session-token" : "authjs.session-token"; + if (req.url?.startsWith("/websockify") && req.url?.split("/")[2]) { + const cookie = `${req.headers.cookie?.includes("__Secure") ? "__Secure-" : ""}authjs.session-token`; const token = await getToken({ - req: { headers: req.headers as Record }, + req: { + headers: req.headers as Record, + }, secret: config.auth.secret, secureCookie: req.headers.cookie?.includes("__Secure"), salt: cookie, @@ -79,14 +41,18 @@ server.on("upgrade", async (req, socket, head) => { const [dbSession] = await db .select() .from(session) - .where(and(eq(session.userId, token?.id as string), eq(session.id, req.url?.split("/")[2] as string))); - websockify.handleUpgrade(req, socket, head, (ws) => { - if (dbSession) { - websockify.emit("connection", ws, req, websockify); - } else { - ws.close(1008, "Unauthorized"); - } - }); + .where(and(eq(session.userId, token?.id as string), eq(session.id, req.url.split("/")[2]))); + if (dbSession) { + const ip = (await docker.getContainer(dbSession.id).inspect()).NetworkSettings.Networks[config.docker.network] + .IPAddress; + const proxy = createProxyMiddleware({ + target: `ws://${ip}:5901`, + ws: true, + }); + proxy.upgrade(req, socket as Socket, head); + } else { + socket.end(); + } } else { nextUpgrade(req, socket, head); } diff --git a/src/actions/user.ts b/src/actions/user.ts index d38f07b..7714d8d 100644 --- a/src/actions/user.ts +++ b/src/actions/user.ts @@ -22,7 +22,7 @@ export async function deleteUser(userId: string, triggeredByUser = false) { } } const sessions = await db.select().from(session).where(eq(session.userId, userId)); - await Promise.all(sessions.map((session) => deleteSession(session.id))); + await Promise.all(sessions.map((session) => deleteSession(session.id))).catch(() => {}); await db.delete(user).where(eq(user.id, userId)); revalidatePath("/admin/users"); return { success: true }; diff --git a/src/app/(main)/error.tsx b/src/app/(main)/error.tsx index 3bf6ede..91ba5ad 100644 --- a/src/app/(main)/error.tsx +++ b/src/app/(main)/error.tsx @@ -2,7 +2,13 @@ import { Button } from "@/components/ui/button"; import { Sparkles } from "lucide-react"; -export default function ErrorPage({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { +export default function ErrorPage({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { return (
@@ -15,7 +21,9 @@ export default function ErrorPage({ error, reset }: { error: Error & { digest?:

Stardust has encountered an error

Digest: {error.digest ?? "none"}

{error.message ? ( - {error.message} + + {process.env.NODE_ENV === "production" ? "Contact the server host for more info" : error.message} + ) : null} + + + + ) : null} + { + setSidebarOpen(value); + filesMutate(); + }} + > + + + Control Panel + + Manage your session with these controls + + +
+
+ + + + + + + + + + + + + Confirm session deletion + + Are you sure you want to delete this session? This action is irreversible. + + + + Cancel + { + vncRef.current?.rfb?.disconnect(); + toast.promise( + async () => { + await deleteSession(params.slug); + router.push("/"); + }, + { + loading: "Deleting session...", + success: "Session deleted", + error: "Failed to delete session", + }, + ); + }} + > + + + + + + +
+ + + +
+ +
+ Clipboard +
+ +