This commit is contained in:
Louis Lam
2023-12-20 01:48:20 +08:00
parent 80e885e85d
commit 0f79b46769
28 changed files with 471 additions and 135 deletions

View File

@@ -0,0 +1,7 @@
import { DockgeServer } from "./dockge-server";
import { AgentSocket } from "../../common/agent-socket";
import { DockgeSocket } from "./util-server";
export abstract class AgentSocketHandler {
abstract create(socket : DockgeSocket, server : DockgeServer, agentSocket : AgentSocket): void;
}

View File

@@ -1,17 +1,16 @@
import { SocketHandler } from "../socket-handler.js";
import { AgentSocketHandler } from "../agent-socket-handler";
import { DockgeServer } from "../dockge-server";
import { callbackError, checkLogin, DockgeSocket, ValidationError } from "../util-server";
import { Stack } from "../stack";
import { AgentSocket } from "../../common/agent-socket";
// @ts-ignore
import composerize from "composerize";
export class DockerSocketHandler extends AgentSocketHandler {
create(s : DockgeSocket, server : DockgeServer, agentSocket : AgentSocket) {
// Do not call super.create()
export class DockerSocketHandler extends SocketHandler {
create(socket : DockgeSocket, server : DockgeServer) {
socket.on("deployStack", async (name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown, callback) => {
agentSocket.on("deployStack", async (name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown, callback) => {
try {
checkLogin(socket);
checkLogin(s);
const stack = await this.saveStack(socket, server, name, composeYAML, composeENV, isAdd);
await stack.deploy(socket);
server.sendStackList();
@@ -25,7 +24,7 @@ export class DockerSocketHandler extends SocketHandler {
}
});
socket.on("saveStack", async (name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown, callback) => {
agentSocket.on("saveStack", async (name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown, callback) => {
try {
checkLogin(socket);
this.saveStack(socket, server, name, composeYAML, composeENV, isAdd);
@@ -39,7 +38,7 @@ export class DockerSocketHandler extends SocketHandler {
}
});
socket.on("deleteStack", async (name : unknown, callback) => {
agentSocket.on("deleteStack", async (name : unknown, callback) => {
try {
checkLogin(socket);
if (typeof(name) !== "string") {
@@ -65,9 +64,9 @@ export class DockerSocketHandler extends SocketHandler {
}
});
socket.on("getStack", async (stackName : unknown, callback) => {
agentSocket.on("getStack", async (stackName : unknown, callback) => {
try {
checkLogin(socket);
checkLogin(s);
if (typeof(stackName) !== "string") {
throw new ValidationError("Stack name must be a string");
@@ -76,7 +75,7 @@ export class DockerSocketHandler extends SocketHandler {
const stack = await Stack.getStack(server, stackName);
if (stack.isManagedByDockge) {
stack.joinCombinedTerminal(socket);
stack.joinCombinedTerminal(s);
}
callback({
@@ -89,7 +88,7 @@ export class DockerSocketHandler extends SocketHandler {
});
// requestStackList
socket.on("requestStackList", async (callback) => {
agentSocket.on("requestStackList", async (callback) => {
try {
checkLogin(socket);
server.sendStackList();
@@ -103,7 +102,7 @@ export class DockerSocketHandler extends SocketHandler {
});
// startStack
socket.on("startStack", async (stackName : unknown, callback) => {
agentSocket.on("startStack", async (stackName : unknown, callback) => {
try {
checkLogin(socket);
@@ -127,7 +126,7 @@ export class DockerSocketHandler extends SocketHandler {
});
// stopStack
socket.on("stopStack", async (stackName : unknown, callback) => {
agentSocket.on("stopStack", async (stackName : unknown, callback) => {
try {
checkLogin(socket);
@@ -148,16 +147,16 @@ export class DockerSocketHandler extends SocketHandler {
});
// restartStack
socket.on("restartStack", async (stackName : unknown, callback) => {
agentSocket.on("restartStack", async (stackName : unknown, callback) => {
try {
checkLogin(socket);
checkLogin(s);
if (typeof(stackName) !== "string") {
throw new ValidationError("Stack name must be a string");
}
const stack = await Stack.getStack(server, stackName);
await stack.restart(socket);
await stack.restart(s);
callback({
ok: true,
msg: "Restarted"
@@ -169,7 +168,7 @@ export class DockerSocketHandler extends SocketHandler {
});
// updateStack
socket.on("updateStack", async (stackName : unknown, callback) => {
agentSocket.on("updateStack", async (stackName : unknown, callback) => {
try {
checkLogin(socket);
@@ -190,7 +189,7 @@ export class DockerSocketHandler extends SocketHandler {
});
// down stack
socket.on("downStack", async (stackName : unknown, callback) => {
agentSocket.on("downStack", async (stackName : unknown, callback) => {
try {
checkLogin(socket);
@@ -211,9 +210,9 @@ export class DockerSocketHandler extends SocketHandler {
});
// Services status
socket.on("serviceStatusList", async (stackName : unknown, callback) => {
agentSocket.on("serviceStatusList", async (stackName : unknown, callback) => {
try {
checkLogin(socket);
checkLogin(s);
if (typeof(stackName) !== "string") {
throw new ValidationError("Stack name must be a string");
@@ -231,7 +230,7 @@ export class DockerSocketHandler extends SocketHandler {
});
// getExternalNetworkList
socket.on("getDockerNetworkList", async (callback) => {
agentSocket.on("getDockerNetworkList", async (callback) => {
try {
checkLogin(socket);
const dockerNetworkList = await server.getDockerNetworkList();
@@ -243,25 +242,6 @@ export class DockerSocketHandler extends SocketHandler {
callbackError(e, callback);
}
});
// composerize
socket.on("composerize", async (dockerRunCommand : unknown, callback) => {
try {
checkLogin(socket);
if (typeof(dockerRunCommand) !== "string") {
throw new ValidationError("dockerRunCommand must be a string");
}
const composeTemplate = composerize(dockerRunCommand);
callback({
ok: true,
composeTemplate,
});
} catch (e) {
callbackError(e, callback);
}
});
}
async saveStack(socket : DockgeSocket, server : DockgeServer, name : unknown, composeYAML : unknown, composeENV : unknown, isAdd : unknown) : Promise<Stack> {

View File

@@ -9,7 +9,7 @@ import knex from "knex";
import Dialect from "knex/lib/dialects/sqlite3/index.js";
import sqlite from "@louislam/sqlite3";
import { sleep } from "./util-common";
import { sleep } from "../common/util-common";
interface DBConfig {
type?: "sqlite" | "mysql";

View File

@@ -0,0 +1,114 @@
import { DockgeSocket } from "./util-server";
import { io, Socket as SocketClient } from "socket.io-client";
import { log } from "./log";
/**
* Dockge Instance Manager
*/
export class DockgeInstanceManager {
protected socket : DockgeSocket;
protected instanceSocketList : Record<string, SocketClient> = {};
constructor(socket: DockgeSocket) {
this.socket = socket;
}
connect(endpoint : string, tls : boolean, username : string, password : string) {
if (this.instanceSocketList[endpoint]) {
log.debug("INSTANCEMANAGER", "Already connected to the socket server: " + endpoint);
return;
}
let url = ((tls) ? "wss://" : "ws://") + endpoint;
log.info("INSTANCEMANAGER", "Connecting to the socket server: " + endpoint);
let client = io(url, {
transports: [ "websocket", "polling" ],
extraHeaders: {
endpoint,
}
});
client.on("connect", () => {
log.info("INSTANCEMANAGER", "Connected to the socket server: " + endpoint);
client.emit("login", {
username: username,
password: password,
}, (res) => {
if (res.ok) {
log.info("INSTANCEMANAGER", "Logged in to the socket server: " + endpoint);
} else {
log.error("INSTANCEMANAGER", "Failed to login to the socket server: " + endpoint);
}
});
});
client.on("error", (err) => {
log.error("INSTANCEMANAGER", "Error from the socket server: " + endpoint);
log.error("INSTANCEMANAGER", err);
});
client.on("disconnect", () => {
log.info("INSTANCEMANAGER", "Disconnected from the socket server: " + endpoint);
});
client.on("agent", (...args : unknown[]) => {
log.debug("INSTANCEMANAGER", "Forward event");
this.socket.emit("agent", ...args);
});
this.instanceSocketList[endpoint] = client;
}
disconnect(endpoint : string) {
let client = this.instanceSocketList[endpoint];
client?.disconnect();
}
connectAll() {
if (this.socket.endpoint) {
log.info("INSTANCEMANAGER", "This connection is connected as an agent, skip connectAll()");
return;
}
let list : Record<string, {tls : boolean, username : string, password : string}> = {
};
if (process.env.DOCKGE_TEST_REMOTE_HOST) {
list[process.env.DOCKGE_TEST_REMOTE_HOST] = {
tls: false,
username: "admin",
password: process.env.DOCKGE_TEST_REMOTE_PW || "",
};
}
if (Object.keys(list).length !== 0) {
log.info("INSTANCEMANAGER", "Connecting to all instance socket server(s)...");
}
for (let endpoint in list) {
let item = list[endpoint];
this.connect(endpoint, item.tls, item.username, item.password);
}
}
disconnectAll() {
for (let endpoint in this.instanceSocketList) {
this.disconnect(endpoint);
}
}
emitToEndpoint(endpoint: string, eventName: string, ...args : unknown[]) {
log.debug("INSTANCEMANAGER", "Emitting event to endpoint: " + endpoint);
let client = this.instanceSocketList[endpoint];
if (!client) {
log.error("INSTANCEMANAGER", "Socket client not found for endpoint: " + endpoint);
throw new Error("Socket client not found for endpoint: " + endpoint);
}
client?.emit("agent", endpoint, eventName, ...args);
}
}

View File

@@ -1,3 +1,4 @@
import "dotenv/config";
import { MainRouter } from "./routers/main-router";
import * as fs from "node:fs";
import { PackageJson } from "type-fest";
@@ -17,11 +18,11 @@ import { Settings } from "./settings";
import checkVersion from "./check-version";
import dayjs from "dayjs";
import { R } from "redbean-node";
import { genSecret, isDev } from "./util-common";
import { genSecret, isDev, LooseObject } from "../common/util-common";
import { generatePasswordHash } from "./password-hash";
import { Bean } from "redbean-node/dist/bean";
import { Arguments, Config, DockgeSocket } from "./util-server";
import { DockerSocketHandler } from "./socket-handlers/docker-socket-handler";
import { DockerSocketHandler } from "./agent-socket-handlers/docker-socket-handler";
import expressStaticGzip from "express-static-gzip";
import path from "path";
import { TerminalSocketHandler } from "./socket-handlers/terminal-socket-handler";
@@ -30,9 +31,10 @@ import { Cron } from "croner";
import gracefulShutdown from "http-graceful-shutdown";
import User from "./models/user";
import childProcessAsync from "promisify-child-process";
import { Terminal } from "./terminal";
import "dotenv/config";
import { DockgeInstanceManager } from "./dockge-instance-manager";
import { AgentProxySocketHandler } from "./socket-handlers/agent-proxy-socket-handler";
import { AgentSocketHandler } from "./agent-socket-handler";
import { AgentSocket } from "../common/agent-socket";
export class DockgeServer {
app : Express;
@@ -54,10 +56,15 @@ export class DockgeServer {
*/
socketHandlerList : SocketHandler[] = [
new MainSocketHandler(),
new DockerSocketHandler(),
new TerminalSocketHandler(),
];
agentProxySocketHandler = new AgentProxySocketHandler();
agentSocketHandlerList : AgentSocketHandler[] = [
new DockerSocketHandler(),
];
/**
* Show Setup Page
*/
@@ -230,20 +237,52 @@ export class DockgeServer {
});
this.io.on("connection", async (socket: Socket) => {
log.info("server", "Socket connected!");
let dockgeSocket = socket as DockgeSocket;
dockgeSocket.instanceManager = new DockgeInstanceManager(dockgeSocket);
dockgeSocket.emitAgent = (event : string, ...args : unknown[]) => {
let obj = args[0];
if (typeof(obj) === "object") {
let obj2 = obj as LooseObject;
obj2.endpoint = dockgeSocket.endpoint;
}
dockgeSocket.emit("agent", event, ...args);
};
this.sendInfo(socket, true);
if (typeof(socket.request.headers.endpoint) === "string") {
dockgeSocket.endpoint = socket.request.headers.endpoint;
} else {
dockgeSocket.endpoint = "";
}
if (dockgeSocket.endpoint) {
log.info("server", "Socket connected (agent), as endpoint " + dockgeSocket.endpoint);
} else {
log.info("server", "Socket connected (direct)");
}
this.sendInfo(dockgeSocket, true);
if (this.needSetup) {
log.info("server", "Redirect to setup page");
socket.emit("setup");
dockgeSocket.emit("setup");
}
// Create socket handlers
// Create socket handlers (original, no agent support)
for (const socketHandler of this.socketHandlerList) {
socketHandler.create(socket as DockgeSocket, this);
socketHandler.create(dockgeSocket, this);
}
// Create Agent Socket
let agentSocket = new AgentSocket();
// Create agent socket handlers
for (const socketHandler of this.agentSocketHandlerList) {
socketHandler.create(dockgeSocket, this, agentSocket);
}
// Create agent proxy socket handlers
this.agentProxySocketHandler.create2(dockgeSocket, this, agentSocket);
// ***************************
// Better do anything after added all socket handlers here
// ***************************
@@ -251,12 +290,18 @@ export class DockgeServer {
log.debug("auth", "check auto login");
if (await Settings.get("disableAuth")) {
log.info("auth", "Disabled Auth: auto login to admin");
this.afterLogin(socket as DockgeSocket, await R.findOne("user") as User);
socket.emit("autoLogin");
this.afterLogin(dockgeSocket, await R.findOne("user") as User);
dockgeSocket.emit("autoLogin");
} else {
log.debug("auth", "need auth");
}
// Socket disconnect
dockgeSocket.on("disconnect", () => {
log.info("server", "Socket disconnected!");
dockgeSocket.instanceManager.disconnectAll();
});
});
this.io.on("disconnect", () => {
@@ -265,7 +310,7 @@ export class DockgeServer {
if (isDev) {
setInterval(() => {
log.debug("terminal", "Terminal count: " + Terminal.getTerminalCount());
//log.debug("terminal", "Terminal count: " + Terminal.getTerminalCount());
}, 5000);
}
}
@@ -281,6 +326,9 @@ export class DockgeServer {
} catch (e) {
log.error("server", e);
}
// Also connect to other dockge instances
socket.instanceManager.connectAll();
}
/**
@@ -519,26 +567,34 @@ export class DockgeServer {
return jwtSecretBean;
}
/**
* Send stack list to all connected sockets
* @param useCache
*/
async sendStackList(useCache = false) {
let roomList = this.io.sockets.adapter.rooms.keys();
let map : Map<string, object> | undefined;
let socketList = this.io.sockets.sockets.values();
let stackList;
for (let socket of socketList) {
let dockgeSocket = socket as DockgeSocket;
for (let room of roomList) {
// Check if the room is a number (user id)
if (Number(room)) {
if (dockgeSocket.userID) {
// Get the list only if there is a room
if (!map) {
map = new Map();
let stackList = await Stack.getStackList(this, useCache);
for (let [ stackName, stack ] of stackList) {
map.set(stackName, stack.toSimpleJSON());
}
// Get the list only if there is a logged in user
if (!stackList) {
stackList = await Stack.getStackList(this, useCache);
}
log.debug("server", "Send stack list to room " + room);
this.io.to(room).emit("stackList", {
let map : Map<string, object> = new Map();
for (let [ stackName, stack ] of stackList) {
map.set(stackName, stack.toSimpleJSON(dockgeSocket.endpoint));
}
log.debug("server", "Send stack list");
dockgeSocket.emitAgent("stackList", {
ok: true,
stackList: Object.fromEntries(map),
});

View File

@@ -1,6 +1,6 @@
// Console colors
// https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color
import { intHash, isDev } from "./util-common";
import { intHash, isDev } from "../common/util-common";
import dayjs from "dayjs";
export const CONSOLE_STYLE_Reset = "\x1b[0m";

View File

@@ -1,6 +1,6 @@
import { R } from "redbean-node";
import { log } from "./log";
import { LooseObject } from "./util-common";
import { LooseObject } from "../common/util-common";
export class Settings {

View File

@@ -0,0 +1,43 @@
import { SocketHandler } from "../socket-handler.js";
import { DockgeServer } from "../dockge-server";
import { log } from "../log";
import { checkLogin, DockgeSocket } from "../util-server";
import { AgentSocket } from "../../common/agent-socket";
export class AgentProxySocketHandler extends SocketHandler {
create2(socket : DockgeSocket, server : DockgeServer, agentSocket : AgentSocket) {
// Agent - proxying requests if needed
socket.on("agent", async (endpoint : unknown, eventName : unknown, ...args : unknown[]) => {
try {
checkLogin(socket);
// Check Type
if (typeof(endpoint) !== "string") {
throw new Error("Endpoint must be a string");
}
if (typeof(eventName) !== "string") {
throw new Error("Event name must be a string");
}
log.debug("agent", "Proxying request to " + endpoint + " for " + eventName);
// Direct connection or matching endpoint
if (!endpoint || endpoint === socket.endpoint) {
log.debug("agent", "Direct connection");
agentSocket.call(eventName, ...args);
} else {
socket.instanceManager.emitToEndpoint(endpoint, eventName, ...args);
}
} catch (e) {
if (e instanceof Error) {
log.warn("agent", e.message);
}
}
});
}
create(socket : DockgeSocket, server : DockgeServer) {
throw new Error("Method not implemented. Please use create2 instead.");
}
}

View File

@@ -1,3 +1,5 @@
// @ts-ignore
import composerize from "composerize";
import { SocketHandler } from "../socket-handler.js";
import { DockgeServer } from "../dockge-server";
import { log } from "../log";
@@ -5,7 +7,14 @@ import { R } from "redbean-node";
import { loginRateLimiter, twoFaRateLimiter } from "../rate-limiter";
import { generatePasswordHash, needRehashPassword, shake256, SHAKE256_LENGTH, verifyPassword } from "../password-hash";
import { User } from "../models/user";
import { checkLogin, DockgeSocket, doubleCheckPassword, JWTDecoded } from "../util-server";
import {
callbackError,
checkLogin,
DockgeSocket,
doubleCheckPassword,
JWTDecoded,
ValidationError
} from "../util-server";
import { passwordStrength } from "check-password-strength";
import jwt from "jsonwebtoken";
import { Settings } from "../settings";
@@ -294,6 +303,25 @@ export class MainSocketHandler extends SocketHandler {
}
}
});
// composerize
socket.on("composerize", async (dockerRunCommand : unknown, callback) => {
try {
checkLogin(socket);
if (typeof(dockerRunCommand) !== "string") {
throw new ValidationError("dockerRunCommand must be a string");
}
const composeTemplate = composerize(dockerRunCommand);
callback({
ok: true,
composeTemplate,
});
} catch (e) {
callbackError(e, callback);
}
});
}
async login(username : string, password : string) : Promise<User | null> {

View File

@@ -11,7 +11,7 @@ import {
getComposeTerminalName, getContainerExecTerminalName,
isDev,
PROGRESS_TERMINAL_ROWS
} from "../util-common";
} from "../../common/util-common";
import { InteractiveTerminal, MainTerminal, Terminal } from "../terminal";
import { Stack } from "../stack";

View File

@@ -15,7 +15,7 @@ import {
PROGRESS_TERMINAL_ROWS,
RUNNING, TERMINAL_ROWS,
UNKNOWN
} from "./util-common";
} from "../common/util-common";
import { InteractiveTerminal, Terminal } from "./terminal";
import childProcessAsync from "promisify-child-process";
@@ -50,8 +50,8 @@ export class Stack {
}
}
toJSON() : object {
let obj = this.toSimpleJSON();
toJSON(endpoint : string) : object {
let obj = this.toSimpleJSON(endpoint);
return {
...obj,
composeYAML: this.composeYAML,
@@ -59,13 +59,14 @@ export class Stack {
};
}
toSimpleJSON() : object {
toSimpleJSON(endpoint : string) : object {
return {
name: this.name,
status: this._status,
tags: [],
isManagedByDockge: this.isManagedByDockge,
composeFileName: this._composeFileName,
endpoint,
};
}
@@ -186,8 +187,8 @@ export class Stack {
}
}
async deploy(socket? : DockgeSocket) : Promise<number> {
const terminalName = getComposeTerminalName(this.name);
async deploy(socket : DockgeSocket) : Promise<number> {
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "up", "-d", "--remove-orphans" ], this.path);
if (exitCode !== 0) {
throw new Error("Failed to deploy, please check the terminal output for more information.");
@@ -195,8 +196,8 @@ export class Stack {
return exitCode;
}
async delete(socket?: DockgeSocket) : Promise<number> {
const terminalName = getComposeTerminalName(this.name);
async delete(socket: DockgeSocket) : Promise<number> {
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "down", "--remove-orphans" ], this.path);
if (exitCode !== 0) {
throw new Error("Failed to delete, please check the terminal output for more information.");
@@ -388,7 +389,7 @@ export class Stack {
}
async start(socket: DockgeSocket) {
const terminalName = getComposeTerminalName(this.name);
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "up", "-d", "--remove-orphans" ], this.path);
if (exitCode !== 0) {
throw new Error("Failed to start, please check the terminal output for more information.");
@@ -397,7 +398,7 @@ export class Stack {
}
async stop(socket: DockgeSocket) : Promise<number> {
const terminalName = getComposeTerminalName(this.name);
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "stop" ], this.path);
if (exitCode !== 0) {
throw new Error("Failed to stop, please check the terminal output for more information.");
@@ -406,7 +407,7 @@ export class Stack {
}
async restart(socket: DockgeSocket) : Promise<number> {
const terminalName = getComposeTerminalName(this.name);
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "restart" ], this.path);
if (exitCode !== 0) {
throw new Error("Failed to restart, please check the terminal output for more information.");
@@ -415,7 +416,7 @@ export class Stack {
}
async down(socket: DockgeSocket) : Promise<number> {
const terminalName = getComposeTerminalName(this.name);
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "down" ], this.path);
if (exitCode !== 0) {
throw new Error("Failed to down, please check the terminal output for more information.");
@@ -424,7 +425,7 @@ export class Stack {
}
async update(socket: DockgeSocket) {
const terminalName = getComposeTerminalName(this.name);
const terminalName = getComposeTerminalName(socket.endpoint, this.name);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "pull" ], this.path);
if (exitCode !== 0) {
throw new Error("Failed to pull, please check the terminal output for more information.");
@@ -445,7 +446,7 @@ export class Stack {
}
async joinCombinedTerminal(socket: DockgeSocket) {
const terminalName = getCombinedTerminalName(this.name);
const terminalName = getCombinedTerminalName(socket.endpoint, this.name);
const terminal = Terminal.getOrCreateTerminal(this.server, terminalName, "docker", [ "compose", "logs", "-f", "--tail", "100" ], this.path);
terminal.enableKeepAlive = true;
terminal.rows = COMBINED_TERMINAL_ROWS;
@@ -455,7 +456,7 @@ export class Stack {
}
async leaveCombinedTerminal(socket: DockgeSocket) {
const terminalName = getCombinedTerminalName(this.name);
const terminalName = getCombinedTerminalName(socket.endpoint, this.name);
const terminal = Terminal.getTerminal(terminalName);
if (terminal) {
terminal.leave(socket);
@@ -463,7 +464,7 @@ export class Stack {
}
async joinContainerTerminal(socket: DockgeSocket, serviceName: string, shell : string = "sh", index: number = 0) {
const terminalName = getContainerExecTerminalName(this.name, serviceName, index);
const terminalName = getContainerExecTerminalName(socket.endpoint, this.name, serviceName, index);
let terminal = Terminal.getTerminal(terminalName);
if (!terminal) {

View File

@@ -8,7 +8,7 @@ import {
PROGRESS_TERMINAL_ROWS,
TERMINAL_COLS,
TERMINAL_ROWS
} from "./util-common";
} from "../common/util-common";
import { sync as commandExistsSync } from "command-exists";
import { log } from "./log";

View File

@@ -1,407 +0,0 @@
/*
* Common utilities for backend and frontend
*/
import yaml, { Document, Pair, Scalar } from "yaml";
import { DotenvParseOutput } from "dotenv";
// Init dayjs
import dayjs from "dayjs";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import relativeTime from "dayjs/plugin/relativeTime";
// @ts-ignore
import { replaceVariablesSync } from "@inventage/envsubst";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(relativeTime);
export interface LooseObject {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any
}
export interface BaseRes {
ok: boolean;
msg?: string;
}
let randomBytes : (numBytes: number) => Uint8Array;
initRandomBytes();
async function initRandomBytes() {
if (typeof window !== "undefined" && window.crypto) {
randomBytes = function randomBytes(numBytes: number) {
const bytes = new Uint8Array(numBytes);
for (let i = 0; i < numBytes; i += 65536) {
window.crypto.getRandomValues(bytes.subarray(i, i + Math.min(numBytes - i, 65536)));
}
return bytes;
};
} else {
randomBytes = (await import("node:crypto")).randomBytes;
}
}
// Stack Status
export const UNKNOWN = 0;
export const CREATED_FILE = 1;
export const CREATED_STACK = 2;
export const RUNNING = 3;
export const EXITED = 4;
export function statusName(status : number) : string {
switch (status) {
case CREATED_FILE:
return "draft";
case CREATED_STACK:
return "created_stack";
case RUNNING:
return "running";
case EXITED:
return "exited";
default:
return "unknown";
}
}
export function statusNameShort(status : number) : string {
switch (status) {
case CREATED_FILE:
return "inactive";
case CREATED_STACK:
return "inactive";
case RUNNING:
return "active";
case EXITED:
return "exited";
default:
return "?";
}
}
export function statusColor(status : number) : string {
switch (status) {
case CREATED_FILE:
return "dark";
case CREATED_STACK:
return "dark";
case RUNNING:
return "primary";
case EXITED:
return "danger";
default:
return "secondary";
}
}
export const isDev = process.env.NODE_ENV === "development";
export const TERMINAL_COLS = 105;
export const TERMINAL_ROWS = 10;
export const PROGRESS_TERMINAL_ROWS = 8;
export const COMBINED_TERMINAL_COLS = 58;
export const COMBINED_TERMINAL_ROWS = 20;
export const ERROR_TYPE_VALIDATION = 1;
export const allowedCommandList : string[] = [
"docker",
"ls",
"cd",
"dir",
];
export const allowedRawKeys = [
"\u0003", // Ctrl + C
];
export const acceptedComposeFileNames = [
"compose.yaml",
"docker-compose.yaml",
"docker-compose.yml",
"compose.yml",
];
/**
* Generate a decimal integer number from a string
* @param str Input
* @param length Default is 10 which means 0 - 9
*/
export function intHash(str : string, length = 10) : number {
// A simple hashing function (you can use more complex hash functions if needed)
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash += str.charCodeAt(i);
}
// Normalize the hash to the range [0, 10]
return (hash % length + length) % length; // Ensure the result is non-negative
}
/**
* Delays for specified number of seconds
* @param ms Number of milliseconds to sleep for
*/
export function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Generate a random alphanumeric string of fixed length
* @param length Length of string to generate
* @returns string
*/
export function genSecret(length = 64) {
let secret = "";
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charsLength = chars.length;
for ( let i = 0; i < length; i++ ) {
secret += chars.charAt(getCryptoRandomInt(0, charsLength - 1));
}
return secret;
}
/**
* Get a random integer suitable for use in cryptography between upper
* and lower bounds.
* @param min Minimum value of integer
* @param max Maximum value of integer
* @returns Cryptographically suitable random integer
*/
export function getCryptoRandomInt(min: number, max: number):number {
// synchronous version of: https://github.com/joepie91/node-random-number-csprng
const range = max - min;
if (range >= Math.pow(2, 32)) {
console.log("Warning! Range is too large.");
}
let tmpRange = range;
let bitsNeeded = 0;
let bytesNeeded = 0;
let mask = 1;
while (tmpRange > 0) {
if (bitsNeeded % 8 === 0) {
bytesNeeded += 1;
}
bitsNeeded += 1;
mask = mask << 1 | 1;
tmpRange = tmpRange >>> 1;
}
const bytes = randomBytes(bytesNeeded);
let randomValue = 0;
for (let i = 0; i < bytesNeeded; i++) {
randomValue |= bytes[i] << 8 * i;
}
randomValue = randomValue & mask;
if (randomValue <= range) {
return min + randomValue;
} else {
return getCryptoRandomInt(min, max);
}
}
export function getComposeTerminalName(stack : string) {
return "compose-" + stack;
}
export function getCombinedTerminalName(stack : string) {
return "combined-" + stack;
}
export function getContainerTerminalName(container : string) {
return "container-" + container;
}
export function getContainerExecTerminalName(stackName : string, container : string, index : number) {
return "container-exec-" + stackName + "-" + container + "-" + index;
}
export function copyYAMLComments(doc : Document, src : Document) {
doc.comment = src.comment;
doc.commentBefore = src.commentBefore;
if (doc && doc.contents && src && src.contents) {
// @ts-ignore
copyYAMLCommentsItems(doc.contents.items, src.contents.items);
}
}
/**
* Copy yaml comments from srcItems to items
* Typescript is super annoying here, so I have to use any here
* TODO: Since comments are belong to the array index, the comments will be lost if the order of the items is changed or removed or added.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function copyYAMLCommentsItems(items : any, srcItems : any) {
if (!items || !srcItems) {
return;
}
for (let i = 0; i < items.length; i++) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const item : any = items[i];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const srcItem : any = srcItems[i];
if (!srcItem) {
continue;
}
if (item.key && srcItem.key) {
item.key.comment = srcItem.key.comment;
item.key.commentBefore = srcItem.key.commentBefore;
}
if (srcItem.comment) {
item.comment = srcItem.comment;
}
if (item.value && srcItem.value) {
if (typeof item.value === "object" && typeof srcItem.value === "object") {
item.value.comment = srcItem.value.comment;
item.value.commentBefore = srcItem.value.commentBefore;
if (item.value.items && srcItem.value.items) {
copyYAMLCommentsItems(item.value.items, srcItem.value.items);
}
}
}
}
}
/**
* Possible Inputs:
* ports:
* - "3000"
* - "3000-3005"
* - "8000:8000"
* - "9090-9091:8080-8081"
* - "49100:22"
* - "8000-9000:80"
* - "127.0.0.1:8001:8001"
* - "127.0.0.1:5000-5010:5000-5010"
* - "6060:6060/udp"
* @param input
* @param defaultHostname
*/
export function parseDockerPort(input : string, defaultHostname : string = "localhost") {
let hostname = defaultHostname;
let port;
let display;
const parts = input.split("/");
const part1 = parts[0];
let protocol = parts[1] || "tcp";
// Split the last ":"
const lastColon = part1.lastIndexOf(":");
if (lastColon === -1) {
// No colon, so it's just a port or port range
// Check if it's a port range
const dash = part1.indexOf("-");
if (dash === -1) {
// No dash, so it's just a port
port = part1;
} else {
// Has dash, so it's a port range, use the first port
port = part1.substring(0, dash);
}
display = part1;
} else {
// Has colon, so it's a port mapping
let hostPart = part1.substring(0, lastColon);
display = hostPart;
// Check if it's a port range
const dash = part1.indexOf("-");
if (dash !== -1) {
// Has dash, so it's a port range, use the first port
hostPart = part1.substring(0, dash);
}
// Check if it has a ip (ip:port)
const colon = hostPart.indexOf(":");
if (colon !== -1) {
// Has colon, so it's a ip:port
hostname = hostPart.substring(0, colon);
port = hostPart.substring(colon + 1);
} else {
// No colon, so it's just a port
port = hostPart;
}
}
let portInt = parseInt(port);
if (portInt == 443) {
protocol = "https";
} else if (protocol === "tcp") {
protocol = "http";
}
return {
url: protocol + "://" + hostname + ":" + portInt,
display: display,
};
}
export function envsubst(string : string, variables : LooseObject) : string {
return replaceVariablesSync(string, variables)[0];
}
/**
* Traverse all values in the yaml and for each value, if there are template variables, replace it environment variables
* Emulates the behavior of how docker-compose handles environment variables in yaml files
* @param content Yaml string
* @param env Environment variables
* @returns string Yaml string with environment variables replaced
*/
export function envsubstYAML(content : string, env : DotenvParseOutput) : string {
const doc = yaml.parseDocument(content);
if (doc.contents) {
// @ts-ignore
for (const item of doc.contents.items) {
traverseYAML(item, env);
}
}
return doc.toString();
}
/**
* Used for envsubstYAML(...)
* @param pair
* @param env
*/
function traverseYAML(pair : Pair, env : DotenvParseOutput) : void {
// @ts-ignore
if (pair.value && pair.value.items) {
// @ts-ignore
for (const item of pair.value.items) {
if (item instanceof Pair) {
traverseYAML(item, env);
} else if (item instanceof Scalar) {
let value = item.value as unknown;
if (typeof(value) === "string") {
item.value = envsubst(value, env);
}
}
}
// @ts-ignore
} else if (pair.value && typeof(pair.value.value) === "string") {
// @ts-ignore
pair.value.value = envsubst(pair.value.value, env);
}
}

View File

@@ -2,10 +2,11 @@ import { Socket } from "socket.io";
import { Terminal } from "./terminal";
import { randomBytes } from "crypto";
import { log } from "./log";
import { ERROR_TYPE_VALIDATION } from "./util-common";
import { ERROR_TYPE_VALIDATION } from "../common/util-common";
import { R } from "redbean-node";
import { verifyPassword } from "./password-hash";
import fs from "fs";
import { DockgeInstanceManager } from "./dockge-instance-manager";
export interface JWTDecoded {
username : string;
@@ -15,6 +16,9 @@ export interface JWTDecoded {
export interface DockgeSocket extends Socket {
userID: number;
consoleTerminal? : Terminal;
instanceManager : DockgeInstanceManager;
endpoint : string;
emitAgent : (eventName : string, ...args : unknown[]) => void;
}
// For command line arguments, so they are nullable