first commit

This commit is contained in:
douboer
2026-03-21 18:57:10 +08:00
commit c49aa1a5e9
570 changed files with 107167 additions and 0 deletions

View File

@@ -0,0 +1,416 @@
/* global console, module, require, clearTimeout, setTimeout */
/* eslint-disable no-control-regex */
const { createGatewayClient } = require("./gateway");
const { validateServerForConnect } = require("./serverValidation");
const { resolveSocketDomainHint } = require("./socketDomain");
/**
* 认证参数与 terminal 页保持一致,避免多处行为分叉。
*/
function normalizeAuthType(value) {
const authType = String(value || "").trim();
if (authType === "privateKey" || authType === "certificate") return authType;
return "password";
}
function resolveCredential(server, prefix) {
const source = server && typeof server === "object" ? server : {};
const fieldPrefix = String(prefix || "");
const authType = normalizeAuthType(
fieldPrefix ? source.jumpHost && source.jumpHost.authType : source.authType
);
const passwordKey = fieldPrefix ? `${fieldPrefix}Password` : "password";
const privateKeyKey = fieldPrefix ? `${fieldPrefix}PrivateKey` : "privateKey";
const passphraseKey = fieldPrefix ? `${fieldPrefix}Passphrase` : "passphrase";
const certificateKey = fieldPrefix ? `${fieldPrefix}Certificate` : "certificate";
if (authType === "privateKey") {
return {
type: "privateKey",
privateKey: String(source[privateKeyKey] || ""),
passphrase: String(source[passphraseKey] || "")
};
}
if (authType === "certificate") {
return {
type: "certificate",
privateKey: String(source[privateKeyKey] || ""),
passphrase: String(source[passphraseKey] || ""),
certificate: String(source[certificateKey] || "")
};
}
return {
type: "password",
password: String(source[passwordKey] || "")
};
}
function normalizeHomePath(input) {
const value = String(input || "").trim();
if (!value || value === "~") return "~";
if (!value.startsWith("~/")) return "~";
const body = value
.slice(2)
.split("/")
.map((part) => part.trim())
.filter((part) => part && part !== "." && part !== "..")
.join("/");
return body ? `~/${body}` : "~";
}
function quoteForShellSingle(value) {
return `'${String(value || "").replace(/'/g, `'"'"'`)}'`;
}
function buildListCommand(path, beginMarker, endMarker) {
const normalized = normalizeHomePath(path);
const relative = normalized === "~" ? "" : normalized.slice(2);
const relQuoted = quoteForShellSingle(relative);
const beginQuoted = quoteForShellSingle(beginMarker);
const endQuoted = quoteForShellSingle(endMarker);
// 外层仅执行 `sh -lc '...'`,避免依赖远端默认 shell 对 `A=B cmd` 前缀赋值语法的支持。
// 目录列举优先 command ls -1tp按修改时间从新到旧且绕过 alias再回退 find。
const script = [
`__RC_BEGIN=${beginQuoted}`,
`__RC_END=${endQuoted}`,
`__RC_REL=${relQuoted}`,
'if [ -z "$__RC_REL" ]; then __RC_DIR="$HOME"; else __RC_DIR="$HOME/$__RC_REL"; fi',
'printf "%s\\n" "$__RC_BEGIN"',
'if ! cd "$__RC_DIR" 2>/dev/null; then printf "__RCERR__CD__\\n"; printf "%s\\n" "$__RC_END"; exit 0; fi',
"if command -v ls >/dev/null 2>&1; then command ls -1tp 2>/dev/null | sed -n '/\\/$/s#/$##p' | sed '/^\\./d'; elif command -v find >/dev/null 2>&1; then find . -mindepth 1 -maxdepth 1 -type d ! -name '.*' -print 2>/dev/null | sed 's#^\\./##'; fi",
'printf "%s\\n" "$__RC_END"'
].join("; ");
const scriptQuoted = quoteForShellSingle(script);
return `sh -lc ${scriptQuoted}\n`;
}
function createStdoutCollector(beginMarker, endMarker) {
let buffer = "";
let done = false;
let collecting = false;
const extractedLines = [];
let beginSignalSeen = false;
function sanitizeChunk(input) {
return sanitizeTerminalChunk(input);
}
function consume(rawChunk) {
if (done) return;
buffer += sanitizeChunk(rawChunk);
if (!beginSignalSeen && buffer.includes(beginMarker)) {
beginSignalSeen = true;
}
// 必须按“整行”识别 begin/end 标记,避免命令回显折行时把标记误判成目录项。
while (!done) {
const newlineAt = buffer.indexOf("\n");
if (newlineAt < 0) break;
const rawLine = buffer.slice(0, newlineAt);
buffer = buffer.slice(newlineAt + 1);
const line = rawLine.replace(/\r/g, "");
const trimmed = line.trim();
if (!collecting) {
if (trimmed === beginMarker) {
collecting = true;
beginSignalSeen = true;
}
continue;
}
if (trimmed === endMarker) {
done = true;
break;
}
extractedLines.push(line);
}
}
function isDone() {
return done;
}
function hasBeginMarker() {
return collecting || done;
}
function hasBeginSignal() {
return beginSignalSeen || collecting || done;
}
function getLines() {
return extractedLines.map((line) => line.trim()).filter((line) => !!line);
}
return { consume, isDone, hasBeginMarker, hasBeginSignal, getLines };
}
function sanitizeTerminalChunk(input) {
return String(input || "")
.replace(/\r/g, "")
.replace(/\u001b\][^\u0007]*(?:\u0007|\u001b\\)/g, "")
.replace(/\u001bP[\s\S]*?\u001b\\/g, "")
.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "")
.replace(/\u001b[@-~]/g, "");
}
function toErrorMessage(error, fallback) {
if (!error) return fallback;
if (typeof error === "string") return error;
if (typeof error.message === "string" && error.message.trim()) return error.message.trim();
if (typeof error.errMsg === "string" && error.errMsg.trim()) return error.errMsg.trim();
return fallback;
}
function isLikelyErrorLine(line) {
const text = String(line || "");
if (!text) return false;
return /^(?:ls|find|sh):|not found|no such file|permission denied/i.test(text);
}
function isInternalMarkerLine(line) {
const text = String(line || "");
const trimmed = text.trim();
if (!trimmed) return false;
if (/__RC_/i.test(trimmed)) return true;
if (/~\/__RC_/i.test(trimmed)) return true;
if (/(?:^|\s)gavin\b.*\b(?:%|#|\$)\b.*__RC_/i.test(trimmed)) return true;
const normalized = trimmed
.replace(/[\u001b\u009b][[()\]#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "")
.replace(/[^a-zA-Z0-9_]/g, "")
.toUpperCase();
if (!normalized) return false;
if (normalized.includes("RCDIRBEGIN")) return true;
if (normalized.includes("RCDIREND")) return true;
if (normalized.includes("RCBEGIN")) return true;
if (normalized.includes("RCEND")) return true;
if (normalized.includes("RCREL")) return true;
if (normalized.includes("RCERR")) return true;
if (normalized.includes("LIST_REMOTEDIRECTORIES_DONE".replace(/[^A-Z0-9_]/g, ""))) return true;
if (!text) return false;
return false;
}
/**
* 通过一次短连接从远端目录读取子目录列表。
* 返回值仅包含“目录名”,不包含完整路径。
*/
function listRemoteDirectories(options) {
const params = options && typeof options === "object" ? options : {};
const server = params.server || {};
const opsConfig = params.opsConfig || {};
const targetPath = normalizeHomePath(params.path || "~");
const requestId = `dir_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
const requestStartedAt = Date.now();
const connectTimeoutMs = Number(opsConfig.gatewayConnectTimeoutMs);
const timeoutMs = Number(params.timeoutMs);
const maxWaitMs =
Number.isFinite(timeoutMs) && timeoutMs >= 1000
? timeoutMs
: Number.isFinite(connectTimeoutMs) && connectTimeoutMs >= 1000
? connectTimeoutMs + 10000
: 22000;
const beginMarker = `__RC_DIR_BEGIN_${Date.now()}_${Math.random().toString(36).slice(2, 8)}__`;
const endMarker = `__RC_DIR_END_${Date.now()}_${Math.random().toString(36).slice(2, 8)}__`;
const collector = createStdoutCollector(beginMarker, endMarker);
const command = buildListCommand(targetPath, beginMarker, endMarker);
console.info(
`[目录请求] 开始 id=${requestId} host=${String(server.host || "").trim() || "-"} ` +
`port=${Number(server.port) || 22} user=${String(server.username || "").trim() || "-"} path=${targetPath}`
);
return new Promise((resolve, reject) => {
let settled = false;
let connected = false;
let timer = null;
let commandAttempts = 0;
// 目录读取改为单次发送,避免重试导致重复 ls 拉长耗时。
const maxCommandAttempts = 1;
let sawFirstOutput = false;
const diagnosticLines = [];
function rememberDiagnostics(rawChunk) {
const lines = sanitizeTerminalChunk(rawChunk)
.split("\n")
.map((line) => line.trim())
.filter((line) => !!line)
.filter((line) => !isInternalMarkerLine(line));
lines.forEach((line) => {
if (diagnosticLines.includes(line)) return;
diagnosticLines.push(line);
});
if (diagnosticLines.length > 6) {
diagnosticLines.splice(0, diagnosticLines.length - 6);
}
}
function sendCommandAttempt(trigger) {
if (settled) return;
if (!connected) return;
if (collector.isDone()) return;
if (commandAttempts >= maxCommandAttempts) return;
commandAttempts += 1;
console.info(
`[目录请求] 发送命令 id=${requestId} attempt=${commandAttempts}/${maxCommandAttempts} ` +
`trigger=${String(trigger || "unknown")} elapsed=${Date.now() - requestStartedAt}ms`
);
try {
client.sendStdin(command);
} catch {
finishReject(new Error("目录命令发送失败"));
return;
}
}
const client = createGatewayClient({
gatewayUrl: opsConfig.gatewayUrl,
gatewayToken: opsConfig.gatewayToken,
connectTimeoutMs: opsConfig.gatewayConnectTimeoutMs,
onFrame: (frame) => {
if (settled) return;
if (
frame.type === "control" &&
frame.payload?.action === "connected" &&
!frame.payload?.fingerprint
) {
connected = true;
console.info(`[目录请求] 已连接 id=${requestId} elapsed=${Date.now() - requestStartedAt}ms`);
// connected 事件里立即首发,避免定时器抖动导致额外等待。
sendCommandAttempt("connected");
return;
}
if (frame.type === "control" && frame.payload?.action === "connected" && frame.payload?.fingerprint) {
return;
}
if (frame.type === "stdout" || frame.type === "stderr") {
const chunkText = frame.payload?.data || "";
if (!collector.hasBeginSignal()) {
rememberDiagnostics(chunkText);
}
if (!sawFirstOutput && String(chunkText || "").trim()) {
sawFirstOutput = true;
console.info(`[目录请求] 首包输出 id=${requestId} elapsed=${Date.now() - requestStartedAt}ms`);
// 若连接后尚未发出目录命令,首包输出到达时立即触发一次发送。
if (connected && commandAttempts === 0 && !collector.isDone()) {
sendCommandAttempt("first_output");
}
}
const beforeDone = collector.isDone();
collector.consume(chunkText);
const afterDone = collector.isDone();
if (beforeDone || !afterDone) return;
if (!collector.isDone()) return;
const rawLines = collector.getLines();
const hasCdError = rawLines.some((line) => line.trim() === "__RCERR__CD__");
if (hasCdError) {
finishReject(new Error(`目录不存在或无权限:${targetPath}`));
return;
}
const names = rawLines
.map((line) => line.trim())
.filter((line) => line && line !== "." && line !== "..")
.filter((line) => !isInternalMarkerLine(line))
.filter((line) => !line.startsWith("."))
.filter((line) => !isLikelyErrorLine(line))
.filter((line, index, arr) => arr.indexOf(line) === index);
console.info(
`[目录请求] 完成 id=${requestId} path=${targetPath} count=${names.length} ` +
`attempts=${commandAttempts} elapsed=${Date.now() - requestStartedAt}ms`
);
finishResolve(names);
return;
}
if (frame.type === "error") {
finishReject(new Error(frame.payload?.message || "网关错误"));
return;
}
if (frame.type === "control" && frame.payload?.action === "disconnect") {
const reason = String(frame.payload?.reason || "").trim();
if (!collector.isDone()) {
finishReject(new Error(reason ? `连接已断开: ${reason}` : "连接已断开"));
}
}
},
onClose: () => {
if (settled || collector.isDone()) return;
finishReject(new Error(connected ? "连接中断" : "网关连接失败"));
},
onError: (error) => {
if (settled) return;
finishReject(new Error(toErrorMessage(error, "网关连接失败")));
}
});
function cleanup() {
if (timer) {
clearTimeout(timer);
timer = null;
}
try {
client.disconnect("list_remote_directories_done");
} catch {
// ignore
}
}
function finishResolve(payload) {
if (settled) return;
settled = true;
cleanup();
resolve(payload);
}
function finishReject(error) {
if (settled) return;
settled = true;
console.warn(
`[目录请求] 失败 id=${requestId} path=${targetPath} attempts=${commandAttempts} ` +
`elapsed=${Date.now() - requestStartedAt}ms reason=${toErrorMessage(error, "unknown")}`
);
cleanup();
reject(error);
}
timer = setTimeout(() => {
const diagnostic = diagnosticLines.find((line) => !isLikelyErrorLine(line)) || diagnosticLines[0] || "";
if (diagnostic) {
finishReject(new Error(`目录读取超时,远端返回:${diagnostic}`));
return;
}
finishReject(new Error("目录读取超时,请稍后重试"));
}, maxWaitMs);
client
.connect({
host: server.jumpHost && server.jumpHost.enabled ? server.jumpHost.host : server.host,
port: server.jumpHost && server.jumpHost.enabled ? Number(server.jumpHost.port) || 22 : server.port,
username: server.jumpHost && server.jumpHost.enabled ? server.jumpHost.username : server.username,
credential:
server.jumpHost && server.jumpHost.enabled
? resolveCredential(server, "jump")
: resolveCredential(server),
...(server.jumpHost && server.jumpHost.enabled
? {
jumpHost: {
host: server.host,
port: server.port,
username: server.username,
credential: resolveCredential(server)
}
}
: {}),
cols: 80,
rows: 24
})
.catch((error) => {
finishReject(new Error(toErrorMessage(error, "网关连接失败")));
});
});
}
module.exports = {
listRemoteDirectories,
normalizeHomePath,
validateServerForConnect,
resolveSocketDomainHint
};