Skip to content

Commit

Permalink
feat: audio impl in wrkspce+client, improve vnc viewer, fix file list…
Browse files Browse the repository at this point in the history
…ing and screenshot (kinda), fix config file resolving
  • Loading branch information
IncognitoTGT committed Jan 19, 2025
1 parent 0c953c5 commit 1c9422c
Show file tree
Hide file tree
Showing 29 changed files with 350 additions and 173 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ tsconfig.tsbuildinfo
npm-debug.log*
yarn-debug.log*
yarn-error.log*
std*.log

# Misc
.DS_Store
Expand Down
3 changes: 2 additions & 1 deletion apps/daemon/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export const app = new Elysia()
};
})
.onBeforeHandle(authCheck)
.onError(({ error }) => {
.onError(({ error, path }) => {
console.error(`✨ Stardust: [${path}] ${error}`);
return {
success: false,
error: error.toString(),
Expand Down
16 changes: 10 additions & 6 deletions apps/daemon/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,24 @@ const config = getConfig();
if (typeof config.service !== "boolean" || config.service === true) {
checkSystemService();
}
const srv = Bun.serve<{ socket: Socket }>({
const srv = Bun.serve<{ socket: Socket; path: string }>({
async fetch(req, server) {
const path = new URL(req.url).pathname;
if (path.startsWith("/sessions") && path.split("/")[3] === "vnc") {
const portMap: Record<string, number> = {
vnc: 5901,
audio: 4713,
};
if (path.startsWith("/sessions") && ["vnc", "audio"].includes(path.split("/")[3])) {
const containerInfo = await docker.getContainer(path.split("/")[2]).inspect();
const socket = connect(
5901,
portMap[path.split("/")[3]],
containerInfo.NetworkSettings.Networks[config.docker.network || "stardust"].IPAddress,
);
const authToken = await generateToken();
if (req.headers.get("Authorization") === authToken) {
if (
server.upgrade(req, {
data: { socket },
data: { socket, path },
})
) {
return;
Expand All @@ -45,7 +49,7 @@ const srv = Bun.serve<{ socket: Socket }>({
});

ws.data.socket.on("error", (err) => {
console.warn(`✨ Stardust: ${err.message}`);
console.error(`✨ Stardust: [${ws.data.path}] ${err.message}`);
ws.close();
});

Expand All @@ -58,7 +62,7 @@ const srv = Bun.serve<{ socket: Socket }>({
},
close(ws, code, reason) {
console.info(
`✨ Stardust: Connection closed with code ${code} and ${
`✨ Stardust: [${ws.data.path}] Connection closed with code ${code} and ${
reason.toString() ? `reason ${reason.toString()}` : "no reason"
}`,
);
Expand Down
25 changes: 16 additions & 9 deletions apps/daemon/src/session/file.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { docker } from "~/lib/docker";
// broken
export async function sendFile(id: string, name: string, file: Uint8Array) {
const container = docker.getContainer(id);
const payload = Buffer.from(Bun.gzipSync(file));
Expand All @@ -11,27 +12,33 @@ export async function sendFile(id: string, name: string, file: Uint8Array) {
});
return true;
}

// broken
export async function getFile(id: string, name: string) {
const file = await docker.getContainer(id).getArchive({ path: `/home/stardust/Downloads/${name}` });
console.log(file.read());
if (!file) throw new Error("no file for some reason");
const unzipped = Bun.gunzipSync(file.read());
return unzipped;
}

export async function listFiles(id: string) {
const exec = await docker.getContainer(id).exec({
Cmd: ["sh", "-c", "ls /home/stardust/Downloads"],
Cmd: ["sh", "-c", "mkdir -p /home/stardust/Downloads;ls /home/stardust/Downloads"],
AttachStdout: true,
AttachStderr: true,
});

const stream = await exec.start({ hijack: true, stdin: true });
const data = await new Promise<string>((res, err) => {
const out: string[] = [];
stream.on("error", err);
stream.on("data", (chunk) => out.push(chunk.toString()));
stream.on("end", () => res(out.join("")));
});
const stream = await exec.start({});
const data = (
await new Promise<string>((res, err) => {
const out: string[] = [];
stream.on("error", err);
stream.on("data", (chunk) => out.push(chunk.toString()));
stream.on("end", () => res(out.join("")));
})
)
.split("\n")
.filter(Boolean)
.map((s) => s.replace("\x01\x00\x00\x00\x00\x00\x00\x1C", ""));
return data;
}
21 changes: 6 additions & 15 deletions apps/daemon/src/session/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { connect } from "node:net";
import { Elysia, t } from "elysia";
import generateToken from "~/lib/auth-token";
import { getConfig } from "~/lib/config";
import { docker } from "~/lib/docker";
import createSession from "./create";
Expand Down Expand Up @@ -71,38 +69,31 @@ export default new Elysia({ prefix: "/sessions" })
await deleteSession(id);
return { success: true };
})
.get("/:id/screenshot", async ({ params: { id }, set }) => {
try {
const res = await screenshot(id);
set.headers["Content-Type"] = "image/png";
return res;
} catch (e) {
set.status = 500;
return {
success: false,
error: e,
};
}
.get("/:id/screenshot", async ({ params: { id } }) => {
const res = await screenshot(id);
return { success: true, encoded: res };
})
.group("/:id/files", (app) =>
app
.get("/list", async ({ params: { id } }) => {
const data = await listFiles(id);
return {
success: true,
list: data.split("\n").filter(Boolean),
list: data,
};
})
.get("/download/:name", async ({ params: { id, name } }) => getFile(id, name))
.put(
"/upload/:name",
async ({ params: { id, name }, body }) => {
console.log(body);
const res = await sendFile(id, name, body);
return {
success: res,
};
},
{
parse: "none",
body: t.Uint8Array(),
},
),
Expand Down
10 changes: 4 additions & 6 deletions apps/daemon/src/session/screenshot.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { docker } from "~/lib/docker";
export default async function screenshot(id: string) {
const container = docker.getContainer(id);
const exec = await container.exec({
Cmd: ["sh", "-c", "xwd -root | convert xwd:- png:- | base64"],
const exec = await docker.getContainer(id).exec({
Cmd: ["sh", "-c", "xwd -root -display :1 | convert xwd:- png:- | base64"],
AttachStdout: true,
AttachStderr: true,
});

const stream = await exec.start({ hijack: true, stdin: true });
const stream = await exec.start({});
const encoded = await new Promise<string>((res, err) => {
const out: string[] = [];
stream.on("error", err);
stream.on("data", (chunk) => out.push(chunk.toString()));
stream.on("end", () => res(out.join("")));
});
const file = Buffer.from(encoded, "base64");
return file;
return encoded.replaceAll(/[^A-Za-z0-9+/=]/g, "");
}
21 changes: 3 additions & 18 deletions apps/daemon/src/stardustd.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,26 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>

<key>Label</key>
<string>org.spaceness.stardustd.plist</string>

<key>RunAtLoad</key>
<false/>

<key>KeepAlive</key>
<true/>

<key>UserName</key>
<string>root</string>

<key>StandardErrorPath</key>
<string>/usr/local/opt/stardust/stderr.log</string>

<string>/opt/stardust/stderr.log</string>
<key>StandardOutPath</key>
<string>/usr/local/opt/stardust/stdout.log</string>

<string>/opt/stardust/stdout.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string><![CDATA[/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin]]></string>
</dict>

<key>WorkingDirectory</key>
<string>/usr/local/opt/stardust/apps/daemon</string>

<key>ProgramArguments</key>
<array>
<!-- use /usr/local/bin for now -->

<string>/usr/local/bin/pnpm</string>
<string>start</string>
<string>/opt/stardust/apps/daemon/stardustd</string>
</array>

</dict>
</plist>
2 changes: 1 addition & 1 deletion apps/daemon/src/stardustd.service
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ After=docker.service

[Service]
WorkingDirectory=/opt/stardust/apps/daemon/
ExecStart=/usr/bin/bun start
ExecStart=stardustd
Restart=on-failure
RestartSec=5

Expand Down
4 changes: 2 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"geist": "^1.3.1",
"http-proxy-middleware": "^3.0.3",
"lucide-react": "^0.460.0",
"next": "15.1.5",
"next-themes": "^0.4.4",
Expand All @@ -46,7 +45,8 @@
"swr": "^2.3.0",
"tailwind-merge": "^2.5.4",
"tailwindcss": "^3.4.15",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"use-local-storage": "^3.0.0"
},
"devDependencies": {
"@types/node": "^20",
Expand Down
57 changes: 5 additions & 52 deletions apps/web/server.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import "@stardust/config/load-config";
import { createServer } from "node:http";
import type { Socket } from "node:net";
import type { SessionSchema } from "@stardust/common/auth";
import { shouldRoute, stardustdUpgrade } from "@stardust/common/session/ws";
import { getConfig } from "@stardust/config";
import db, { session } from "@stardust/db";
import { eq } from "@stardust/db/utils";
import { createProxyMiddleware } from "http-proxy-middleware";
import next from "next";
const dev = process.env.NODE_ENV !== "production";
const config = getConfig();
const port = config.port || 3000;
const port = getConfig().port || 3000;
console.log(
`✨ Stardust: Starting ${dev ? "development" : "production"} server ${process.argv.includes("--turbo") ? "with turbopack" : ""}...`,
);
Expand All @@ -27,51 +22,9 @@ const nextRequest = app.getRequestHandler();
const nextUpgrade = app.getUpgradeHandler();
httpServer
.on("request", nextRequest)
.on("upgrade", async (req, socket, head) => {
if (req.url?.startsWith("/vnc") && req.url?.split("/")[2]) {
const proto = req.headers["x-forwarded-proto"] || "http";
const host = req.headers["x-forwarded-host"] || req.headers.host;
const res = await fetch(`${proto}://${host}/api/auth/get-session`, {
headers: {
cookie: req.headers.cookie || "",
},
});
const userSession: SessionSchema = await res.json();
const dbSession = await db.query.session.findFirst({
where: (session, { and, eq }) =>
and(eq(session.id, req.url?.split("/")[2] as string), eq(session.userId, userSession?.user.id || "")),
});
if (!dbSession || dbSession?.userId !== userSession?.user.id) return socket.end();
const nodeConfig = config.nodes.find(({ id }) => id === dbSession.node);
const intervalId = setInterval(async () => {
try {
console.log(`✨ Stardust: Updating keepalive for session ${dbSession?.id}`);
const expiresAt = new Date();
expiresAt.setMinutes(expiresAt.getMinutes() + (config.session?.keepaliveDuration || 1440));
await db
.update(session)
.set({ expiresAt })
.where(eq(session.id, dbSession?.id || ""));
} catch (e) {
console.log(`✨ Stardust: Error updating keepalive for session ${dbSession?.id} - ${e}`);
}
}, 60000);

socket.on("close", () => {
console.log(`✨ Stardust: Client disconnected from ${dbSession?.id}`);
clearInterval(intervalId);
});
const middleware = createProxyMiddleware({
target: `ws://${nodeConfig?.hostname || "0.0.0.0"}:${nodeConfig?.port || 4000}/sessions/${dbSession.id}/vnc`,
ignorePath: true,
headers: {
Authorization: nodeConfig?.token as string,
},
});
return middleware.upgrade(req, socket as Socket, head);
}
nextUpgrade(req, socket, head);
})
.on("upgrade", (req, socket, head) =>
shouldRoute(req) ? stardustdUpgrade(req, socket, head) : nextUpgrade(req, socket, head),
)
.listen(port, () => {
console.log(`✨ Stardust: Server listening on ${port}`);
});
6 changes: 3 additions & 3 deletions apps/web/src/app/api/session/[slug]/files/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export async function GET(req: NextRequest, props: { params: Promise<{ slug: str
const nodeSession = getNode(session).sessions({ id: session.id });
if (name) {
const { data, error } = await nodeSession.files.download({ name }).get();
if (error) return Response.json({ error }, { status: 500 });
if (error) return Response.json(error, { status: 500 });
return new Response(new Blob([data]), {
headers: {
"Content-Disposition": `attachment; filename=${name}`,
Expand All @@ -18,7 +18,7 @@ export async function GET(req: NextRequest, props: { params: Promise<{ slug: str
});
}
const { data, error } = await nodeSession.files.list.get();
if (error) return Response.json({ error }, { status: 500 });
if (error) return Response.json(error, { status: 500 });
return Response.json(data.list);
}
export async function PUT(req: NextRequest, props: { params: Promise<{ slug: string }> }) {
Expand All @@ -28,6 +28,6 @@ export async function PUT(req: NextRequest, props: { params: Promise<{ slug: str
const session = await getSession(params.slug);
const nodeSession = getNode(session).sessions({ id: session.id });
const { data, error } = await nodeSession.files.upload({ name }).put(new Uint8Array(await req.arrayBuffer()));
if (error) return Response.json({ error }, { status: 500 });
if (error) return Response.json(error, { status: 500 });
return Response.json(data);
}
13 changes: 5 additions & 8 deletions apps/web/src/app/api/session/[slug]/preview/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@ export async function GET(_req: NextRequest, props: { params: Promise<{ slug: st
const nodeSession = getNode(session).sessions({ id: session.id });
const { data, error } = await nodeSession.screenshot.get();
if (error) return Response.json({ error }, { status: 500 });
if (data instanceof Buffer) {
return new Response(data, {
headers: {
"Content-Type": "image/png",
},
});
}
return Response.json(data, { status: 500 });
return new Response(Buffer.from(data.encoded, "base64"), {
headers: {
"Content-Type": "image/png",
},
});
}
1 change: 0 additions & 1 deletion apps/web/src/app/api/session/[slug]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export async function GET(_req: NextRequest, props: { params: Promise<{ slug: st
return Response.json({
exists: true,
password: data.password,
url: `/vnc/${session.id}`,
});
}
export const dynamic = "force-dynamic";
Loading

0 comments on commit 1c9422c

Please sign in to comment.