2023-01-11 20:39:48 +08:00
|
|
|
|
// @ts-ignore
|
2024-06-19 00:21:13 +08:00
|
|
|
|
import ssh2, { ConnectConfig } from 'ssh2';
|
2024-05-27 18:38:41 +08:00
|
|
|
|
import path from 'path';
|
2024-07-15 00:30:33 +08:00
|
|
|
|
import * as _ from 'lodash-es';
|
2024-05-27 18:38:41 +08:00
|
|
|
|
import { ILogger } from '@certd/pipeline';
|
2024-07-15 00:30:33 +08:00
|
|
|
|
import { SshAccess } from '../access/index.js';
|
2024-09-04 18:29:39 +08:00
|
|
|
|
import stripAnsi from 'strip-ansi';
|
2024-09-29 11:50:59 +08:00
|
|
|
|
import { SocksClient } from 'socks';
|
|
|
|
|
|
import { SocksProxy, SocksProxyType } from 'socks/typings/common/constants.js';
|
|
|
|
|
|
|
2024-06-25 12:25:57 +08:00
|
|
|
|
export class AsyncSsh2Client {
|
|
|
|
|
|
conn: ssh2.Client;
|
|
|
|
|
|
logger: ILogger;
|
2024-09-29 11:50:59 +08:00
|
|
|
|
connConf: SshAccess & ssh2.ConnectConfig;
|
2024-07-15 00:30:33 +08:00
|
|
|
|
windows = false;
|
|
|
|
|
|
encoding: string;
|
2024-06-27 16:38:43 +08:00
|
|
|
|
constructor(connConf: SshAccess, logger: ILogger) {
|
2024-06-25 12:25:57 +08:00
|
|
|
|
this.connConf = connConf;
|
|
|
|
|
|
this.logger = logger;
|
2024-06-27 16:38:43 +08:00
|
|
|
|
this.windows = connConf.windows || false;
|
|
|
|
|
|
this.encoding = connConf.encoding;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-04 18:29:39 +08:00
|
|
|
|
convert(iconv: any, buffer: Buffer) {
|
2024-07-15 00:30:33 +08:00
|
|
|
|
if (this.encoding) {
|
2024-06-27 16:38:43 +08:00
|
|
|
|
return iconv.decode(buffer, this.encoding);
|
|
|
|
|
|
}
|
|
|
|
|
|
return buffer.toString();
|
2024-06-25 12:25:57 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async connect() {
|
|
|
|
|
|
this.logger.info(`开始连接,${this.connConf.host}:${this.connConf.port}`);
|
2024-09-29 11:50:59 +08:00
|
|
|
|
if (this.connConf.socksProxy) {
|
|
|
|
|
|
this.logger.info(`使用代理${this.connConf.socksProxy}`);
|
|
|
|
|
|
if (typeof this.connConf.port === 'string') {
|
|
|
|
|
|
this.connConf.port = parseInt(this.connConf.port);
|
|
|
|
|
|
}
|
|
|
|
|
|
const proxyOption: SocksProxy = this.parseSocksProxyFromUri(this.connConf.socksProxy);
|
|
|
|
|
|
const info = await SocksClient.createConnection({
|
|
|
|
|
|
proxy: proxyOption,
|
|
|
|
|
|
command: 'connect',
|
|
|
|
|
|
destination: {
|
|
|
|
|
|
host: this.connConf.host,
|
|
|
|
|
|
port: this.connConf.port,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
this.logger.info('代理连接成功');
|
|
|
|
|
|
this.connConf.sock = info.socket;
|
|
|
|
|
|
}
|
2024-06-25 12:25:57 +08:00
|
|
|
|
return new Promise((resolve, reject) => {
|
2024-07-18 21:10:13 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const conn = new ssh2.Client();
|
|
|
|
|
|
conn
|
|
|
|
|
|
.on('error', (err: any) => {
|
|
|
|
|
|
this.logger.error('连接失败', err);
|
|
|
|
|
|
reject(err);
|
|
|
|
|
|
})
|
|
|
|
|
|
.on('ready', () => {
|
|
|
|
|
|
this.logger.info('连接成功');
|
|
|
|
|
|
this.conn = conn;
|
|
|
|
|
|
resolve(this.conn);
|
|
|
|
|
|
})
|
|
|
|
|
|
.connect(this.connConf);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
reject(e);
|
|
|
|
|
|
}
|
2024-06-25 12:25:57 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
async getSftp() {
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
2024-06-25 12:28:37 +08:00
|
|
|
|
this.logger.info('获取sftp');
|
2024-06-25 12:25:57 +08:00
|
|
|
|
this.conn.sftp((err: any, sftp: any) => {
|
|
|
|
|
|
if (err) {
|
|
|
|
|
|
reject(err);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
resolve(sftp);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async fastPut(options: { sftp: any; localPath: string; remotePath: string }) {
|
|
|
|
|
|
const { sftp, localPath, remotePath } = options;
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
2024-06-26 13:58:17 +08:00
|
|
|
|
this.logger.info(`开始上传:${localPath} => ${remotePath}`);
|
2024-06-25 12:25:57 +08:00
|
|
|
|
sftp.fastPut(localPath, remotePath, (err: Error) => {
|
|
|
|
|
|
if (err) {
|
|
|
|
|
|
reject(err);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2024-06-26 13:58:17 +08:00
|
|
|
|
this.logger.info(`上传文件成功:${localPath} => ${remotePath}`);
|
2024-06-25 12:25:57 +08:00
|
|
|
|
resolve({});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async exec(script: string) {
|
2024-08-05 16:00:04 +08:00
|
|
|
|
if (!script) {
|
|
|
|
|
|
this.logger.info('script 为空,取消执行');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2024-09-05 00:04:31 +08:00
|
|
|
|
let iconv: any = await import('iconv-lite');
|
|
|
|
|
|
iconv = iconv.default;
|
2024-06-25 12:25:57 +08:00
|
|
|
|
return new Promise((resolve, reject) => {
|
2024-06-27 16:38:43 +08:00
|
|
|
|
this.logger.info(`执行命令:[${this.connConf.host}][exec]: ` + script);
|
2024-06-25 12:25:57 +08:00
|
|
|
|
this.conn.exec(script, (err: Error, stream: any) => {
|
|
|
|
|
|
if (err) {
|
|
|
|
|
|
reject(err);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2024-07-15 00:30:33 +08:00
|
|
|
|
let data = '';
|
2024-06-25 12:25:57 +08:00
|
|
|
|
stream
|
|
|
|
|
|
.on('close', (code: any, signal: any) => {
|
|
|
|
|
|
this.logger.info(`[${this.connConf.host}][close]:code:${code}`);
|
|
|
|
|
|
if (code === 0) {
|
|
|
|
|
|
resolve(data);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
reject(new Error(data));
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2024-06-27 16:38:43 +08:00
|
|
|
|
.on('data', (ret: Buffer) => {
|
2024-09-04 18:29:39 +08:00
|
|
|
|
const out = this.convert(iconv, ret);
|
2024-07-15 00:30:33 +08:00
|
|
|
|
data += out;
|
2024-07-03 18:30:38 +08:00
|
|
|
|
this.logger.info(`[${this.connConf.host}][info]: ` + out.trimEnd());
|
2024-06-25 12:25:57 +08:00
|
|
|
|
})
|
2024-08-05 16:00:04 +08:00
|
|
|
|
.on('error', (err: any) => {
|
|
|
|
|
|
reject(err);
|
|
|
|
|
|
this.logger.error(err);
|
|
|
|
|
|
})
|
2024-07-15 00:30:33 +08:00
|
|
|
|
.stderr.on('data', (ret: Buffer) => {
|
2024-09-04 18:29:39 +08:00
|
|
|
|
const err = this.convert(iconv, ret);
|
2024-07-15 00:30:33 +08:00
|
|
|
|
data += err;
|
2024-07-18 21:10:13 +08:00
|
|
|
|
this.logger.info(`[${this.connConf.host}][error]: ` + err.trimEnd());
|
2024-06-25 12:25:57 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-06-25 12:28:37 +08:00
|
|
|
|
async shell(script: string | string[]): Promise<string[]> {
|
2024-06-25 12:25:57 +08:00
|
|
|
|
return new Promise<any>((resolve, reject) => {
|
2024-07-15 00:30:33 +08:00
|
|
|
|
this.logger.info(`执行shell脚本:[${this.connConf.host}][shell]: ` + script);
|
2024-06-25 12:25:57 +08:00
|
|
|
|
this.conn.shell((err: Error, stream: any) => {
|
|
|
|
|
|
if (err) {
|
|
|
|
|
|
reject(err);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const output: string[] = [];
|
2024-09-04 18:29:39 +08:00
|
|
|
|
function ansiHandle(data: string) {
|
|
|
|
|
|
data = data.replace(/\[[0-9]+;1H/g, '\n');
|
|
|
|
|
|
data = stripAnsi(data);
|
|
|
|
|
|
return data;
|
|
|
|
|
|
}
|
2024-06-25 12:25:57 +08:00
|
|
|
|
stream
|
|
|
|
|
|
.on('close', () => {
|
|
|
|
|
|
this.logger.info('Stream :: close');
|
|
|
|
|
|
resolve(output);
|
|
|
|
|
|
})
|
2024-06-27 16:38:43 +08:00
|
|
|
|
.on('data', (ret: Buffer) => {
|
2024-09-04 18:29:39 +08:00
|
|
|
|
const data = ansiHandle(ret.toString());
|
|
|
|
|
|
this.logger.info(data);
|
2024-06-27 16:38:43 +08:00
|
|
|
|
output.push(data);
|
|
|
|
|
|
})
|
2024-09-04 18:29:39 +08:00
|
|
|
|
.on('error', (err: any) => {
|
|
|
|
|
|
reject(err);
|
|
|
|
|
|
this.logger.error(err);
|
|
|
|
|
|
})
|
2024-07-15 00:30:33 +08:00
|
|
|
|
.stderr.on('data', (ret: Buffer) => {
|
2024-09-04 18:29:39 +08:00
|
|
|
|
const data = ansiHandle(ret.toString());
|
2024-06-27 16:38:43 +08:00
|
|
|
|
output.push(data);
|
|
|
|
|
|
this.logger.info(`[${this.connConf.host}][error]: ` + data);
|
2024-07-15 00:30:33 +08:00
|
|
|
|
});
|
2024-09-04 18:29:39 +08:00
|
|
|
|
//保证windows下正常退出
|
|
|
|
|
|
const exit = '\r\nexit\r\n';
|
|
|
|
|
|
stream.end(script + exit);
|
2024-06-25 12:25:57 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
end() {
|
|
|
|
|
|
if (this.conn) {
|
|
|
|
|
|
this.conn.end();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-09-29 11:50:59 +08:00
|
|
|
|
|
|
|
|
|
|
private parseSocksProxyFromUri(socksProxyUri: string): SocksProxy {
|
|
|
|
|
|
const url = new URL(socksProxyUri);
|
|
|
|
|
|
let type: SocksProxyType = 5;
|
|
|
|
|
|
if (url.protocol.startsWith('socks4')) {
|
|
|
|
|
|
type = 4;
|
|
|
|
|
|
}
|
|
|
|
|
|
const proxy: SocksProxy = {
|
|
|
|
|
|
host: url.hostname,
|
|
|
|
|
|
port: parseInt(url.port),
|
|
|
|
|
|
type,
|
|
|
|
|
|
};
|
|
|
|
|
|
if (url.username) {
|
|
|
|
|
|
proxy.userId = url.username;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (url.password) {
|
|
|
|
|
|
proxy.password = url.password;
|
|
|
|
|
|
}
|
|
|
|
|
|
return proxy;
|
|
|
|
|
|
}
|
2024-06-25 12:25:57 +08:00
|
|
|
|
}
|
2024-08-28 14:40:50 +08:00
|
|
|
|
|
2022-11-07 23:31:20 +08:00
|
|
|
|
export class SshClient {
|
2023-01-11 20:39:48 +08:00
|
|
|
|
logger: ILogger;
|
|
|
|
|
|
constructor(logger: ILogger) {
|
2022-11-07 23:31:20 +08:00
|
|
|
|
this.logger = logger;
|
|
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param connectConf
|
|
|
|
|
|
{
|
|
|
|
|
|
host: '192.168.100.100',
|
|
|
|
|
|
port: 22,
|
|
|
|
|
|
username: 'frylock',
|
|
|
|
|
|
password: 'nodejsrules'
|
|
|
|
|
|
}
|
|
|
|
|
|
* @param options
|
|
|
|
|
|
*/
|
2024-07-18 21:10:13 +08:00
|
|
|
|
async uploadFiles(options: { connectConf: SshAccess; transports: any; mkdirs: boolean }) {
|
2024-07-08 11:19:02 +08:00
|
|
|
|
const { connectConf, transports, mkdirs } = options;
|
2024-06-25 12:25:57 +08:00
|
|
|
|
await this._call({
|
|
|
|
|
|
connectConf,
|
|
|
|
|
|
callable: async (conn: AsyncSsh2Client) => {
|
|
|
|
|
|
const sftp = await conn.getSftp();
|
2024-06-26 13:58:17 +08:00
|
|
|
|
this.logger.info('开始上传');
|
2024-06-25 12:25:57 +08:00
|
|
|
|
for (const transport of transports) {
|
2024-07-08 11:19:02 +08:00
|
|
|
|
if (mkdirs !== false) {
|
|
|
|
|
|
const filePath = path.dirname(transport.remotePath);
|
|
|
|
|
|
let mkdirCmd = `mkdir -p ${filePath} `;
|
|
|
|
|
|
if (conn.windows) {
|
|
|
|
|
|
if (filePath.indexOf('/') > -1) {
|
|
|
|
|
|
this.logger.info('--------------------------');
|
2024-07-18 21:10:13 +08:00
|
|
|
|
this.logger.info('请注意:windows下,文件目录分隔应该写成\\而不是/');
|
2024-07-08 11:19:02 +08:00
|
|
|
|
this.logger.info('--------------------------');
|
|
|
|
|
|
}
|
2024-09-05 01:39:46 +08:00
|
|
|
|
const isCmd = await this.isCmd(conn);
|
|
|
|
|
|
if (!isCmd) {
|
2024-07-08 11:19:02 +08:00
|
|
|
|
mkdirCmd = `New-Item -ItemType Directory -Path "${filePath}" -Force`;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
mkdirCmd = `if not exist "${filePath}" mkdir "${filePath}"`;
|
|
|
|
|
|
}
|
2024-07-03 18:30:38 +08:00
|
|
|
|
}
|
2024-09-04 18:29:39 +08:00
|
|
|
|
await conn.shell(mkdirCmd);
|
2024-06-27 16:38:43 +08:00
|
|
|
|
}
|
2024-07-08 11:19:02 +08:00
|
|
|
|
|
2024-06-25 12:25:57 +08:00
|
|
|
|
await conn.fastPut({ sftp, ...transport });
|
|
|
|
|
|
}
|
2024-06-26 13:58:17 +08:00
|
|
|
|
this.logger.info('文件全部上传成功');
|
2024-06-25 12:25:57 +08:00
|
|
|
|
},
|
2022-11-07 23:31:20 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-05 00:04:31 +08:00
|
|
|
|
async isCmd(conn: AsyncSsh2Client) {
|
|
|
|
|
|
const spec = await conn.exec('echo %COMSPEC%');
|
|
|
|
|
|
if (spec.toString().trim() === '%COMSPEC%') {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
*
|
|
|
|
|
|
* Set-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
|
|
|
|
|
|
* Start-Service sshd
|
|
|
|
|
|
*
|
|
|
|
|
|
* Set-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\cmd.exe"
|
|
|
|
|
|
* @param options
|
|
|
|
|
|
*/
|
2024-07-15 00:30:33 +08:00
|
|
|
|
async exec(options: { connectConf: SshAccess; script: string | Array<string> }) {
|
2022-11-07 23:31:20 +08:00
|
|
|
|
let { script } = options;
|
|
|
|
|
|
const { connectConf } = options;
|
2024-09-05 00:04:31 +08:00
|
|
|
|
|
2024-09-04 18:29:39 +08:00
|
|
|
|
this.logger.info('命令:', script);
|
2024-06-25 12:25:57 +08:00
|
|
|
|
return await this._call({
|
|
|
|
|
|
connectConf,
|
|
|
|
|
|
callable: async (conn: AsyncSsh2Client) => {
|
2024-09-05 00:04:31 +08:00
|
|
|
|
let isWinCmd = false;
|
|
|
|
|
|
if (connectConf.windows) {
|
|
|
|
|
|
isWinCmd = await this.isCmd(conn);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isWinCmd) {
|
|
|
|
|
|
//组合成&&的形式
|
|
|
|
|
|
if (typeof script === 'string') {
|
|
|
|
|
|
script = script.split('\n');
|
|
|
|
|
|
}
|
|
|
|
|
|
script = script as Array<string>;
|
2024-09-19 10:17:31 +08:00
|
|
|
|
script = script.join(' && ');
|
2024-09-05 00:04:31 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
if (_.isArray(script)) {
|
|
|
|
|
|
script = script as Array<string>;
|
|
|
|
|
|
script = script.join('\n');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
await conn.exec(script);
|
2024-06-25 12:25:57 +08:00
|
|
|
|
},
|
2022-11-07 23:31:20 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-04 18:29:39 +08:00
|
|
|
|
//废弃
|
|
|
|
|
|
async shell(options: { connectConf: SshAccess; script: string | Array<string> }): Promise<string[]> {
|
|
|
|
|
|
let { script } = options;
|
|
|
|
|
|
const { connectConf } = options;
|
|
|
|
|
|
if (_.isArray(script)) {
|
|
|
|
|
|
script = script as Array<string>;
|
|
|
|
|
|
if (connectConf.windows) {
|
|
|
|
|
|
script = script.join('\r\n');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
script = script.join('\n');
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (connectConf.windows) {
|
|
|
|
|
|
script = script.replaceAll('\n', '\r\n');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-06-25 12:25:57 +08:00
|
|
|
|
return await this._call({
|
|
|
|
|
|
connectConf,
|
|
|
|
|
|
callable: async (conn: AsyncSsh2Client) => {
|
|
|
|
|
|
return await conn.shell(script as string);
|
|
|
|
|
|
},
|
2022-11-07 23:31:20 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-07-15 00:30:33 +08:00
|
|
|
|
async _call(options: { connectConf: SshAccess; callable: any }): Promise<string[]> {
|
2024-06-25 12:25:57 +08:00
|
|
|
|
const { connectConf, callable } = options;
|
|
|
|
|
|
const conn = new AsyncSsh2Client(connectConf, this.logger);
|
2024-09-20 11:11:25 +08:00
|
|
|
|
try {
|
|
|
|
|
|
await conn.connect();
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
if (e.message?.indexOf('All configured authentication methods failed') > -1) {
|
|
|
|
|
|
this.logger.error(e);
|
|
|
|
|
|
throw new Error('登录失败,请检查用户名/密码/密钥是否正确');
|
|
|
|
|
|
}
|
|
|
|
|
|
throw e;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-06-25 12:25:57 +08:00
|
|
|
|
try {
|
|
|
|
|
|
return await callable(conn);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
conn.end();
|
|
|
|
|
|
}
|
2022-11-07 23:31:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|