Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
services:
termix:
build:
context: ..
dockerfile: docker/Dockerfile
container_name: termix
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- termix_data:/app/db/data
environment:
- NODE_ENV=production
- PORT=8080
- GUACD_HOST=guacd
- GUACD_PORT=4822
- ENABLE_GUACAMOLE=true
depends_on:
- guacd
networks:
- termix-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s

guacd:
image: guacamole/guacd:latest
container_name: termix-guacd
restart: unless-stopped
networks:
- termix-network
healthcheck:
test: ["CMD", "nc", "-z", "localhost", "4822"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s

networks:
termix-network:
driver: bridge

volumes:
termix_data:
driver: local

35 changes: 35 additions & 0 deletions docker/nginx-https.conf
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,41 @@ http {
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
}

# Guacamole WebSocket for RDP/VNC/Telnet
# ^~ modifier ensures this takes precedence over the regex location below
location ^~ /guacamole/websocket/ {
proxy_pass http://127.0.0.1:30007/;
proxy_http_version 1.1;

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
proxy_connect_timeout 10s;

proxy_buffering off;
proxy_request_buffering off;

proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
}

# Guacamole REST API
location ~ ^/guacamole(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

location /ssh/tunnel/ {
proxy_pass http://127.0.0.1:30003;
proxy_http_version 1.1;
Expand Down
35 changes: 35 additions & 0 deletions docker/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,41 @@ http {
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
}

# Guacamole WebSocket for RDP/VNC/Telnet
# ^~ modifier ensures this takes precedence over the regex location below
location ^~ /guacamole/websocket/ {
proxy_pass http://127.0.0.1:30007/;
proxy_http_version 1.1;

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
proxy_connect_timeout 10s;

proxy_buffering off;
proxy_request_buffering off;

proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;
}

# Guacamole REST API
location ~ ^/guacamole(/.*)?$ {
proxy_pass http://127.0.0.1:30001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

location /ssh/tunnel/ {
proxy_pass http://127.0.0.1:30003;
proxy_http_version 1.1;
Expand Down
29 changes: 27 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@tailwindcss/vite": "^4.1.14",
"@types/bcryptjs": "^2.4.6",
"@types/cookie-parser": "^1.4.9",
"@types/guacamole-common-js": "^1.5.5",
"@types/jszip": "^3.4.0",
"@types/multer": "^2.0.0",
"@types/qrcode": "^1.5.5",
Expand All @@ -76,6 +77,8 @@
"dotenv": "^17.2.0",
"drizzle-orm": "^0.44.3",
"express": "^5.1.0",
"guacamole-common-js": "^1.5.0",
"guacamole-lite": "^1.2.0",
"i18next": "^25.4.2",
"i18next-browser-languagedetector": "^8.2.0",
"jose": "^5.2.3",
Expand Down
2 changes: 2 additions & 0 deletions src/backend/database/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import alertRoutes from "./routes/alerts.js";
import credentialsRoutes from "./routes/credentials.js";
import snippetsRoutes from "./routes/snippets.js";
import terminalRoutes from "./routes/terminal.js";
import guacamoleRoutes from "../guacamole/routes.js";
import cors from "cors";
import fetch from "node-fetch";
import fs from "fs";
Expand Down Expand Up @@ -1436,6 +1437,7 @@ app.use("/alerts", alertRoutes);
app.use("/credentials", credentialsRoutes);
app.use("/snippets", snippetsRoutes);
app.use("/terminal", terminalRoutes);
app.use("/guacamole", guacamoleRoutes);

app.use(
(
Expand Down
7 changes: 7 additions & 0 deletions src/backend/database/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,13 @@ const migrateSchema = () => {
);
addColumnIfNotExists("ssh_data", "docker_config", "TEXT");

// Connection type columns for RDP/VNC/Telnet support
addColumnIfNotExists("ssh_data", "connection_type", 'TEXT NOT NULL DEFAULT "ssh"');
addColumnIfNotExists("ssh_data", "domain", "TEXT");
addColumnIfNotExists("ssh_data", "security", "TEXT");
addColumnIfNotExists("ssh_data", "ignore_cert", "INTEGER NOT NULL DEFAULT 0");
addColumnIfNotExists("ssh_data", "guacamole_config", "TEXT");

addColumnIfNotExists("ssh_credentials", "private_key", "TEXT");
addColumnIfNotExists("ssh_credentials", "public_key", "TEXT");
addColumnIfNotExists("ssh_credentials", "detected_key_type", "TEXT");
Expand Down
8 changes: 8 additions & 0 deletions src/backend/database/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ export const sshData = sqliteTable("ssh_data", {
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
// Connection type: ssh, rdp, vnc, telnet
connectionType: text("connection_type").notNull().default("ssh"),
name: text("name"),
ip: text("ip").notNull(),
port: integer("port").notNull(),
Expand Down Expand Up @@ -94,6 +96,12 @@ export const sshData = sqliteTable("ssh_data", {
dockerConfig: text("docker_config"),
terminalConfig: text("terminal_config"),
quickActions: text("quick_actions"),
// RDP/VNC specific fields
domain: text("domain"),
security: text("security"),
ignoreCert: integer("ignore_cert", { mode: "boolean" }).default(false),
// RDP/VNC extended configuration (stored as JSON)
guacamoleConfig: text("guacamole_config"),
createdAt: text("created_at")
.notNull()
.default(sql`CURRENT_TIMESTAMP`),
Expand Down
37 changes: 37 additions & 0 deletions src/backend/database/routes/ssh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ router.post(
}

const {
connectionType,
name,
folder,
tags,
Expand All @@ -244,6 +245,11 @@ router.post(
dockerConfig,
terminalConfig,
forceKeyboardInteractive,
// RDP/VNC specific fields
domain,
security,
ignoreCert,
guacamoleConfig,
} = hostData;
if (
!isNonEmptyString(userId) ||
Expand All @@ -261,8 +267,10 @@ router.post(
}

const effectiveAuthType = authType || authMethod;
const effectiveConnectionType = connectionType || "ssh";
const sshDataObj: Record<string, unknown> = {
userId: userId,
connectionType: effectiveConnectionType,
name,
folder: folder || null,
tags: Array.isArray(tags) ? tags.join(",") : tags || "",
Expand All @@ -288,6 +296,11 @@ router.post(
dockerConfig: dockerConfig ? JSON.stringify(dockerConfig) : null,
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
// RDP/VNC specific fields
domain: domain || null,
security: security || null,
ignoreCert: ignoreCert ? 1 : 0,
guacamoleConfig: guacamoleConfig ? JSON.stringify(guacamoleConfig) : null,
};

if (effectiveAuthType === "password") {
Expand Down Expand Up @@ -352,6 +365,9 @@ router.post(
dockerConfig: createdHost.dockerConfig
? JSON.parse(createdHost.dockerConfig as string)
: undefined,
guacamoleConfig: createdHost.guacamoleConfig
? JSON.parse(createdHost.guacamoleConfig as string)
: undefined,
};

const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost;
Expand Down Expand Up @@ -448,6 +464,7 @@ router.put(
}

const {
connectionType,
name,
folder,
tags,
Expand All @@ -474,6 +491,11 @@ router.put(
dockerConfig,
terminalConfig,
forceKeyboardInteractive,
// RDP/VNC specific fields
domain,
security,
ignoreCert,
guacamoleConfig,
} = hostData;
if (
!isNonEmptyString(userId) ||
Expand All @@ -494,6 +516,7 @@ router.put(

const effectiveAuthType = authType || authMethod;
const sshDataObj: Record<string, unknown> = {
connectionType: connectionType || "ssh",
name,
folder,
tags: Array.isArray(tags) ? tags.join(",") : tags || "",
Expand All @@ -519,6 +542,11 @@ router.put(
dockerConfig: dockerConfig ? JSON.stringify(dockerConfig) : null,
terminalConfig: terminalConfig ? JSON.stringify(terminalConfig) : null,
forceKeyboardInteractive: forceKeyboardInteractive ? "true" : "false",
// RDP/VNC specific fields
domain: domain || null,
security: security || null,
ignoreCert: ignoreCert ? 1 : 0,
guacamoleConfig: guacamoleConfig ? JSON.stringify(guacamoleConfig) : null,
};

if (effectiveAuthType === "password") {
Expand Down Expand Up @@ -601,6 +629,9 @@ router.put(
dockerConfig: updatedHost.dockerConfig
? JSON.parse(updatedHost.dockerConfig as string)
: undefined,
guacamoleConfig: updatedHost.guacamoleConfig
? JSON.parse(updatedHost.guacamoleConfig as string)
: undefined,
};

const resolvedHost = (await resolveHostCredentials(baseHost)) || baseHost;
Expand Down Expand Up @@ -709,6 +740,9 @@ router.get(
terminalConfig: row.terminalConfig
? JSON.parse(row.terminalConfig as string)
: undefined,
guacamoleConfig: row.guacamoleConfig
? JSON.parse(row.guacamoleConfig as string)
: undefined,
forceKeyboardInteractive: row.forceKeyboardInteractive === "true",
};

Expand Down Expand Up @@ -784,6 +818,9 @@ router.get(
terminalConfig: host.terminalConfig
? JSON.parse(host.terminalConfig)
: undefined,
guacamoleConfig: host.guacamoleConfig
? JSON.parse(host.guacamoleConfig)
: undefined,
forceKeyboardInteractive: host.forceKeyboardInteractive === "true",
};

Expand Down
Loading