py-web-ssh 0.1.6__tar.gz → 0.1.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py-web-ssh
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: A Python web SSH client with xterm.js, reconnectable sessions, logs, and file transfer.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -27,13 +27,14 @@ Description-Content-Type: text/markdown
27
27
  ## 功能
28
28
 
29
29
  - SSH 交互终端:Paramiko 后端 `invoke_shell`,xterm.js 前端实时交互。
30
- - 登录方式:密码、浏览器上传私钥、私钥口令、SSH agent、服务端本机 `~/.ssh` 密钥、免口令/none auth。
30
+ - 登录方式:密码、浏览器上传私钥、私钥口令、服务端本机默认 `~/.ssh` 密钥、免口令/none auth。
31
+ - Host key 确认:不使用 `known_hosts` 校验;每次交互 SSH 连接都会在 xterm.js 终端里显示服务器 host key 指纹,并要求用户输入 `Y` 或 `N` 后才继续认证。
31
32
  - Legacy 兼容:启动时按当前 Paramiko 运行时能力尽量启用旧 KEX/Cipher/MAC/HostKey/Pubkey 算法,并在日志中列出不可用算法。
32
33
  - UUID 会话:创建会话后返回 UUID,WebSocket 断开后可用同 UUID 重连。
33
34
  - 会话回收:同一个 UUID 如果所有浏览器连接都断开并且 5 分钟内无人重连,服务端会主动断开 SSH 并清理内存缓存。
34
35
  - 终端恢复:服务端保存 SSH 输出流,浏览器定期回传 xterm serialize 快照;重连时先恢复快照,再补放快照之后的输出。
35
36
  - 日志页面:`/sessions/{uuid}/logs` 展示完整连接、认证、错误和文件传输日志。
36
- - 文件传输:参考 `simple-ssh-copy` 思路,不使用 SFTP/SCP;每次传输都按连接配置新建独立 SSH 连接,使用远端 `base64` shell 命令上传/下载。上传先写远端临时文件,完成后再 `mv` 到最终路径,并支持进度显示和取消。
37
+ - 文件传输:参考 `simple-ssh-copy` 思路,不使用 SFTP/SCP;每次传输都按连接配置新建独立 SSH 连接,使用远端 `base64` shell 命令上传/下载。上传先写远端临时文件,完成后再 `mv` 到最终路径,并支持进度显示和取消。文件传输连接会校验服务器 host key 必须等于用户在交互终端里确认过的 host key。
37
38
  - 可选 PIN 门禁:服务端传入 `--pin` 后,网页启动时必须先输入正确 PIN;验证成功后浏览器会保存加盐哈希 cookie,后端会保护 HTTP API、日志页面、文件接口和 WebSocket。
38
39
  - 启动锁定策略:支持 `--lock-host`、`--lock-username`、`--lock-pwd`、`--lock-private-key`,可从服务端强制绑定目标主机、用户名、密码和服务端侧私钥文件。前端会锁定或隐藏对应控件,后端仍会校验并覆盖敏感字段。
39
40
  - 中英双语:默认英文,网页和日志页都支持中英切换;语言选择会长期保存到 `py_web_ssh_lang` cookie。
@@ -93,5 +94,5 @@ py-web-ssh --lock-private-key C:\secrets\id_ed25519
93
94
 
94
95
  ## 安全提示
95
96
 
96
- 这个项目默认面向可信内网或本机使用。私钥和口令只保存在进程内存中,不写入日志;如果要暴露到公网,请务必加 HTTPS、登录认证、CSRF/来源限制、审计和会话回收策略。
97
+ 这个项目默认面向可信内网或本机使用。私钥和口令只保存在进程内存中,不写入日志;SSH agent 已禁用,`known_hosts` 校验已移除,用户必须在 xterm.js 终端里确认服务器 host key 指纹后才会继续认证。如果要暴露到公网,请务必加 HTTPS、登录认证、CSRF/来源限制、审计和会话回收策略。
97
98
 
@@ -5,13 +5,14 @@
5
5
  ## 功能
6
6
 
7
7
  - SSH 交互终端:Paramiko 后端 `invoke_shell`,xterm.js 前端实时交互。
8
- - 登录方式:密码、浏览器上传私钥、私钥口令、SSH agent、服务端本机 `~/.ssh` 密钥、免口令/none auth。
8
+ - 登录方式:密码、浏览器上传私钥、私钥口令、服务端本机默认 `~/.ssh` 密钥、免口令/none auth。
9
+ - Host key 确认:不使用 `known_hosts` 校验;每次交互 SSH 连接都会在 xterm.js 终端里显示服务器 host key 指纹,并要求用户输入 `Y` 或 `N` 后才继续认证。
9
10
  - Legacy 兼容:启动时按当前 Paramiko 运行时能力尽量启用旧 KEX/Cipher/MAC/HostKey/Pubkey 算法,并在日志中列出不可用算法。
10
11
  - UUID 会话:创建会话后返回 UUID,WebSocket 断开后可用同 UUID 重连。
11
12
  - 会话回收:同一个 UUID 如果所有浏览器连接都断开并且 5 分钟内无人重连,服务端会主动断开 SSH 并清理内存缓存。
12
13
  - 终端恢复:服务端保存 SSH 输出流,浏览器定期回传 xterm serialize 快照;重连时先恢复快照,再补放快照之后的输出。
13
14
  - 日志页面:`/sessions/{uuid}/logs` 展示完整连接、认证、错误和文件传输日志。
14
- - 文件传输:参考 `simple-ssh-copy` 思路,不使用 SFTP/SCP;每次传输都按连接配置新建独立 SSH 连接,使用远端 `base64` shell 命令上传/下载。上传先写远端临时文件,完成后再 `mv` 到最终路径,并支持进度显示和取消。
15
+ - 文件传输:参考 `simple-ssh-copy` 思路,不使用 SFTP/SCP;每次传输都按连接配置新建独立 SSH 连接,使用远端 `base64` shell 命令上传/下载。上传先写远端临时文件,完成后再 `mv` 到最终路径,并支持进度显示和取消。文件传输连接会校验服务器 host key 必须等于用户在交互终端里确认过的 host key。
15
16
  - 可选 PIN 门禁:服务端传入 `--pin` 后,网页启动时必须先输入正确 PIN;验证成功后浏览器会保存加盐哈希 cookie,后端会保护 HTTP API、日志页面、文件接口和 WebSocket。
16
17
  - 启动锁定策略:支持 `--lock-host`、`--lock-username`、`--lock-pwd`、`--lock-private-key`,可从服务端强制绑定目标主机、用户名、密码和服务端侧私钥文件。前端会锁定或隐藏对应控件,后端仍会校验并覆盖敏感字段。
17
18
  - 中英双语:默认英文,网页和日志页都支持中英切换;语言选择会长期保存到 `py_web_ssh_lang` cookie。
@@ -71,4 +72,4 @@ py-web-ssh --lock-private-key C:\secrets\id_ed25519
71
72
 
72
73
  ## 安全提示
73
74
 
74
- 这个项目默认面向可信内网或本机使用。私钥和口令只保存在进程内存中,不写入日志;如果要暴露到公网,请务必加 HTTPS、登录认证、CSRF/来源限制、审计和会话回收策略。
75
+ 这个项目默认面向可信内网或本机使用。私钥和口令只保存在进程内存中,不写入日志;SSH agent 已禁用,`known_hosts` 校验已移除,用户必须在 xterm.js 终端里确认服务器 host key 指纹后才会继续认证。如果要暴露到公网,请务必加 HTTPS、登录认证、CSRF/来源限制、审计和会话回收策略。
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "py-web-ssh"
3
- version = "0.1.6"
3
+ version = "0.1.7"
4
4
  description = "A Python web SSH client with xterm.js, reconnectable sessions, logs, and file transfer."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.1.6"
5
+ __version__ = "0.1.7"
@@ -142,6 +142,12 @@ def upload(
142
142
  total_bytes: Annotated[int | None, Form()] = None,
143
143
  ) -> FileTransferResponse:
144
144
  session = _require_session(session_id)
145
+ expected_host_key = session.confirmed_host_key
146
+ if expected_host_key is None:
147
+ raise HTTPException(
148
+ status_code=409,
149
+ detail="SSH server host key has not been confirmed in the terminal yet.",
150
+ )
145
151
  tracker = transfers.get(transfer_id) if transfer_id else None
146
152
  if transfer_id and tracker is None:
147
153
  raise HTTPException(status_code=404, detail="Transfer not found.")
@@ -153,6 +159,7 @@ def upload(
153
159
  file.file,
154
160
  remote_path,
155
161
  total_bytes or _content_length(file),
162
+ expected_host_key,
156
163
  cancel_event=tracker.cancel_event,
157
164
  progress=tracker.update_progress,
158
165
  )
@@ -229,8 +236,14 @@ def cancel_transfer(transfer_id: str) -> JSONResponse:
229
236
  @app.get("/api/sessions/{session_id}/files/download")
230
237
  def download(session_id: str, remote_path: str) -> StreamingResponse:
231
238
  session = _require_session(session_id)
239
+ expected_host_key = session.confirmed_host_key
240
+ if expected_host_key is None:
241
+ raise HTTPException(
242
+ status_code=409,
243
+ detail="SSH server host key has not been confirmed in the terminal yet.",
244
+ )
232
245
  try:
233
- method, stream = download_file_via_ssh(session.config, remote_path)
246
+ method, stream = download_file_via_ssh(session.config, remote_path, expected_host_key)
234
247
  except Exception as exc:
235
248
  session.log("error", f"File download failed: {exc}", None)
236
249
  raise HTTPException(status_code=500, detail=str(exc)) from exc
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import secrets
4
3
  import threading
5
4
  import uuid
6
5
  from dataclasses import dataclass, field
@@ -11,7 +11,7 @@ from collections.abc import Callable, Iterator
11
11
  from typing import BinaryIO
12
12
 
13
13
  from .models import ConnectRequest
14
- from .ssh_client import connect_ssh
14
+ from .ssh_client import HostKeyInfo, connect_ssh
15
15
 
16
16
 
17
17
  class FileTransferError(RuntimeError):
@@ -30,6 +30,7 @@ def upload_file_via_ssh(
30
30
  source: BinaryIO,
31
31
  remote_path: str,
32
32
  size: int | None,
33
+ expected_host_key: HostKeyInfo,
33
34
  cancel_event: threading.Event | None = None,
34
35
  progress: ProgressCallback | None = None,
35
36
  ) -> tuple[str, int]:
@@ -40,7 +41,11 @@ def upload_file_via_ssh(
40
41
  atomically move the temp file into place after the complete upload succeeds.
41
42
  """
42
43
 
43
- connected = connect_ssh(config, lambda _level, _message, _details=None: None)
44
+ connected = connect_ssh(
45
+ config,
46
+ lambda _level, _message, _details=None: None,
47
+ expected_host_key=expected_host_key,
48
+ )
44
49
  client = connected.client
45
50
  remote_dir = posixpath.dirname(remote_path) or "."
46
51
  temp_path = posixpath.join(remote_dir, f".py-web-ssh-upload-{uuid.uuid4().hex}.tmp")
@@ -90,8 +95,16 @@ def upload_file_via_ssh(
90
95
  return "shell", transferred
91
96
 
92
97
 
93
- def download_file_via_ssh(config: ConnectRequest, remote_path: str) -> tuple[str, Iterator[bytes]]:
94
- connected = connect_ssh(config, lambda _level, _message, _details=None: None)
98
+ def download_file_via_ssh(
99
+ config: ConnectRequest,
100
+ remote_path: str,
101
+ expected_host_key: HostKeyInfo,
102
+ ) -> tuple[str, Iterator[bytes]]:
103
+ connected = connect_ssh(
104
+ config,
105
+ lambda _level, _message, _details=None: None,
106
+ expected_host_key=expected_host_key,
107
+ )
95
108
  client = connected.client
96
109
  command = f"base64 < {shlex.quote(remote_path)}"
97
110
  stdin, stdout, stderr = client.exec_command(f"sh -c {shlex.quote(command)}")
@@ -18,10 +18,8 @@ class ConnectRequest(BaseModel):
18
18
  password: str | None = None
19
19
  private_key: str | None = None
20
20
  private_key_passphrase: str | None = None
21
- allow_agent: bool = False
22
21
  look_for_keys: bool = False
23
22
  legacy_algorithms: bool = True
24
- strict_host_key: bool = False
25
23
  term: str = Field(default="xterm-256color", min_length=1, max_length=64)
26
24
  size: TerminalSize = Field(default_factory=TerminalSize)
27
25
  timeout_seconds: float = Field(default=20.0, ge=3.0, le=120.0)
@@ -16,10 +16,10 @@ from starlette.websockets import WebSocket
16
16
 
17
17
  from .history import OutputChunk, OutputHistory
18
18
  from .models import ConnectRequest, LogEntry, SessionSummary
19
- from .ssh_client import ConnectedClient, connect_ssh
19
+ from .ssh_client import ConnectedClient, HostKeyInfo, connect_ssh
20
20
 
21
21
 
22
- SessionState = Literal["connecting", "connected", "closing", "closed", "error"]
22
+ SessionState = Literal["connecting", "waiting_host_key", "connected", "closing", "closed", "error"]
23
23
 
24
24
 
25
25
  @dataclass(eq=False)
@@ -41,6 +41,10 @@ class TerminalSession:
41
41
  self._clients: set[BrowserConnection] = set()
42
42
  self._lock = threading.RLock()
43
43
  self._channel_lock = threading.RLock()
44
+ self._host_key_lock = threading.Condition(threading.RLock())
45
+ self._awaiting_host_key_confirmation = False
46
+ self._host_key_decision: bool | None = None
47
+ self._confirmed_host_key: HostKeyInfo | None = None
44
48
  self._stop = threading.Event()
45
49
  self._thread = threading.Thread(target=self._run, name=f"ssh-session-{self.id}", daemon=True)
46
50
  self._connected: ConnectedClient | None = None
@@ -134,6 +138,8 @@ class TerminalSession:
134
138
  }
135
139
 
136
140
  def send_input(self, data: bytes) -> None:
141
+ if self._handle_host_key_confirmation_input(data):
142
+ return
137
143
  with self._channel_lock:
138
144
  if self._channel is None or self.state != "connected":
139
145
  raise RuntimeError("SSH channel is not connected.")
@@ -174,10 +180,19 @@ class TerminalSession:
174
180
  raise RuntimeError("SSH connection is not ready.")
175
181
  return self._connected.client
176
182
 
183
+ @property
184
+ def confirmed_host_key(self) -> HostKeyInfo | None:
185
+ with self._host_key_lock:
186
+ return self._confirmed_host_key
187
+
177
188
  def _run(self) -> None:
178
189
  try:
179
190
  self.log("info", "Creating SSH connection.", None)
180
- self._connected = connect_ssh(self.config, self.log)
191
+ self._connected = connect_ssh(
192
+ self.config,
193
+ self.log,
194
+ confirm_host_key=self._confirm_host_key,
195
+ )
181
196
  channel = self._connected.transport.open_session()
182
197
  channel.get_pty(
183
198
  term=self.config.term,
@@ -238,6 +253,74 @@ class TerminalSession:
238
253
  self.updated_at = datetime.now(timezone.utc)
239
254
  self._broadcast(self._chunk_payload(chunk) | {"type": "output"})
240
255
 
256
+ def _confirm_host_key(self, host_key: HostKeyInfo) -> bool:
257
+ with self._host_key_lock:
258
+ self._awaiting_host_key_confirmation = True
259
+ self._host_key_decision = None
260
+ self._set_state("waiting_host_key")
261
+ self.log("warning", "Waiting for browser user to confirm SSH server host key.", host_key.details())
262
+ self._append_output(self._host_key_prompt(host_key).encode("utf-8"))
263
+
264
+ while not self._stop.is_set():
265
+ with self._host_key_lock:
266
+ if self._host_key_decision is not None:
267
+ accepted = self._host_key_decision
268
+ self._awaiting_host_key_confirmation = False
269
+ if accepted:
270
+ self._confirmed_host_key = host_key
271
+ break
272
+ self._host_key_lock.wait(timeout=0.2)
273
+ else:
274
+ accepted = False
275
+
276
+ if accepted:
277
+ self._append_output(b"Y\r\nContinuing SSH authentication...\r\n")
278
+ self._set_state("connecting")
279
+ return True
280
+
281
+ self._append_output(b"N\r\nSSH connection cancelled before authentication.\r\n")
282
+ return False
283
+
284
+ def _handle_host_key_confirmation_input(self, data: bytes) -> bool:
285
+ with self._host_key_lock:
286
+ if not self._awaiting_host_key_confirmation:
287
+ return False
288
+
289
+ text = data.decode("utf-8", errors="ignore")
290
+ decision: bool | None = None
291
+ invalid = False
292
+ for char in text:
293
+ if char in "\r\n\t ":
294
+ continue
295
+ if char in ("y", "Y"):
296
+ decision = True
297
+ break
298
+ if char in ("n", "N"):
299
+ decision = False
300
+ break
301
+ invalid = True
302
+ break
303
+
304
+ if decision is None:
305
+ if invalid:
306
+ self._append_output(b"\r\nPlease type Y or N: ")
307
+ return True
308
+
309
+ with self._host_key_lock:
310
+ self._host_key_decision = decision
311
+ self._host_key_lock.notify_all()
312
+ return True
313
+
314
+ def _host_key_prompt(self, host_key: HostKeyInfo) -> str:
315
+ return (
316
+ "\r\n[py-web-ssh] SSH server host key fingerprint\r\n"
317
+ f"Host: {self.config.host}:{self.config.port}\r\n"
318
+ f"Key type: {host_key.key_type}\r\n"
319
+ f"SHA256 fingerprint: {host_key.sha256_fingerprint}\r\n"
320
+ f"MD5 fingerprint: {host_key.md5_fingerprint}\r\n"
321
+ "Continue connecting? Type Y or N: "
322
+ )
323
+
241
324
  def _chunk_payload(self, chunk: OutputChunk) -> dict[str, Any]:
242
325
  return {
243
326
  "seq": chunk.seq,
@@ -1,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import base64
4
+ import hashlib
3
5
  import io
4
6
  import socket
5
7
  import traceback
@@ -21,7 +23,48 @@ class ConnectedClient:
21
23
  transport: paramiko.Transport
22
24
 
23
25
 
24
- def connect_ssh(config: ConnectRequest, log: LogCallback) -> ConnectedClient:
26
+ @dataclass(frozen=True)
27
+ class HostKeyInfo:
28
+ key_type: str
29
+ sha256_fingerprint: str
30
+ md5_fingerprint: str
31
+ key_base64: str
32
+
33
+ @classmethod
34
+ def from_key(cls, key: paramiko.PKey) -> "HostKeyInfo":
35
+ key_bytes = key.asbytes()
36
+ sha256_digest = hashlib.sha256(key_bytes).digest()
37
+ sha256_value = base64.b64encode(sha256_digest).decode("ascii").rstrip("=")
38
+ md5_value = ":".join(f"{byte:02x}" for byte in key.get_fingerprint())
39
+ return cls(
40
+ key_type=key.get_name(),
41
+ sha256_fingerprint=f"SHA256:{sha256_value}",
42
+ md5_fingerprint=f"MD5:{md5_value}",
43
+ key_base64=base64.b64encode(key_bytes).decode("ascii"),
44
+ )
45
+
46
+ def matches(self, key: paramiko.PKey) -> bool:
47
+ return self.key_base64 == base64.b64encode(key.asbytes()).decode("ascii")
48
+
49
+ def details(self) -> str:
50
+ return "\n".join(
51
+ [
52
+ f"type={self.key_type}",
53
+ f"sha256={self.sha256_fingerprint}",
54
+ f"md5={self.md5_fingerprint}",
55
+ ]
56
+ )
57
+
58
+
59
+ HostKeyConfirmation = Callable[[HostKeyInfo], bool]
60
+
61
+
62
+ def connect_ssh(
63
+ config: ConnectRequest,
64
+ log: LogCallback,
65
+ confirm_host_key: HostKeyConfirmation | None = None,
66
+ expected_host_key: HostKeyInfo | None = None,
67
+ ) -> ConnectedClient:
25
68
  """Open an SSH connection, including legacy algorithms Paramiko still supports."""
26
69
 
27
70
  sock = socket.create_connection((config.host, config.port), timeout=config.timeout_seconds)
@@ -35,10 +78,21 @@ def connect_ssh(config: ConnectRequest, log: LogCallback) -> ConnectedClient:
35
78
  if config.keepalive_seconds:
36
79
  transport.set_keepalive(config.keepalive_seconds)
37
80
 
38
- if config.strict_host_key:
39
- _check_known_host(config, transport, log)
81
+ host_key = transport.get_remote_server_key()
82
+ host_key_info = HostKeyInfo.from_key(host_key)
83
+ log("info", "SSH server host key received.", host_key_info.details())
84
+ if expected_host_key is not None:
85
+ if not expected_host_key.matches(host_key):
86
+ raise paramiko.SSHException(
87
+ "SSH server host key changed since the browser user confirmed it."
88
+ )
89
+ log("info", "SSH server host key matches the browser-confirmed key.", None)
90
+ elif confirm_host_key is not None:
91
+ if not confirm_host_key(host_key_info):
92
+ raise paramiko.SSHException("SSH server host key was rejected by the browser user.")
93
+ log("info", "SSH server host key accepted by the browser user.", None)
40
94
  else:
41
- log("warning", "Strict host key checking is disabled for this browser session.", None)
95
+ raise paramiko.SSHException("SSH server host key confirmation is required.")
42
96
 
43
97
  _authenticate(transport, config, log)
44
98
 
@@ -158,29 +212,6 @@ def _supported_algorithms(options: paramiko.transport.SecurityOptions, attr: str
158
212
  return set(getattr(transport, info_attr).keys())
159
213
 
160
214
 
161
- def _check_known_host(config: ConnectRequest, transport: paramiko.Transport, log: LogCallback) -> None:
162
- host_key = transport.get_remote_server_key()
163
- known_hosts = paramiko.HostKeys()
164
- paths = [
165
- Path.home() / ".ssh" / "known_hosts",
166
- Path.home() / ".ssh" / "known_hosts2",
167
- ]
168
- for path in paths:
169
- if path.exists():
170
- known_hosts.load(str(path))
171
-
172
- candidates = [config.host, f"[{config.host}]:{config.port}"]
173
- for candidate in candidates:
174
- if known_hosts.check(candidate, host_key):
175
- log("info", f"Host key verified for {candidate}.", None)
176
- return
177
-
178
- raise paramiko.SSHException(
179
- "Strict host key checking failed. The remote host key is not present in "
180
- "~/.ssh/known_hosts for this server and port."
181
- )
182
-
183
-
184
215
  def _authenticate(transport: paramiko.Transport, config: ConnectRequest, log: LogCallback) -> None:
185
216
  username = config.username
186
217
  errors: list[str] = []
@@ -196,21 +227,6 @@ def _authenticate(transport: paramiko.Transport, config: ConnectRequest, log: Lo
196
227
  if transport.is_authenticated():
197
228
  return
198
229
 
199
- if config.allow_agent:
200
- try:
201
- for key in paramiko.Agent().get_keys():
202
- try:
203
- log("info", f"Trying SSH agent key {key.get_name()}.", None)
204
- transport.auth_publickey(username, key)
205
- except Exception as exc:
206
- errors.append(f"agent key {key.get_name()}: {exc}")
207
- log("debug", f"SSH agent key failed: {exc}", None)
208
- if transport.is_authenticated():
209
- return
210
- except Exception as exc:
211
- errors.append(f"agent: {exc}")
212
- log("warning", f"SSH agent authentication failed: {exc}", traceback.format_exc())
213
-
214
230
  if config.look_for_keys:
215
231
  for key in _load_default_private_keys(config.private_key_passphrase, log):
216
232
  try:
@@ -34,9 +34,7 @@ const translations = {
34
34
  privateKeyLocked: "Private key file is forced by the server.",
35
35
  privateKeyPassphrase: "Private key passphrase",
36
36
  legacyAlgorithms: "Legacy algorithms",
37
- sshAgent: "SSH agent",
38
37
  serverKeys: "Server local keys",
39
- knownHosts: "known_hosts check",
40
38
  reconnect: "Reconnect",
41
39
  disconnect: "Disconnect SSH",
42
40
  openLogs: "Open full logs",
@@ -95,9 +93,7 @@ const translations = {
95
93
  privateKeyLocked: "私钥文件已强制绑定",
96
94
  privateKeyPassphrase: "私钥口令",
97
95
  legacyAlgorithms: "legacy 算法",
98
- sshAgent: "SSH agent",
99
96
  serverKeys: "服务端本机密钥",
100
- knownHosts: "known_hosts 校验",
101
97
  reconnect: "重连",
102
98
  disconnect: "断开 SSH",
103
99
  openLogs: "打开完整日志",
@@ -222,10 +218,8 @@ document.querySelector("#connect-form").addEventListener("submit", async (event)
222
218
  password: valueOf("#password"),
223
219
  private_key: privateKey,
224
220
  private_key_passphrase: valueOf("#private-key-passphrase"),
225
- allow_agent: checked("#allow-agent"),
226
221
  look_for_keys: checked("#look-for-keys"),
227
222
  legacy_algorithms: checked("#legacy-algorithms"),
228
- strict_host_key: checked("#strict-host-key"),
229
223
  term: "xterm-256color",
230
224
  size: { cols: term.cols, rows: term.rows },
231
225
  };
@@ -70,9 +70,7 @@
70
70
  </label>
71
71
  <div class="checks">
72
72
  <label><input id="legacy-algorithms" type="checkbox" checked /> <span data-i18n="legacyAlgorithms">Legacy algorithms</span></label>
73
- <label><input id="allow-agent" type="checkbox" /> <span data-i18n="sshAgent">SSH agent</span></label>
74
73
  <label><input id="look-for-keys" type="checkbox" /> <span data-i18n="serverKeys">Server local keys</span></label>
75
- <label><input id="strict-host-key" type="checkbox" /> <span data-i18n="knownHosts">known_hosts check</span></label>
76
74
  </div>
77
75
  <button class="primary" type="submit" data-i18n="connect">Connect</button>
78
76
  </form>
File without changes
File without changes
File without changes