mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-15 21:03:47 +00:00
release-2.4.1 (#921)
* chore(deps-dev): bump the dev-minor-updates group across 1 directory with 12 updates (#910) Bumps the dev-minor-updates group with 12 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.2.6` | `2.3.1` | | [@radix-ui/react-slider](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slider) | `1.3.6` | `1.4.1` | | [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.2.5` | `1.3.0` | | [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.2.6` | `1.3.1` | | [cytoscape](https://github.com/cytoscape/cytoscape.js) | `3.33.4` | `3.34.0` | | [electron](https://github.com/electron/electron) | `42.2.0` | `42.4.1` | | [electron-builder](https://github.com/electron-userland/electron-builder/tree/HEAD/packages/electron-builder) | `26.8.1` | `26.15.3` | | [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.16.0` | `1.20.0` | | [radix-ui](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/radix-ui) | `1.4.3` | `1.6.0` | | [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.76.1` | `7.79.0` | | [sharp](https://github.com/lovell/sharp) | `0.34.5` | `0.35.1` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.60.0` | `8.61.1` | Updates `@radix-ui/react-select` from 2.2.6 to 2.3.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select) Updates `@radix-ui/react-slider` from 1.3.6 to 1.4.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slider/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slider) Updates `@radix-ui/react-slot` from 1.2.5 to 1.3.0 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot) Updates `@radix-ui/react-switch` from 1.2.6 to 1.3.1 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch) Updates `cytoscape` from 3.33.4 to 3.34.0 - [Release notes](https://github.com/cytoscape/cytoscape.js/releases) - [Commits](https://github.com/cytoscape/cytoscape.js/compare/v3.33.4...v3.34.0) Updates `electron` from 42.2.0 to 42.4.1 - [Release notes](https://github.com/electron/electron/releases) - [Commits](https://github.com/electron/electron/compare/v42.2.0...v42.4.1) Updates `electron-builder` from 26.8.1 to 26.15.3 - [Release notes](https://github.com/electron-userland/electron-builder/releases) - [Changelog](https://github.com/electron-userland/electron-builder/blob/master/packages/electron-builder/CHANGELOG.md) - [Commits](https://github.com/electron-userland/electron-builder/commits/electron-builder@26.15.3/packages/electron-builder) Updates `lucide-react` from 1.16.0 to 1.20.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/1.20.0/packages/lucide-react) Updates `radix-ui` from 1.4.3 to 1.6.0 - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/radix-ui/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/radix-ui) Updates `react-hook-form` from 7.76.1 to 7.79.0 - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.76.1...v7.79.0) Updates `sharp` from 0.34.5 to 0.35.1 - [Release notes](https://github.com/lovell/sharp/releases) - [Commits](https://github.com/lovell/sharp/compare/v0.34.5...v0.35.1) Updates `typescript-eslint` from 8.60.0 to 8.61.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.61.1/packages/typescript-eslint) --- updated-dependencies: - dependency-name: "@radix-ui/react-select" dependency-version: 2.3.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: "@radix-ui/react-slider" dependency-version: 1.4.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: "@radix-ui/react-slot" dependency-version: 1.3.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: "@radix-ui/react-switch" dependency-version: 1.3.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: cytoscape dependency-version: 3.34.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: electron dependency-version: 42.4.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: electron-builder dependency-version: 26.15.3 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: lucide-react dependency-version: 1.18.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: radix-ui dependency-version: 1.6.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: react-hook-form dependency-version: 7.79.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: sharp dependency-version: 0.35.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates - dependency-name: typescript-eslint dependency-version: 8.61.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-minor-updates ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump node in /docker in the docker-major-updates group (#914) Bumps the docker-major-updates group in /docker with 1 update: node. Updates `node` from 24-slim to 26-slim --- updated-dependencies: - dependency-name: node dependency-version: 26-slim dependency-type: direct:production dependency-group: docker-major-updates ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * ci(deps): bump the github-actions group with 2 updates (#915) Bumps the github-actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [actions/setup-node](https://github.com/actions/setup-node). Updates `actions/checkout` from 5 to 6 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) Updates `actions/setup-node` from 4 to 6 - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/setup-node dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump the prod-minor-updates group across 1 directory with 5 updates (#916) Bumps the prod-minor-updates group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [axios](https://github.com/axios/axios) | `1.17.0` | `1.18.0` | | [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) | `12.10.0` | `12.11.1` | | [body-parser](https://github.com/expressjs/body-parser) | `2.2.2` | `2.3.0` | | [multer](https://github.com/expressjs/multer) | `2.1.1` | `2.2.0` | | [undici](https://github.com/nodejs/undici) | `8.4.0` | `8.5.0` | Updates `axios` from 1.17.0 to 1.18.0 - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.17.0...v1.18.0) Updates `better-sqlite3` from 12.10.0 to 12.11.1 - [Release notes](https://github.com/WiseLibs/better-sqlite3/releases) - [Commits](https://github.com/WiseLibs/better-sqlite3/compare/v12.10.0...v12.11.1) Updates `body-parser` from 2.2.2 to 2.3.0 - [Release notes](https://github.com/expressjs/body-parser/releases) - [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md) - [Commits](https://github.com/expressjs/body-parser/compare/v2.2.2...v2.3.0) Updates `multer` from 2.1.1 to 2.2.0 - [Release notes](https://github.com/expressjs/multer/releases) - [Changelog](https://github.com/expressjs/multer/blob/main/CHANGELOG.md) - [Commits](https://github.com/expressjs/multer/compare/v2.1.1...v2.2.0) Updates `undici` from 8.4.0 to 8.5.0 - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v8.4.0...v8.5.0) --- updated-dependencies: - dependency-name: axios dependency-version: 1.18.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: better-sqlite3 dependency-version: 12.11.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: body-parser dependency-version: 2.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: multer dependency-version: 2.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: undici dependency-version: 8.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat: add unlink button for dual auth * fix: general bug fixes * fix: general qol/small feature additions * fix: 100mb max upload size * fix: terminal syntax not handling carriage returns or shell prompt lines properly * feat: continued general improvements * fix: terminal outputting success right after folder path * chore: update release notes * feat: continued fixes and improvements * chore: lint, format, and bump version to 2.4.1 * chore: sync Crowdin translations for 2.4.1 * fix: rebase dev branch onto main before Crowdin download * fix: use merge instead of rebase to sync main before Crowdin --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
+131
-119
@@ -5,7 +5,7 @@ import ssh2Pkg, {
|
||||
type PseudoTtyOptions,
|
||||
} from "ssh2";
|
||||
const { Client, utils: ssh2Utils } = ssh2Pkg;
|
||||
import { SSH_ALGORITHMS } from "../utils/ssh-algorithms.js";
|
||||
import { buildSSHAlgorithms } from "../utils/ssh-algorithms.js";
|
||||
import axios from "axios";
|
||||
import { getDb } from "../database/db/index.js";
|
||||
import { hosts } from "../database/db/schema.js";
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
waitForTmuxSession,
|
||||
} from "./tmux-helper.js";
|
||||
import { MemoryAgent, performPortKnocking } from "./terminal-auth-helpers.js";
|
||||
import { isWindowsSftpPath, sftpPathToLocalPath } from "./transfer-paths.js";
|
||||
|
||||
interface ConnectToHostData {
|
||||
cols: number;
|
||||
@@ -187,9 +188,8 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
let isConnecting = false;
|
||||
let isConnected = false;
|
||||
let isCleaningUp = false;
|
||||
let cwdPending = false;
|
||||
let cwdBuffer = "";
|
||||
let isShellInitializing = false;
|
||||
let isDuplicateConnDiscarded = false;
|
||||
let warpgateAuthPromptSent = false;
|
||||
let warpgateAuthTimeout: NodeJS.Timeout | null = null;
|
||||
let isAwaitingAuthCredentials = false;
|
||||
@@ -237,7 +237,12 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
if (currentSessionId) {
|
||||
const session = sessionManager.getSession(currentSessionId);
|
||||
if (session?.isConnected) {
|
||||
sessionManager.detachWs(currentSessionId);
|
||||
// Only detach if this WS is still the one attached to the session.
|
||||
// If a refresh reconnected and reattached a new WS before this close
|
||||
// event fired, we must not clobber that new attachment.
|
||||
if (session.attachedWs === ws || session.attachedWs === null) {
|
||||
sessionManager.detachWs(currentSessionId);
|
||||
}
|
||||
} else {
|
||||
sessionManager.destroySession(currentSessionId);
|
||||
currentSessionId = null;
|
||||
@@ -446,15 +451,80 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
break;
|
||||
|
||||
case "get_cwd": {
|
||||
const activeStream =
|
||||
sessionManager.getSession(currentSessionId)?.sshStream ?? sshStream;
|
||||
if (!activeStream) {
|
||||
const activeConn =
|
||||
sessionManager.getSession(currentSessionId)?.sshConn ?? sshConn;
|
||||
if (!activeConn) {
|
||||
ws.send(JSON.stringify({ type: "cwd", path: "/" }));
|
||||
break;
|
||||
}
|
||||
cwdPending = true;
|
||||
cwdBuffer = "";
|
||||
activeStream.write('\x15a=TERMIX_CWD; echo "$a:$(pwd)"\r');
|
||||
activeConn.exec("pwd", (err, execStream) => {
|
||||
if (err) {
|
||||
ws.send(JSON.stringify({ type: "cwd", path: "/" }));
|
||||
return;
|
||||
}
|
||||
let stdout = "";
|
||||
execStream.on("data", (chunk: Buffer) => {
|
||||
stdout += chunk.toString("utf-8");
|
||||
});
|
||||
execStream.stderr.on("data", () => {});
|
||||
execStream.on("close", () => {
|
||||
const cwd = stdout.trim() || "/";
|
||||
const attachedWs =
|
||||
sessionManager.getSession(currentSessionId)?.attachedWs ?? ws;
|
||||
if (attachedWs.readyState === WebSocket.OPEN) {
|
||||
attachedWs.send(JSON.stringify({ type: "cwd", path: cwd }));
|
||||
}
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "open_file_in_editor": {
|
||||
const { path: requestedPath } = data as { path: string };
|
||||
const activeConn =
|
||||
sessionManager.getSession(currentSessionId)?.sshConn ?? sshConn;
|
||||
if (!activeConn || !requestedPath) {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "open_file_in_editor",
|
||||
path: requestedPath || "/",
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
const escapedPath = requestedPath.replace(/'/g, "'\\''");
|
||||
activeConn.exec(
|
||||
`realpath '${escapedPath}' 2>/dev/null || echo '${escapedPath}'`,
|
||||
(err, execStream) => {
|
||||
if (err) {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "open_file_in_editor",
|
||||
path: requestedPath,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
let stdout = "";
|
||||
execStream.on("data", (chunk: Buffer) => {
|
||||
stdout += chunk.toString("utf-8");
|
||||
});
|
||||
execStream.stderr.on("data", () => {});
|
||||
execStream.on("close", () => {
|
||||
const resolvedPath = stdout.trim() || requestedPath;
|
||||
const attachedWs =
|
||||
sessionManager.getSession(currentSessionId)?.attachedWs ?? ws;
|
||||
if (attachedWs.readyState === WebSocket.OPEN) {
|
||||
attachedWs.send(
|
||||
JSON.stringify({
|
||||
type: "open_file_in_editor",
|
||||
path: resolvedPath,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -990,7 +1060,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (!hostConfig.useSocks5 && resolvedHostData.useSocks5) {
|
||||
if (resolvedHostData.useSocks5) {
|
||||
hostConfig.useSocks5 = resolvedHostData.useSocks5;
|
||||
hostConfig.socks5Host = resolvedHostData.socks5Host;
|
||||
hostConfig.socks5Port = resolvedHostData.socks5Port;
|
||||
@@ -1134,27 +1204,43 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
!existingSession.sshStream.destroyed &&
|
||||
existingSession.sshConn !== sshConn
|
||||
) {
|
||||
const reusedSessionId = currentSessionId;
|
||||
sshLogger.info(
|
||||
"Reusing existing live session after duplicate connectToHost, closing new SSH conn",
|
||||
{
|
||||
operation: "terminal_reuse_existing_session",
|
||||
sessionId: currentSessionId,
|
||||
sessionId: reusedSessionId,
|
||||
tabInstanceId,
|
||||
userId,
|
||||
},
|
||||
);
|
||||
// Null out currentSessionId before ending the duplicate connection so
|
||||
// the sshConn "close" handler does not destroy the reused session.
|
||||
// Set isDuplicateConnDiscarded so the close handler does not send a
|
||||
// "disconnected" message to the new WS that is now attached to the live session.
|
||||
// Null out currentSessionId before ending the duplicate connection so
|
||||
// the sshConn "close" handler does not destroy the reused session.
|
||||
// Set isDuplicateConnDiscarded so the close handler exits without
|
||||
// sending a "disconnected" message to the new WS.
|
||||
currentSessionId = null;
|
||||
isDuplicateConnDiscarded = true;
|
||||
clearTimeout(connectionTimeout);
|
||||
try {
|
||||
sshConn?.end();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
sshConn = null;
|
||||
sshStream = null;
|
||||
|
||||
// Point this WS handler's closure at the live session so the input
|
||||
// handler can forward keystrokes via currentSessionId.
|
||||
currentSessionId = reusedSessionId;
|
||||
sshStream = existingSession.sshStream;
|
||||
sshConn = existingSession.sshConn;
|
||||
isConnecting = false;
|
||||
isConnected = true;
|
||||
sessionManager.attachWs(currentSessionId, userId, ws, tabInstanceId);
|
||||
sessionManager.attachWs(reusedSessionId, userId, ws, tabInstanceId);
|
||||
|
||||
const buffered = sessionManager.getBuffer(existingSession);
|
||||
if (buffered) {
|
||||
@@ -1163,20 +1249,18 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "sessionCreated",
|
||||
sessionId: currentSessionId,
|
||||
sessionId: reusedSessionId,
|
||||
}),
|
||||
);
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "sessionAttached",
|
||||
sessionId: currentSessionId,
|
||||
sessionId: reusedSessionId,
|
||||
}),
|
||||
);
|
||||
ws.send(
|
||||
JSON.stringify({ type: "connected", message: "Session reattached" }),
|
||||
);
|
||||
|
||||
cleanupAuthState(connectionTimeout);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1350,44 +1434,9 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
|
||||
const boundSessionId = currentSessionId;
|
||||
|
||||
const CWD_SENTINEL = "TERMIX_CWD:";
|
||||
|
||||
stream.on("data", (data: Buffer) => {
|
||||
try {
|
||||
let utf8String = data.toString("utf-8");
|
||||
|
||||
if (cwdPending) {
|
||||
cwdBuffer += utf8String;
|
||||
const sentinelIdx = cwdBuffer.indexOf(CWD_SENTINEL);
|
||||
if (sentinelIdx !== -1) {
|
||||
const afterSentinel = cwdBuffer.slice(
|
||||
sentinelIdx + CWD_SENTINEL.length,
|
||||
);
|
||||
const newlineIdx = afterSentinel.search(/[\r\n]/);
|
||||
if (newlineIdx !== -1) {
|
||||
const cwd =
|
||||
afterSentinel.slice(0, newlineIdx).trim() || "/";
|
||||
cwdPending = false;
|
||||
// Strip the sentinel line from output sent to terminal
|
||||
const beforeSentinel = cwdBuffer.slice(0, sentinelIdx);
|
||||
const afterNewline = afterSentinel.slice(newlineIdx);
|
||||
utf8String = beforeSentinel + afterNewline;
|
||||
cwdBuffer = "";
|
||||
const attachedWs =
|
||||
sessionManager.getSession(boundSessionId)?.attachedWs ??
|
||||
ws;
|
||||
if (attachedWs.readyState === WebSocket.OPEN) {
|
||||
attachedWs.send(
|
||||
JSON.stringify({ type: "cwd", path: cwd }),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const utf8String = data.toString("utf-8");
|
||||
|
||||
if (!utf8String) return;
|
||||
|
||||
@@ -1475,7 +1524,14 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
const runPostShellCommands = (delay: number) => {
|
||||
setTimeout(() => {
|
||||
if (initialPath && initialPath.trim() !== "") {
|
||||
const cdCommand = `cd "${initialPath.replace(/"/g, '\\"')}"\r`;
|
||||
let cdCommand: string;
|
||||
if (isWindowsSftpPath(initialPath)) {
|
||||
const winPath = sftpPathToLocalPath(initialPath);
|
||||
const escaped = winPath.replace(/"/g, '""');
|
||||
cdCommand = `cd "${escaped}"\r`;
|
||||
} else {
|
||||
cdCommand = `cd "${initialPath.replace(/"/g, '\\"')}"\r`;
|
||||
}
|
||||
stream.write(cdCommand);
|
||||
}
|
||||
if (executeCommand && executeCommand.trim() !== "") {
|
||||
@@ -1543,27 +1599,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
}),
|
||||
);
|
||||
runPostShellCommands(0);
|
||||
} else if (detection.sessions.length === 1) {
|
||||
attachOrCreateTmuxSession(stream, detection.sessions[0].name);
|
||||
const sessionName = detection.sessions[0].name;
|
||||
const session = sessionManager.getSession(boundSessionId);
|
||||
if (session) {
|
||||
session.tmuxSessionName = sessionName;
|
||||
}
|
||||
sshLogger.info("Auto-attached to existing tmux session", {
|
||||
operation: "tmux_auto_attach",
|
||||
sessionName,
|
||||
hostId: id,
|
||||
});
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "tmux_session_attached",
|
||||
sessionName,
|
||||
}),
|
||||
);
|
||||
// Reattaching to existing session -- don't re-run
|
||||
// initialPath/executeCommand since the session already
|
||||
// has its own state
|
||||
} else {
|
||||
sshLogger.info(
|
||||
"Multiple tmux sessions found, sending list to frontend",
|
||||
@@ -1910,6 +1945,11 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
hostId: id,
|
||||
});
|
||||
|
||||
if (isDuplicateConnDiscarded) {
|
||||
cleanupAuthState(connectionTimeout);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAwaitingAuthCredentials) {
|
||||
if (currentSessionId) {
|
||||
sessionManager.destroySession(currentSessionId);
|
||||
@@ -1991,6 +2031,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
resolvedCredentials as unknown as Parameters<
|
||||
typeof sshAuthManager.handleKeyboardInteractive
|
||||
>[5],
|
||||
hostConfig,
|
||||
);
|
||||
|
||||
isKeyboardInteractive = sshAuthManager.context.isKeyboardInteractive;
|
||||
@@ -2008,19 +2049,24 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
const hostKeepaliveInterval = hostConfig.terminalConfig?.keepaliveInterval;
|
||||
const hostKeepaliveCountMax = hostConfig.terminalConfig?.keepaliveCountMax;
|
||||
|
||||
// Pre-fetch the stored host key before connect so the verifier callback
|
||||
// runs synchronously during SSH key exchange, avoiding LoginGraceTime
|
||||
// expiry on slow connections (especially through jump host tunnels).
|
||||
const preloadedHostData = await SSHHostKeyVerifier.preloadHostData(id);
|
||||
|
||||
const connectConfig: Record<string, unknown> = {
|
||||
host: ip,
|
||||
port,
|
||||
username,
|
||||
tryKeyboard:
|
||||
resolvedCredentials.authType !== "none" &&
|
||||
resolvedCredentials.authType !== "tailscale",
|
||||
tryKeyboard: resolvedCredentials.authType !== "tailscale",
|
||||
keepaliveInterval:
|
||||
typeof hostKeepaliveInterval === "number"
|
||||
? hostKeepaliveInterval * 1000
|
||||
? Math.max(5000, hostKeepaliveInterval * 1000)
|
||||
: 30000,
|
||||
keepaliveCountMax:
|
||||
typeof hostKeepaliveCountMax === "number" ? hostKeepaliveCountMax : 3,
|
||||
typeof hostKeepaliveCountMax === "number"
|
||||
? Math.max(1, hostKeepaliveCountMax)
|
||||
: 5,
|
||||
readyTimeout: 120000,
|
||||
tcpKeepAlive: true,
|
||||
tcpKeepAliveInitialDelay: 30000,
|
||||
@@ -2032,6 +2078,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
ws,
|
||||
userId,
|
||||
false,
|
||||
preloadedHostData,
|
||||
),
|
||||
env: {
|
||||
TERM: "xterm-256color",
|
||||
@@ -2045,51 +2092,16 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
||||
LC_COLLATE: "en_US.UTF-8",
|
||||
COLORTERM: "truecolor",
|
||||
},
|
||||
algorithms: {
|
||||
kex: [
|
||||
"curve25519-sha256",
|
||||
"curve25519-sha256@libssh.org",
|
||||
"ecdh-sha2-nistp521",
|
||||
"ecdh-sha2-nistp384",
|
||||
"ecdh-sha2-nistp256",
|
||||
"diffie-hellman-group-exchange-sha256",
|
||||
"diffie-hellman-group18-sha512",
|
||||
"diffie-hellman-group17-sha512",
|
||||
"diffie-hellman-group16-sha512",
|
||||
"diffie-hellman-group15-sha512",
|
||||
"diffie-hellman-group14-sha256",
|
||||
"diffie-hellman-group14-sha1",
|
||||
"diffie-hellman-group-exchange-sha1",
|
||||
"diffie-hellman-group1-sha1",
|
||||
],
|
||||
serverHostKey: [
|
||||
"ssh-ed25519",
|
||||
"ecdsa-sha2-nistp521",
|
||||
"ecdsa-sha2-nistp384",
|
||||
"ecdsa-sha2-nistp256",
|
||||
"rsa-sha2-512",
|
||||
"rsa-sha2-256",
|
||||
"ssh-rsa",
|
||||
"ssh-dss",
|
||||
],
|
||||
cipher: SSH_ALGORITHMS.cipher,
|
||||
hmac: [
|
||||
"hmac-sha2-512-etm@openssh.com",
|
||||
"hmac-sha2-256-etm@openssh.com",
|
||||
"hmac-sha2-512",
|
||||
"hmac-sha2-256",
|
||||
"hmac-sha1",
|
||||
"hmac-md5",
|
||||
],
|
||||
compress: ["none", "zlib@openssh.com", "zlib"],
|
||||
},
|
||||
algorithms: buildSSHAlgorithms(
|
||||
hostConfig.terminalConfig?.allowLegacyAlgorithms !== false,
|
||||
),
|
||||
};
|
||||
|
||||
if (
|
||||
resolvedCredentials.authType === "none" ||
|
||||
resolvedCredentials.authType === "tailscale"
|
||||
) {
|
||||
// Tailscale SSH and "none" auth: daemon handles authorization, no credentials needed
|
||||
// Tailscale SSH and "none": no static credentials needed
|
||||
} else if (resolvedCredentials.authType === "password") {
|
||||
if (!resolvedCredentials.password) {
|
||||
sshLogger.error(
|
||||
|
||||
Reference in New Issue
Block a user