mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-19 14:15:21 +02:00
Compare commits
14 Commits
feat/add-l
...
v0.26.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0ea8b5283 | ||
|
|
060a053fdb | ||
|
|
304069d3c8 | ||
|
|
5967f48c6b | ||
|
|
f3bb56910a | ||
|
|
24c1c2a377 | ||
|
|
6fdb2e4a21 | ||
|
|
15e90e9ca9 | ||
|
|
d1553e1bda | ||
|
|
880a377e54 | ||
|
|
74e0bd5fe3 | ||
|
|
74aecf6828 | ||
|
|
7362cc49d2 | ||
|
|
bcbf433607 |
@@ -41,7 +41,7 @@ const profileSchema = z.object({
|
||||
password: z.string().nullable(),
|
||||
currentPassword: z.string().nullable(),
|
||||
image: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
firstName: z.string().optional(),
|
||||
lastName: z.string().optional(),
|
||||
allowImpersonation: z.boolean().optional().default(false),
|
||||
});
|
||||
@@ -91,7 +91,7 @@ export const ProfileForm = () => {
|
||||
image: data?.user?.image || "",
|
||||
currentPassword: "",
|
||||
allowImpersonation: data?.user?.allowImpersonation || false,
|
||||
name: data?.user?.firstName || "",
|
||||
firstName: data?.user?.firstName || "",
|
||||
lastName: data?.user?.lastName || "",
|
||||
},
|
||||
resolver: zodResolver(profileSchema),
|
||||
@@ -106,7 +106,7 @@ export const ProfileForm = () => {
|
||||
image: data?.user?.image || "",
|
||||
currentPassword: form.getValues("currentPassword") || "",
|
||||
allowImpersonation: data?.user?.allowImpersonation,
|
||||
name: data?.user?.firstName || "",
|
||||
firstName: data?.user?.firstName || "",
|
||||
lastName: data?.user?.lastName || "",
|
||||
},
|
||||
{
|
||||
@@ -131,7 +131,7 @@ export const ProfileForm = () => {
|
||||
image: values.image,
|
||||
currentPassword: values.currentPassword || undefined,
|
||||
allowImpersonation: values.allowImpersonation,
|
||||
name: values.name || undefined,
|
||||
firstName: values.firstName || undefined,
|
||||
lastName: values.lastName || undefined,
|
||||
});
|
||||
await refetch();
|
||||
@@ -141,7 +141,7 @@ export const ProfileForm = () => {
|
||||
password: "",
|
||||
image: values.image,
|
||||
currentPassword: "",
|
||||
name: values.name || "",
|
||||
firstName: values.firstName || "",
|
||||
lastName: values.lastName || "",
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -184,7 +184,7 @@ export const ProfileForm = () => {
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
name="firstName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>First Name</FormLabel>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type http from "node:http";
|
||||
import { findServerById, validateRequest } from "@dokploy/server";
|
||||
import { findServerById, IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { spawn } from "node-pty";
|
||||
import { Client } from "ssh2";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { getShell } from "./utils";
|
||||
import { getShell, isValidContainerId } from "./utils";
|
||||
|
||||
export const setupDockerContainerLogsWebSocketServer = (
|
||||
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||
@@ -42,6 +42,12 @@ export const setupDockerContainerLogsWebSocketServer = (
|
||||
return;
|
||||
}
|
||||
|
||||
// Security: Validate containerId to prevent command injection
|
||||
if (!isValidContainerId(containerId)) {
|
||||
ws.close(4000, "Invalid container ID format");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user || !session) {
|
||||
ws.close();
|
||||
return;
|
||||
@@ -111,6 +117,11 @@ export const setupDockerContainerLogsWebSocketServer = (
|
||||
client.end();
|
||||
});
|
||||
} else {
|
||||
if (IS_CLOUD) {
|
||||
ws.send("This feature is not available in the cloud version.");
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
const shell = getShell();
|
||||
const baseCommand = `docker ${runType === "swarm" ? "service" : "container"} logs --timestamps ${
|
||||
runType === "swarm" ? "--raw" : ""
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type http from "node:http";
|
||||
import { findServerById, validateRequest } from "@dokploy/server";
|
||||
import { findServerById, IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { spawn } from "node-pty";
|
||||
import { Client } from "ssh2";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { getShell } from "./utils";
|
||||
import { isValidContainerId, isValidShell } from "./utils";
|
||||
|
||||
export const setupDockerContainerTerminalWebSocketServer = (
|
||||
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||
@@ -35,10 +35,25 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
||||
const { user, session } = await validateRequest(req);
|
||||
|
||||
if (!containerId) {
|
||||
ws.close(4000, "containerId no provided");
|
||||
ws.close(4000, "containerId not provided");
|
||||
return;
|
||||
}
|
||||
|
||||
// Security: Validate containerId to prevent command injection
|
||||
if (!isValidContainerId(containerId)) {
|
||||
ws.close(4000, "Invalid container ID format");
|
||||
return;
|
||||
}
|
||||
|
||||
// Security: Validate shell to prevent command injection
|
||||
if (activeWay && !isValidShell(activeWay)) {
|
||||
ws.close(4000, "Invalid shell specified");
|
||||
return;
|
||||
}
|
||||
|
||||
// Default to 'sh' if no shell specified
|
||||
const shell = activeWay || "sh";
|
||||
|
||||
if (!user || !session) {
|
||||
ws.close();
|
||||
return;
|
||||
@@ -54,55 +69,61 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
||||
let _stderr = "";
|
||||
conn
|
||||
.once("ready", () => {
|
||||
conn.exec(
|
||||
`docker exec -it -w / ${containerId} ${activeWay}`,
|
||||
{ pty: true },
|
||||
(err, stream) => {
|
||||
if (err) {
|
||||
console.error("SSH exec error:", err);
|
||||
ws.close();
|
||||
// Use array-style arguments to prevent shell injection
|
||||
const dockerCommand = [
|
||||
"docker",
|
||||
"exec",
|
||||
"-it",
|
||||
"-w",
|
||||
"/",
|
||||
containerId,
|
||||
shell,
|
||||
].join(" ");
|
||||
conn.exec(dockerCommand, { pty: true }, (err, stream) => {
|
||||
if (err) {
|
||||
console.error("SSH exec error:", err);
|
||||
ws.close();
|
||||
conn.end();
|
||||
return;
|
||||
}
|
||||
|
||||
stream
|
||||
.on("close", (code: number, _signal: string) => {
|
||||
ws.send(`\nContainer closed with code: ${code}\n`);
|
||||
conn.end();
|
||||
return;
|
||||
}
|
||||
})
|
||||
.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();
|
||||
// Ensure SSH connection is closed when WebSocket closes
|
||||
conn.end();
|
||||
});
|
||||
},
|
||||
);
|
||||
ws.on("close", () => {
|
||||
stream.end();
|
||||
// Ensure SSH connection is closed when WebSocket closes
|
||||
conn.end();
|
||||
});
|
||||
});
|
||||
})
|
||||
.on("error", (err) => {
|
||||
console.error("SSH connection error:", err);
|
||||
@@ -119,10 +140,14 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
||||
privateKey: server.sshKey?.privateKey,
|
||||
});
|
||||
} else {
|
||||
const shell = getShell();
|
||||
if (IS_CLOUD) {
|
||||
ws.send("This feature is not available in the cloud version.");
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
const ptyProcess = spawn(
|
||||
shell,
|
||||
["-c", `docker exec -it -w / ${containerId} ${activeWay}`],
|
||||
"docker",
|
||||
["exec", "-it", "-w", "/", containerId, shell],
|
||||
{},
|
||||
);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
execAsync,
|
||||
getHostSystemStats,
|
||||
getLastAdvancedStatsFile,
|
||||
IS_CLOUD,
|
||||
recordAdvancedStats,
|
||||
validateRequest,
|
||||
} from "@dokploy/server";
|
||||
@@ -32,6 +33,12 @@ export const setupDockerStatsMonitoringSocketServer = (
|
||||
|
||||
wssTerm.on("connection", async (ws, req) => {
|
||||
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
||||
|
||||
if (IS_CLOUD) {
|
||||
ws.send("This feature is not available in the cloud version.");
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
const appName = url.searchParams.get("appName");
|
||||
const appType = (url.searchParams.get("appType") || "application") as
|
||||
| "application"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import type http from "node:http";
|
||||
import { findServerById, validateRequest } from "@dokploy/server";
|
||||
import { findServerById, IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||
import { Client } from "ssh2";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { readValidDirectory } from "./utils";
|
||||
|
||||
export const setupDeploymentLogsWebSocketServer = (
|
||||
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||
@@ -40,6 +41,11 @@ export const setupDeploymentLogsWebSocketServer = (
|
||||
return;
|
||||
}
|
||||
|
||||
if (!readValidDirectory(logPath)) {
|
||||
ws.close(4000, "Invalid log path");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user || !session) {
|
||||
ws.close();
|
||||
return;
|
||||
@@ -108,6 +114,11 @@ export const setupDeploymentLogsWebSocketServer = (
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (IS_CLOUD) {
|
||||
ws.send("This feature is not available in the cloud version.");
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
tailProcess = spawn("tail", ["-n", "+1", "-f", logPath]);
|
||||
|
||||
const stdout = tailProcess.stdout;
|
||||
|
||||
@@ -97,7 +97,12 @@ export const setupTerminalWebSocketServer = (
|
||||
|
||||
const isLocalServer = serverId === "local";
|
||||
|
||||
if (isLocalServer && !IS_CLOUD) {
|
||||
if (isLocalServer) {
|
||||
if (IS_CLOUD) {
|
||||
ws.send("This feature is not available in the cloud version.");
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
const port = Number(url.searchParams.get("port"));
|
||||
const username = url.searchParams.get("username");
|
||||
|
||||
|
||||
@@ -1,9 +1,52 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { execAsync, paths } from "@dokploy/server";
|
||||
import { execAsync, IS_CLOUD, paths } from "@dokploy/server";
|
||||
|
||||
/**
|
||||
* Validates that the container ID matches Docker's expected format.
|
||||
* Docker container IDs are 64-character hex strings (or 12-char short form).
|
||||
* Also allows container names: alphanumeric, underscores, hyphens, and dots.
|
||||
*/
|
||||
export const isValidContainerId = (id: string): boolean => {
|
||||
// Match full ID (64 hex chars), short ID (12 hex chars), or container name
|
||||
const hexPattern = /^[a-f0-9]{12,64}$/i;
|
||||
const namePattern = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
|
||||
return hexPattern.test(id) || (namePattern.test(id) && id.length <= 128);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates that the shell is one of the allowed shells.
|
||||
*/
|
||||
export const isValidShell = (shell: string): boolean => {
|
||||
const allowedShells = [
|
||||
"sh",
|
||||
"bash",
|
||||
"zsh",
|
||||
"ash",
|
||||
"/bin/sh",
|
||||
"/bin/bash",
|
||||
"/bin/zsh",
|
||||
"/bin/ash",
|
||||
];
|
||||
return allowedShells.includes(shell);
|
||||
};
|
||||
|
||||
export const readValidDirectory = (directory: string) => {
|
||||
const { BASE_PATH } = paths();
|
||||
|
||||
const resolvedBase = path.resolve(BASE_PATH);
|
||||
const resolvedDir = path.resolve(directory);
|
||||
|
||||
return (
|
||||
resolvedDir === resolvedBase ||
|
||||
resolvedDir.startsWith(resolvedBase + path.sep)
|
||||
);
|
||||
};
|
||||
export const getShell = () => {
|
||||
if (IS_CLOUD) {
|
||||
return "NO_AVAILABLE";
|
||||
}
|
||||
switch (os.platform()) {
|
||||
case "win32":
|
||||
return "powershell.exe";
|
||||
|
||||
@@ -214,6 +214,6 @@ export const apiUpdateUser = createSchema.partial().extend({
|
||||
.optional(),
|
||||
password: z.string().optional(),
|
||||
currentPassword: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
firstName: z.string().optional(),
|
||||
lastName: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -131,7 +131,10 @@ export const apiAssignDomain = z
|
||||
.object({
|
||||
host: z.string(),
|
||||
certificateType: z.enum(["letsencrypt", "none", "custom"]),
|
||||
letsEncryptEmail: z.string().email().optional().nullable(),
|
||||
letsEncryptEmail: z
|
||||
.union([z.string().email(), z.literal("")])
|
||||
.optional()
|
||||
.nullable(),
|
||||
https: z.boolean().optional(),
|
||||
})
|
||||
.required()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createWriteStream } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { IS_CLOUD, paths } from "@dokploy/server/constants";
|
||||
import type { Schedule } from "@dokploy/server/db/schema/schedule";
|
||||
import {
|
||||
createDeploymentSchedule,
|
||||
@@ -93,6 +93,13 @@ export const runCommand = async (scheduleId: string) => {
|
||||
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
|
||||
|
||||
try {
|
||||
if (IS_CLOUD) {
|
||||
writeStream.write(
|
||||
"This feature is not available in the cloud version.",
|
||||
);
|
||||
writeStream.end();
|
||||
return;
|
||||
}
|
||||
writeStream.write(
|
||||
`docker exec ${containerId} ${shellType} -c ${command}\n`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user