feat(ssh): support JumpServer keyboard-interactive authentication

This commit is contained in:
Rson9 authored and GitHub committed 2026-08-04 14:23:44 +08:00
1 parent 8228c2af17
commit 947cc16d92
28 files changed
+873 -72

No files matched your search

@@ -309,6 +309,7 @@ function defaultSshTunnel(): SshTunnelConfig {
use_ssh_agent: false,
ssh_agent_sock_path: "",
auth_method: "password",
allow_exec_channel_proxy: false,
};
}
@@ -342,6 +343,7 @@ function normalizeSshTunnel(hop: Partial<SshTunnelConfig>): SshTunnelConfig {
use_ssh_agent: !!hop.use_ssh_agent,
ssh_agent_sock_path: hop.ssh_agent_sock_path || "",
auth_method: hop.auth_method || inferSshAuthMethod(hop),
allow_exec_channel_proxy: !!hop.allow_exec_channel_proxy,
profile_id: hop.profile_id || undefined,
};
}
@@ -6978,6 +6980,13 @@ function openExternalUrl(url: string) {
<span class="text-xs text-muted-foreground">{{ t("connection.sshExposeLan") }}</span>
</label>
</div>
<div class="grid grid-cols-4 items-start gap-4">
<span />
<label class="col-span-3 flex items-start gap-2 cursor-pointer">
<input type="checkbox" v-model="selectedSshLayer.allow_exec_channel_proxy" class="mt-0.5 mr-0" :disabled="selectedSshLayer.enabled === false" />
<span class="text-xs text-muted-foreground">{{ t("connection.sshAllowExecChannelProxy") }}</span>
</label>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">{{ t("connection.sshConnectTimeout") }}</Label>
<Input v-model.number="selectedSshLayer.connect_timeout_secs" type="number" min="1" max="300" step="1" class="col-span-3" :disabled="selectedSshLayer.enabled === false" />
@@ -528,10 +528,10 @@ async function toggle() {
node.isExpanded = false;
if (!connectionStore.sidebarSearchQuery) connectionStore.releaseCollapsedTreeNodeChildren(node.id);
connectionStore.cancelTreeNodeLoad(node.id);
emit("node-toggled", node, true);
emitNodeToggled(node, true, false);
} else {
node.isExpanded = true;
emit("node-toggled", node, false);
emitNodeToggled(node, false);
}
return;
}
@@ -541,13 +541,13 @@ async function toggle() {
if (node.type === "connection-group") {
node.isExpanded = !node.isExpanded;
connectionStore.toggleConnectionGroupCollapsed(node.id);
emit("node-toggled", node, wasExpanded);
emitNodeToggled(node, wasExpanded);
return;
}
if (node.type === "group-partitions") {
node.isExpanded = !node.isExpanded;
emit("node-toggled", node, wasExpanded);
emitNodeToggled(node, wasExpanded);
return;
}
@@ -558,27 +558,27 @@ async function toggle() {
if (databaseObjectGroup && connectionStore.canUseLoadedTreeNodeToggle(node)) {
node.isExpanded = !node.isExpanded;
if (wasExpanded && !connectionStore.sidebarSearchQuery) connectionStore.releaseCollapsedTreeNodeChildren(node.id);
emit("node-toggled", node, wasExpanded);
emitNodeToggled(node, wasExpanded);
return;
}
if (node.type === "group-extensions" && connectionStore.canUseLoadedTreeNodeToggle(node)) {
node.isExpanded = !node.isExpanded;
if (wasExpanded && !connectionStore.sidebarSearchQuery) connectionStore.releaseCollapsedTreeNodeChildren(node.id);
emit("node-toggled", node, wasExpanded);
emitNodeToggled(node, wasExpanded);
return;
}
if (node.isExpanded) {
node.isExpanded = false;
if (!connectionStore.sidebarSearchQuery) connectionStore.releaseCollapsedTreeNodeChildren(node.id);
emit("node-toggled", node, wasExpanded);
emitNodeToggled(node, wasExpanded, false);
return;
}
try {
if (await loadSidebarObjectGroup(node, connectionStore)) {
emit("node-toggled", node, wasExpanded);
emitNodeToggled(node, wasExpanded);
return;
}
@@ -715,7 +715,7 @@ async function toggle() {
} else if (node.type === "group-extensions" && node.connectionId && hasTreeNodeDatabaseContext(node)) {
await connectionStore.refreshTreeNode(node);
}
emit("node-toggled", node, wasExpanded);
emitNodeToggled(node, wasExpanded);
} catch (e: any) {
if (!wasExpanded) node.isExpanded = false;
const errMsg = e?.message || String(e);
@@ -4673,6 +4673,15 @@ function activateRuntimeNode(node: TreeNode) {
activeNode.value = node;
}
// Async loaders can rebuild a connection node while awaiting the backend.
// Publish the live tree node so a stale rendered row cannot reset expansion.
function emitNodeToggled(node: TreeNode, wasExpanded: boolean, expandedOverride?: boolean) {
const liveNode = findSidebarActionTarget(connectionStore.treeNodes, createSidebarActionTarget(node)) ?? node;
if (expandedOverride !== undefined) liveNode.isExpanded = expandedOverride;
activeNode.value = liveNode;
emit("node-toggled", liveNode, wasExpanded);
}
function activateActionTarget(target: SidebarActionTarget) {
activeNode.value = findSidebarActionTarget(connectionStore.treeNodes, target) ?? target;
}
@@ -4,6 +4,14 @@ import { describe, expect, it } from "vitest";
const runtimeSource = readFileSync(new URL("../SidebarTreeRuntimeHost.vue", import.meta.url), "utf8");
describe("cross-database table paste", () => {
it("publishes the live node after async tree loads", () => {
expect(runtimeSource).toContain("function emitNodeToggled(node: TreeNode, wasExpanded: boolean, expandedOverride?: boolean)");
expect(runtimeSource).toContain("findSidebarActionTarget(connectionStore.treeNodes, createSidebarActionTarget(node)) ?? node");
expect(runtimeSource).toContain("emitNodeToggled(node, wasExpanded, false)");
expect(runtimeSource).toMatch(/await connectionStore\.loadMongoDatabases\(node\.connectionId\);[\s\S]*?emitNodeToggled\(node, wasExpanded\)/);
expect(runtimeSource).toContain("connectionStore.cancelTreeNodeLoad(node.id)");
});
it("routes a table clipboard from another context to data transfer", () => {
expect(runtimeSource).toContain("function canTransferTreeClipboardToCurrentNode()");
expect(runtimeSource).toContain("function openTransferFromTreeClipboard()");
@@ -17,6 +17,7 @@ interface SshPromptRequest {
key_type?: string | null;
fingerprint?: string | null;
prompt?: string | null;
echo?: boolean;
}
interface SshHostKeyNotice {
@@ -35,6 +36,7 @@ const { toast } = useToast();
// Using a queue keyed by request id means every prompt is shown and answered.
const queue = ref<SshPromptRequest[]>([]);
const current = computed<SshPromptRequest | null>(() => queue.value[0] ?? null);
const isSecretPrompt = computed(() => current.value?.kind === "SecretInput");
const visible = ref(false);
const remember = ref(true);
@@ -235,17 +237,19 @@ function submitSecret() {
</script>
<template>
<Dialog v-model:open="visible" :dismissible="false">
<DialogContent class="sm:max-w-[460px]">
<Dialog v-model:open="visible">
<DialogContent class="sm:max-w-[460px]" :show-close-button="false" @interact-outside.prevent @escape-key-down.prevent>
<DialogHeader>
<DialogTitle>{{ t("connection.sshHostKeyVerifyTitle") }}</DialogTitle>
<DialogTitle>
{{ t(isSecretPrompt ? "connection.sshInteractiveTitle" : "connection.sshHostKeyVerifyTitle") }}
</DialogTitle>
<DialogDescription class="text-muted-foreground">
{{ t("connection.sshHostKeyVerifyMessage", { host: current?.host ?? "", port: current?.port ?? "" }) }}
{{ t(isSecretPrompt ? "connection.sshInteractiveMessage" : "connection.sshHostKeyVerifyMessage", { host: current?.host ?? "", port: current?.port ?? "" }) }}
</DialogDescription>
</DialogHeader>
<div v-if="current" class="space-y-3 py-1">
<div class="rounded-md border border-border bg-muted/40 p-3 text-sm">
<div v-if="current.kind === 'HostKeyVerify'" class="rounded-md border border-border bg-muted/40 p-3 text-sm">
<div class="flex items-center justify-between gap-3">
<span class="text-muted-foreground">{{ t("connection.sshHostKeyVerifyKeyType") }}</span>
<span class="font-medium">{{ current.key_type || "—" }}</span>
@@ -262,18 +266,29 @@ function submitSecret() {
</label>
<div v-else-if="current.kind === 'SecretInput'" class="space-y-2">
<p class="text-sm text-muted-foreground">{{ current.prompt || "Enter verification code" }}</p>
<input v-model="secretCode" type="text" autocomplete="off" class="w-full rounded-md border border-border bg-background px-3 py-2 font-mono text-sm outline-none focus:ring-2 focus:ring-ring" :placeholder="'••••••'" />
<p class="whitespace-pre-wrap text-sm text-muted-foreground">
{{ current.prompt || t("connection.sshInteractiveDefaultPrompt") }}
</p>
<input
v-model="secretCode"
:type="current.echo ? 'text' : 'password'"
autocomplete="one-time-code"
class="w-full rounded-md border border-border bg-background px-3 py-2 font-mono text-sm outline-none focus:ring-2 focus:ring-ring"
:placeholder="t('connection.sshInteractivePlaceholder')"
@keydown.enter="submitSecret"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" :disabled="resolving" @click="reject">{{ t("connection.sshHostKeyVerifyReject") }}</Button>
<Button variant="outline" :disabled="resolving" @click="reject">
{{ t(isSecretPrompt ? "connection.sshInteractiveCancel" : "connection.sshHostKeyVerifyReject") }}
</Button>
<Button v-if="current?.kind !== 'SecretInput'" :disabled="resolving" @click="accept">
{{ t("connection.sshHostKeyVerifyAccept") }}
</Button>
<Button v-else :disabled="resolving || !secretCode" @click="submitSecret">
{{ t("connection.sshHostKeyVerifyAccept") }}
{{ t("connection.sshInteractiveSubmit") }}
</Button>
</DialogFooter>
</DialogContent>
@@ -1,5 +1,7 @@
// @vitest-environment happy-dom
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { createApp, defineComponent, h, nextTick, type App } from "vue";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import i18n from "@/i18n";
@@ -49,6 +51,8 @@ vi.mock("@/components/ui/button", async () => {
import SshHostKeyPromptDialog from "@/components/ssh/SshHostKeyPromptDialog.vue";
const dialogSource = readFileSync(resolve(process.cwd(), "apps/desktop/src/components/ssh/SshHostKeyPromptDialog.vue"), "utf8");
class MockEventSource {
static instances: MockEventSource[] = [];
@@ -113,6 +117,13 @@ async function mountDialog() {
}
describe("SshHostKeyPromptDialog web bridge", () => {
it("requires an explicit answer for blocking SSH prompts", () => {
expect(dialogSource).toContain(':show-close-button="false"');
expect(dialogSource).toContain("@interact-outside.prevent");
expect(dialogSource).toContain("@escape-key-down.prevent");
expect(dialogSource).not.toContain(':dismissible="false"');
});
it("shows an SSE host-key prompt and posts the user's acceptance", async () => {
await mountDialog();
@@ -217,4 +228,44 @@ describe("SshHostKeyPromptDialog web bridge", () => {
eventSource?.error();
expect(setSpy).toHaveBeenCalledTimes(2);
});
it("submits a keyboard-interactive TOTP challenge as a secret response", async () => {
await mountDialog();
const eventSource = MockEventSource.instances[0];
eventSource?.emit({
type: "prompt",
request: {
id: "totp-1",
kind: "SecretInput",
host: "jump.example.test",
port: 2222,
prompt: "JumpServer\n\nOTP Code:",
echo: false,
},
});
await nextTick();
expect(document.body.textContent).toContain("SSH Verification Required");
expect(document.body.textContent).toContain("OTP Code:");
const input = document.body.querySelector<HTMLInputElement>("input");
expect(input?.type).toBe("password");
if (!input) throw new Error("TOTP input was not rendered");
input.value = "123456";
input.dispatchEvent(new Event("input", { bubbles: true }));
await nextTick();
const buttons = document.body.querySelectorAll<HTMLButtonElement>("button");
buttons.item(buttons.length - 1).click();
await vi.waitFor(() => {
expect(resolveSshPromptMock).toHaveBeenCalledWith({
id: "totp-1",
action: "secret",
remember: undefined,
secret: "123456",
});
});
});
});
+5
View File
@@ -54,6 +54,11 @@ const WINDOWS_JRE_REMOVE_ERROR = [
// Every backend message changed away from hardcoded Chinese, paired with the
// key and params it must resolve to.
const CASES: { name: string; message: string; key: string; params?: Record<string, string> }[] = [
{
name: "SSH TOTP prompt cancelled",
message: "SSH layer 1 failed: SSH keyboard-interactive authentication was cancelled",
key: "connection.sshTotpCancelled",
},
{
name: "streaming export unsupported",
message: "Streaming export is unsupported for this query. Simplify it or use a supported driver.",
+3
View File
@@ -66,6 +66,9 @@ const patterns: [RegExp, string][] = [
[/^SOCKS proxy connect rejected \(code (\d+)\)$/, "settings.tunnelsSocksConnectRejected"],
[/^Unsupported SOCKS bound address type: (\d+)$/, "settings.tunnelsSocksUnsupportedAddrType"],
// SSH keyboard-interactive prompts (for example JumpServer TOTP).
[/^(?:SSH layer \d+ failed:\s*)?SSH keyboard-interactive authentication was cancelled$/, "connection.sshTotpCancelled"],
// Query result export limits (crates/dbx-core/src/query_result_export.rs)
[/^Streaming export is unsupported for this query\. Simplify it or use a supported driver\.$/, "exportProgress.streamingUnsupported"],
[/^Streaming export needs a result-set session, but this driver returned no session_id\.$/, "exportProgress.agentSessionMissing"],
+8
View File
@@ -625,6 +625,7 @@ export default {
sshKeyPassphrasePlaceholder: "Leave empty if key is not encrypted",
sshKeyPathBrowse: "Browse",
sshExposeLan: "Expose tunnel to LAN",
sshAllowExecChannelProxy: "Enable SSH exec-channel fallback (nc)",
sshConnectTimeout: "SSH Timeout (seconds)",
sshHops: "Tunnel / Proxy Layers",
sshHopAdd: "Add SSH layer",
@@ -649,6 +650,12 @@ export default {
sshHostKeyVerifyRemember: "Trust this host (remember this key for future connections)",
sshHostKeyVerifyAccept: "Accept & Connect",
sshHostKeyVerifyReject: "Cancel",
sshInteractiveTitle: "SSH Verification Required",
sshInteractiveMessage: "The SSH server at {host}:{port} requires additional verification.",
sshInteractiveDefaultPrompt: "Enter the requested verification response.",
sshInteractivePlaceholder: "Enter verification code",
sshInteractiveSubmit: "Verify",
sshInteractiveCancel: "Cancel",
sshHostKeyNoticeChanged: "Host key for {host}:{port} changed — this may indicate a man-in-the-middle attack. Remove the old entry and reconnect only if you expect this change.",
sshHostKeyNoticeRejected: "You rejected the host key for {host}:{port}; the connection was aborted and no credentials were sent.",
sshHostKeyNoticeLearnFailed: "Could not save the host key for {host}:{port} — the host is trusted for this session only and you will be asked to verify it again next time.",
@@ -727,6 +734,7 @@ export default {
colorCustom: "Custom color",
cancelConnecting: "Cancel connecting",
connectCancelled: "Connection cancelled",
sshTotpCancelled: "One-time code entry cancelled",
},
editor: {
duckdbDraining: "The previous DuckDB query is still stopping. Please try again shortly.",
+8
View File
@@ -498,6 +498,7 @@ export default withEnglishFallback({
sshKeyPassphrasePlaceholder: "Dejar vacío si la clave no está cifrada",
sshKeyPathBrowse: "Examinar",
sshExposeLan: "Exponer túnel a la red local",
sshAllowExecChannelProxy: "Activar fallback del canal exec SSH (nc)",
sshConnectTimeout: "Tiempo de espera SSH (segundos)",
sshHops: "Capas de túnel SSH / proxy",
sshHopAdd: "Agregar capa SSH",
@@ -576,6 +577,7 @@ export default withEnglishFallback({
colorCustom: "Color personalizado",
cancelConnecting: "Cancelar conexión",
connectCancelled: "Conexión cancelada",
sshTotpCancelled: "Se canceló la introducción del código de un solo uso",
kafkaKerberosPrincipal: "Principal",
kafkaKerberosKeytab: "Keytab",
kafkaKerberosServiceName: "Nombre del servicio",
@@ -697,6 +699,12 @@ export default withEnglishFallback({
sshHostKeyVerifyRemember: "Confiar en este host (recordar esta clave para futuras conexiones)",
sshHostKeyVerifyAccept: "Aceptar y conectar",
sshHostKeyVerifyReject: "Cancelar",
sshInteractiveTitle: "Se requiere verificación SSH",
sshInteractiveMessage: "El servidor SSH {host}:{port} requiere verificación adicional.",
sshInteractiveDefaultPrompt: "Introduzca la respuesta de verificación solicitada.",
sshInteractivePlaceholder: "Introducir código de verificación",
sshInteractiveSubmit: "Verificar",
sshInteractiveCancel: "Cancelar",
sshHostKeyNoticeChanged: "La clave del host {host}:{port} ha cambiado, puede haber un ataque de intermediario. Si es esperado, elimine el registro antiguo y vuelva a conectar.",
sshHostKeyNoticeRejected: "Ha rechazado la verificación de la clave del host {host}:{port}. La conexión se ha abortado, no se enviaron credenciales.",
sshHostKeyNoticeGeneric: "Falló la verificación de la clave del host SSH {host}:{port}.",
+8
View File
@@ -496,6 +496,7 @@ export default withEnglishFallback({
sshKeyPassphrasePlaceholder: "Lascia vuoto se la chiave non è crittografata",
sshKeyPathBrowse: "Sfoglia",
sshExposeLan: "Esponi tunnel alla LAN",
sshAllowExecChannelProxy: "Abilita il fallback del canale exec SSH (nc)",
sshConnectTimeout: "Timeout SSH (secondi)",
sshHops: "Livelli tunnel SSH / proxy",
sshHopAdd: "Aggiungi livello SSH",
@@ -574,6 +575,7 @@ export default withEnglishFallback({
colorCustom: "Colore personalizzato",
cancelConnecting: "Annulla connessione",
connectCancelled: "Connessione annullata",
sshTotpCancelled: "Inserimento del codice monouso annullato",
kafkaKerberosPrincipal: "Principal",
kafkaKerberosKeytab: "Keytab",
kafkaKerberosServiceName: "Nome del servizio",
@@ -695,6 +697,12 @@ export default withEnglishFallback({
sshHostKeyVerifyRemember: "Fidati di questo host (ricorda la chiave per connessioni future)",
sshHostKeyVerifyAccept: "Accetta e connetti",
sshHostKeyVerifyReject: "Annulla",
sshInteractiveTitle: "Verifica SSH richiesta",
sshInteractiveMessage: "Il server SSH {host}:{port} richiede una verifica aggiuntiva.",
sshInteractiveDefaultPrompt: "Inserisci la risposta di verifica richiesta.",
sshInteractivePlaceholder: "Inserisci il codice di verifica",
sshInteractiveSubmit: "Verifica",
sshInteractiveCancel: "Annulla",
sshHostKeyNoticeChanged: "La chiave dell'host {host}:{port} è cambiata, potrebbe esserci un attacco man-in-the-middle. Se il cambiamento è previsto, elimina il vecchio record e riconnettiti.",
sshHostKeyNoticeRejected: "Hai rifiutato la verifica della chiave host di {host}:{port}, la connessione è stata interrotta e le credenziali non sono state inviate.",
sshHostKeyNoticeGeneric: "Verifica della chiave host SSH {host}:{port} fallita.",
+8
View File
@@ -490,6 +490,7 @@ export default withEnglishFallback({
sshKeyPassphrasePlaceholder: "鍵が暗号化されていない場合は空のまま",
sshKeyPathBrowse: "参照",
sshExposeLan: "トンネルをLANに公開",
sshAllowExecChannelProxy: "SSH 実行チャネルのフォールバックを有効化(nc)",
sshConnectTimeout: "SSHタイムアウト(秒)",
sshHops: "SSHトンネル / プロキシ階層",
sshHopAdd: "SSH階層を追加",
@@ -574,6 +575,7 @@ export default withEnglishFallback({
zookeeperCreateModeEphemeralSequential: "エフェメラル順序",
cancelConnecting: "接続をキャンセル",
connectCancelled: "接続がキャンセルされました",
sshTotpCancelled: "ワンタイムコードの入力をキャンセルしました",
kafkaKerberosPrincipal: "プリンシパル",
kafkaKerberosKeytab: "Keytab",
kafkaKerberosServiceName: "サービス名",
@@ -695,6 +697,12 @@ export default withEnglishFallback({
sshHostKeyVerifyRemember: "このホストを信頼する(今後の接続のためにキーを記憶する)",
sshHostKeyVerifyAccept: "承諾して接続",
sshHostKeyVerifyReject: "キャンセル",
sshInteractiveTitle: "SSH検証が必要です",
sshInteractiveMessage: "SSHサーバー {host}:{port} で追加の検証が必要です。",
sshInteractiveDefaultPrompt: "サーバーから要求された検証情報を入力してください。",
sshInteractivePlaceholder: "検証コードを入力",
sshInteractiveSubmit: "検証",
sshInteractiveCancel: "キャンセル",
sshHostKeyNoticeChanged: "ホスト {host}:{port} のキーが変更されました。中間者攻撃の可能性があります。予期された変更である場合は、古いレコードを削除して再接続してください。",
sshHostKeyNoticeRejected: "ホスト {host}:{port} のホストキー検証を拒否しました。接続は中止され、資格情報は送信されませんでした。",
sshHostKeyNoticeGeneric: "SSHホスト {host}:{port} のキー検証に失敗しました。",
+8
View File
@@ -612,6 +612,7 @@ export default withEnglishFallback({
sshKeyPassphrasePlaceholder: "키가 암호화되지 않은 경우 비워 두세요",
sshKeyPathBrowse: "찾아보기",
sshExposeLan: "터널을 LAN에 노출",
sshAllowExecChannelProxy: "SSH exec 채널 대체(nc) 활성화",
sshConnectTimeout: "SSH 제한 시간 (초)",
sshHops: "터널 / 프록시 계층",
sshHopAdd: "SSH 계층 추가",
@@ -636,6 +637,12 @@ export default withEnglishFallback({
sshHostKeyVerifyRemember: "이 호스트 신뢰 (향후 연결을 위해 이 키 기억)",
sshHostKeyVerifyAccept: "수락 후 연결",
sshHostKeyVerifyReject: "취소",
sshInteractiveTitle: "SSH 인증 필요",
sshInteractiveMessage: "SSH 서버 {host}:{port}에서 추가 인증을 요구합니다.",
sshInteractiveDefaultPrompt: "서버에서 요청한 인증 정보를 입력하세요.",
sshInteractivePlaceholder: "인증 코드 입력",
sshInteractiveSubmit: "인증",
sshInteractiveCancel: "취소",
sshHostKeyNoticeChanged: "{host}:{port}의 호스트 키가 변경되었습니다 — 이는 중간자 공격을 나타낼 수 있습니다. 이 변경을 예상한 경우에만 이전 항목을 제거하고 다시 연결하세요.",
sshHostKeyNoticeRejected: "{host}:{port}의 호스트 키를 거부했습니다. 연결이 중단되고 자격 증명이 전송되지 않았습니다.",
sshHostKeyNoticeLearnFailed: "{host}:{port}의 호스트 키를 저장할 수 없습니다 — 이 호스트는 이 세션에만 신뢰되며 다음에 다시 확인을 요청받습니다.",
@@ -714,6 +721,7 @@ export default withEnglishFallback({
colorCustom: "사용자 지정 색상",
cancelConnecting: "연결 취소",
connectCancelled: "연결이 취소되었습니다",
sshTotpCancelled: "일회용 코드 입력이 취소되었습니다",
},
editor: {
duckdbDraining: "이전 DuckDB 쿼리가 아직 중지 중입니다. 잠시 후 다시 시도해 주세요.",
+8
View File
@@ -497,6 +497,7 @@ export default withEnglishFallback({
sshKeyPassphrasePlaceholder: "Deixe vazio se a chave não estiver criptografada",
sshKeyPathBrowse: "Procurar",
sshExposeLan: "Expor túnel à LAN",
sshAllowExecChannelProxy: "Ativar fallback do canal exec SSH (nc)",
sshConnectTimeout: "Timeout SSH (segundos)",
sshHops: "Camadas de túnel SSH / proxy",
sshHopAdd: "Adicionar camada SSH",
@@ -575,6 +576,7 @@ export default withEnglishFallback({
colorCustom: "Cor personalizada",
cancelConnecting: "Cancelar conexão",
connectCancelled: "Conexão cancelada",
sshTotpCancelled: "Entrada do código de uso único cancelada",
kafkaKerberosPrincipal: "Principal",
kafkaKerberosKeytab: "Keytab",
kafkaKerberosServiceName: "Nome do serviço",
@@ -696,6 +698,12 @@ export default withEnglishFallback({
sshHostKeyVerifyRemember: "Confiar neste host (lembrar desta chave para conexões futuras)",
sshHostKeyVerifyAccept: "Aceitar e conectar",
sshHostKeyVerifyReject: "Cancelar",
sshInteractiveTitle: "Verificação SSH necessária",
sshInteractiveMessage: "O servidor SSH {host}:{port} exige verificação adicional.",
sshInteractiveDefaultPrompt: "Digite a resposta de verificação solicitada.",
sshInteractivePlaceholder: "Digite o código de verificação",
sshInteractiveSubmit: "Verificar",
sshInteractiveCancel: "Cancelar",
sshHostKeyNoticeChanged: "A chave do host {host}:{port} foi alterada, pode haver um ataque man-in-the-middle. Se esta alteração era esperada, exclua o registro antigo e reconecte.",
sshHostKeyNoticeRejected: "Você rejeitou a verificação da chave do host {host}:{port}. A conexão foi abortada e as credenciais não foram enviadas.",
sshHostKeyNoticeGeneric: "Falha na verificação da chave do host SSH {host}:{port}.",
+8
View File
@@ -564,6 +564,7 @@ export default withEnglishFallback({
connecting: "正在连接 {name}...",
cancelConnecting: "取消连接",
connectCancelled: "连接已取消",
sshTotpCancelled: "已取消输入一次性验证码",
connectSuccess: "已连接 {name}",
connectFailed: "连接失败:{message}",
driverNotInstalled: "{driver} 驱动未安装,请在驱动管理器中安装。",
@@ -628,6 +629,7 @@ export default withEnglishFallback({
sshKeyPassphrasePlaceholder: "密钥未加密则留空",
sshKeyPathBrowse: "浏览",
sshExposeLan: "允许局域网访问隧道",
sshAllowExecChannelProxy: "启用 SSH 执行通道回退(nc)",
sshConnectTimeout: "SSH 超时时间(秒)",
sshHops: "隧道/代理层级",
sshHopAdd: "添加 SSH 层",
@@ -652,6 +654,12 @@ export default withEnglishFallback({
sshHostKeyVerifyRemember: "信任此主机(记住该密钥用于以后的连接)",
sshHostKeyVerifyAccept: "接受并连接",
sshHostKeyVerifyReject: "取消",
sshInteractiveTitle: "需要 SSH 验证",
sshInteractiveMessage: "SSH 服务器 {host}:{port} 要求完成额外验证。",
sshInteractiveDefaultPrompt: "请输入服务器要求的验证信息。",
sshInteractivePlaceholder: "输入验证码",
sshInteractiveSubmit: "验证",
sshInteractiveCancel: "取消",
sshHostKeyNoticeChanged: "主机 {host}:{port} 的密钥已变更,可能存在中间人攻击。如确属预期,请删除旧记录后重新连接。",
sshHostKeyNoticeRejected: "你已拒绝 {host}:{port} 的主机密钥验证,连接已中止,凭据未发送。",
sshHostKeyNoticeLearnFailed: "无法保存 {host}:{port} 的主机密钥 — 该主机仅在本次会话内受信任,下次连接仍需重新确认。",
+8
View File
@@ -496,6 +496,7 @@ export default withEnglishFallback({
sshKeyPassphrasePlaceholder: "金鑰未加密則留空",
sshKeyPathBrowse: "瀏覽",
sshExposeLan: "允許區域網路存取隧道",
sshAllowExecChannelProxy: "啟用 SSH 執行通道回退(nc)",
sshConnectTimeout: "SSH 逾時時間(秒)",
sshHops: "SSH 隧道/代理層級",
sshHopAdd: "新增 SSH 層",
@@ -574,6 +575,7 @@ export default withEnglishFallback({
colorCustom: "自訂顏色",
cancelConnecting: "取消連接",
connectCancelled: "連接已取消",
sshTotpCancelled: "已取消輸入一次性驗證碼",
kafkaKerberosPrincipal: "Principal",
kafkaKerberosKeytab: "Keytab",
kafkaKerberosServiceName: "服務名稱",
@@ -695,6 +697,12 @@ export default withEnglishFallback({
sshHostKeyVerifyRemember: "信任此主機(記住此金鑰用於之後的連線)",
sshHostKeyVerifyAccept: "接受並連線",
sshHostKeyVerifyReject: "取消",
sshInteractiveTitle: "需要 SSH 驗證",
sshInteractiveMessage: "SSH 伺服器 {host}:{port} 要求完成額外驗證。",
sshInteractiveDefaultPrompt: "請輸入伺服器要求的驗證資訊。",
sshInteractivePlaceholder: "輸入驗證碼",
sshInteractiveSubmit: "驗證",
sshInteractiveCancel: "取消",
sshHostKeyNoticeChanged: "主機 {host}:{port} 的金鑰已變更,可能存在中間人攻擊。如確屬預期,請刪除舊記錄後重新連線。",
sshHostKeyNoticeRejected: "您已拒絕 {host}:{port} 的主機金鑰驗證,連線已中止,憑證未發送。",
sshHostKeyNoticeGeneric: "SSH 主機 {host}:{port} 的金鑰驗證失敗。",
+2
View File
@@ -239,6 +239,8 @@ export interface SshTunnelConfig {
* for connections that already have `use_ssh_agent` configured.
*/
auth_method?: "password" | "key" | "key+password" | "agent" | "none";
/** Allow `nc` through an SSH exec channel when direct-tcpip is prohibited. */
allow_exec_channel_proxy?: boolean;
/**
* When set, this layer references a shared tunnel profile; the profile's
* configuration replaces this layer's fields at connect time (only `id`