refactor🎨: (阅读代码):connection.ts增加注释

master
yetao 3 weeks ago
parent d44badc702
commit ca4fc9a03b

@ -1,115 +1,222 @@
// 导入 FileSystemConfig 类型,该类型定义了文件系统配置的结构
import type { FileSystemConfig } from 'common/fileSystemConfig';
// 导入 fs 模块的 readFile 函数,用于读取文件
import { readFile } from 'fs';
// 导入 net 模块的 Socket 类,用于创建网络套接字
import { Socket } from 'net';
// 导入 os 模块的 userInfo 函数,用于获取当前用户的信息
import { userInfo } from 'os';
// 导入 ssh2 模块的相关类型和函数,用于创建 SSH 连接和处理认证
import { AuthHandlerFunction, AuthHandlerObject, Client, ClientChannel, ConnectConfig } from 'ssh2';
// 导入 ssh2 模块的 SFTP 类,用于创建 SFTP 客户端
import { SFTP } from 'ssh2/lib/protocol/SFTP';
// 导入 vscode 模块,用于与 VS Code 编辑器进行交互
import * as vscode from 'vscode';
// 从当前目录下的 config 模块中导入 getConfig 函数,用于获取配置信息
import { getConfig } from './config';
// 从当前目录下的 flags 模块中导入 getFlagBoolean 函数,用于获取布尔类型的标志
import { getFlagBoolean } from './flags';
// 从当前目录下的 logging 模块中导入 Logger 和 Logging 类型,用于日志记录
import { Logger, Logging } from './logging';
// 导入 PuttySession 类型,该类型定义了 Putty 会话的结构
import type { PuttySession } from './putty';
// 从当前目录下的 utils 模块中导入 toPromise 函数,用于将回调函数转换为 Promise。导入 validatePort 函数,用于验证端口号是否合法
import { toPromise, validatePort } from './utils';
/**
*
* SSH
* @type {ConnectConfig} - ConnectConfig
*/
const DEFAULT_CONFIG: ConnectConfig = {
// 尝试使用键盘交互进行认证
tryKeyboard: true,
// 设置保持连接的间隔时间为 30 秒30e3 毫秒)
keepaliveInterval: 30e3,
};
/**
*
* $
*
* @param string -
* @returns
*/
function replaceVariables(string?: string) {
// 如果传入的参数不是字符串,则直接返回该参数
if (typeof string !== 'string') return string;
// 使用正则表达式匹配字符串中的环境变量,并将其替换为实际的值
return string.replace(/\$\w+/g, key => process.env[key.substr(1)] || '');
}
/**
*
*
* @type {Partial<Record<keyof FileSystemConfig, [placeholder: string, prompt: (config: FileSystemConfig) => string, promptOnEmpty: boolean, password?: boolean]>>} - FileSystemConfig
*/
const PROMPT_FIELDS: Partial<Record<keyof FileSystemConfig, [
placeholder: string,
prompt: (config: FileSystemConfig) => string,
promptOnEmpty: boolean, password?: boolean]>> = {
// 定义了 host 字段的占位符、提示信息和是否在为空时提示
host: ['Host', c => `Host for ${c.name}`, true],
// 定义了 username 字段的占位符、提示信息和是否在为空时提示
username: ['Username', c => `Username for ${c.name}`, true],
// 定义了 password 字段的占位符、提示信息、是否在为空时提示和是否是密码字段
password: ['Password', c => `Password for '${c.username}' for ${c.name}`, false, true],
// 定义了 passphrase 字段的占位符、提示信息、是否在为空时提示和是否是密码字段
passphrase: ['Passphrase', c => `Passphrase for provided export/private key for '${c.username}' for ${c.name}`, false, true],
};
/**
*
*
* 使 PROMPT_FIELDS
* @param config -
* @param fields -
* @returns Promise
*/
async function promptFields(config: FileSystemConfig, ...fields: (keyof FileSystemConfig)[]): Promise<void> {
// 遍历 fields 数组中的每个字段
for (const field of fields) {
// 从 PROMPT_FIELDS 中获取当前字段的提示信息
const prompt = PROMPT_FIELDS[field];
// 如果没有找到对应的提示信息,则记录错误并继续循环
if (!prompt) {
Logging.error`Prompting unexpected field '${field}'`;
continue;
}
// 获取配置对象中当前字段的值
const value = config[field];
// 如果值存在且不是 true则跳过当前字段的提示
if (value && value !== true) continue; // Truthy and not true
// 如果值不存在且不需要在为空时提示,则跳过当前字段的提示
if (!value && !prompt[2]) continue; // Falsy but not promptOnEmpty
// 显示输入框,提示用户输入当前字段的值
const result = await vscode.window.showInputBox({
// 忽略失去焦点事件,即输入框不会因为失去焦点而关闭
ignoreFocusOut: true,
// 根据 prompt 数组的第四个元素来确定是否将输入内容显示为密码形式
password: !!prompt[3],
// 输入框中的占位符文本,用于提示用户应该输入什么样的信息
placeHolder: prompt[0],
// 输入框上方的提示信息,这里使用了 prompt 数组的第二个元素,并将 config 对象作为参数传递给该函数
prompt: prompt[1](config),
});
// 将用户输入的值赋给配置对象中的当前字段
(config[field] as string | undefined) = result;
}
}
/**
*
* PuTTY
* @param config -
* @returns null
*/
export async function calculateActualConfig(config: FileSystemConfig): Promise<FileSystemConfig | null> {
// 如果配置对象已经计算过,则直接返回
if (config._calculated) return config;
// 创建一个日志记录器,用于记录函数执行过程中的信息
const logging = Logging.scope();
// Add the internal _calculated field to cache the actual config for the next calculateActualConfig call
// (and it also allows accessing the original config that generated this actual config, if ever necessary)
// 将内部 _calculated 字段添加到配置对象中,以缓存实际配置,供下一次 calculateActualConfig 调用使用
// (并且它还允许在必要时访问生成此实际配置的原始配置)
config = { ...config, _calculated: config };
// Windows uses `$USERNAME` while Unix uses `$USER`, let's normalize it here
// Windows 使用 `$USERNAME` 而 Unix 使用 `$USER`,我们在这里进行标准化
if (config.username === '$USERNAME') config.username = '$USER';
// Delay handling just `$USER` until later, as PuTTY might handle it specially
// 延迟处理仅为 `$USER` 的情况,因为 PuTTY 可能会特别处理它
if (config.username !== '$USER') config.username = replaceVariables(config.username);
// 替换主机名中的环境变量
config.host = replaceVariables(config.host);
// 替换端口号中的环境变量,并验证端口号是否有效
const port = replaceVariables((config.port || '') + '');
config.port = port ? validatePort(port) : 22;
// 替换代理中的环境变量
config.agent = replaceVariables(config.agent);
// 替换私钥路径中的环境变量
config.privateKeyPath = replaceVariables(config.privateKeyPath);
// 记录日志,表示正在计算实际配置
logging.info`Calculating actual config`;
// 如果配置对象是从即时连接字符串创建的,则启用 PuTTY在尝试模式下
if (config.instantConnection) {
// Created from an instant connection string, so enable PuTTY (in try mode)
config.putty = '<TRY>'; // Could just set it to `true` but... consistency?
}
// 如果配置对象启用了 PuTTY
if (config.putty) {
// 如果当前平台不是 Windows则记录警告日志
if (process.platform !== 'win32') {
logging.warning(`\tConfigurating uses putty, but platform is ${process.platform}`);
}
// 导入 getCachedFinder 函数,用于获取 PuTTY 会话查找器
const { getCachedFinder } = await import('./putty');
// 获取会话查找器
const getSession = await getCachedFinder();
// 如果用户名是 `$USER`,则将其设置为 undefined
const cUsername = config.username === '$USER' ? undefined : config.username;
// 如果是尝试模式,则尝试查找 PuTTY 会话
const tryPutty = config.instantConnection || config.putty === '<TRY>';
let session: PuttySession | undefined;
// 如果 tryPutty 为真,则尝试查找名为 config.host 的 PuTTY 会话
if (tryPutty) {
// If we're trying to find one, we also check whether `config.host` represents the name of a PuTTY session
// 如果我们正在尝试查找一个会话,我们也会检查 config.host 是否代表一个 PuTTY 会话的名称
session = await getSession(config.host);
// 记录日志,表明正在尝试查找名为 config.host 的 PuTTY 会话,并显示查找结果
logging.info`\ttryPutty is true, tried finding a config named '${config.host}' and found ${session ? `'${session.name}'` : 'no match'}`;
}
// 如果没有找到会话,则执行以下操作
if (!session) {
// 初始化一个变量,用于标记是否仅提供了会话名称
let nameOnly = true;
// 如果 config.putty 被设置为 true
if (config.putty === true) {
// 提示用户输入主机字段
await promptFields(config, 'host');
// TODO: `config.putty === true` without config.host should prompt the user with *all* PuTTY sessions
// TODO: 如果 config.putty 为 true 且没有配置 host则应提示用户输入所有 PuTTY 会话
// 如果没有提供主机,则抛出错误
if (!config.host) throw new Error(`'putty' was true but 'host' is empty/missing`);
// 将 config.putty 设置为提供的主机名
config.putty = config.host;
// 标记为不仅仅提供了会话名称
nameOnly = false;
} else {
// 如果 config.putty 不是 true则替换其中的变量
config.putty = replaceVariables(config.putty);
}
// 根据提供的信息获取会话
session = await getSession(config.putty, config.host, cUsername, nameOnly);
}
// 如果找到了会话
if (session) {
// 如果会话协议不是 SSH则抛出错误
if (session.protocol !== 'ssh') throw new Error(`The requested PuTTY session isn't a SSH session`);
// 设置用户名
config.username = cUsername || session.username;
// 如果用户名不存在,且主机名中包含 `@`,则从主机名中提取用户名
if (!config.username && session.hostname && session.hostname.indexOf('@') >= 1) {
config.username = session.hostname.substr(0, session.hostname.indexOf('@'));
}
// 主机名设置为会话中的主机名
// Used to be `config.host || session.hostname`, but `config.host` could've been just the session name
config.host = session.hostname.replace(/^.*?@/, '');
// 端口号设置为会话中的端口号
config.port = session.portnumber || config.port;
// 代理设置为会话中的代理
config.agent = config.agent || (session.tryagent ? 'pageant' : undefined);
// 如果会话设置了从环境变量获取用户名,则设置用户名
if (session.usernamefromenvironment) config.username = '$USER';
// 私钥路径设置为会话中的私钥路径
config.privateKeyPath = config.privateKeyPath || (!config.agent && session.publickeyfile) || undefined;
// 根据会话中的代理方法设置代理配置
switch (session.proxymethod) {
case 0:
break;
@ -126,94 +233,156 @@ export async function calculateActualConfig(config: FileSystemConfig): Promise<F
default:
throw new Error(`The requested PuTTY session uses an unsupported proxy method`);
}
// 记录日志,显示读取 PuTTY 配置后的最终配置
logging.debug`\tReading PuTTY configuration lead to the following configuration:\n${JSON.stringify(config, null, 4)}`;
// 如果没有找到会话,且不是尝试模式,则抛出错误
} else if (!tryPutty) {
throw new Error(`Couldn't find the requested PuTTY session`);
// 如果没有找到会话,但处于尝试模式,则记录日志
} else {
logging.debug`\tConfig suggested finding a PuTTY configuration, did not find one`;
}
}
// If the username is (still) `$USER` at this point, use the local user's username
// 如果配置对象中的用户名是 $USER则将其替换为当前用户的用户名
if (config.username === '$USER') config.username = userInfo().username;
// 如果配置对象中存在私钥路径
if (config.privateKeyPath) {
// 尝试读取私钥文件
try {
// 使用 readFile 方法异步读取私钥文件,并将结果转换为 Buffer 类型
const key = await toPromise<Buffer>(cb => readFile(config.privateKeyPath!, cb));
// 将读取到的私钥内容赋值给配置对象中的 privateKey 属性
config.privateKey = key;
// 记录日志,显示已从指定路径读取私钥
logging.debug`\tRead private key from ${config.privateKeyPath}`;
} catch (e) {
// 如果读取私钥文件时发生错误,抛出一个包含错误信息的新错误
throw new Error(`Error while reading the keyfile at:\n${config.privateKeyPath}`);
}
}
// 提示用户输入主机名、用户名和密码
await promptFields(config, 'host', 'username', 'password');
// 如果用户输入了密码,则将代理设置为 undefined
if (config.password) config.agent = undefined;
// 如果 passphrase 被设置为 true
if ((config.passphrase as any) === true) {
// 如果存在私钥,则提示用户输入 passphrase
if (config.privateKey) {
await promptFields(config, 'passphrase');
} else {
}
// 如果没有提供私钥
else {
// 显示一个警告信息,提示用户没有为指定的用户名和主机名提供密钥
const answer = await vscode.window.showWarningMessage(
`The field 'passphrase' was set to true, but no key was provided for ${config.username}@${config.name}`, 'Configure', 'Ignore');
// 如果用户选择配置,则导航到配置页面
if (answer === 'Configure') {
const webview = await import('./webview');
webview.navigate({ type: 'editconfig', config });
return null;
}
}
// 如果 passphrase 被设置为 false
} else if ((config.passphrase as any) === false) {
// Issue with the ssh2 dependency apparently not liking false
// 删除 passphrase 属性,因为 ssh2 库不支持 false 值
delete config.passphrase;
}
// 如果配置了代理转发但没有配置代理,则禁用代理转发并记录日志
if (config.agentForward && !config.agent) {
// 如果配置了代理转发但没有配置代理,则禁用代理转发并记录日志
logging.debug`\tNo agent while having agentForward, disabling agent forwarding`;
// 将 agentForward 属性设置为 false表示禁用代理转发
config.agentForward = false;
}
// 如果没有私钥、代理和密码,则提示用户输入密码并记录日志
if (!config.privateKey && !config.agent && !config.password) {
// 如果没有私钥、代理和密码,则提示用户输入密码并记录日志
logging.debug`\tNo privateKey, agent or password. Gonna prompt for password`;
// 将密码设置为 true表示需要用户输入密码
config.password = true as any;
// 提示用户输入密码
await promptFields(config, 'password');
}
// 记录最终的配置信息
logging.debug`\tFinal configuration:\n${config}`;
// 返回最终的配置对象
return config;
}
/**
*
* @param config -
* @returns Promise NodeJS.ReadableStream null
*/
export async function createSocket(config: FileSystemConfig): Promise<NodeJS.ReadableStream | null> {
// 计算文件系统配置的实际值,并将结果赋值给 config 变量
config = (await calculateActualConfig(config))!;
// 如果 config 为空,则返回 null
if (!config) return null;
// 使用 Logging.scope 方法创建一个日志记录器,并将其赋值给 logging 变量
const logging = Logging.scope(`createSocket(${config.name})`);
// 记录一条信息日志,表示正在创建套接字
logging.info`Creating socket`;
// 检查是否配置了跳转主机
if (config.hop) {
// 记录日志,显示正在通过指定的跳转主机进行跳转
logging.debug`\tHopping through ${config.hop}`;
// 获取跳转主机的配置
const hop = getConfig(config.hop);
// 如果没有找到跳转主机的配置,则抛出错误
if (!hop) throw new Error(`A SSH FS configuration with the name '${config.hop}' doesn't exist`);
// 创建到跳转主机的 SSH 连接
const ssh = await createSSH(hop);
// 如果连接失败,则记录日志并返回 null
if (!ssh) {
logging.debug`\tFailed in connecting to hop ${config.hop}`;
return null;
}
// 创建一个 Promise用于处理与目标主机的连接
return new Promise<NodeJS.ReadableStream>((resolve, reject) => {
// 使用 SSH 客户端的 forwardOut 方法,通过跳转主机连接到目标主机
ssh.forwardOut('localhost', 0, config.host!, config.port || 22, (err, channel) => {
// 如果发生错误,记录日志并拒绝 Promise
if (err) {
logging.debug`\tError connecting to hop ${config.hop} for ${config.name}: ${err}`;
// 为错误消息添加额外的上下文信息
err.message = `Couldn't connect through the hop:\n${err}`;
return reject(err);
} else if (!channel) {
err = new Error('Did not receive a channel');
}
// 如果没有接收到通道,则抛出错误
else if (!channel) {
// 如果没有接收到通道,则创建一个错误对象
const err = new Error('Did not receive a channel');
// 记录一条调试日志,显示在连接到跳转主机时没有接收到通道
logging.debug`\tGot no channel when connecting to hop ${config.hop} for ${config.name}`;
// 拒绝 Promise并将错误对象传递给调用者
return reject(err);
}
// 当通道关闭时,结束 SSH 连接
channel.once('close', () => ssh.end());
// 成功连接到目标主机,解析 Promise 并返回通道
resolve(channel);
});
});
}
// 根据配置对象中的代理类型,选择相应的代理处理方法
switch (config.proxy && config.proxy.type) {
// 如果代理类型为空或未定义,则不进行任何操作
case null:
case undefined:
break;
// 如果代理类型为 socks4 或 socks5则使用 socks 代理处理方法
case 'socks4':
case 'socks5':
return await (await import('./proxy')).socks(config);
// 如果代理类型为 http则使用 http 代理处理方法
case 'http':
return await (await import('./proxy')).http(config);
// 如果代理类型为未知类型,则抛出错误
default:
throw new Error(`Unknown proxy method`);
}
@ -225,44 +394,79 @@ export async function createSocket(config: FileSystemConfig): Promise<NodeJS.Rea
});
}
/**
* SSH
* @param config -
* @param logging -
* @returns Promise false
*/
function makeAuthHandler(config: FileSystemConfig, logging: Logger): AuthHandlerFunction {
// 允许的认证方法列表,初始包含 'none'
const authsAllowed: (AuthHandlerObject | AuthHandlerObject['type'])[] = ['none'];
// 检查是否启用了 OPENSSH-SHA1 标志
const [flagV, flagR] = getFlagBoolean('OPENSSH-SHA1', true, config.flags);
// 如果配置了密码,则允许 'password' 认证
if (config.password) authsAllowed.push('password');
// 如果配置了私钥,则允许 'publickey' 认证
if (config.privateKey) {
if (flagV) {
// 如果启用了 OPENSSH-SHA1 标志,则记录日志并在认证对象中包含 convertSha1 选项
logging.info`Flag "OPENSSH-SHA1" enabled due to '${flagR}', including convertSha1 for publickey authentication`;
authsAllowed.push({ type: 'publickey', username: config.username!, key: config.privateKey, convertSha1: true });
} else {
// 否则,只允许 'publickey' 认证
authsAllowed.push('publickey');
}
}
// 如果配置了代理,则允许 'agent' 认证
if (config.agent) {
if (flagV) {
// 如果启用了 OPENSSH-SHA1 标志,则记录日志并在认证对象中包含 convertSha1 选项
logging.info`Flag "OPENSSH-SHA1" enabled due to '${flagR}', including convertSha1 for agent authentication`;
authsAllowed.push({ type: 'agent', username: config.username!, agent: config.agent, convertSha1: true });
} else {
// 否则,只允许 'agent' 认证
authsAllowed.push('agent');
}
}
// 如果配置了尝试键盘交互,则允许 'keyboard-interactive' 认证
if (config.tryKeyboard) authsAllowed.push('keyboard-interactive');
// 如果配置了私钥、本地主机名和本地用户名,则允许 'hostbased' 认证
if (config.privateKey && config.localHostname && config.localUsername) authsAllowed.push('hostbased');
// 如果启用了 OPENSSH-SHA1 标志,则记录日志
if (flagV) {
logging.info`Flag "OPENSSH-SHA1" enabled due to '${flagR}'`;
}
// 返回一个认证处理函数,该函数从允许的认证方法列表中返回第一个认证方法或 false
return () => authsAllowed.shift() || false;
}
/**
* SSH
* @param config -
* @param sock - SSH
* @returns Promise SSH null
*/
export async function createSSH(config: FileSystemConfig, sock?: NodeJS.ReadableStream): Promise<Client | null> {
// 计算实际的配置对象
config = (await calculateActualConfig(config))!;
// 如果配置对象为空,则返回 null
if (!config) return null;
// 如果未提供套接字,则创建一个新的套接字
sock = sock || (await createSocket(config))!;
// 如果套接字为空,则返回 null
if (!sock) return null;
// 创建一个日志记录器,用于记录创建 SSH 连接的过程
const logging = Logging.scope(`createSSH(${config.name})`);
// 返回一个 Promise成功时解析为 SSH 客户端对象
return new Promise<Client>((resolve, reject) => {
// 创建一个新的 SSH 客户端对象
const client = new Client();
// 当客户端准备就绪时,解析 Promise 并返回客户端对象
client.once('ready', () => resolve(client));
// 当连接超时时,拒绝 Promise 并抛出错误
client.once('timeout', () => reject(new Error(`Socket timed out while connecting SSH FS '${config.name}'`)));
// 当接收到键盘交互请求时,处理用户输入
client.on('keyboard-interactive', (name, instructions, lang, prompts, finish) => {
logging.debug`Received keyboard-interactive request with prompts ${prompts}`;
Promise.all<string>(prompts.map(prompt =>
@ -273,6 +477,7 @@ export async function createSSH(config: FileSystemConfig, sock?: NodeJS.Readable
}),
)).then(finish).catch(e => logging.error(e));
});
// 当发生错误时,记录错误并拒绝 Promise
client.on('error', (error: Error & { description?: string }) => {
if (error.description) {
error.message = `${error.description}\n${error}`;
@ -281,19 +486,28 @@ export async function createSSH(config: FileSystemConfig, sock?: NodeJS.Readable
reject(error);
});
try {
// 创建最终的配置对象,包含默认配置和用户自定义配置
const finalConfig: FileSystemConfig = { ...config, sock, ...DEFAULT_CONFIG };
// 创建一个认证处理函数
finalConfig.authHandler = makeAuthHandler(finalConfig, logging);
// 如果启用了调试模式,则设置调试日志记录器
if (config.debug || getFlagBoolean('DEBUG_SSH2', false, config.flags)[0]) {
const scope = Logging.scope(`ssh2(${config.name})`);
finalConfig.debug = (msg: string) => scope.debug(msg);
}
// Unless the flag 'DF-GE' is specified, disable DiffieHellman groupex algorithms (issue #239)
// Note: If the config already specifies a custom `algorithms.key`, ignore it (trust the user?)
// 除非指定了 'DF-GE' 标志,否则禁用 DiffieHellman groupex 算法
const [flagV, flagR] = getFlagBoolean('DF-GE', false, config.flags);
// 如果 flagV 为真,则表示启用了 "DF-GE" 标志
if (flagV) {
// 记录一条信息日志,说明由于 flagR 的原因启用了 "DF-GE" 标志,并禁用了 DiffieHellman kex groupex 算法
logging.info`Flag "DF-GE" enabled due to '${flagR}', disabling DiffieHellman kex groupex algorithms`;
// 从 finalConfig.algorithms 中获取 kex 算法列表,如果存在的话
const removeKex = finalConfig.algorithms?.kex;
// 如果 removeKex 存在,则记录一条调试日志,说明已经存在的 algorithms.kex
if (removeKex) logging.debug`\tAlready present algorithms.kex: ${removeKex}`;
// 更新 finalConfig.algorithms将 kex 算法列表中的 'diffie-hellman-group-exchange' 移除
finalConfig.algorithms = {
...finalConfig.algorithms,
kex: {
@ -304,29 +518,63 @@ export async function createSSH(config: FileSystemConfig, sock?: NodeJS.Readable
],
},
};
// 记录一条调试日志,说明更新后的 algorithms.kex
logging.debug`\tResulting algorithms.kex: ${finalConfig.algorithms.kex}`;
}
// 连接到 SSH 服务器
client.connect(finalConfig);
} catch (e) {
// 如果发生异常,拒绝 Promise 并抛出错误
reject(e);
}
});
}
/**
* sudo SSH root
* @param shell - SSH
* @param config -
* @param user - 使
* @returns Promise void sudo
* @throws sudo
*/
function startSudo(shell: ClientChannel, config: FileSystemConfig, user: string | boolean = true): Promise<void> {
// 记录一条调试信息,表明正在将 shell 转换为 sudo shell
Logging.debug`Turning shell into a sudo shell for ${typeof user === 'string' ? `'${user}'` : 'default sudo user'}`;
/**
* sudo SSH root
* @param shell - SSH
* @param config -
* @param user - 使
* @returns Promise void sudo
* @throws sudo
*/
return new Promise((resolve, reject) => {
/**
*
* @param data -
*/
function stdout(data: Buffer | string) {
data = data.toString();
// 如果数据是 "SUDO OK",则表示 sudo 会话已成功启动
if (data.trim() === 'SUDO OK') {
// 清理资源并解决 Promise
return cleanup(), resolve();
} else {
// 记录意外的标准输出数据
Logging.debug`Unexpected STDOUT: ${data}`;
}
}
/**
*
* @param data -
* @returns sudo Promise Promise
*/
async function stderr(data: Buffer | string) {
data = data.toString();
// 如果数据以 "[sudo]" 开头,则表示接收到了 sudo 密码提示
if (data.match(/^\[sudo\]/)) {
// 获取密码,如果配置中没有提供密码,则提示用户输入
const password = typeof config.password === 'string' ? config.password :
await vscode.window.showInputBox({
password: true,
@ -334,86 +582,168 @@ function startSudo(shell: ClientChannel, config: FileSystemConfig, user: string
placeHolder: 'Password',
prompt: data.substr(7),
});
// 如果没有输入密码,则拒绝 Promise 并抛出错误
if (!password) return cleanup(), reject(new Error('No password given'));
// 将密码写入 shell
return shell.write(`${password}\n`);
}
// 如果不是 sudo 密码提示,则记录错误并拒绝 Promise
return cleanup(), reject(new Error(`Sudo error: ${data}`));
}
/**
*
*/
function cleanup() {
shell.removeListener('data', stdout);
shell.stderr!.removeListener('data', stderr);
}
// 监听 shell 的标准输出数据
shell.on('data', stdout);
// 监听 shell 的标准错误输出数据
shell.stderr!.on('data', stderr);
// 根据用户参数构建 sudo 命令
const uFlag = typeof user === 'string' ? `-u ${user} ` : '';
// 向 shell 写入 sudo 命令,启动 sudo 会话
shell.write(`sudo -S ${uFlag}bash -c "echo SUDO OK; cat | bash"\n`);
});
}
/**
* sudo
* sudo sudo
* sudo sudo
* @param cmd -
* @returns sudo
*/
function stripSudo(cmd: string) {
// 首先去除字符串开头的 "sudo " 前缀
cmd = cmd.replace(/^sudo\s+/, '');
// 初始化结果变量为原始命令字符串
let res = cmd;
// 进入一个无限循环,直到找到一个非 sudo 前缀或参数的部分
while (true) {
// 将结果字符串修剪掉首尾的空格
cmd = res.trim();
// 尝试去除结果字符串开头的 "-- " 前缀
res = cmd.replace(/^\-\-\s+/, '');
// 如果结果字符串发生了变化,说明找到了一个 "-- " 前缀,跳出当前循环
if (res !== cmd) break;
// 尝试去除结果字符串开头的单个短横线和一个字符的组合,如 "-A"、"-b" 等
res = cmd.replace(/^\-[AbEeKklnPSsVv]/, '');
// 如果结果字符串发生了变化,说明找到了一个短横线和一个字符的组合,继续循环
if (res !== cmd) continue;
// 尝试去除结果字符串开头的单个短横线和一个字符以及一个空格和一个参数的组合,如 "-C file"、"-h host" 等
res = cmd.replace(/^\-[CHhprtUu]\s+\S+/, '');
// 如果结果字符串发生了变化,说明找到了一个短横线和一个字符以及一个空格和一个参数的组合,继续循环
if (res !== cmd) continue;
// 尝试去除结果字符串开头的双短横线和一个参数的组合,如 "--close-from=123"、"--group=users" 等
res = cmd.replace(/^\-\-(close\-from|group|host|role|type|other\-user|user)=\S+/, '');
// 如果结果字符串发生了变化,说明找到了一个双短横线和一个参数的组合,继续循环
if (res !== cmd) continue;
// 如果以上所有尝试都没有改变结果字符串,说明已经找到了一个非 sudo 前缀或参数的部分,跳出循环
break;
}
// 返回去除 sudo 前缀和相关参数后的命令字符串
return cmd;
}
/**
* SFTP
* @param client - SSH
* @param config -
* @returns Promise SFTP
* @throws SFTP
*/
export async function getSFTP(client: Client, config: FileSystemConfig): Promise<SFTP> {
// 计算实际的配置对象
config = (await calculateActualConfig(config))!;
// 如果配置对象不存在,则抛出一个错误
if (!config) throw new Error('Couldn\'t calculate the config');
// 创建日志记录器
const logging = Logging.scope(`getSFTP(${config.name})`);
// 检查是否设置了 sftpSudo 但没有设置 sftpCommand
if (config.sftpSudo && !config.sftpCommand) {
// 如果是,则记录一条警告信息,指出 sftpSudo 已设置但未设置 sftpCommand
logging.warning(`sftpSudo is set without sftpCommand. Assuming /usr/lib/openssh/sftp-server`);
// 然后,将默认的 sftpCommand 设置为 /usr/lib/openssh/sftp-server
config.sftpCommand = '/usr/lib/openssh/sftp-server';
}
// 检查是否没有设置 sftpCommand
if (!config.sftpCommand) {
// 如果没有指定 sftpCommand则使用默认的 sftp 子系统来创建 SFTP 会话
logging.info`Creating SFTP session using standard sftp subsystem`;
// 将 client.sftp 方法包装成一个 Promise并返回这个 Promise
return toPromise<SFTP>(cb => client.sftp(cb));
}
// 从配置对象中获取 sftpCommand 命令,并将其赋值给 cmd 变量
let cmd = config.sftpCommand;
// 记录一条调试信息,表明正在将 shell 转换为 sudo shell
logging.info`Creating SFTP session using specified command: ${cmd}`;
// 获取一个 shell 通道
const shell = await toPromise<ClientChannel>(cb => client.shell(false, cb));
// 监听 shell 的标准输出和标准错误输出
// shell.stdout.on('data', (d: string | Buffer) => logging.debug`[SFTP-STDOUT] ${d}`);
// shell.stderr.on('data', (d: string | Buffer) => logging.debug`[SFTP-STDERR] ${d}`);
// Maybe the user hasn't specified `sftpSudo`, but did put `sudo` in `sftpCommand`
// I can't find a good way of differentiating welcome messages, SFTP traffic, sudo password prompts, ...
// so convert the `sftpCommand` to make use of `sftpSudo`, since that seems to work
// 检查命令是否以 sudo 开头
if (cmd.match(/^sudo/)) {
// If the -u flag is given, use that too
// 如果 -u 标志被给出,也使用它
const mat = cmd.match(/\-u\s+(\S+)/) || cmd.match(/\-\-user=(\S+)/);
config.sftpSudo = mat ? mat[1] : true;
// Now the tricky part of splitting the sudo and sftp command
// 现在拆分 sudo 和 sftp 命令的棘手部分
config.sftpCommand = cmd = stripSudo(cmd);
logging.warning(`Reformed sftpCommand due to sudo to: ${cmd}`);
}
// If the user wants sudo, we'll first convert this shell into a sudo shell
// 如果用户想要 sudo我们将首先把这个 shell 转换为一个 sudo shell
if (config.sftpSudo) await startSudo(shell, config, config.sftpSudo);
// 写入一个标记,表示 SFTP 准备就绪
shell.write(`echo SFTP READY\n`);
// Wait until we see "SFTP READY" (skipping welcome messages etc)
// 等待直到我们看到 "SFTP READY"(跳过欢迎消息等)
await new Promise<void>((ready, nvm) => {
// 定义一个处理函数,用于处理 shell 通道接收到的数据
const handler = (data: string | Buffer) => {
// 将数据转换为字符串并去除首尾空格
if (data.toString().trim() !== 'SFTP READY') return;
// 如果接收到的数据不是 "SFTP READY",则忽略
shell.removeListener('data', handler);
// 移除 data 事件的监听器
ready();
// 调用 ready 函数,表示 SFTP 会话已经准备就绪
};
// 监听 shell 通道的 data 事件
shell.on('data', handler);
// 监听 shell 通道的 close 事件
shell.on('close', nvm);
});
// Start sftpCommand (e.g. /usr/lib/openssh/sftp-server) and wrap everything nicely
// 启动 sftpCommand例如 /usr/lib/openssh/sftp-server并将所有内容包装得很好
const sftp = new SFTP(client, shell, { debug: config.debug });
// 监听 shell 的 data 事件,当有数据时,将数据推送到 sftp 实例中
shell.on('data', data => sftp.push(data));
// 监听 shell 的 close 事件,当 shell 关闭时,结束数据传输
shell.on('close', data => data.end());
// 向 shell 写入 sftpCommand 命令,并等待命令执行完成
await toPromise(cb => shell.write(`${cmd}\n`, cb));
// 初始化 sftp 实例
sftp._init();
// 返回 sftp 实例
return sftp;
}

Loading…
Cancel
Save