mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
parent
d20f86ffe1
commit
fc2b0abdb1
@ -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<Props> = ({
|
||||
}
|
||||
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<Props> = ({
|
||||
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<Props> = ({
|
||||
}, [containerId, activeWay, id]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span>
|
||||
Select way to connect to <b>{containerId}</b>
|
||||
</span>
|
||||
@ -66,7 +66,7 @@ export const DockerTerminal: React.FC<Props> = ({
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
<div className="w-full h-full rounded-lg p-2 border min-h-[50vh]">
|
||||
<div className="w-full h-full rounded-lg p-2 bg-[#19191A]">
|
||||
<div id={id} ref={termRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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,
|
||||
});
|
||||
|
||||
|
@ -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<Props> = ({ 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<Props> = ({ 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 (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<div className="w-full h-full border rounded-lg p-2 min-h-[50vh]">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="w-full h-full bg-input rounded-lg p-2 ">
|
||||
<div id={id} ref={termRef} className="rounded-xl" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -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") {
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
133
packages/server/src/wss/docker-container-logs.ts
Normal file
133
packages/server/src/wss/docker-container-logs.ts
Normal file
@ -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<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||
) => {
|
||||
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<void>((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);
|
||||
}
|
||||
});
|
||||
};
|
152
packages/server/src/wss/docker-container-terminal.ts
Normal file
152
packages/server/src/wss/docker-container-terminal.ts
Normal file
@ -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<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||
) => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
96
packages/server/src/wss/docker-stats.ts
Normal file
96
packages/server/src/wss/docker-stats.ts
Normal file
@ -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<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||
) => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
};
|
101
packages/server/src/wss/listen-deployment.ts
Normal file
101
packages/server/src/wss/listen-deployment.ts
Normal file
@ -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<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||
) => {
|
||||
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<void>((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);
|
||||
}
|
||||
});
|
||||
};
|
107
packages/server/src/wss/terminal.ts
Normal file
107
packages/server/src/wss/terminal.ts
Normal file
@ -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<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||
) => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
};
|
@ -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;
|
||||
};
|
||||
|
@ -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: {}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user