@zhangfengshun/dsh-remote-ssh 2.3.6 → 2.3.8
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.
- package/CHANGELOG.md +12 -0
- package/lib/client.js +1367 -1367
- package/lib/index.js +49 -11
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -216,7 +216,7 @@ class CommandSession {
|
|
|
216
216
|
});
|
|
217
217
|
const onEnd = () => {
|
|
218
218
|
this.alive = false;
|
|
219
|
-
const errMsg = "ssh 会话已关闭" + (this.errBuf.trim() ? ": " + this.errBuf.trim().slice(-300) : "");
|
|
219
|
+
const errMsg = "ssh 会话已关闭" + (this.errBuf.trim() ? ": " + stripPqBanner(this.errBuf).trim().slice(-300) : "");
|
|
220
220
|
if (this.current) { this.current.reject(new Error(errMsg)); this.current = null; }
|
|
221
221
|
while (this.queue.length) this.queue.shift().reject(new Error(errMsg));
|
|
222
222
|
};
|
|
@@ -336,7 +336,7 @@ class CommandSession {
|
|
|
336
336
|
}
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
-
function sshArgv(p, remoteCmd, tty) {
|
|
339
|
+
function sshArgv(p, remoteCmd, tty, extraOpts) {
|
|
340
340
|
const opts = ["ssh"];
|
|
341
341
|
if (tty) opts.push("-tt");
|
|
342
342
|
opts.push("-p", String(p.port || 22));
|
|
@@ -356,6 +356,7 @@ function sshArgv(p, remoteCmd, tty) {
|
|
|
356
356
|
opts.push("-o", "BatchMode=yes");
|
|
357
357
|
opts.push("-o", "PreferredAuthentications=publickey");
|
|
358
358
|
}
|
|
359
|
+
if (Array.isArray(extraOpts) && extraOpts.length) opts.push(...extraOpts);
|
|
359
360
|
const target = String(p.user || "") + "@" + String(p.host || "");
|
|
360
361
|
const head = (p.authMethod === "password" && p.password) ? ["sshpass", "-p", String(p.password)] : [];
|
|
361
362
|
const argv = head.concat(opts, [target]);
|
|
@@ -363,12 +364,23 @@ function sshArgv(p, remoteCmd, tty) {
|
|
|
363
364
|
return argv;
|
|
364
365
|
}
|
|
365
366
|
|
|
366
|
-
|
|
367
|
+
/** 过滤 OpenSSH 客户端的“后量子 KEX”警告横幅(stderr 噪音,与认证成败无关,
|
|
368
|
+
* 混进错误信息会误导用户以为是不支持加密算法)。 */
|
|
369
|
+
function stripPqBanner(text) {
|
|
370
|
+
return String(text || "")
|
|
371
|
+
.split("\n")
|
|
372
|
+
.filter(function (line) {
|
|
373
|
+
return !/^\s*\*\*\s*(WARNING: connection is not using a post-quantum|This session may be vulnerable to .?store now|The server may need to be upgraded)/i.test(line);
|
|
374
|
+
})
|
|
375
|
+
.join("\n");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function runRemote(subprocess, p, remoteCmd, stdinData, maxBytes, extraOpts) {
|
|
367
379
|
const max = maxBytes || MAX_BYTES;
|
|
368
380
|
let handle;
|
|
369
381
|
try {
|
|
370
382
|
handle = subprocess.spawn({
|
|
371
|
-
argv: sshArgv(p, remoteCmd, false),
|
|
383
|
+
argv: sshArgv(p, remoteCmd, false, extraOpts),
|
|
372
384
|
cwd: process.cwd(),
|
|
373
385
|
stdio: {
|
|
374
386
|
stdin: stdinData !== undefined ? { data: String(stdinData) } : "ignore",
|
|
@@ -389,7 +401,7 @@ async function runRemote(subprocess, p, remoteCmd, stdinData, maxBytes) {
|
|
|
389
401
|
const so = (handle.collected && handle.collected.stdout) ? handle.collected.stdout.readFrom(0) : { text: "", nextOffset: 0, lossy: false };
|
|
390
402
|
const se = (handle.collected && handle.collected.stderr) ? handle.collected.stderr.readFrom(0) : { text: "", nextOffset: 0, lossy: false };
|
|
391
403
|
const outText = so.text;
|
|
392
|
-
const errText = se.text;
|
|
404
|
+
const errText = stripPqBanner(se.text);
|
|
393
405
|
let error = "";
|
|
394
406
|
if (outcome.exitCode !== 0 && !outText && !errText) {
|
|
395
407
|
error = "ssh 退出码 " + outcome.exitCode + (outcome.signal ? " (signal " + outcome.signal + ")" : "");
|
|
@@ -933,13 +945,18 @@ function normExec(r) {
|
|
|
933
945
|
// HTTP 工具
|
|
934
946
|
// ---------------------------------------------------------------------------
|
|
935
947
|
|
|
936
|
-
function isTrusted(req) {
|
|
948
|
+
function isTrusted(req, requireRequestedWith) {
|
|
937
949
|
const secFetchSite = req.headers["sec-fetch-site"];
|
|
938
950
|
if (secFetchSite === "cross-site") return false;
|
|
939
951
|
const host = req.headers["host"];
|
|
940
952
|
if (!host) return false;
|
|
941
953
|
const hostname = String(host).split(":")[0].replace(/^\[/, "").replace(/\]$/, "");
|
|
942
|
-
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "0.0.0.0")
|
|
954
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "0.0.0.0") {
|
|
955
|
+
// CSRF 加固(PR #4 的服务端半边):/remote-ssh/api/* 仅由本插件客户端调用,
|
|
956
|
+
// 其请求必带 x-requested-with;跨站 no-cors POST 无法携带非简单头,预检也会被拒。
|
|
957
|
+
if (requireRequestedWith && String(req.headers["x-requested-with"] || "").trim() !== "XMLHttpRequest") return false;
|
|
958
|
+
return true;
|
|
959
|
+
}
|
|
943
960
|
return false;
|
|
944
961
|
}
|
|
945
962
|
|
|
@@ -1054,12 +1071,12 @@ function apply(ctx, config) {
|
|
|
1054
1071
|
}
|
|
1055
1072
|
/** 把 SSH 常见失败翻译成带修复建议的中文提示。 */
|
|
1056
1073
|
function sshErrorHint(text) {
|
|
1057
|
-
const t = String(text || "");
|
|
1074
|
+
const t = stripPqBanner(String(text || ""));
|
|
1058
1075
|
if (/remote port forwarding failed for listen port (\d+)/i.test(t)) {
|
|
1059
1076
|
const m = t.match(/remote port forwarding failed for listen port (\d+)/i);
|
|
1060
1077
|
return "SSH 端口转发失败(本地端口 " + m[1] + " 已被占用)。这通常由 ~/.ssh/config 里的 RemoteForward 造成:远端该端口被上次会话占用时,ExitOnForwardFailure yes 会让 ssh 直接退出。本插件已对该连接加 ClearAllForwardings,若仍出现请检查 ssh config。原始信息: " + t.trim().slice(0, 300);
|
|
1061
1078
|
}
|
|
1062
|
-
if (/permission denied \(publickey/i.test(t)) return "
|
|
1079
|
+
if (/permission denied \(publickey/i.test(t)) return "公钥认证失败(服务器拒绝了公钥)。请依次检查:① keyPath 指向的私钥是否正确、是否带口令——测试连接以批处理模式运行无法交互输口令,可先 ssh-add 加载或改用无口令密钥;② 远端是否收录对应公钥——Windows 主机且目标用户在 Administrators 组时需写入 C:\\ProgramData\\ssh\\administrators_authorized_keys;③ profile 的用户名写法与手动连接是否一致(user / .\\user / user@domain 在 Windows sshd 中解析不同)。原始信息: " + t.trim().slice(0, 300);
|
|
1063
1080
|
if (/connection refused/i.test(t)) return "连接被拒绝:请确认远端 sshd 已启动且端口正确。原始信息: " + t.trim().slice(0, 300);
|
|
1064
1081
|
if (/connection timed out|timed out/i.test(t)) return "连接超时:请确认网络可达、端口开放,或检查 ProxyJump 配置。原始信息: " + t.trim().slice(0, 300);
|
|
1065
1082
|
if (/could not resolve hostname/i.test(t)) return "无法解析主机名:请检查连接配置中的 host 拼写。原始信息: " + t.trim().slice(0, 300);
|
|
@@ -1239,7 +1256,26 @@ function apply(ctx, config) {
|
|
|
1239
1256
|
testConnection: async (args) => {
|
|
1240
1257
|
const p = getProfile(args && args.id);
|
|
1241
1258
|
if (!p) return { ok: false, error: "未找到连接配置" };
|
|
1242
|
-
|
|
1259
|
+
// 认证探测只用跨平台 echo:Windows host 无 uname/pwd,且默认 shell 语义各异
|
|
1260
|
+
// (旧命令尾部 uname -a 会让认证成功的 Windows host 也误报失败)。
|
|
1261
|
+
const r = await runRemote(subprocess, p, "echo __DSH_OK__", undefined, 16 * 1024);
|
|
1262
|
+
if (r.ok) return r;
|
|
1263
|
+
const baseErr = sshErrorHint([r.error, stripPqBanner(r.stderr).trim()].filter(Boolean).join(" ") || ("ssh 退出码 " + r.exitCode));
|
|
1264
|
+
// 认证类失败:带 ssh -v 重跑一次握手 + 认证,抽取关键诊断行
|
|
1265
|
+
// (私钥加载 / 公钥提供 / 服务器拒绝方法),定位 Windows 常见坑。
|
|
1266
|
+
let error = baseErr;
|
|
1267
|
+
if (/permission denied|publickey/i.test(baseErr + " " + stripPqBanner(r.stderr))) {
|
|
1268
|
+
const diag = await runRemote(subprocess, p, "exit 0", undefined, 16 * 1024, ["-v"]);
|
|
1269
|
+
const diagText = String(diag.stderr || "")
|
|
1270
|
+
.split("\n")
|
|
1271
|
+
.filter(function (l) {
|
|
1272
|
+
return /debug1: (Offering public key|Authentications that can continue|Server accepts key|Trying private key|no such identity)|Load key|Permission denied|passphrase/i.test(l);
|
|
1273
|
+
})
|
|
1274
|
+
.join("\n")
|
|
1275
|
+
.slice(0, 1200);
|
|
1276
|
+
if (diagText) error = baseErr + "\n\nssh -v 诊断:\n" + diagText;
|
|
1277
|
+
}
|
|
1278
|
+
return { ok: false, exitCode: r.exitCode, signal: r.signal, stdout: r.stdout, stderr: r.stderr, truncated: r.truncated, error: error };
|
|
1243
1279
|
},
|
|
1244
1280
|
listWorkspaces: async () => ({ ok: true, workspaces: settingsFace.read().workspaces }),
|
|
1245
1281
|
createRemoteWorkspace: async (args) => {
|
|
@@ -1472,7 +1508,9 @@ function apply(ctx, config) {
|
|
|
1472
1508
|
kind: "prefix",
|
|
1473
1509
|
path: "/remote-ssh/api",
|
|
1474
1510
|
handler: async (req, res) => {
|
|
1475
|
-
|
|
1511
|
+
// CSRF 加固:/remote-ssh/api/* 仅由本插件客户端调用,强制校验其必带的
|
|
1512
|
+
// x-requested-with 头(跨站攻击者无法在 no-cors POST 中携带非简单头)。
|
|
1513
|
+
if (!isTrusted(req, true)) { writeJson(res, 403, { ok: false, error: { code: "csrf", message: "missing x-requested-with header" } }); return; }
|
|
1476
1514
|
if (req.method !== "POST") { writeError(res, new Error("method not allowed"), 405); return; }
|
|
1477
1515
|
const pathname = new URL(req.url || "/", "http://dsh.internal").pathname;
|
|
1478
1516
|
if (!pathname.startsWith(API_BASE)) { writeError(res, new Error("not-found"), 404); return; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhangfengshun/dsh-remote-ssh",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.8",
|
|
4
4
|
"description": "DSH web plugin: VSCode Remote-SSH-like remote development (SSH to supercomputers/servers, remote workspace, file explorer, integrated terminal), integrated with dsh-better-sidebar and DSH settings.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|