mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-06 14:30:13 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cfd0dc855 | ||
|
|
7a0e2f4127 |
No files matched your search
@@ -15,6 +15,11 @@ test("GroupDetailsPanel treats an empty SSH identity as an explicit setting", ()
|
||||
assert.equal(hasGroupSshFields({}), false);
|
||||
});
|
||||
|
||||
test("GroupDetailsPanel keeps the SSH section when X11 forwarding is set", () => {
|
||||
assert.equal(hasGroupSshFields({ x11Forwarding: true }), true);
|
||||
assert.equal(hasGroupSshFields({ x11Forwarding: false }), true);
|
||||
});
|
||||
|
||||
test("GroupDetailsPanel treats cleared telnet credentials as explicit settings", () => {
|
||||
assert.equal(hasGroupTelnetFields({ telnetUsername: "" }), true);
|
||||
assert.equal(hasGroupTelnetFields({ telnetPassword: "" }), true);
|
||||
|
||||
@@ -102,7 +102,7 @@ export const hasGroupSshFields = (c: Partial<GroupConfig>): boolean =>
|
||||
c.protocol === 'ssh' ||
|
||||
c.port !== undefined || !!c.username || !!c.password || !!c.identityFileId ||
|
||||
c.deviceType !== undefined ||
|
||||
c.agentForwarding !== undefined || c.authMethod !== undefined || c.identityId !== undefined ||
|
||||
c.agentForwarding !== undefined || c.x11Forwarding !== undefined || c.authMethod !== undefined || c.identityId !== undefined ||
|
||||
!!c.proxyProfileId || !!c.proxyConfig || !!c.hostChain || !!c.startupCommand || c.startupCommandRunMode !== undefined || c.legacyAlgorithms !== undefined || c.skipEcdsaHostKey !== undefined || c.algorithms !== undefined || c.backspaceBehavior !== undefined ||
|
||||
Boolean(c.environmentVariables && c.environmentVariables.length > 0) ||
|
||||
c.moshEnabled !== undefined || !!c.moshServerPath ||
|
||||
@@ -262,6 +262,7 @@ const GroupDetailsPanel: React.FC<GroupDetailsPanelPropsWithResize> = ({
|
||||
delete next.identityFilePaths;
|
||||
delete next.deviceType;
|
||||
delete next.agentForwarding;
|
||||
delete next.x11Forwarding;
|
||||
delete next.startupCommand;
|
||||
delete next.startupCommandRunMode;
|
||||
delete next.legacyAlgorithms;
|
||||
@@ -499,6 +500,10 @@ const GroupDetailsPanel: React.FC<GroupDetailsPanelPropsWithResize> = ({
|
||||
if (!parentGroup || groupConfigs.length === 0) return false;
|
||||
return !!resolveGroupDefaults(parentGroup, groupConfigs).skipEcdsaHostKey;
|
||||
}, [groupConfigs, parentGroup]);
|
||||
const inheritedX11Forwarding = useMemo(() => {
|
||||
if (!parentGroup || groupConfigs.length === 0) return false;
|
||||
return !!resolveGroupDefaults(parentGroup, groupConfigs).x11Forwarding;
|
||||
}, [groupConfigs, parentGroup]);
|
||||
const inheritedStartupCommandRunMode = useMemo(() => {
|
||||
if (!parentGroup || groupConfigs.length === 0) return "paste";
|
||||
return resolveGroupDefaults(parentGroup, groupConfigs).startupCommandRunMode ?? "paste";
|
||||
@@ -568,6 +573,7 @@ const GroupDetailsPanel: React.FC<GroupDetailsPanelPropsWithResize> = ({
|
||||
...(form.identityFilePaths !== undefined && { identityFilePaths: form.identityFilePaths }),
|
||||
...(form.deviceType !== undefined && { deviceType: form.deviceType }),
|
||||
...(form.agentForwarding !== undefined && { agentForwarding: form.agentForwarding }),
|
||||
...(form.x11Forwarding !== undefined && { x11Forwarding: form.x11Forwarding }),
|
||||
...(form.startupCommand !== undefined && { startupCommand: form.startupCommand }),
|
||||
...(form.startupCommandRunMode !== undefined && { startupCommandRunMode: form.startupCommandRunMode }),
|
||||
...(form.legacyAlgorithms !== undefined && { legacyAlgorithms: form.legacyAlgorithms }),
|
||||
@@ -778,6 +784,7 @@ const GroupDetailsPanel: React.FC<GroupDetailsPanelPropsWithResize> = ({
|
||||
setNewKeyFilePath={setNewKeyFilePath}
|
||||
inheritedLegacyAlgorithms={inheritedLegacyAlgorithms}
|
||||
inheritedSkipEcdsaHostKey={inheritedSkipEcdsaHostKey}
|
||||
inheritedX11Forwarding={inheritedX11Forwarding}
|
||||
inheritedStartupCommandRunMode={inheritedStartupCommandRunMode}
|
||||
showAlgorithmOverrides={showAlgorithmOverrides}
|
||||
setShowAlgorithmOverrides={setShowAlgorithmOverrides}
|
||||
|
||||
@@ -47,6 +47,7 @@ export const GroupSshSettingsSection: React.FC<GroupSshSettingsSectionProps> = (
|
||||
setNewKeyFilePath,
|
||||
inheritedLegacyAlgorithms,
|
||||
inheritedSkipEcdsaHostKey,
|
||||
inheritedX11Forwarding,
|
||||
inheritedStartupCommandRunMode,
|
||||
showAlgorithmOverrides,
|
||||
setShowAlgorithmOverrides,
|
||||
@@ -395,6 +396,16 @@ export const GroupSshSettingsSection: React.FC<GroupSshSettingsSectionProps> = (
|
||||
onToggle={() => update("agentForwarding", !form.agentForwarding)}
|
||||
/>
|
||||
|
||||
<ToggleRow
|
||||
label={t("hostDetails.x11Forwarding")}
|
||||
hint={t("hostDetails.x11Forwarding.desc")}
|
||||
enabled={!!(form.x11Forwarding ?? inheritedX11Forwarding)}
|
||||
onToggle={() => update(
|
||||
"x11Forwarding",
|
||||
!(form.x11Forwarding ?? inheritedX11Forwarding),
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Startup Command — Textarea so multi-line sequences are typeable
|
||||
here just like on the per-host details panel (#1083 follow-up). */}
|
||||
<Textarea
|
||||
|
||||
@@ -30,6 +30,17 @@ test('enabling SSH agent login clears an imported none sentinel', () => {
|
||||
assert.match(source, /resolveSshAgentToggleUpdate/);
|
||||
});
|
||||
|
||||
test('X11 forwarding display follows the effective host after group defaults', () => {
|
||||
assert.match(source, /applyGroupDefaults\(form, effectiveGroupDefaults\)/);
|
||||
assert.match(source, /const showX11Forwarding = \(!effectiveConnectionHost\.protocol \|\| effectiveConnectionHost\.protocol === "ssh"\)/);
|
||||
assert.match(source, /!effectiveConnectionHost\.moshEnabled/);
|
||||
assert.match(source, /!effectiveConnectionHost\.etEnabled/);
|
||||
assert.match(source, /const effectiveX11Forwarding = !!effectiveConnectionHost\.x11Forwarding/);
|
||||
assert.match(source, /\{showX11Forwarding && \(/);
|
||||
assert.match(source, /enabled=\{effectiveX11Forwarding\}/);
|
||||
assert.doesNotMatch(source, /effectiveGroupDefaults\?\.x11Forwarding/);
|
||||
});
|
||||
|
||||
test('login and forwarding warnings use independent agent status', () => {
|
||||
assert.match(source, /systemSshAgentEnabled && sshAgentStatus && !sshAgentStatus\.running/);
|
||||
assert.match(source, /form\.agentForwarding && sshForwardingAgentStatus && !sshForwardingAgentStatus\.running/);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getHostOsSelection, HOST_OS_SELECTIONS, resolveHostOs } from '../domain/host';
|
||||
import { applyGroupDefaults } from "../domain/groupConfig";
|
||||
import React from "react";
|
||||
import { AlertTriangle, ChevronDown, ChevronUp, Forward, Globe, HeartPulse, KeyRound, Link2, Palette, Plus, Router, ShieldAlert, TerminalSquare, Timer, Wifi, X, Variable } from "lucide-react";
|
||||
import { customThemeStore } from "../application/state/customThemeStore";
|
||||
@@ -63,6 +64,15 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
|
||||
const systemSshAgentEnabled = effectiveAuthMethod === "auto"
|
||||
? form.useSshAgent !== false
|
||||
: effectiveAuthMethod === "key" && form.useSshAgent === true;
|
||||
// Match the connection: inherited X11 is dropped when the effective transport
|
||||
// is Mosh, Eternal Terminal, or anything other than plain SSH.
|
||||
const effectiveConnectionHost = effectiveGroupDefaults
|
||||
? applyGroupDefaults(form, effectiveGroupDefaults)
|
||||
: form;
|
||||
const showX11Forwarding = (!effectiveConnectionHost.protocol || effectiveConnectionHost.protocol === "ssh")
|
||||
&& !effectiveConnectionHost.moshEnabled
|
||||
&& !effectiveConnectionHost.etEnabled;
|
||||
const effectiveX11Forwarding = !!effectiveConnectionHost.x11Forwarding;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -337,7 +347,7 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
|
||||
</HostDetailsSection>
|
||||
|
||||
{/* X11 Forwarding */}
|
||||
{(!form.protocol || form.protocol === "ssh") && !form.moshEnabled && !form.etEnabled && (
|
||||
{showX11Forwarding && (
|
||||
<HostDetailsSection
|
||||
icon={<TerminalSquare size={14} className="text-muted-foreground" />}
|
||||
title={t("hostDetails.section.x11Forwarding")}
|
||||
@@ -345,8 +355,8 @@ export const HostDetailsAdvancedSections: React.FC<HostDetailsAdvancedSectionsPr
|
||||
<ToggleRow
|
||||
label={t("hostDetails.x11Forwarding")}
|
||||
hint={t("hostDetails.x11Forwarding.desc")}
|
||||
enabled={!!form.x11Forwarding}
|
||||
onToggle={() => update("x11Forwarding", !form.x11Forwarding)}
|
||||
enabled={effectiveX11Forwarding}
|
||||
onToggle={() => update("x11Forwarding", !effectiveX11Forwarding)}
|
||||
/>
|
||||
</HostDetailsSection>
|
||||
)}
|
||||
|
||||
@@ -595,6 +595,58 @@ test("applyGroupDefaults keeps host-level skipEcdsaHostKey instead of group defa
|
||||
assert.equal(result.skipEcdsaHostKey, false);
|
||||
});
|
||||
|
||||
test("resolveGroupDefaults uses the nearest group X11 toggle and stays off otherwise", () => {
|
||||
const configs: GroupConfig[] = [
|
||||
{ path: "prod", x11Forwarding: true },
|
||||
{ path: "prod/edge", x11Forwarding: false },
|
||||
];
|
||||
assert.equal(resolveGroupDefaults("prod", configs).x11Forwarding, true);
|
||||
assert.equal(resolveGroupDefaults("prod/web", configs).x11Forwarding, true);
|
||||
assert.equal(resolveGroupDefaults("prod/edge", configs).x11Forwarding, false);
|
||||
assert.equal(resolveGroupDefaults("prod/edge/api", configs).x11Forwarding, false);
|
||||
assert.equal(resolveGroupDefaults("other", configs).x11Forwarding, undefined);
|
||||
});
|
||||
|
||||
test("applyGroupDefaults inherits group X11 forwarding when the host leaves it unset", () => {
|
||||
assert.equal(applyGroupDefaults(host(), { x11Forwarding: true }).x11Forwarding, true);
|
||||
});
|
||||
|
||||
test("applyGroupDefaults keeps an explicit host X11 toggle ahead of the group", () => {
|
||||
assert.equal(
|
||||
applyGroupDefaults(host({ x11Forwarding: false }), { x11Forwarding: true }).x11Forwarding,
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
applyGroupDefaults(host({ x11Forwarding: true }), { x11Forwarding: false }).x11Forwarding,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("applyGroupDefaults leaves X11 forwarding off when neither host nor group sets it", () => {
|
||||
assert.equal(applyGroupDefaults(host(), {}).x11Forwarding, undefined);
|
||||
});
|
||||
|
||||
test("applyGroupDefaults does not inherit X11 forwarding onto non-SSH transports", () => {
|
||||
const groupDefaults = { x11Forwarding: true as const };
|
||||
assert.equal(applyGroupDefaults(host({ moshEnabled: true }), groupDefaults).x11Forwarding, undefined);
|
||||
assert.equal(applyGroupDefaults(host({ etEnabled: true }), groupDefaults).x11Forwarding, undefined);
|
||||
assert.equal(applyGroupDefaults(host({ protocol: "telnet" }), groupDefaults).x11Forwarding, undefined);
|
||||
assert.equal(applyGroupDefaults(host({ protocol: "serial" }), groupDefaults).x11Forwarding, undefined);
|
||||
assert.equal(applyGroupDefaults(host({ protocol: "local" }), groupDefaults).x11Forwarding, undefined);
|
||||
assert.equal(
|
||||
applyGroupDefaults(host(), { ...groupDefaults, moshEnabled: true }).x11Forwarding,
|
||||
undefined,
|
||||
);
|
||||
assert.equal(
|
||||
applyGroupDefaults(host(), { ...groupDefaults, protocol: "telnet" }).x11Forwarding,
|
||||
undefined,
|
||||
);
|
||||
assert.equal(
|
||||
applyGroupDefaults(host({ moshEnabled: false }), { ...groupDefaults, moshEnabled: true }).x11Forwarding,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("applyGroupDefaults inherits algorithm overrides from the group", () => {
|
||||
const overrides = { serverHostKey: ["ssh-rsa", "ssh-dss"] };
|
||||
const result = applyGroupDefaults(host(), { algorithms: overrides });
|
||||
@@ -678,6 +730,7 @@ test("applyGroupDefaults does not inherit SSH transport/input defaults into loca
|
||||
legacyAlgorithms: true,
|
||||
skipEcdsaHostKey: true,
|
||||
agentForwarding: true,
|
||||
x11Forwarding: true,
|
||||
hostChain: { hostIds: ["bastion-1"] },
|
||||
charset: "UTF-8",
|
||||
fontSize: 14,
|
||||
@@ -702,6 +755,7 @@ test("applyGroupDefaults does not inherit SSH transport/input defaults into loca
|
||||
assert.equal(result.legacyAlgorithms, undefined);
|
||||
assert.equal(result.skipEcdsaHostKey, undefined);
|
||||
assert.equal(result.agentForwarding, undefined);
|
||||
assert.equal(result.x11Forwarding, undefined);
|
||||
assert.equal(result.hostChain, undefined);
|
||||
// Display/appearance defaults still apply to local shells.
|
||||
assert.equal(result.charset, "UTF-8");
|
||||
|
||||
+11
-2
@@ -149,7 +149,7 @@ export function resolveGroupDefaults(
|
||||
|
||||
const INHERITABLE_KEYS: (keyof GroupConfig)[] = [
|
||||
'username', 'password', 'savePassword', 'authMethod', 'identityId', 'identityFileId', 'identityFilePaths',
|
||||
'port', 'protocol', 'deviceType', 'agentForwarding', 'proxyProfileId', 'proxyConfig', 'hostChain', 'startupCommand', 'startupCommandRunMode',
|
||||
'port', 'protocol', 'deviceType', 'agentForwarding', 'x11Forwarding', 'proxyProfileId', 'proxyConfig', 'hostChain', 'startupCommand', 'startupCommandRunMode',
|
||||
'legacyAlgorithms', 'skipEcdsaHostKey', 'algorithms',
|
||||
'environmentVariables', 'charset', 'moshEnabled', 'moshServerPath',
|
||||
'etEnabled', 'etPort',
|
||||
@@ -178,12 +178,13 @@ const SSH_CREDENTIAL_KEYS = new Set<keyof GroupConfig>([
|
||||
|
||||
// SSH transport/input settings the local-shell editor does not expose: input
|
||||
// remapping (backspace), Mosh/Eternal-Terminal transports, and SSH-only
|
||||
// algorithm/agent/chain flags. A local shell never runs through SSH, so these
|
||||
// algorithm/agent/X11/chain flags. A local shell never runs through SSH, so these
|
||||
// defaults would either remap local PTY input (`backspaceBehavior`,
|
||||
// createXTermRuntime) or make flag-first consumers report a purely local
|
||||
// session as Mosh/ET — with no way to override them in the local editor.
|
||||
const SSH_ONLY_TRANSPORT_KEYS = new Set<keyof GroupConfig>([
|
||||
'agentForwarding',
|
||||
'x11Forwarding',
|
||||
'legacyAlgorithms',
|
||||
'skipEcdsaHostKey',
|
||||
'algorithms',
|
||||
@@ -246,11 +247,19 @@ export function applyGroupDefaults(
|
||||
// local CMD/PowerShell/POSIX shell be treated as a network-device CLI
|
||||
// (no normal autocomplete, network semantics for AI/command execution).
|
||||
const isLocalShellHost = host.protocol === 'local';
|
||||
// X11 forwarding only applies to plain SSH. Use the host value when it is
|
||||
// set, otherwise the group transport, so a group Mosh/ET/Telnet default
|
||||
// does not also turn X11 on.
|
||||
const effectiveProtocol = host.protocol ?? groupDefaults.protocol;
|
||||
const inheritsX11Forwarding = (!effectiveProtocol || effectiveProtocol === 'ssh')
|
||||
&& !(host.moshEnabled ?? groupDefaults.moshEnabled)
|
||||
&& !(host.etEnabled ?? groupDefaults.etEnabled);
|
||||
|
||||
for (const key of INHERITABLE_KEYS) {
|
||||
if (shouldSkipGroupSshCredentialBundle && SSH_CREDENTIAL_KEYS.has(key)) continue;
|
||||
if (isSerialHost && SSH_CREDENTIAL_KEYS.has(key)) continue;
|
||||
if (isLocalShellHost && (SSH_CREDENTIAL_KEYS.has(key) || SSH_ONLY_TRANSPORT_KEYS.has(key) || key === 'startupCommand' || key === 'deviceType')) continue;
|
||||
if (key === 'x11Forwarding' && !inheritsX11Forwarding) continue;
|
||||
if (key === 'password' && effective.savePassword === false) continue;
|
||||
if (key === 'telnetIdentityId' && hostHasManualTelnetCredentials) continue;
|
||||
if (key === 'proxyProfileId') {
|
||||
|
||||
@@ -427,6 +427,7 @@ export interface GroupConfig {
|
||||
protocol?: 'ssh' | 'telnet';
|
||||
deviceType?: 'general' | 'network';
|
||||
agentForwarding?: boolean;
|
||||
x11Forwarding?: boolean;
|
||||
proxyProfileId?: string;
|
||||
proxyConfig?: ProxyConfig;
|
||||
hostChain?: HostChainConfig;
|
||||
|
||||
@@ -3,6 +3,7 @@ import test from "node:test";
|
||||
|
||||
import type { GroupConfig, Host, ProxyProfile, TerminalSession } from "./models";
|
||||
import {
|
||||
resolveEffectiveTerminalHost,
|
||||
resolveTerminalChainHosts,
|
||||
resolveTerminalSessionHost,
|
||||
} from "./terminalHostResolution";
|
||||
@@ -84,6 +85,45 @@ test("resolveTerminalSessionHost applies group default proxy profiles before ope
|
||||
assert.deepEqual(resolved.proxyConfig, proxyProfiles[0].config);
|
||||
});
|
||||
|
||||
test("resolveEffectiveTerminalHost applies the nearest group X11 default on the connect path", () => {
|
||||
const host: Host = {
|
||||
id: "target",
|
||||
label: "Target",
|
||||
hostname: "target.example.test",
|
||||
username: "alice",
|
||||
port: 22,
|
||||
protocol: "ssh",
|
||||
tags: [],
|
||||
os: "linux",
|
||||
group: "prod/web",
|
||||
};
|
||||
const groupConfigs: GroupConfig[] = [
|
||||
{ path: "prod", x11Forwarding: true },
|
||||
{ path: "prod/edge", x11Forwarding: false },
|
||||
];
|
||||
|
||||
assert.equal(
|
||||
resolveEffectiveTerminalHost({ host, groupConfigs, proxyProfiles: [] }).x11Forwarding,
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
resolveEffectiveTerminalHost({
|
||||
host: { ...host, x11Forwarding: false },
|
||||
groupConfigs,
|
||||
proxyProfiles: [],
|
||||
}).x11Forwarding,
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
resolveEffectiveTerminalHost({
|
||||
host: { ...host, group: "prod/edge/api" },
|
||||
groupConfigs,
|
||||
proxyProfiles: [],
|
||||
}).x11Forwarding,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveTerminalSessionHost defaults missing saved remote sessions to SSH", () => {
|
||||
const resolved = resolveTerminalSessionHost({
|
||||
session: {
|
||||
|
||||
Reference in new issue
Block a user