mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-15 21:03:47 +00:00
v2.0.0 (#621)
* Guacd, Docker-Compose, RDP (#475) * fix select edit host but not update view (#438) * fix: Checksum issue with chocolatey * fix: Remove homebrew old stuff * Add Korean translation (#439) Co-authored-by: 송준우 <2484@coreit.co.kr> * feat: Automate flatpak * fix: Add imagemagik to electron builder to resolve build error * fix: Build error with runtime repo flag * fix: Flatpak runtime error and install freedesktop ver warning * fix: Flatpak runtime error and install freedesktop ver warning * feat: Re-add homebrew cask and move scripts to backend * fix: No sandbox flag issue * fix: Change name for electron macos cask output * fix: Sandbox error with Linux * fix: Remove comming soon for app stores in readme * Adding Comment at the end of the public_key on the host on deploy (#440) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * -Add New Interface for Credential DB -Add Credential Name as a comment into the server authorized_key file --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Sudo auto fill password (#441) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Feature Sudo password auto-fill; * Fix locale json shema; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Added Italian Language; (#445) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Added Italian Language; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Auto collapse snippet folders (#448) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * feat: Add collapsable snippets (customizable in user profile) * Translations (#447) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Added Italian Language; * Fix translations; Removed duplicate keys, synchronised other languages using English as the source, translated added keys, fixed inaccurate translations. --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Remove PTY-level keepalive (#449) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Remove PTY-level keepalive to prevent unwanted terminal output; use SSH-level keepalive instead --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add Guacamole support for RDP, VNC, and Telnet connections - Implemented WebSocket support for Guacamole in Nginx configuration. - Added REST API endpoints for generating connection tokens and checking guacd status. - Created Guacamole server using guacamole-lite for handling connections. - Developed frontend components for testing RDP/VNC connections and displaying the remote session. - Updated package dependencies to include guacamole-common-js and guacamole-lite. - Enhanced logging for Guacamole operations. * feat: enhance Guacamole support with RDP and VNC connection settings and UI updates * feat: Seperate server stats and tunnel management (improved both UI's) then started initial docker implementation * fix: finalize adding docker to db * fix: merge syntax errors * feat: implement mouse coordinate adjustment based on scale factor in GuacamoleDisplay * feat: add TypeScript definitions for guacamole-common-js module * feat: enhance Mouse.State constructor to accept optional parameters and object destructuring * feat: Add support for RDP and VNC connections in SSH host management - Introduced connectionType field to differentiate between SSH, RDP, VNC, and Telnet in host data structures. - Updated backend routes to handle RDP/VNC specific fields: domain, security, and ignoreCert. - Enhanced the HostManagerEditor to include RDP/VNC specific settings and authentication options. - Implemented token retrieval for RDP/VNC connections using Guacamole API. - Updated UI components to reflect connection type changes and provide appropriate connection buttons. - Removed the GuacamoleTestDialog component as its functionality is integrated into the HostManagerEditor. - Adjusted the TopNavbar and Host components to accommodate new connection types and their respective actions. * feat: Enhance Guacamole integration with extended configuration options - Added detailed Guacamole configuration interface for RDP/VNC/Telnet connections, including display, audio, performance, and session settings. - Implemented logging for token requests and received options for better debugging. - Updated HostManagerEditor to support new Guacamole configuration fields with validation and default values. - Integrated Guacamole configuration parsing in HostManagerViewer and Host components. - Enhanced API requests to include extended Guacamole configuration parameters in the token request. - Refactored code to convert camelCase configuration keys to kebab-case for compatibility with Guacamole API. * feat: merge guacd into 2.0.0 and improve UI for host manager and made general bug fixes --------- Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: junu <bigdwarf_@naver.com> Co-authored-by: 송준우 <2484@coreit.co.kr> Co-authored-by: SlimGary <trash.slim@gmail.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: rename api routes and files * feat: improve guacd ui/backend * feat: improve guacd ui/backend * fix: state persistance issues causing refresh * feat: improge guacd connections, fixed telnet not opening, and improved general guacd integration * feat: continue improving integration also with bug fixes * Merge 2.0.0 with 2.0.0 that includes bug fixes (#620) * Guacd, Docker-Compose, RDP (#475) * fix select edit host but not update view (#438) * fix: Checksum issue with chocolatey * fix: Remove homebrew old stuff * Add Korean translation (#439) Co-authored-by: 송준우 <2484@coreit.co.kr> * feat: Automate flatpak * fix: Add imagemagik to electron builder to resolve build error * fix: Build error with runtime repo flag * fix: Flatpak runtime error and install freedesktop ver warning * fix: Flatpak runtime error and install freedesktop ver warning * feat: Re-add homebrew cask and move scripts to backend * fix: No sandbox flag issue * fix: Change name for electron macos cask output * fix: Sandbox error with Linux * fix: Remove comming soon for app stores in readme * Adding Comment at the end of the public_key on the host on deploy (#440) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * -Add New Interface for Credential DB -Add Credential Name as a comment into the server authorized_key file --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Sudo auto fill password (#441) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Feature Sudo password auto-fill; * Fix locale json shema; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Added Italian Language; (#445) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Added Italian Language; --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Auto collapse snippet folders (#448) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * feat: Add collapsable snippets (customizable in user profile) * Translations (#447) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Added Italian Language; * Fix translations; Removed duplicate keys, synchronised other languages using English as the source, translated added keys, fixed inaccurate translations. --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * Remove PTY-level keepalive (#449) * Add termix.rb Cask file * Update Termix to version 1.9.0 with new checksum * Update README to remove 'coming soon' notes * Remove PTY-level keepalive to prevent unwanted terminal output; use SSH-level keepalive instead --------- Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> * feat: add Guacamole support for RDP, VNC, and Telnet connections - Implemented WebSocket support for Guacamole in Nginx configuration. - Added REST API endpoints for generating connection tokens and checking guacd status. - Created Guacamole server using guacamole-lite for handling connections. - Developed frontend components for testing RDP/VNC connections and displaying the remote session. - Updated package dependencies to include guacamole-common-js and guacamole-lite. - Enhanced logging for Guacamole operations. * feat: enhance Guacamole support with RDP and VNC connection settings and UI updates * feat: Seperate server stats and tunnel management (improved both UI's) then started initial docker implementation * fix: finalize adding docker to db * fix: merge syntax errors * feat: implement mouse coordinate adjustment based on scale factor in GuacamoleDisplay * feat: add TypeScript definitions for guacamole-common-js module * feat: enhance Mouse.State constructor to accept optional parameters and object destructuring * feat: Add support for RDP and VNC connections in SSH host management - Introduced connectionType field to differentiate between SSH, RDP, VNC, and Telnet in host data structures. - Updated backend routes to handle RDP/VNC specific fields: domain, security, and ignoreCert. - Enhanced the HostManagerEditor to include RDP/VNC specific settings and authentication options. - Implemented token retrieval for RDP/VNC connections using Guacamole API. - Updated UI components to reflect connection type changes and provide appropriate connection buttons. - Removed the GuacamoleTestDialog component as its functionality is integrated into the HostManagerEditor. - Adjusted the TopNavbar and Host components to accommodate new connection types and their respective actions. * feat: Enhance Guacamole integration with extended configuration options - Added detailed Guacamole configuration interface for RDP/VNC/Telnet connections, including display, audio, performance, and session settings. - Implemented logging for token requests and received options for better debugging. - Updated HostManagerEditor to support new Guacamole configuration fields with validation and default values. - Integrated Guacamole configuration parsing in HostManagerViewer and Host components. - Enhanced API requests to include extended Guacamole configuration parameters in the token request. - Refactored code to convert camelCase configuration keys to kebab-case for compatibility with Guacamole API. * feat: merge guacd into 2.0.0 and improve UI for host manager and made general bug fixes --------- Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com> Co-authored-by: LukeGus <bugattiguy527@gmail.com> Co-authored-by: junu <bigdwarf_@naver.com> Co-authored-by: 송준우 <2484@coreit.co.kr> Co-authored-by: SlimGary <trash.slim@gmail.com> Co-authored-by: Luke Gustafson <88517757+LukeGus@users.noreply.github.com> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: rename api routes and files * feat: improve guacd ui/backend * feat: improve guacd ui/backend * fix: state persistance issues causing refresh * feat: improge guacd connections, fixed telnet not opening, and improved general guacd integration * feat: continue improving integration also with bug fixes --------- Co-authored-by: Wesley Reid <starhound@lostsouls.org> Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com> Co-authored-by: junu <bigdwarf_@naver.com> Co-authored-by: 송준우 <2484@coreit.co.kr> Co-authored-by: SlimGary <trash.slim@gmail.com> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: allow customizing guacd backened url * fix: ssh route mistmatching and guacamole url not changing * chore: increment ver * feat: change default to work with default compose, added splits creen support, updated readmes * fix: linux app not starting due to better sqlite isuses, improved copy/paste system so no context menu, added oidc remember me toggle, improved OS detection for sessions, flatpak invalid key, and sharing hosts with other users errors * fix: global settings not setting * chore: update compose * feat: improve the global status input * chore: cleanup files * chore: update export/improt with new host fields * fix: file manager and docker not loading properly --------- Co-authored-by: Wesley Reid <starhound@lostsouls.org> Co-authored-by: Tran Trung Kien <kientt13.7@gmail.com> Co-authored-by: junu <bigdwarf_@naver.com> Co-authored-by: 송준우 <2484@coreit.co.kr> Co-authored-by: SlimGary <trash.slim@gmail.com> Co-authored-by: Nunzio Marfè <nunzio.marfe@protonmail.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import { Client as SSHClient } from "ssh2";
|
||||
import { WebSocketServer, WebSocket } from "ws";
|
||||
import { parse as parseUrl } from "url";
|
||||
import { AuthManager } from "../utils/auth-manager.js";
|
||||
import { sshData, sshCredentials } from "../database/db/schema.js";
|
||||
import { hosts, sshCredentials } from "../database/db/schema.js";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { getDb } from "../database/db/index.js";
|
||||
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
||||
@@ -24,7 +24,7 @@ const activeSessions = new Map<string, SSHSession>();
|
||||
|
||||
const wss = new WebSocketServer({
|
||||
host: "0.0.0.0",
|
||||
port: 30008,
|
||||
port: 30009,
|
||||
verifyClient: async (info) => {
|
||||
try {
|
||||
const url = parseUrl(info.req.url || "", true);
|
||||
@@ -107,8 +107,8 @@ async function createJumpHostChain(
|
||||
const jumpHostData = await SimpleDBOps.select(
|
||||
getDb()
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(and(eq(sshData.id, jumpHostId), eq(sshData.userId, userId))),
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.id, jumpHostId), eq(hosts.userId, userId))),
|
||||
"ssh_data",
|
||||
userId,
|
||||
);
|
||||
|
||||
+26
-28
@@ -4,7 +4,7 @@ import cookieParser from "cookie-parser";
|
||||
import axios from "axios";
|
||||
import { Client as SSHClient } from "ssh2";
|
||||
import { getDb } from "../database/db/index.js";
|
||||
import { sshData, sshCredentials } from "../database/db/schema.js";
|
||||
import { hosts, sshCredentials } from "../database/db/schema.js";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
||||
@@ -136,20 +136,20 @@ async function resolveJumpHost(
|
||||
userId: string,
|
||||
): Promise<JumpHostConfig | null> {
|
||||
try {
|
||||
const hosts = await SimpleDBOps.select(
|
||||
const hostResults = await SimpleDBOps.select(
|
||||
getDb()
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))),
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId))),
|
||||
"ssh_data",
|
||||
userId,
|
||||
);
|
||||
|
||||
if (hosts.length === 0) {
|
||||
if (hostResults.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const host = hosts[0];
|
||||
const host = hostResults[0];
|
||||
|
||||
if (host.credentialId) {
|
||||
const credentials = await SimpleDBOps.select(
|
||||
@@ -570,25 +570,24 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
||||
);
|
||||
|
||||
try {
|
||||
const hosts = await SimpleDBOps.select(
|
||||
getDb().select().from(sshData).where(eq(sshData.id, hostId)),
|
||||
const hostResults = await SimpleDBOps.select(
|
||||
getDb().select().from(hosts).where(eq(hosts.id, hostId)),
|
||||
"ssh_data",
|
||||
userId,
|
||||
);
|
||||
|
||||
if (hosts.length === 0) {
|
||||
if (hostResults.length === 0) {
|
||||
connectionLogs.push(
|
||||
createConnectionLog("error", "docker_connecting", "Host not found"),
|
||||
);
|
||||
return res.status(404).json({ error: "Host not found", connectionLogs });
|
||||
}
|
||||
|
||||
const host = hosts[0] as unknown as SSHHost;
|
||||
const host = hostResults[0] as unknown as SSHHost;
|
||||
|
||||
if (host.userId !== userId) {
|
||||
const { PermissionManager } = await import(
|
||||
"../utils/permission-manager.js"
|
||||
);
|
||||
const { PermissionManager } =
|
||||
await import("../utils/permission-manager.js");
|
||||
const permissionManager = PermissionManager.getInstance();
|
||||
const accessInfo = await permissionManager.canAccessHost(
|
||||
userId,
|
||||
@@ -693,9 +692,8 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
||||
|
||||
if (userId !== ownerId) {
|
||||
try {
|
||||
const { SharedCredentialManager } = await import(
|
||||
"../utils/shared-credential-manager.js"
|
||||
);
|
||||
const { SharedCredentialManager } =
|
||||
await import("../utils/shared-credential-manager.js");
|
||||
const sharedCredManager = SharedCredentialManager.getInstance();
|
||||
const sharedCred = await sharedCredManager.getSharedCredentialForUser(
|
||||
host.id,
|
||||
@@ -1678,14 +1676,14 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
|
||||
if (session.hostId && session.userId) {
|
||||
(async () => {
|
||||
try {
|
||||
const hosts = await SimpleDBOps.select(
|
||||
const hostResults = await SimpleDBOps.select(
|
||||
getDb()
|
||||
.select()
|
||||
.from(sshData)
|
||||
.from(hosts)
|
||||
.where(
|
||||
and(
|
||||
eq(sshData.id, session.hostId!),
|
||||
eq(sshData.userId, session.userId!),
|
||||
eq(hosts.id, session.hostId!),
|
||||
eq(hosts.userId, session.userId!),
|
||||
),
|
||||
),
|
||||
"ssh_data",
|
||||
@@ -1693,8 +1691,8 @@ app.post("/docker/ssh/connect-totp", async (req, res) => {
|
||||
);
|
||||
|
||||
const hostName =
|
||||
hosts.length > 0 && hosts[0].name
|
||||
? hosts[0].name
|
||||
hostResults.length > 0 && hostResults[0].name
|
||||
? hostResults[0].name
|
||||
: `${session.username}@${session.ip}:${session.port}`;
|
||||
|
||||
await axios.post(
|
||||
@@ -1863,14 +1861,14 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
|
||||
if (session.hostId && session.userId) {
|
||||
(async () => {
|
||||
try {
|
||||
const hosts = await SimpleDBOps.select(
|
||||
const hostResults = await SimpleDBOps.select(
|
||||
getDb()
|
||||
.select()
|
||||
.from(sshData)
|
||||
.from(hosts)
|
||||
.where(
|
||||
and(
|
||||
eq(sshData.id, session.hostId!),
|
||||
eq(sshData.userId, session.userId!),
|
||||
eq(hosts.id, session.hostId!),
|
||||
eq(hosts.userId, session.userId!),
|
||||
),
|
||||
),
|
||||
"ssh_data",
|
||||
@@ -1878,8 +1876,8 @@ app.post("/docker/ssh/connect-warpgate", async (req, res) => {
|
||||
);
|
||||
|
||||
const hostName =
|
||||
hosts.length > 0 && hosts[0].name
|
||||
? hosts[0].name
|
||||
hostResults.length > 0 && hostResults[0].name
|
||||
? hostResults[0].name
|
||||
: `${session.username}@${session.ip}:${session.port}`;
|
||||
|
||||
await axios.post(
|
||||
|
||||
+137
-64
@@ -4,7 +4,7 @@ import cookieParser from "cookie-parser";
|
||||
import axios from "axios";
|
||||
import { Client as SSHClient } from "ssh2";
|
||||
import { getDb } from "../database/db/index.js";
|
||||
import { sshCredentials, sshData } from "../database/db/schema.js";
|
||||
import { sshCredentials, hosts } from "../database/db/schema.js";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { fileLogger } from "../utils/logger.js";
|
||||
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
||||
@@ -179,20 +179,20 @@ async function resolveJumpHost(
|
||||
userId: string,
|
||||
): Promise<JumpHostConfig | null> {
|
||||
try {
|
||||
const hosts = await SimpleDBOps.select(
|
||||
const hostResults = await SimpleDBOps.select(
|
||||
getDb()
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))),
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId))),
|
||||
"ssh_data",
|
||||
userId,
|
||||
);
|
||||
|
||||
if (hosts.length === 0) {
|
||||
if (hostResults.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const host = hosts[0];
|
||||
const host = hostResults[0];
|
||||
|
||||
if (host.credentialId) {
|
||||
const credentials = await SimpleDBOps.select(
|
||||
@@ -803,64 +803,137 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
||||
|
||||
let resolvedCredentials = { password, sshKey, keyPassword, authType };
|
||||
if (credentialId && hostId && userId) {
|
||||
try {
|
||||
const credentials = await SimpleDBOps.select(
|
||||
getDb()
|
||||
.select()
|
||||
.from(sshCredentials)
|
||||
.where(
|
||||
and(
|
||||
eq(sshCredentials.id, credentialId),
|
||||
eq(sshCredentials.userId, userId),
|
||||
),
|
||||
),
|
||||
"ssh_credentials",
|
||||
userId,
|
||||
);
|
||||
const hostRow = await getDb()
|
||||
.select({ userId: hosts.userId })
|
||||
.from(hosts)
|
||||
.where(eq(hosts.id, hostId))
|
||||
.limit(1);
|
||||
const ownerId = hostRow[0]?.userId ?? null;
|
||||
|
||||
if (credentials.length > 0) {
|
||||
const credential = credentials[0];
|
||||
resolvedCredentials = {
|
||||
password: credential.password,
|
||||
sshKey: credential.privateKey,
|
||||
keyPassword: credential.keyPassword,
|
||||
authType: credential.authType,
|
||||
};
|
||||
if (ownerId && userId !== ownerId) {
|
||||
try {
|
||||
const { SharedCredentialManager } =
|
||||
await import("../utils/shared-credential-manager.js");
|
||||
const sharedCredManager = SharedCredentialManager.getInstance();
|
||||
const sharedCred = await sharedCredManager.getSharedCredentialForUser(
|
||||
hostId,
|
||||
userId,
|
||||
);
|
||||
|
||||
if (sharedCred) {
|
||||
resolvedCredentials = {
|
||||
password: sharedCred.password,
|
||||
sshKey: sharedCred.key,
|
||||
keyPassword: sharedCred.keyPassword,
|
||||
authType: sharedCred.authType,
|
||||
};
|
||||
connectionLogs.push(
|
||||
createConnectionLog(
|
||||
"info",
|
||||
"sftp_auth",
|
||||
"Credentials resolved from shared credential store",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
fileLogger.warn(`No shared credentials found for host ${hostId}`, {
|
||||
operation: "ssh_credentials",
|
||||
hostId,
|
||||
userId,
|
||||
});
|
||||
connectionLogs.push(
|
||||
createConnectionLog(
|
||||
"warning",
|
||||
"sftp_auth",
|
||||
"No shared credentials found, using provided credentials",
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
fileLogger.warn(
|
||||
`Failed to resolve shared credential for host ${hostId}`,
|
||||
{
|
||||
operation: "ssh_credentials",
|
||||
hostId,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
);
|
||||
connectionLogs.push(
|
||||
createConnectionLog(
|
||||
"info",
|
||||
"warning",
|
||||
"sftp_auth",
|
||||
"Credentials resolved from credential store",
|
||||
`Failed to resolve shared credentials: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
fileLogger.warn(`No credentials found for host ${hostId}`, {
|
||||
}
|
||||
} else if (ownerId) {
|
||||
try {
|
||||
const credentials = await SimpleDBOps.select(
|
||||
getDb()
|
||||
.select()
|
||||
.from(sshCredentials)
|
||||
.where(
|
||||
and(
|
||||
eq(sshCredentials.id, credentialId),
|
||||
eq(sshCredentials.userId, ownerId),
|
||||
),
|
||||
),
|
||||
"ssh_credentials",
|
||||
ownerId,
|
||||
);
|
||||
|
||||
if (credentials.length > 0) {
|
||||
const credential = credentials[0];
|
||||
resolvedCredentials = {
|
||||
password: credential.password,
|
||||
sshKey: credential.privateKey,
|
||||
keyPassword: credential.keyPassword,
|
||||
authType: credential.authType,
|
||||
};
|
||||
connectionLogs.push(
|
||||
createConnectionLog(
|
||||
"info",
|
||||
"sftp_auth",
|
||||
"Credentials resolved from credential store",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
fileLogger.warn(`No credentials found for host ${hostId}`, {
|
||||
operation: "ssh_credentials",
|
||||
hostId,
|
||||
credentialId,
|
||||
userId: ownerId,
|
||||
});
|
||||
connectionLogs.push(
|
||||
createConnectionLog(
|
||||
"warning",
|
||||
"sftp_auth",
|
||||
"No stored credentials found, using provided credentials",
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
fileLogger.warn(`Failed to resolve credentials for host ${hostId}`, {
|
||||
operation: "ssh_credentials",
|
||||
hostId,
|
||||
credentialId,
|
||||
userId,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
connectionLogs.push(
|
||||
createConnectionLog(
|
||||
"warning",
|
||||
"sftp_auth",
|
||||
"No stored credentials found, using provided credentials",
|
||||
`Failed to resolve credentials: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
fileLogger.warn(`Failed to resolve credentials for host ${hostId}`, {
|
||||
operation: "ssh_credentials",
|
||||
hostId,
|
||||
credentialId,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
connectionLogs.push(
|
||||
createConnectionLog(
|
||||
"warning",
|
||||
"sftp_auth",
|
||||
`Failed to resolve credentials: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
),
|
||||
} else {
|
||||
fileLogger.warn(
|
||||
"Missing userId for credential resolution in file manager",
|
||||
{
|
||||
operation: "ssh_credentials",
|
||||
hostId,
|
||||
credentialId,
|
||||
},
|
||||
);
|
||||
}
|
||||
} else if (credentialId && hostId) {
|
||||
@@ -1201,18 +1274,18 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
||||
if (hostId && userId) {
|
||||
(async () => {
|
||||
try {
|
||||
const hosts = await SimpleDBOps.select(
|
||||
const hostResults = await SimpleDBOps.select(
|
||||
getDb()
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))),
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId))),
|
||||
"ssh_data",
|
||||
userId,
|
||||
);
|
||||
|
||||
const hostName =
|
||||
hosts.length > 0 && hosts[0].name
|
||||
? hosts[0].name
|
||||
hostResults.length > 0 && hostResults[0].name
|
||||
? hostResults[0].name
|
||||
: `${username}@${ip}:${port}`;
|
||||
|
||||
const authManager = AuthManager.getInstance();
|
||||
@@ -1844,14 +1917,14 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
|
||||
if (session.hostId && session.userId) {
|
||||
(async () => {
|
||||
try {
|
||||
const hosts = await SimpleDBOps.select(
|
||||
const hostResults = await SimpleDBOps.select(
|
||||
getDb()
|
||||
.select()
|
||||
.from(sshData)
|
||||
.from(hosts)
|
||||
.where(
|
||||
and(
|
||||
eq(sshData.id, session.hostId!),
|
||||
eq(sshData.userId, session.userId!),
|
||||
eq(hosts.id, session.hostId!),
|
||||
eq(hosts.userId, session.userId!),
|
||||
),
|
||||
),
|
||||
"ssh_data",
|
||||
@@ -1859,8 +1932,8 @@ app.post("/ssh/file_manager/ssh/connect-totp", async (req, res) => {
|
||||
);
|
||||
|
||||
const hostName =
|
||||
hosts.length > 0 && hosts[0].name
|
||||
? hosts[0].name
|
||||
hostResults.length > 0 && hostResults[0].name
|
||||
? hostResults[0].name
|
||||
: `${session.username}@${session.ip}:${session.port}`;
|
||||
|
||||
const authManager = AuthManager.getInstance();
|
||||
@@ -2045,14 +2118,14 @@ app.post("/ssh/file_manager/ssh/connect-warpgate", async (req, res) => {
|
||||
if (session.hostId && session.userId) {
|
||||
(async () => {
|
||||
try {
|
||||
const hosts = await SimpleDBOps.select(
|
||||
const hostResults = await SimpleDBOps.select(
|
||||
getDb()
|
||||
.select()
|
||||
.from(sshData)
|
||||
.from(hosts)
|
||||
.where(
|
||||
and(
|
||||
eq(sshData.id, session.hostId!),
|
||||
eq(sshData.userId, session.userId!),
|
||||
eq(hosts.id, session.hostId!),
|
||||
eq(hosts.userId, session.userId!),
|
||||
),
|
||||
),
|
||||
"ssh_data",
|
||||
@@ -2060,8 +2133,8 @@ app.post("/ssh/file_manager/ssh/connect-warpgate", async (req, res) => {
|
||||
);
|
||||
|
||||
const hostName =
|
||||
hosts.length > 0 && hosts[0].name
|
||||
? hosts[0].name
|
||||
hostResults.length > 0 && hostResults[0].name
|
||||
? hostResults[0].name
|
||||
: `${session.username}@${session.ip}:${session.port}`;
|
||||
|
||||
await axios.post(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { WebSocket } from "ws";
|
||||
import { db } from "../database/db/index.js";
|
||||
import { sshData } from "../database/db/schema.js";
|
||||
import { hosts } from "../database/db/schema.js";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { sshLogger } from "../utils/logger.js";
|
||||
|
||||
@@ -21,17 +21,6 @@ interface VerificationResponse {
|
||||
}
|
||||
|
||||
export class SSHHostKeyVerifier {
|
||||
/**
|
||||
* Creates a hostVerifier callback for ssh2 Client.connect()
|
||||
*
|
||||
* @param hostId - Database ID of the host (null for quick connect)
|
||||
* @param ip - IP address or hostname
|
||||
* @param port - SSH port
|
||||
* @param ws - WebSocket for user prompts (null for non-interactive connections)
|
||||
* @param userId - User ID for logging
|
||||
* @param isJumpHost - If true, auto-accepts without prompting
|
||||
* @returns async hostVerifier callback
|
||||
*/
|
||||
static async createHostVerifier(
|
||||
hostId: number | null,
|
||||
ip: string,
|
||||
@@ -63,8 +52,8 @@ export class SSHHostKeyVerifier {
|
||||
return;
|
||||
}
|
||||
|
||||
const host = await db.query.sshData.findFirst({
|
||||
where: eq(sshData.id, hostId),
|
||||
const host = await db.query.hosts.findFirst({
|
||||
where: eq(hosts.id, hostId),
|
||||
});
|
||||
|
||||
if (!host) {
|
||||
@@ -153,11 +142,11 @@ export class SSHHostKeyVerifier {
|
||||
|
||||
if (host.hostKeyFingerprint === fingerprint) {
|
||||
await db
|
||||
.update(sshData)
|
||||
.update(hosts)
|
||||
.set({
|
||||
hostKeyLastVerified: new Date().toISOString(),
|
||||
})
|
||||
.where(eq(sshData.id, hostId));
|
||||
.where(eq(hosts.id, hostId));
|
||||
|
||||
sshLogger.info("Host key verified successfully", {
|
||||
operation: "host_key_verified",
|
||||
@@ -287,7 +276,7 @@ export class SSHHostKeyVerifier {
|
||||
algorithm: string,
|
||||
): Promise<void> {
|
||||
await db
|
||||
.update(sshData)
|
||||
.update(hosts)
|
||||
.set({
|
||||
hostKeyFingerprint: fingerprint,
|
||||
hostKeyType: keyType,
|
||||
@@ -295,7 +284,7 @@ export class SSHHostKeyVerifier {
|
||||
hostKeyFirstSeen: new Date().toISOString(),
|
||||
hostKeyLastVerified: new Date().toISOString(),
|
||||
})
|
||||
.where(eq(sshData.id, hostId));
|
||||
.where(eq(hosts.id, hostId));
|
||||
}
|
||||
|
||||
private static async updateHostKey(
|
||||
@@ -306,7 +295,7 @@ export class SSHHostKeyVerifier {
|
||||
currentChangeCount: number,
|
||||
): Promise<void> {
|
||||
await db
|
||||
.update(sshData)
|
||||
.update(hosts)
|
||||
.set({
|
||||
hostKeyFingerprint: fingerprint,
|
||||
hostKeyType: keyType,
|
||||
@@ -314,7 +303,7 @@ export class SSHHostKeyVerifier {
|
||||
hostKeyLastVerified: new Date().toISOString(),
|
||||
hostKeyChangedCount: currentChangeCount + 1,
|
||||
})
|
||||
.where(eq(sshData.id, hostId));
|
||||
.where(eq(hosts.id, hostId));
|
||||
}
|
||||
|
||||
private static async promptUserForNewKey(
|
||||
|
||||
@@ -123,7 +123,7 @@ async function checkOPKConfigExists(): Promise<{
|
||||
return {
|
||||
exists: false,
|
||||
configPath,
|
||||
error: `OPKSSH configuration is missing 'redirect_uris' field. This field must contain the Termix callback URL that you registered with your OAuth provider (e.g., http://localhost:8080/ssh/opkssh-callback for Docker). The static callback route will internally redirect to the dynamic route for proper URL rewriting.`,
|
||||
error: `OPKSSH configuration is missing 'redirect_uris' field. This field must contain the Termix callback URL that you registered with your OAuth provider (e.g., http://localhost:8080/host/opkssh-callback for Docker). The static callback route will internally redirect to the dynamic route for proper URL rewriting.`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ export async function startOPKSSHAuth(
|
||||
}
|
||||
|
||||
const requestId = randomUUID();
|
||||
const remoteRedirectUri = `${requestOrigin}/ssh/opkssh-callback`;
|
||||
const remoteRedirectUri = `${requestOrigin}/host/opkssh-callback`;
|
||||
|
||||
const session: Partial<OPKSSHAuthSession> = {
|
||||
requestId,
|
||||
@@ -352,7 +352,7 @@ function handleOPKSSHOutput(requestId: string, output: string): void {
|
||||
/\/ssh\/opkssh-callback$/,
|
||||
"",
|
||||
);
|
||||
const proxiedChooserUrl = `${baseUrl}/ssh/opkssh-chooser/${requestId}`;
|
||||
const proxiedChooserUrl = `${baseUrl}/host/opkssh-chooser/${requestId}`;
|
||||
|
||||
session.status = "waiting_for_auth";
|
||||
session.ws.send(
|
||||
|
||||
@@ -4,7 +4,7 @@ import cors from "cors";
|
||||
import cookieParser from "cookie-parser";
|
||||
import { Client, type ConnectConfig } from "ssh2";
|
||||
import { getDb } from "../database/db/index.js";
|
||||
import { sshData, sshCredentials } from "../database/db/schema.js";
|
||||
import { hosts, sshCredentials } from "../database/db/schema.js";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { statsLogger } from "../utils/logger.js";
|
||||
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
||||
@@ -29,6 +29,15 @@ import {
|
||||
import { SSHHostKeyVerifier } from "./host-key-verifier.js";
|
||||
import { connectionPool, withConnection } from "./ssh-connection-pool.js";
|
||||
|
||||
function supportsMetrics(host: SSHHostWithCredentials): boolean {
|
||||
const connectionType = host.connectionType || "ssh";
|
||||
return connectionType === "ssh";
|
||||
}
|
||||
|
||||
function isTcpPingEnabled(statsConfig: StatsConfig): boolean {
|
||||
return statsConfig.statusCheckEnabled && !statsConfig.disableTcpPing;
|
||||
}
|
||||
|
||||
function createConnectionLog(
|
||||
type: "info" | "success" | "warning" | "error",
|
||||
stage: ConnectionStage,
|
||||
@@ -62,20 +71,20 @@ async function resolveJumpHost(
|
||||
userId: string,
|
||||
): Promise<JumpHostConfig | null> {
|
||||
try {
|
||||
const hosts = await SimpleDBOps.select(
|
||||
const hostResults = await SimpleDBOps.select(
|
||||
getDb()
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))),
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId))),
|
||||
"ssh_data",
|
||||
userId,
|
||||
);
|
||||
|
||||
if (hosts.length === 0) {
|
||||
if (hostResults.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const host = hosts[0];
|
||||
const host = hostResults[0];
|
||||
|
||||
if (host.credentialId) {
|
||||
const credentials = await SimpleDBOps.select(
|
||||
@@ -644,6 +653,7 @@ interface SSHHostWithCredentials {
|
||||
socks5Username?: string;
|
||||
socks5Password?: string;
|
||||
socks5ProxyChain?: ProxyNode[];
|
||||
connectionType?: "ssh" | "rdp" | "vnc" | "telnet";
|
||||
}
|
||||
|
||||
type StatusEntry = {
|
||||
@@ -659,6 +669,7 @@ interface StatsConfig {
|
||||
metricsEnabled: boolean;
|
||||
metricsInterval: number;
|
||||
useGlobalMetricsInterval?: boolean;
|
||||
disableTcpPing?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_STATS_CONFIG: StatsConfig = {
|
||||
@@ -783,9 +794,13 @@ class PollingManager {
|
||||
const statusOnly = options?.statusOnly ?? false;
|
||||
const viewerUserId = options?.viewerUserId;
|
||||
|
||||
const canCollectMetrics = supportsMetrics(host);
|
||||
|
||||
const enabledCollectors: string[] = [];
|
||||
if (statsConfig.statusCheckEnabled) enabledCollectors.push("status");
|
||||
if (!statusOnly && statsConfig.metricsEnabled) {
|
||||
if (isTcpPingEnabled(statsConfig)) {
|
||||
enabledCollectors.push("status");
|
||||
}
|
||||
if (!statusOnly && statsConfig.metricsEnabled && canCollectMetrics) {
|
||||
enabledCollectors.push(
|
||||
"cpu",
|
||||
"memory",
|
||||
@@ -810,7 +825,7 @@ class PollingManager {
|
||||
}
|
||||
}
|
||||
|
||||
if (!statsConfig.statusCheckEnabled && !statsConfig.metricsEnabled) {
|
||||
if (!isTcpPingEnabled(statsConfig) && !statsConfig.metricsEnabled) {
|
||||
this.pollingConfigs.delete(host.id);
|
||||
this.statusStore.delete(host.id);
|
||||
this.metricsStore.delete(host.id);
|
||||
@@ -823,14 +838,14 @@ class PollingManager {
|
||||
viewerUserId,
|
||||
};
|
||||
|
||||
if (statsConfig.statusCheckEnabled) {
|
||||
if (isTcpPingEnabled(statsConfig)) {
|
||||
const intervalMs = statsConfig.statusCheckInterval * 1000;
|
||||
|
||||
this.pollHostStatus(host, viewerUserId);
|
||||
|
||||
config.statusTimer = setInterval(() => {
|
||||
const latestConfig = this.pollingConfigs.get(host.id);
|
||||
if (latestConfig && latestConfig.statsConfig.statusCheckEnabled) {
|
||||
if (latestConfig && isTcpPingEnabled(latestConfig.statsConfig)) {
|
||||
this.pollHostStatus(latestConfig.host, latestConfig.viewerUserId);
|
||||
}
|
||||
}, intervalMs);
|
||||
@@ -838,14 +853,18 @@ class PollingManager {
|
||||
this.statusStore.delete(host.id);
|
||||
}
|
||||
|
||||
if (!statusOnly && statsConfig.metricsEnabled) {
|
||||
if (!statusOnly && statsConfig.metricsEnabled && canCollectMetrics) {
|
||||
const intervalMs = statsConfig.metricsInterval * 1000;
|
||||
|
||||
await this.pollHostMetrics(host, viewerUserId);
|
||||
|
||||
config.metricsTimer = setInterval(() => {
|
||||
const latestConfig = this.pollingConfigs.get(host.id);
|
||||
if (latestConfig && latestConfig.statsConfig.metricsEnabled) {
|
||||
if (
|
||||
latestConfig &&
|
||||
latestConfig.statsConfig.metricsEnabled &&
|
||||
supportsMetrics(latestConfig.host)
|
||||
) {
|
||||
this.pollHostMetrics(latestConfig.host, latestConfig.viewerUserId);
|
||||
}
|
||||
}, intervalMs);
|
||||
@@ -896,6 +915,15 @@ class PollingManager {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!supportsMetrics(refreshedHost)) {
|
||||
statsLogger.debug("Skipping metrics collection for non-SSH host", {
|
||||
operation: "poll_host_metrics_skipped",
|
||||
hostId: refreshedHost.id,
|
||||
connectionType: refreshedHost.connectionType || "ssh",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const config = this.pollingConfigs.get(refreshedHost.id);
|
||||
if (!config || !config.statsConfig.metricsEnabled) {
|
||||
return;
|
||||
@@ -1170,14 +1198,14 @@ async function fetchAllHosts(
|
||||
userId: string,
|
||||
): Promise<SSHHostWithCredentials[]> {
|
||||
try {
|
||||
const hosts = await SimpleDBOps.select(
|
||||
getDb().select().from(sshData).where(eq(sshData.userId, userId)),
|
||||
const hostResults = await SimpleDBOps.select(
|
||||
getDb().select().from(hosts).where(eq(hosts.userId, userId)),
|
||||
"ssh_data",
|
||||
userId,
|
||||
);
|
||||
|
||||
const hostsWithCredentials: SSHHostWithCredentials[] = [];
|
||||
for (const host of hosts) {
|
||||
for (const host of hostResults) {
|
||||
try {
|
||||
const hostWithCreds = await resolveHostCredentials(host, userId);
|
||||
if (hostWithCreds) {
|
||||
@@ -1221,17 +1249,17 @@ async function fetchHostById(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const hosts = await SimpleDBOps.select(
|
||||
getDb().select().from(sshData).where(eq(sshData.id, id)),
|
||||
const hostResults = await SimpleDBOps.select(
|
||||
getDb().select().from(hosts).where(eq(hosts.id, id)),
|
||||
"ssh_data",
|
||||
userId,
|
||||
);
|
||||
|
||||
if (hosts.length === 0) {
|
||||
if (hostResults.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const host = hosts[0];
|
||||
const host = hostResults[0];
|
||||
return await resolveHostCredentials(host, userId);
|
||||
} catch (err) {
|
||||
statsLogger.error(`Failed to fetch host ${id}`, err);
|
||||
@@ -1757,6 +1785,10 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
|
||||
os: string | null;
|
||||
};
|
||||
}> {
|
||||
if (!supportsMetrics(host)) {
|
||||
throw new Error("Metrics collection only supported for SSH hosts");
|
||||
}
|
||||
|
||||
if (authFailureTracker.shouldSkip(host.id)) {
|
||||
const reason = authFailureTracker.getSkipReason(host.id);
|
||||
throw new Error(reason || "Authentication failed");
|
||||
@@ -1935,7 +1967,8 @@ function tcpPing(
|
||||
|
||||
socket.once("data", (data) => {
|
||||
clearTimeout(dataTimeout);
|
||||
if (data.toString().startsWith("SSH-")) {
|
||||
const dataStr = data.toString("utf8");
|
||||
if (dataStr.startsWith("SSH-")) {
|
||||
try {
|
||||
socket.end("SSH-2.0-TermixHealthCheck\r\n");
|
||||
} catch {
|
||||
|
||||
+93
-52
@@ -3,7 +3,7 @@ import { Client, type ClientChannel, type PseudoTtyOptions } from "ssh2";
|
||||
import { parse as parseUrl } from "url";
|
||||
import axios from "axios";
|
||||
import { getDb } from "../database/db/index.js";
|
||||
import { sshCredentials, sshData } from "../database/db/schema.js";
|
||||
import { sshCredentials, hosts } from "../database/db/schema.js";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { sshLogger, authLogger } from "../utils/logger.js";
|
||||
import { SimpleDBOps } from "../utils/simple-db-ops.js";
|
||||
@@ -97,20 +97,20 @@ async function resolveJumpHost(
|
||||
hostId,
|
||||
});
|
||||
try {
|
||||
const hosts = await SimpleDBOps.select(
|
||||
const hostResults = await SimpleDBOps.select(
|
||||
getDb()
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(and(eq(sshData.id, hostId), eq(sshData.userId, userId))),
|
||||
.from(hosts)
|
||||
.where(and(eq(hosts.id, hostId), eq(hosts.userId, userId))),
|
||||
"ssh_data",
|
||||
userId,
|
||||
);
|
||||
|
||||
if (hosts.length === 0) {
|
||||
if (hostResults.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const host = hosts[0];
|
||||
const host = hostResults[0];
|
||||
|
||||
if (host.credentialId) {
|
||||
const credentials = await SimpleDBOps.select(
|
||||
@@ -814,8 +814,8 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
const db = getDb();
|
||||
const hostRow = await db
|
||||
.select()
|
||||
.from(sshData)
|
||||
.where(eq(sshData.id, opksshData.hostId))
|
||||
.from(hosts)
|
||||
.where(eq(hosts.id, opksshData.hostId))
|
||||
.limit(1);
|
||||
if (!hostRow || hostRow.length === 0) {
|
||||
sshLogger.error(
|
||||
@@ -1057,55 +1057,96 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
authType,
|
||||
};
|
||||
const authMethodNotAvailable = false;
|
||||
if (credentialId && id && hostConfig.userId) {
|
||||
try {
|
||||
const credentials = await SimpleDBOps.select(
|
||||
getDb()
|
||||
.select()
|
||||
.from(sshCredentials)
|
||||
.where(
|
||||
and(
|
||||
eq(sshCredentials.id, credentialId),
|
||||
eq(sshCredentials.userId, hostConfig.userId),
|
||||
),
|
||||
),
|
||||
"ssh_credentials",
|
||||
hostConfig.userId,
|
||||
);
|
||||
if (credentialId && id) {
|
||||
const hostRow = await getDb()
|
||||
.select({ userId: hosts.userId })
|
||||
.from(hosts)
|
||||
.where(eq(hosts.id, id))
|
||||
.limit(1);
|
||||
const ownerId = hostRow[0]?.userId ?? null;
|
||||
|
||||
if (credentials.length > 0) {
|
||||
const credential = credentials[0];
|
||||
resolvedCredentials = {
|
||||
username: (credential.username as string | undefined) || username,
|
||||
password: credential.password as string | undefined,
|
||||
key: credential.privateKey as string | undefined,
|
||||
keyPassword: credential.keyPassword as string | undefined,
|
||||
keyType: credential.keyType as string | undefined,
|
||||
authType: credential.authType as string | undefined,
|
||||
};
|
||||
} else {
|
||||
sshLogger.warn(`No credentials found for host ${id}`, {
|
||||
if (ownerId && userId !== ownerId) {
|
||||
try {
|
||||
const { SharedCredentialManager } =
|
||||
await import("../utils/shared-credential-manager.js");
|
||||
const sharedCredManager = SharedCredentialManager.getInstance();
|
||||
const sharedCred = await sharedCredManager.getSharedCredentialForUser(
|
||||
id,
|
||||
userId,
|
||||
);
|
||||
|
||||
if (sharedCred) {
|
||||
resolvedCredentials = {
|
||||
username: sharedCred.username || username,
|
||||
password: sharedCred.password,
|
||||
key: sharedCred.key,
|
||||
keyPassword: sharedCred.keyPassword,
|
||||
keyType: sharedCred.keyType,
|
||||
authType: sharedCred.authType,
|
||||
};
|
||||
} else {
|
||||
sshLogger.warn(`No shared credentials found for host ${id}`, {
|
||||
operation: "ssh_credentials",
|
||||
userId,
|
||||
hostId: id,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
sshLogger.warn(`Failed to resolve shared credential for host ${id}`, {
|
||||
operation: "ssh_credentials",
|
||||
hostId: id,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
} else if (ownerId) {
|
||||
try {
|
||||
const credentials = await SimpleDBOps.select(
|
||||
getDb()
|
||||
.select()
|
||||
.from(sshCredentials)
|
||||
.where(
|
||||
and(
|
||||
eq(sshCredentials.id, credentialId),
|
||||
eq(sshCredentials.userId, ownerId),
|
||||
),
|
||||
),
|
||||
"ssh_credentials",
|
||||
ownerId,
|
||||
);
|
||||
|
||||
if (credentials.length > 0) {
|
||||
const credential = credentials[0];
|
||||
resolvedCredentials = {
|
||||
username: (credential.username as string | undefined) || username,
|
||||
password: credential.password as string | undefined,
|
||||
key: credential.privateKey as string | undefined,
|
||||
keyPassword: credential.keyPassword as string | undefined,
|
||||
keyType: credential.keyType as string | undefined,
|
||||
authType: credential.authType as string | undefined,
|
||||
};
|
||||
} else {
|
||||
sshLogger.warn(`No credentials found for host ${id}`, {
|
||||
operation: "ssh_credentials",
|
||||
hostId: id,
|
||||
credentialId,
|
||||
userId: ownerId,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
sshLogger.warn(`Failed to resolve credentials for host ${id}`, {
|
||||
operation: "ssh_credentials",
|
||||
hostId: id,
|
||||
credentialId,
|
||||
userId: hostConfig.userId,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
sshLogger.warn(`Failed to resolve credentials for host ${id}`, {
|
||||
} else {
|
||||
sshLogger.warn("Missing userId for credential resolution in terminal", {
|
||||
operation: "ssh_credentials",
|
||||
hostId: id,
|
||||
credentialId,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
} else if (credentialId && id) {
|
||||
sshLogger.warn("Missing userId for credential resolution in terminal", {
|
||||
operation: "ssh_credentials",
|
||||
hostId: id,
|
||||
credentialId,
|
||||
hasUserId: !!hostConfig.userId,
|
||||
});
|
||||
}
|
||||
|
||||
sshConn.on("ready", () => {
|
||||
@@ -1400,14 +1441,14 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
if (id && hostConfig.userId) {
|
||||
(async () => {
|
||||
try {
|
||||
const hosts = await SimpleDBOps.select(
|
||||
const hostResults = await SimpleDBOps.select(
|
||||
getDb()
|
||||
.select()
|
||||
.from(sshData)
|
||||
.from(hosts)
|
||||
.where(
|
||||
and(
|
||||
eq(sshData.id, id),
|
||||
eq(sshData.userId, hostConfig.userId!),
|
||||
eq(hosts.id, id),
|
||||
eq(hosts.userId, hostConfig.userId!),
|
||||
),
|
||||
),
|
||||
"ssh_data",
|
||||
@@ -1415,8 +1456,8 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
);
|
||||
|
||||
const hostName =
|
||||
hosts.length > 0 && hosts[0].name
|
||||
? hosts[0].name
|
||||
hostResults.length > 0 && hostResults[0].name
|
||||
? hostResults[0].name
|
||||
: `${username}@${ip}:${port}`;
|
||||
|
||||
await axios.post(
|
||||
|
||||
@@ -608,9 +608,8 @@ async function connectSSHTunnel(
|
||||
tunnelConfig.requestingUserId &&
|
||||
tunnelConfig.requestingUserId !== tunnelConfig.sourceUserId
|
||||
) {
|
||||
const { SharedCredentialManager } = await import(
|
||||
"../utils/shared-credential-manager.js"
|
||||
);
|
||||
const { SharedCredentialManager } =
|
||||
await import("../utils/shared-credential-manager.js");
|
||||
const sharedCredManager = SharedCredentialManager.getInstance();
|
||||
|
||||
if (tunnelConfig.sourceHostId) {
|
||||
@@ -1750,7 +1749,7 @@ app.post(
|
||||
const internalAuthToken = await systemCrypto.getInternalAuthToken();
|
||||
|
||||
const allHostsResponse = await axios.get(
|
||||
"http://localhost:30001/ssh/db/host/internal/all",
|
||||
"http://localhost:30001/host/db/host/internal/all",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -2028,7 +2027,7 @@ async function initializeAutoStartTunnels(): Promise<void> {
|
||||
const internalAuthToken = await systemCrypto.getInternalAuthToken();
|
||||
|
||||
const autostartResponse = await axios.get(
|
||||
"http://localhost:30001/ssh/db/host/internal",
|
||||
"http://localhost:30001/host/db/host/internal",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -2038,7 +2037,7 @@ async function initializeAutoStartTunnels(): Promise<void> {
|
||||
);
|
||||
|
||||
const allHostsResponse = await axios.get(
|
||||
"http://localhost:30001/ssh/db/host/internal/all",
|
||||
"http://localhost:30001/host/db/host/internal/all",
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -2050,7 +2049,6 @@ async function initializeAutoStartTunnels(): Promise<void> {
|
||||
const autostartHosts: SSHHost[] = autostartResponse.data || [];
|
||||
const allHosts: SSHHost[] = allHostsResponse.data || [];
|
||||
const autoStartTunnels: TunnelConfig[] = [];
|
||||
|
||||
tunnelLogger.info(
|
||||
`Found ${autostartHosts.length} autostart hosts and ${allHosts.length} total hosts for endpointHost resolution`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user