From fc2b0abdb1d37979844f5fed5b1a26c36ffd7271 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 14 Dec 2024 23:05:19 -0600 Subject: [PATCH] Revert "refactor: remove unsued files" This reverts commit d20f86ffe13a4a285381d31c9be03f7c3e3a52b3. --- .../docker/terminal/docker-terminal.tsx | 12 +- .../settings/web-server/terminal-modal.tsx | 2 +- .../settings/web-server/terminal.tsx | 11 +- apps/dokploy/package.json | 8 +- .../server/wss/docker-container-terminal.ts | 12 +- apps/dokploy/server/wss/terminal.ts | 79 +++++---- packages/server/src/index.ts | 5 + packages/server/src/services/auth.ts | 2 +- .../server/src/wss/docker-container-logs.ts | 133 +++++++++++++++ .../src/wss/docker-container-terminal.ts | 152 ++++++++++++++++++ packages/server/src/wss/docker-stats.ts | 96 +++++++++++ packages/server/src/wss/listen-deployment.ts | 101 ++++++++++++ packages/server/src/wss/terminal.ts | 107 ++++++++++++ packages/server/src/wss/utils.ts | 29 ++-- pnpm-lock.yaml | 39 ++--- 15 files changed, 681 insertions(+), 107 deletions(-) create mode 100644 packages/server/src/wss/docker-container-logs.ts create mode 100644 packages/server/src/wss/docker-container-terminal.ts create mode 100644 packages/server/src/wss/docker-stats.ts create mode 100644 packages/server/src/wss/listen-deployment.ts create mode 100644 packages/server/src/wss/terminal.ts diff --git a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx index 098860cf..4008d6fd 100644 --- a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx +++ b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx @@ -4,7 +4,6 @@ import { FitAddon } from "xterm-addon-fit"; import "@xterm/xterm/css/xterm.css"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { AttachAddon } from "@xterm/addon-attach"; -import { WebLinksAddon } from "@xterm/addon-web-links"; interface Props { id: string; @@ -26,11 +25,13 @@ export const DockerTerminal: React.FC = ({ } const term = new Terminal({ cursorBlink: true, + cols: 80, + rows: 30, lineHeight: 1.4, convertEol: true, theme: { cursor: "transparent", - background: "transparent", + background: "rgba(0, 0, 0, 0)", }, }); const addonFit = new FitAddon(); @@ -46,7 +47,6 @@ export const DockerTerminal: React.FC = ({ term.open(termRef.current); term.loadAddon(addonFit); term.loadAddon(addonAttach); - term.loadAddon(new WebLinksAddon()); addonFit.fit(); return () => { ws.readyState === WebSocket.OPEN && ws.close(); @@ -54,8 +54,8 @@ export const DockerTerminal: React.FC = ({ }, [containerId, activeWay, id]); return ( -
-
+
+
Select way to connect to {containerId} @@ -66,7 +66,7 @@ export const DockerTerminal: React.FC = ({
-
+
diff --git a/apps/dokploy/components/dashboard/settings/web-server/terminal-modal.tsx b/apps/dokploy/components/dashboard/settings/web-server/terminal-modal.tsx index 19053879..5bdba8b8 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/terminal-modal.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/terminal-modal.tsx @@ -11,7 +11,7 @@ import { api } from "@/utils/api"; import dynamic from "next/dynamic"; import type React from "react"; -const Terminal = dynamic(async () => (await import("./terminal")).Terminal, { +const Terminal = dynamic(() => import("./terminal").then((e) => e.Terminal), { ssr: false, }); diff --git a/apps/dokploy/components/dashboard/settings/web-server/terminal.tsx b/apps/dokploy/components/dashboard/settings/web-server/terminal.tsx index f5febcda..2fe7f83c 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/terminal.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/terminal.tsx @@ -4,7 +4,6 @@ import { useEffect, useRef } from "react"; import { FitAddon } from "xterm-addon-fit"; import "@xterm/xterm/css/xterm.css"; import { AttachAddon } from "@xterm/addon-attach"; -import { WebLinksAddon } from "@xterm/addon-web-links"; interface Props { id: string; @@ -21,11 +20,13 @@ export const Terminal: React.FC = ({ id, serverId }) => { } const term = new XTerm({ cursorBlink: true, + cols: 80, + rows: 30, lineHeight: 1.4, convertEol: true, theme: { cursor: "transparent", - background: "transparent", + background: "#19191A", }, }); const addonFit = new FitAddon(); @@ -41,17 +42,15 @@ export const Terminal: React.FC = ({ id, serverId }) => { term.open(termRef.current); term.loadAddon(addonFit); term.loadAddon(addonAttach); - term.loadAddon(new WebLinksAddon()); addonFit.fit(); - return () => { ws.readyState === WebSocket.OPEN && ws.close(); }; }, [id, serverId]); return ( -
-
+
+
diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 14286dc1..292d3efb 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -35,11 +35,6 @@ "test": "vitest --config __test__/vitest.config.ts" }, "dependencies": { - "xterm-addon-fit": "0.8.0", - "@xterm/xterm": "^5.3.0", - "xterm": "5.2.1", - "@xterm/addon-attach": "^0.11.0", - "@xterm/addon-web-links": "^0.10.0", "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-yaml": "^6.1.1", "@codemirror/language": "^6.10.1", @@ -76,6 +71,8 @@ "@trpc/server": "^10.43.6", "@uiw/codemirror-theme-github": "^4.22.1", "@uiw/react-codemirror": "^4.22.1", + "@xterm/addon-attach": "0.10.0", + "@xterm/xterm": "^5.4.0", "adm-zip": "^0.5.14", "bcrypt": "5.1.1", "bullmq": "5.4.2", @@ -120,6 +117,7 @@ "undici": "^6.19.2", "use-resize-observer": "9.1.0", "ws": "8.16.0", + "xterm-addon-fit": "^0.8.0", "zod": "^3.23.4", "zod-form-data": "^2.0.2" }, diff --git a/apps/dokploy/server/wss/docker-container-terminal.ts b/apps/dokploy/server/wss/docker-container-terminal.ts index 4bf49bf3..eeba72d5 100644 --- a/apps/dokploy/server/wss/docker-container-terminal.ts +++ b/apps/dokploy/server/wss/docker-container-terminal.ts @@ -110,12 +110,12 @@ export const setupDockerContainerTerminalWebSocketServer = ( shell, ["-c", `docker exec -it ${containerId} ${activeWay}`], { - // name: "xterm-256color", - // cwd: process.env.HOME, - // env: process.env, - // encoding: "utf8", - // cols: 80, - // rows: 30, + name: "xterm-256color", + cwd: process.env.HOME, + env: process.env, + encoding: "utf8", + cols: 80, + rows: 30, }, ); diff --git a/apps/dokploy/server/wss/terminal.ts b/apps/dokploy/server/wss/terminal.ts index a6338afe..eb0bf2e2 100644 --- a/apps/dokploy/server/wss/terminal.ts +++ b/apps/dokploy/server/wss/terminal.ts @@ -70,44 +70,53 @@ export const setupTerminalWebSocketServer = ( let stderr = ""; conn .once("ready", () => { - conn.shell({}, (err, stream) => { - if (err) throw err; + conn.shell( + { + term: "terminal", + cols: 80, + rows: 30, + height: 30, + width: 80, + }, + (err, stream) => { + if (err) throw err; - stream - .on("close", (code: number, signal: string) => { - ws.send(`\nContainer closed with code: ${code}\n`); - conn.end(); - }) - .on("data", (data: string) => { - stdout += data.toString(); - ws.send(data.toString()); - }) - .stderr.on("data", (data) => { - stderr += data.toString(); - ws.send(data.toString()); - console.error("Error: ", data.toString()); + stream + .on("close", (code: number, signal: string) => { + ws.send(`\nContainer closed with code: ${code}\n`); + conn.end(); + }) + .on("data", (data: string) => { + stdout += data.toString(); + ws.send(data.toString()); + }) + .stderr.on("data", (data) => { + stderr += data.toString(); + ws.send(data.toString()); + console.error("Error: ", data.toString()); + }); + + ws.on("message", (message) => { + try { + let command: string | Buffer[] | Buffer | ArrayBuffer; + if (Buffer.isBuffer(message)) { + command = message.toString("utf8"); + } else { + command = message; + } + stream.write(command.toString()); + } catch (error) { + // @ts-ignore + const errorMessage = error?.message as unknown as string; + ws.send(errorMessage); + } }); - ws.on("message", (message) => { - try { - let command: string | Buffer[] | Buffer | ArrayBuffer; - if (Buffer.isBuffer(message)) { - command = message.toString("utf8"); - } else { - command = message; - } - stream.write(command.toString()); - } catch (error) { - // @ts-ignore - const errorMessage = error?.message as unknown as string; - ws.send(errorMessage); - } - }); - - ws.on("close", () => { - stream.end(); - }); - }); + ws.on("close", () => { + stream.end(); + }); + }, + ); }) .on("error", (err) => { if (err.level === "client-authentication") { diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 41f2b0fd..f3f1e96f 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -102,6 +102,11 @@ export * from "./utils/traefik/security"; export * from "./utils/traefik/types"; export * from "./utils/traefik/web-server"; +export * from "./wss/docker-container-logs"; +export * from "./wss/docker-container-terminal"; +export * from "./wss/docker-stats"; +export * from "./wss/listen-deployment"; +export * from "./wss/terminal"; export * from "./wss/utils"; export * from "./utils/access-log/handler"; diff --git a/packages/server/src/services/auth.ts b/packages/server/src/services/auth.ts index 598e39e3..11e2d24c 100644 --- a/packages/server/src/services/auth.ts +++ b/packages/server/src/services/auth.ts @@ -7,7 +7,7 @@ import { auth, users, } from "@dokploy/server/db/schema"; -import { getPublicIpWithFallback } from "@dokploy/server/wss/utils"; +import { getPublicIpWithFallback } from "@dokploy/server/wss/terminal"; import { TRPCError } from "@trpc/server"; import * as bcrypt from "bcrypt"; import { eq } from "drizzle-orm"; diff --git a/packages/server/src/wss/docker-container-logs.ts b/packages/server/src/wss/docker-container-logs.ts new file mode 100644 index 00000000..75292018 --- /dev/null +++ b/packages/server/src/wss/docker-container-logs.ts @@ -0,0 +1,133 @@ +import type http from "node:http"; +import { findServerById } from "@dokploy/server/services/server"; +import { spawn } from "node-pty"; +import { Client } from "ssh2"; +import { WebSocketServer } from "ws"; +import { validateWebSocketRequest } from "../auth/auth"; +import { getShell } from "./utils"; + +export const setupDockerContainerLogsWebSocketServer = ( + server: http.Server, +) => { + const wssTerm = new WebSocketServer({ + noServer: true, + path: "/docker-container-logs", + }); + + server.on("upgrade", (req, socket, head) => { + const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); + + if (pathname === "/_next/webpack-hmr") { + return; + } + if (pathname === "/docker-container-logs") { + wssTerm.handleUpgrade(req, socket, head, function done(ws) { + wssTerm.emit("connection", ws, req); + }); + } + }); + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + wssTerm.on("connection", async (ws, req) => { + const url = new URL(req.url || "", `http://${req.headers.host}`); + const containerId = url.searchParams.get("containerId"); + const tail = url.searchParams.get("tail"); + const serverId = url.searchParams.get("serverId"); + const { user, session } = await validateWebSocketRequest(req); + + if (!containerId) { + ws.close(4000, "containerId no provided"); + return; + } + + if (!user || !session) { + ws.close(); + return; + } + try { + if (serverId) { + const server = await findServerById(serverId); + + if (!server.sshKeyId) return; + const client = new Client(); + new Promise((resolve, reject) => { + client + .once("ready", () => { + const command = ` + bash -c "docker container logs --tail ${tail} --follow ${containerId}" + `; + client.exec(command, (err, stream) => { + if (err) { + console.error("Execution error:", err); + reject(err); + return; + } + stream + .on("close", () => { + client.end(); + resolve(); + }) + .on("data", (data: string) => { + ws.send(data.toString()); + }) + .stderr.on("data", (data) => { + ws.send(data.toString()); + }); + }); + }) + .connect({ + host: server.ipAddress, + port: server.port, + username: server.username, + privateKey: server.sshKey?.privateKey, + timeout: 99999, + }); + }); + } else { + const shell = getShell(); + const ptyProcess = spawn( + shell, + [ + "-c", + `docker container logs --tail ${tail} --follow ${containerId}`, + ], + { + name: "xterm-256color", + cwd: process.env.HOME, + env: process.env, + encoding: "utf8", + cols: 80, + rows: 30, + }, + ); + + ptyProcess.onData((data) => { + ws.send(data); + }); + ws.on("close", () => { + ptyProcess.kill(); + }); + ws.on("message", (message) => { + try { + let command: string | Buffer[] | Buffer | ArrayBuffer; + if (Buffer.isBuffer(message)) { + command = message.toString("utf8"); + } else { + command = message; + } + ptyProcess.write(command.toString()); + } catch (error) { + // @ts-ignore + const errorMessage = error?.message as unknown as string; + ws.send(errorMessage); + } + }); + } + } catch (error) { + // @ts-ignore + const errorMessage = error?.message as unknown as string; + + ws.send(errorMessage); + } + }); +}; diff --git a/packages/server/src/wss/docker-container-terminal.ts b/packages/server/src/wss/docker-container-terminal.ts new file mode 100644 index 00000000..0cb174b6 --- /dev/null +++ b/packages/server/src/wss/docker-container-terminal.ts @@ -0,0 +1,152 @@ +import type http from "node:http"; +import { findServerById } from "@dokploy/server/services/server"; +import { spawn } from "node-pty"; +import { Client } from "ssh2"; +import { WebSocketServer } from "ws"; +import { validateWebSocketRequest } from "../auth/auth"; +import { getShell } from "./utils"; + +export const setupDockerContainerTerminalWebSocketServer = ( + server: http.Server, +) => { + const wssTerm = new WebSocketServer({ + noServer: true, + path: "/docker-container-terminal", + }); + + server.on("upgrade", (req, socket, head) => { + const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); + + if (pathname === "/_next/webpack-hmr") { + return; + } + if (pathname === "/docker-container-terminal") { + wssTerm.handleUpgrade(req, socket, head, function done(ws) { + wssTerm.emit("connection", ws, req); + }); + } + }); + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + wssTerm.on("connection", async (ws, req) => { + const url = new URL(req.url || "", `http://${req.headers.host}`); + const containerId = url.searchParams.get("containerId"); + const activeWay = url.searchParams.get("activeWay"); + const serverId = url.searchParams.get("serverId"); + const { user, session } = await validateWebSocketRequest(req); + + if (!containerId) { + ws.close(4000, "containerId no provided"); + return; + } + + if (!user || !session) { + ws.close(); + return; + } + try { + if (serverId) { + const server = await findServerById(serverId); + if (!server.sshKeyId) + throw new Error("No SSH key available for this server"); + + const conn = new Client(); + let stdout = ""; + let stderr = ""; + conn + .once("ready", () => { + conn.exec( + `docker exec -it ${containerId} ${activeWay}`, + { pty: true }, + (err, stream) => { + if (err) throw err; + + stream + .on("close", (code: number, signal: string) => { + ws.send(`\nContainer closed with code: ${code}\n`); + conn.end(); + }) + .on("data", (data: string) => { + stdout += data.toString(); + ws.send(data.toString()); + }) + .stderr.on("data", (data) => { + stderr += data.toString(); + ws.send(data.toString()); + console.error("Error: ", data.toString()); + }); + + ws.on("message", (message) => { + try { + let command: string | Buffer[] | Buffer | ArrayBuffer; + if (Buffer.isBuffer(message)) { + command = message.toString("utf8"); + } else { + command = message; + } + stream.write(command.toString()); + } catch (error) { + // @ts-ignore + const errorMessage = error?.message as unknown as string; + ws.send(errorMessage); + } + }); + + ws.on("close", () => { + stream.end(); + }); + }, + ); + }) + .connect({ + host: server.ipAddress, + port: server.port, + username: server.username, + privateKey: server.sshKey?.privateKey, + timeout: 99999, + }); + } else { + const shell = getShell(); + const ptyProcess = spawn( + shell, + ["-c", `docker exec -it ${containerId} ${activeWay}`], + { + name: "xterm-256color", + cwd: process.env.HOME, + env: process.env, + encoding: "utf8", + cols: 80, + rows: 30, + }, + ); + + ptyProcess.onData((data) => { + ws.send(data); + }); + ws.on("close", () => { + ptyProcess.kill(); + }); + ws.on("message", (message) => { + try { + let command: string | Buffer[] | Buffer | ArrayBuffer; + if (Buffer.isBuffer(message)) { + command = message.toString("utf8"); + } else { + command = message; + } + ptyProcess.write(command.toString()); + } catch (error) { + // @ts-ignore + const errorMessage = error?.message as unknown as string; + ws.send(errorMessage); + } + }); + } + } catch (error) { + // @ts-ignore + const errorMessage = error?.message as unknown as string; + + ws.send(errorMessage); + } + }); +}; diff --git a/packages/server/src/wss/docker-stats.ts b/packages/server/src/wss/docker-stats.ts new file mode 100644 index 00000000..ed1dc46f --- /dev/null +++ b/packages/server/src/wss/docker-stats.ts @@ -0,0 +1,96 @@ +import type http from "node:http"; +import { WebSocketServer } from "ws"; +import { validateWebSocketRequest } from "../auth/auth"; +import { docker } from "../constants"; +import { + getLastAdvancedStatsFile, + recordAdvancedStats, +} from "../monitoring/utilts"; + +export const setupDockerStatsMonitoringSocketServer = ( + server: http.Server, +) => { + const wssTerm = new WebSocketServer({ + noServer: true, + path: "/listen-docker-stats-monitoring", + }); + + server.on("upgrade", (req, socket, head) => { + const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); + + if (pathname === "/_next/webpack-hmr") { + return; + } + if (pathname === "/listen-docker-stats-monitoring") { + wssTerm.handleUpgrade(req, socket, head, function done(ws) { + wssTerm.emit("connection", ws, req); + }); + } + }); + + wssTerm.on("connection", async (ws, req) => { + const url = new URL(req.url || "", `http://${req.headers.host}`); + const appName = url.searchParams.get("appName"); + const appType = (url.searchParams.get("appType") || "application") as + | "application" + | "stack" + | "docker-compose"; + const { user, session } = await validateWebSocketRequest(req); + + if (!appName) { + ws.close(4000, "appName no provided"); + return; + } + + if (!user || !session) { + ws.close(); + return; + } + const intervalId = setInterval(async () => { + try { + const filter = { + status: ["running"], + ...(appType === "application" && { + label: [`com.docker.swarm.service.name=${appName}`], + }), + ...(appType === "stack" && { + label: [`com.docker.swarm.task.name=${appName}`], + }), + ...(appType === "docker-compose" && { + name: [appName], + }), + }; + + const containers = await docker.listContainers({ + filters: JSON.stringify(filter), + }); + + const container = containers[0]; + if (!container || container?.State !== "running") { + ws.close(4000, "Container not running"); + return; + } + + const stats = await docker.getContainer(container.Id).stats({ + stream: false, + }); + + await recordAdvancedStats(stats, appName); + const data = await getLastAdvancedStatsFile(appName); + + ws.send( + JSON.stringify({ + data, + }), + ); + } catch (error) { + // @ts-ignore + ws.close(4000, `Error: ${error.message}`); + } + }, 1300); + + ws.on("close", () => { + clearInterval(intervalId); + }); + }); +}; diff --git a/packages/server/src/wss/listen-deployment.ts b/packages/server/src/wss/listen-deployment.ts new file mode 100644 index 00000000..363a3cc8 --- /dev/null +++ b/packages/server/src/wss/listen-deployment.ts @@ -0,0 +1,101 @@ +import { spawn } from "node:child_process"; +import type http from "node:http"; +import { findServerById } from "@dokploy/server/services/server"; +import { Client } from "ssh2"; +import { WebSocketServer } from "ws"; +import { validateWebSocketRequest } from "../auth/auth"; + +export const setupDeploymentLogsWebSocketServer = ( + server: http.Server, +) => { + const wssTerm = new WebSocketServer({ + noServer: true, + path: "/listen-deployment", + }); + + server.on("upgrade", (req, socket, head) => { + const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); + + if (pathname === "/_next/webpack-hmr") { + return; + } + if (pathname === "/listen-deployment") { + wssTerm.handleUpgrade(req, socket, head, function done(ws) { + wssTerm.emit("connection", ws, req); + }); + } + }); + + wssTerm.on("connection", async (ws, req) => { + const url = new URL(req.url || "", `http://${req.headers.host}`); + const logPath = url.searchParams.get("logPath"); + const serverId = url.searchParams.get("serverId"); + const { user, session } = await validateWebSocketRequest(req); + + if (!logPath) { + ws.close(4000, "logPath no provided"); + return; + } + + if (!user || !session) { + ws.close(); + return; + } + + try { + if (serverId) { + const server = await findServerById(serverId); + + if (!server.sshKeyId) return; + const client = new Client(); + new Promise((resolve, reject) => { + client + .on("ready", () => { + const command = ` + tail -n +1 -f ${logPath}; + `; + client.exec(command, (err, stream) => { + if (err) { + console.error("Execution error:", err); + reject(err); + return; + } + stream + .on("close", () => { + client.end(); + resolve(); + }) + .on("data", (data: string) => { + ws.send(data.toString()); + }) + .stderr.on("data", (data) => { + ws.send(data.toString()); + }); + }); + }) + .connect({ + host: server.ipAddress, + port: server.port, + username: server.username, + privateKey: server.sshKey?.privateKey, + timeout: 99999, + }); + }); + } else { + const tail = spawn("tail", ["-n", "+1", "-f", logPath]); + + tail.stdout.on("data", (data) => { + ws.send(data.toString()); + }); + + tail.stderr.on("data", (data) => { + ws.send(new Error(`tail error: ${data.toString()}`).message); + }); + } + } catch (error) { + // @ts-ignore + // const errorMessage = error?.message as unknown as string; + ws.send(errorMessage); + } + }); +}; diff --git a/packages/server/src/wss/terminal.ts b/packages/server/src/wss/terminal.ts new file mode 100644 index 00000000..562040d7 --- /dev/null +++ b/packages/server/src/wss/terminal.ts @@ -0,0 +1,107 @@ +import type http from "node:http"; +import path from "node:path"; +import { findServerById } from "@dokploy/server/services/server"; +import { spawn } from "node-pty"; +import { publicIpv4, publicIpv6 } from "public-ip"; +import { WebSocketServer } from "ws"; +import { validateWebSocketRequest } from "../auth/auth"; +import { paths } from "../constants"; + +export const getPublicIpWithFallback = async () => { + // @ts-ignore + let ip = null; + try { + ip = await publicIpv4(); + } catch (error) { + console.log( + "Error to obtain public IPv4 address, falling back to IPv6", + // @ts-ignore + error.message, + ); + try { + ip = await publicIpv6(); + } catch (error) { + // @ts-ignore + console.error("Error to obtain public IPv6 address", error.message); + ip = null; + } + } + return ip; +}; + +export const setupTerminalWebSocketServer = ( + server: http.Server, +) => { + const wssTerm = new WebSocketServer({ + noServer: true, + path: "/terminal", + }); + + server.on("upgrade", (req, socket, head) => { + const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); + if (pathname === "/_next/webpack-hmr") { + return; + } + if (pathname === "/terminal") { + wssTerm.handleUpgrade(req, socket, head, function done(ws) { + wssTerm.emit("connection", ws, req); + }); + } + }); + + wssTerm.on("connection", async (ws, req) => { + const url = new URL(req.url || "", `http://${req.headers.host}`); + const serverId = url.searchParams.get("serverId"); + const { user, session } = await validateWebSocketRequest(req); + if (!user || !session || !serverId) { + ws.close(); + return; + } + + const server = await findServerById(serverId); + + if (!server) { + ws.close(); + return; + } + const { SSH_PATH } = paths(); + const privateKey = path.join(SSH_PATH, `${server.sshKeyId}_rsa`); + const sshCommand = [ + "ssh", + "-o", + "StrictHostKeyChecking=no", + "-i", + privateKey, + `${server.username}@${server.ipAddress}`, + ]; + const ptyProcess = spawn("ssh", sshCommand.slice(1), { + name: "xterm-256color", + cwd: process.env.HOME, + env: process.env, + encoding: "utf8", + cols: 80, + rows: 30, + }); + + ptyProcess.onData((data) => { + ws.send(data); + }); + ws.on("message", (message) => { + try { + let command: string | Buffer[] | Buffer | ArrayBuffer; + if (Buffer.isBuffer(message)) { + command = message.toString("utf8"); + } else { + command = message; + } + ptyProcess.write(command.toString()); + } catch (error) { + console.log(error); + } + }); + + ws.on("close", () => { + ptyProcess.kill(); + }); + }); +}; diff --git a/packages/server/src/wss/utils.ts b/packages/server/src/wss/utils.ts index d9190f3c..b5567127 100644 --- a/packages/server/src/wss/utils.ts +++ b/packages/server/src/wss/utils.ts @@ -1,23 +1,12 @@ -import { publicIpv4, publicIpv6 } from "public-ip"; +import os from "node:os"; -export const getPublicIpWithFallback = async () => { - // @ts-ignore - let ip = null; - try { - ip = await publicIpv4(); - } catch (error) { - console.log( - "Error to obtain public IPv4 address, falling back to IPv6", - // @ts-ignore - error.message, - ); - try { - ip = await publicIpv6(); - } catch (error) { - // @ts-ignore - console.error("Error to obtain public IPv6 address", error.message); - ip = null; - } +export const getShell = () => { + switch (os.platform()) { + case "win32": + return "powershell.exe"; + case "darwin": + return "zsh"; + default: + return "bash"; } - return ip; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a3069f2..09d71bbf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -206,13 +206,10 @@ importers: specifier: ^4.22.1 version: 4.23.0(@babel/runtime@7.25.0)(@codemirror/autocomplete@6.17.0(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.29.0)(@lezer/common@1.2.1))(@codemirror/language@6.10.2)(@codemirror/lint@6.8.1)(@codemirror/search@6.5.6)(@codemirror/state@6.4.1)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.29.0)(codemirror@6.0.1(@lezer/common@1.2.1))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@xterm/addon-attach': - specifier: ^0.11.0 - version: 0.11.0(@xterm/xterm@5.5.0) - '@xterm/addon-web-links': - specifier: ^0.10.0 + specifier: 0.10.0 version: 0.10.0(@xterm/xterm@5.5.0) '@xterm/xterm': - specifier: ^5.3.0 + specifier: ^5.4.0 version: 5.5.0 adm-zip: specifier: ^0.5.14 @@ -346,12 +343,9 @@ importers: ws: specifier: 8.16.0 version: 8.16.0 - xterm: - specifier: 5.2.1 - version: 5.2.1 xterm-addon-fit: - specifier: 0.8.0 - version: 0.8.0(xterm@5.2.1) + specifier: ^0.8.0 + version: 0.8.0(xterm@5.3.0) zod: specifier: ^3.23.4 version: 3.23.8 @@ -3406,13 +3400,8 @@ packages: '@webassemblyjs/wast-printer@1.12.1': resolution: {integrity: sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==} - '@xterm/addon-attach@0.11.0': - resolution: {integrity: sha512-JboCN0QAY6ZLY/SSB/Zl2cQ5zW1Eh4X3fH7BnuR1NB7xGRhzbqU2Npmpiw/3zFlxDaU88vtKzok44JKi2L2V2Q==} - peerDependencies: - '@xterm/xterm': ^5.0.0 - - '@xterm/addon-web-links@0.10.0': - resolution: {integrity: sha512-QhrHCUr8w6ATGviyXwcAIM1qN3nD1hdxwMC8fsW7z/6aaQlb2nt7zmByJt4eOn7ZzrHOzczljqV5S2pkdQp2xw==} + '@xterm/addon-attach@0.10.0': + resolution: {integrity: sha512-ES/XO8pC1tPHSkh4j7qzM8ajFt++u8KMvfRc9vKIbjHTDOxjl9IUVo+vcQgLn3FTCM3w2czTvBss8nMWlD83Cg==} peerDependencies: '@xterm/xterm': ^5.0.0 @@ -6796,8 +6785,8 @@ packages: peerDependencies: xterm: ^5.0.0 - xterm@5.2.1: - resolution: {integrity: sha512-cs5Y1fFevgcdoh2hJROMVIWwoBHD80P1fIP79gopLHJIE4kTzzblanoivxTiQ4+92YM9IxS36H1q0MxIJXQBcA==} + xterm@5.3.0: + resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==} deprecated: This package is now deprecated. Move to @xterm/xterm instead. y18n@4.0.3: @@ -9646,11 +9635,7 @@ snapshots: '@webassemblyjs/ast': 1.12.1 '@xtuc/long': 4.2.2 - '@xterm/addon-attach@0.11.0(@xterm/xterm@5.5.0)': - dependencies: - '@xterm/xterm': 5.5.0 - - '@xterm/addon-web-links@0.10.0(@xterm/xterm@5.5.0)': + '@xterm/addon-attach@0.10.0(@xterm/xterm@5.5.0)': dependencies: '@xterm/xterm': 5.5.0 @@ -13113,11 +13098,11 @@ snapshots: xtend@4.0.2: {} - xterm-addon-fit@0.8.0(xterm@5.2.1): + xterm-addon-fit@0.8.0(xterm@5.3.0): dependencies: - xterm: 5.2.1 + xterm: 5.3.0 - xterm@5.2.1: {} + xterm@5.3.0: {} y18n@4.0.3: {}