2023-01-11 20:39:48 +08:00
// @ts-ignore
2024-11-29 19:00:05 +08:00
import path from "path" ;
2025-04-15 23:43:01 +08:00
import { isArray } from "lodash-es" ;
2025-04-30 09:38:44 +08:00
import { ILogger , safePromise } from "@certd/basic" ;
2024-11-29 19:00:05 +08:00
import { SshAccess } from "./ssh-access.js" ;
2025-04-15 23:43:01 +08:00
2025-03-24 23:45:45 +08:00
import fs from "fs" ;
2025-04-15 23:43:01 +08:00
import { SocksProxyType } from "socks/typings/common/constants" ;
2025-03-24 23:48:34 +08:00
2025-01-02 00:28:13 +08:00
export type TransportItem = { localPath : string ; remotePath : string } ;
2025-04-15 23:43:01 +08:00
export interface SocksProxy {
ipaddress? : string ;
host? : string ;
port : number ;
type : any ;
userId? : string ;
password? : string ;
custom_auth_method? : number ;
custom_auth_request_handler ? : ( ) = > Promise < Buffer > ;
custom_auth_response_size? : number ;
custom_auth_response_handler ? : ( data : Buffer ) = > Promise < boolean > ;
}
export type SshConnectConfig = {
sock? : any ;
} ;
2024-09-29 11:50:59 +08:00
2024-06-25 12:25:57 +08:00
export class AsyncSsh2Client {
2025-04-15 23:43:01 +08:00
conn : any ;
2024-06-25 12:25:57 +08:00
logger : ILogger ;
2025-04-15 23:43:01 +08:00
connConf : SshAccess & SshConnectConfig ;
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 ) ;
}
2024-11-30 17:36:47 +08:00
return buffer . toString ( ) . replaceAll ( "\r\n" , "\n" ) ;
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 } ` ) ;
2024-11-29 19:00:05 +08:00
if ( typeof this . connConf . port === "string" ) {
2024-09-29 11:50:59 +08:00
this . connConf . port = parseInt ( this . connConf . port ) ;
}
2025-04-15 23:43:01 +08:00
const { SocksClient } = await import ( "socks" ) ;
const proxyOption = this . parseSocksProxyFromUri ( this . connConf . socksProxy ) ;
2024-09-29 11:50:59 +08:00
const info = await SocksClient . createConnection ( {
proxy : proxyOption ,
2024-11-29 19:00:05 +08:00
command : "connect" ,
2024-09-29 11:50:59 +08:00
destination : {
host : this.connConf.host ,
port : this.connConf.port ,
} ,
} ) ;
2024-11-29 19:00:05 +08:00
this . logger . info ( "代理连接成功" ) ;
2024-09-29 11:50:59 +08:00
this . connConf . sock = info . socket ;
}
2025-04-02 00:20:09 +08:00
2025-04-15 23:43:01 +08:00
const ssh2 = await import ( "ssh2" ) ;
const ssh2Constants = await import ( "ssh2/lib/protocol/constants.js" ) ;
const { SUPPORTED_KEX , SUPPORTED_SERVER_HOST_KEY , SUPPORTED_CIPHER , SUPPORTED_MAC } = ssh2Constants . default ;
2025-04-30 09:38:44 +08:00
return safePromise ( ( resolve , reject ) = > {
2024-07-18 21:10:13 +08:00
try {
2025-04-15 23:43:01 +08:00
const conn = new ssh2 . default . Client ( ) ;
2024-07-18 21:10:13 +08:00
conn
2024-11-29 19:00:05 +08:00
. on ( "error" , ( err : any ) = > {
this . logger . error ( "连接失败" , err ) ;
2024-07-18 21:10:13 +08:00
reject ( err ) ;
} )
2024-11-29 19:00:05 +08:00
. on ( "ready" , ( ) = > {
this . logger . info ( "连接成功" ) ;
2024-07-18 21:10:13 +08:00
this . conn = conn ;
resolve ( this . conn ) ;
} )
2025-04-02 00:20:09 +08:00
. on ( "keyboard-interactive" , ( name , descr , lang , prompts , finish ) = > {
// For illustration purposes only! It's not safe to do this!
// You can read it from process.stdin or whatever else...
const password = this . connConf . password ;
return finish ( [ password ] ) ;
// And remember, server may trigger this event multiple times
// and for different purposes (not only auth)
} )
2025-03-21 01:02:57 +08:00
. connect ( {
. . . this . connConf ,
2025-04-02 00:20:09 +08:00
tryKeyboard : true ,
2025-03-21 01:02:57 +08:00
algorithms : {
2025-04-02 00:20:09 +08:00
serverHostKey : SUPPORTED_SERVER_HOST_KEY ,
cipher : SUPPORTED_CIPHER ,
hmac : SUPPORTED_MAC ,
kex : SUPPORTED_KEX ,
2025-03-21 01:02:57 +08:00
} ,
} ) ;
2024-07-18 21:10:13 +08:00
} catch ( e ) {
reject ( e ) ;
}
2024-06-25 12:25:57 +08:00
} ) ;
}
async getSftp() {
2025-04-30 09:38:44 +08:00
return safePromise ( ( resolve , reject ) = > {
2024-11-29 19:00:05 +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 ) ;
} ) ;
} ) ;
}
2025-01-20 23:30:54 +08:00
async fastPut ( options : { sftp : any ; localPath : string ; remotePath : string ; opts ? : { mode? : string } } ) {
2025-01-20 23:29:03 +08:00
const { sftp , localPath , remotePath , opts } = options ;
2025-04-30 09:38:44 +08:00
return safePromise ( ( resolve , reject ) = > {
2024-06-26 13:58:17 +08:00
this . logger . info ( ` 开始上传: ${ localPath } => ${ remotePath } ` ) ;
2025-01-20 23:29:03 +08:00
sftp . fastPut ( localPath , remotePath , { . . . ( opts ? ? { } ) } , ( err : Error ) = > {
2024-06-25 12:25:57 +08:00
if ( err ) {
reject ( err ) ;
2024-11-29 19:00:05 +08:00
this . logger . error ( "请确认路径是否包含文件名,路径本身不能是目录,路径不能有*?之类的特殊符号,要有写入权限" ) ;
2024-06-25 12:25:57 +08:00
return ;
}
2024-06-26 13:58:17 +08:00
this . logger . info ( ` 上传文件成功: ${ localPath } => ${ remotePath } ` ) ;
2024-06-25 12:25:57 +08:00
resolve ( { } ) ;
} ) ;
} ) ;
}
2025-04-27 01:31:46 +08:00
async listDir ( options : { sftp : any ; remotePath : string } ) {
const { sftp , remotePath } = options ;
2025-04-30 09:38:44 +08:00
return safePromise ( ( resolve , reject ) = > {
2025-04-27 01:31:46 +08:00
this . logger . info ( ` listDir: ${ remotePath } ` ) ;
sftp . readdir ( remotePath , ( err : Error , list : any ) = > {
if ( err ) {
reject ( err ) ;
return ;
}
resolve ( list ) ;
} ) ;
} ) ;
}
2025-01-02 00:28:13 +08:00
async unlink ( options : { sftp : any ; remotePath : string } ) {
const { sftp , remotePath } = options ;
2025-04-30 09:38:44 +08:00
return safePromise ( ( resolve , reject ) = > {
2025-01-02 00:28:13 +08:00
this . logger . info ( ` 开始删除远程文件: ${ remotePath } ` ) ;
sftp . unlink ( remotePath , ( err : Error ) = > {
if ( err ) {
reject ( err ) ;
return ;
}
this . logger . info ( ` 删除文件成功: ${ remotePath } ` ) ;
resolve ( { } ) ;
} ) ;
} ) ;
}
2025-06-03 23:52:43 +08:00
/**
*
* @param script
* @param opts {withStdErr 返回{stdOut,stdErr}}
*/
2024-11-30 17:36:47 +08:00
async exec (
script : string ,
opts : {
throwOnStdErr? : boolean ;
2025-06-03 23:52:43 +08:00
withStdErr? : boolean ;
2025-04-08 18:06:12 +08:00
env? : any ;
2024-11-30 17:36:47 +08:00
} = { }
) : Promise < string > {
2024-08-05 16:00:04 +08:00
if ( ! script ) {
2024-11-29 19:00:05 +08:00
this . logger . info ( "script 为空,取消执行" ) ;
2024-08-05 16:00:04 +08:00
return ;
}
2024-11-29 19:00:05 +08:00
let iconv : any = await import ( "iconv-lite" ) ;
2024-09-05 00:04:31 +08:00
iconv = iconv . default ;
2024-11-29 19:00:05 +08:00
// if (this.connConf.windows) {
// script += "\r\nexit\r\n";
// //保证windows下正常退出
// }
2025-10-24 22:48:32 +08:00
if ( script . includes ( " -i " ) ) {
this . logger . warn ( "不支持交互式命令,请不要使用-i参数" ) ;
}
2025-04-30 09:38:44 +08:00
return safePromise ( ( resolve , reject ) = > {
2024-10-16 12:20:42 +08:00
this . logger . info ( ` 执行命令:[ ${ this . connConf . host } ][exec]: \ n ` + script ) ;
2025-04-21 17:34:26 +08:00
// pty 伪终端,window下的输出会带上conhost.exe之类的多余的字符串,影响返回结果判断
// linux下 当使用keyboard-interactive 登录时,需要pty
2025-04-22 15:53:19 +08:00
const pty = this . connConf . pty ; //linux下开启伪终端,windows下不开启
2025-04-21 17:34:26 +08:00
this . conn . exec ( script , { pty , env : opts.env } , ( err : Error , stream : any ) = > {
2024-06-25 12:25:57 +08:00
if ( err ) {
reject ( err ) ;
return ;
}
2024-11-29 19:00:05 +08:00
let data = "" ;
2025-06-03 23:52:43 +08:00
let stdErr = "" ;
2024-11-30 17:36:47 +08:00
let hasErrorLog = false ;
2024-06-25 12:25:57 +08:00
stream
2024-11-29 19:00:05 +08:00
. on ( "close" , ( code : any , signal : any ) = > {
2024-06-25 12:25:57 +08:00
this . logger . info ( ` [ ${ this . connConf . host } ][close]:code: ${ code } ` ) ;
2025-07-09 15:43:25 +08:00
/**
* ]pipeline 执行命令:[10.123.0.2][exec]:cd /d D:\nginx-1.27.5 && D:\nginx-1.27.5\nginx.exe -t && D:\nginx-1.27.5\nginx.exe -s reload
* [2025-07-09T10:24:11.219] [ERROR]pipeline - [10. 123.0. 2][error]: nginx: the configuration file D: \nginx-1.27. 5/conf/nginx. conf syntax is ok
* [2025-07-09T10:24:11.231] [ERROR][10. 123. 0. 2] [error]: nginx: configuration file D: \nginx-1.27.5/conf/nginx.conf test is successful
* pipeline-
* [2025-07-09T10:24:11.473] [INFO]pipeline -[10.123.0.2][close]:code:0
* [2025-07-09T10:24:11.473][ERRoR] pipeline- [step][主机一执行远程主机脚本命令]<id:53hyarN3yvmbijNuMiNAt>: [Eror: nginx: the configuration fileD:\nginx-1.27.5/conf/nginx.conf syntax is ok
//需要忽略windows的错误
*/
// if (opts.throwOnStdErr == null && this.windows) {
// opts.throwOnStdErr = true;
// }
2024-11-30 17:36:47 +08:00
if ( opts . throwOnStdErr && hasErrorLog ) {
reject ( new Error ( data ) ) ;
}
2024-06-25 12:25:57 +08:00
if ( code === 0 ) {
2025-06-03 23:52:43 +08:00
if ( opts . withStdErr === true ) {
//@ts-ignore
resolve ( {
stdErr ,
stdOut : data ,
} ) ;
} else {
resolve ( data ) ;
}
2024-06-25 12:25:57 +08:00
} else {
reject ( new Error ( data ) ) ;
}
} )
2024-11-29 19:00:05 +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-11-29 19:00:05 +08:00
. on ( "error" , ( err : any ) = > {
2024-08-05 16:00:04 +08:00
reject ( err ) ;
this . logger . error ( err ) ;
} )
2024-11-29 19:00:05 +08:00
. stderr . on ( "data" , ( ret : Buffer ) = > {
2024-09-04 18:29:39 +08:00
const err = this . convert ( iconv , ret ) ;
2025-06-03 23:52:43 +08:00
stdErr += err ;
2024-11-30 17:36:47 +08:00
hasErrorLog = true ;
2025-08-19 17:06:14 +08:00
if ( err . includes ( "sudo: a password is required" ) ) {
this . logger . warn ( "请配置sudo免密,否则命令无法执行" ) ;
}
2024-11-30 17:36:47 +08:00
this . logger . error ( ` [ ${ this . connConf . host } ][error]: ` + err . trimEnd ( ) ) ;
2024-06-25 12:25:57 +08:00
} ) ;
} ) ;
} ) ;
}
2024-11-30 17:36:47 +08:00
async shell ( script : string | string [ ] ) : Promise < string > {
2025-04-15 23:43:01 +08:00
const stripAnsiModule = await import ( "strip-ansi" ) ;
const stripAnsi = stripAnsiModule . default ;
2025-04-30 09:38:44 +08:00
return safePromise < 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 ;
}
2024-11-30 17:36:47 +08:00
let output = "" ;
2024-09-04 18:29:39 +08:00
function ansiHandle ( data : string ) {
2024-11-30 17:36:47 +08:00
data = data . replace ( /\[[0-9]+;1H/g , "" ) ;
2024-09-04 18:29:39 +08:00
data = stripAnsi ( data ) ;
2024-11-30 17:36:47 +08:00
return data . replaceAll ( "\r\n" , "\n" ) ;
2024-09-04 18:29:39 +08:00
}
2024-06-25 12:25:57 +08:00
stream
2024-11-29 19:00:05 +08:00
. on ( "close" , ( code : any ) = > {
this . logger . info ( "Stream :: close,code: " + code ) ;
2024-06-25 12:25:57 +08:00
resolve ( output ) ;
} )
2024-11-29 19:00:05 +08:00
. on ( "data" , ( ret : Buffer ) = > {
2024-09-04 18:29:39 +08:00
const data = ansiHandle ( ret . toString ( ) ) ;
this . logger . info ( data ) ;
2024-11-30 17:36:47 +08:00
output += data ;
2024-06-27 16:38:43 +08:00
} )
2024-11-29 19:00:05 +08:00
. on ( "error" , ( err : any ) = > {
2024-09-04 18:29:39 +08:00
reject ( err ) ;
this . logger . error ( err ) ;
} )
2024-11-29 19:00:05 +08:00
. stderr . on ( "data" , ( ret : Buffer ) = > {
2024-09-04 18:29:39 +08:00
const data = ansiHandle ( ret . toString ( ) ) ;
2024-11-30 17:36:47 +08:00
output += data ;
this . logger . error ( ` [ ${ this . connConf . host } ][error]: ` + data ) ;
2024-07-15 00:30:33 +08:00
} ) ;
2024-09-04 18:29:39 +08:00
//保证windows下正常退出
2024-11-29 19:00:05 +08:00
const exit = "\r\nexit\r\n" ;
2024-09-04 18:29:39 +08:00
stream . end ( script + exit ) ;
2024-06-25 12:25:57 +08:00
} ) ;
} ) ;
}
end() {
if ( this . conn ) {
this . conn . end ( ) ;
2024-10-15 19:27:55 +08:00
this . conn . destroy ( ) ;
this . conn = null ;
2024-06-25 12:25:57 +08:00
}
}
2024-09-29 11:50:59 +08:00
private parseSocksProxyFromUri ( socksProxyUri : string ) : SocksProxy {
const url = new URL ( socksProxyUri ) ;
let type : SocksProxyType = 5 ;
2024-11-29 19:00:05 +08:00
if ( url . protocol . startsWith ( "socks4" ) ) {
2024-09-29 11:50:59 +08:00
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 ;
}
2025-04-27 01:31:46 +08:00
async download ( param : { remotePath : string ; savePath : string ; sftp : any } ) {
2025-04-30 09:38:44 +08:00
return safePromise ( ( resolve , reject ) = > {
2025-04-27 01:31:46 +08:00
const { remotePath , savePath , sftp } = param ;
sftp . fastGet (
remotePath ,
savePath ,
{
step : ( transferred : any , chunk : any , total : any ) = > {
this . logger . info ( ` ${ transferred } / ${ total } ` ) ;
} ,
} ,
( err : any ) = > {
if ( err ) {
reject ( err ) ;
} else {
resolve ( { } ) ;
}
}
) ;
} ) ;
}
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 ;
2022-11-07 23:31:20 +08:00
/**
*
* @param connectConf
{
host: '192.168.100.100',
port: 22,
username: 'frylock',
password: 'nodejsrules'
}
* @param options
*/
2025-03-24 23:45:45 +08:00
async uploadFiles ( options : { connectConf : SshAccess ; transports : TransportItem [ ] ; mkdirs : boolean ; opts ? : { mode? : string } ; uploadType? : string } ) {
2025-01-20 23:29:03 +08:00
const { connectConf , transports , mkdirs , opts } = options ;
2024-06-25 12:25:57 +08:00
await this . _call ( {
connectConf ,
callable : async ( conn : AsyncSsh2Client ) = > {
2024-11-29 19:00:05 +08:00
this . logger . info ( "开始上传" ) ;
2025-03-24 23:45:45 +08:00
if ( mkdirs !== false ) {
this . logger . info ( "初始化父目录" ) ;
for ( const transport of transports ) {
2024-07-08 11:19:02 +08:00
const filePath = path . dirname ( transport . remotePath ) ;
let mkdirCmd = ` mkdir -p ${ filePath } ` ;
if ( conn . windows ) {
2024-11-29 19:00:05 +08:00
if ( filePath . indexOf ( "/" ) > - 1 ) {
this . logger . info ( "--------------------------" ) ;
this . logger . info ( "请注意:windows下,文件目录分隔应该写成\\而不是/" ) ;
this . logger . info ( "--------------------------" ) ;
2024-07-08 11:19:02 +08:00
}
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-10-15 19:27:55 +08:00
await conn . exec ( mkdirCmd ) ;
2024-06-27 16:38:43 +08:00
}
2024-06-25 12:25:57 +08:00
}
2025-03-24 23:45:45 +08:00
2025-04-27 01:31:46 +08:00
if ( options . uploadType === "scp" ) {
2025-03-24 23:45:45 +08:00
//scp
for ( const transport of transports ) {
await this . scpUpload ( { conn , . . . transport , opts } ) ;
2025-03-26 21:48:51 +08:00
await new Promise ( resolve = > setTimeout ( resolve , 1000 ) ) ;
2025-03-24 23:45:45 +08:00
}
2025-04-27 01:31:46 +08:00
} else {
const sftp = await conn . getSftp ( ) ;
for ( const transport of transports ) {
await conn . fastPut ( { sftp , . . . transport , opts } ) ;
}
2025-03-24 23:45:45 +08:00
}
2024-11-29 19:00:05 +08:00
this . logger . info ( "文件全部上传成功" ) ;
2024-06-25 12:25:57 +08:00
} ,
2022-11-07 23:31:20 +08:00
} ) ;
}
2025-06-03 23:52:43 +08:00
constructor ( logger : ILogger ) {
this . logger = logger ;
}
2022-11-07 23:31:20 +08:00
2025-03-24 23:45:45 +08:00
async scpUpload ( options : { conn : any ; localPath : string ; remotePath : string ; opts ? : { mode? : string } } ) {
2025-03-24 23:48:34 +08:00
const { conn , localPath , remotePath } = options ;
2025-04-30 09:38:44 +08:00
return safePromise ( ( resolve , reject ) = > {
2025-03-24 23:45:45 +08:00
// 关键步骤:构造 SCP 命令
2025-04-30 09:38:44 +08:00
this . logger . info ( ` 开始上传: ${ localPath } => ${ remotePath } ` ) ;
conn . conn . exec (
` scp -t ${ remotePath } ` , // -t 表示目标模式
( err , stream ) = > {
if ( err ) {
return reject ( err ) ;
}
try {
// 准备 SCP 协议头
const fileStats = fs . statSync ( localPath ) ;
const fileName = path . basename ( localPath ) ;
2025-04-27 01:31:46 +08:00
2025-04-30 09:38:44 +08:00
// SCP 协议格式:C[权限] [文件大小] [文件名]\n
stream . write ( ` C0644 ${ fileStats . size } ${ fileName } \ n ` ) ;
2025-04-27 01:31:46 +08:00
2025-04-30 09:38:44 +08:00
// 通过管道传输文件
fs . createReadStream ( localPath )
. on ( "error" , e = > {
this . logger . info ( "read stream error" , e ) ;
reject ( e ) ;
} )
. pipe ( stream )
. on ( "finish" , async ( ) = > {
this . logger . info ( ` 上传完成: ${ localPath } => ${ remotePath } ` ) ;
resolve ( true ) ;
} )
. on ( "error" , reject ) ;
} catch ( e ) {
reject ( e ) ;
2025-03-24 23:45:45 +08:00
}
2025-04-30 09:38:44 +08:00
}
) ;
2025-03-24 23:45:45 +08:00
} ) ;
}
2025-01-02 00:28:13 +08:00
async removeFiles ( opts : { connectConf : SshAccess ; files : string [ ] } ) {
const { connectConf , files } = opts ;
await this . _call ( {
connectConf ,
callable : async ( conn : AsyncSsh2Client ) = > {
const sftp = await conn . getSftp ( ) ;
this . logger . info ( "开始删除" ) ;
for ( const file of files ) {
await conn . unlink ( {
sftp ,
remotePath : file ,
} ) ;
}
this . logger . info ( "文件全部删除成功" ) ;
} ,
} ) ;
}
2024-09-05 00:04:31 +08:00
async isCmd ( conn : AsyncSsh2Client ) {
2024-11-29 19:00:05 +08:00
const spec = await conn . exec ( "echo %COMSPEC% " ) ;
2025-09-05 18:08:23 +08:00
const ret = spec . toString ( ) . trim ( ) ;
if ( ret . includes ( "%COMSPEC%" ) && ! ret . includes ( "echo %COMSPEC%" ) ) {
2024-09-05 00:04:31 +08:00
return false ;
} else {
return true ;
}
}
2024-11-29 19:00:05 +08:00
async getIsCmd ( options : { connectConf : SshAccess } ) {
const { connectConf } = options ;
2024-11-30 17:36:47 +08:00
return await this . _call < boolean > ( {
2024-11-29 19:00:05 +08:00
connectConf ,
callable : async ( conn : AsyncSsh2Client ) = > {
return await this . isCmd ( conn ) ;
} ,
} ) ;
}
2024-09-05 00:04:31 +08:00
/**
*
* 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
*/
2025-07-11 17:37:33 +08:00
async exec ( options : { connectConf : SshAccess ; script : string | Array < string > ; env? : any ; throwOnStdErr? : boolean ; stopOnError? : boolean } ) : Promise < string > {
2022-11-07 23:31:20 +08:00
let { script } = options ;
2025-07-09 15:43:25 +08:00
const { connectConf , throwOnStdErr } = options ;
2024-09-05 00:04:31 +08:00
2024-10-16 12:20:42 +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 ;
2024-10-10 16:18:37 +08:00
const isLinux = ! connectConf . windows ;
const envScripts = [ ] ;
2024-09-05 00:04:31 +08:00
if ( connectConf . windows ) {
isWinCmd = await this . isCmd ( conn ) ;
}
2024-10-10 16:18:37 +08:00
if ( options . env ) {
for ( const key in options . env ) {
if ( isLinux ) {
envScripts . push ( ` export ${ key } = ${ options . env [ key ] } ` ) ;
} else if ( isWinCmd ) {
//win cmd
envScripts . push ( ` set ${ key } = ${ options . env [ key ] } ` ) ;
} else {
//powershell
envScripts . push ( ` $ env: ${ key } =" ${ options . env [ key ] } " ` ) ;
}
}
}
2024-09-05 00:04:31 +08:00
if ( isWinCmd ) {
2024-11-29 19:00:05 +08:00
if ( typeof script === "string" ) {
script = script . split ( "\n" ) ;
2024-09-05 00:04:31 +08:00
}
2025-07-15 16:41:15 +08:00
//组合成&&的形式
2024-10-10 16:18:37 +08:00
script = envScripts . concat ( script ) ;
2024-09-05 00:04:31 +08:00
script = script as Array < string > ;
2024-11-29 19:00:05 +08:00
script = script . join ( " && " ) ;
2024-09-05 00:04:31 +08:00
} else {
2024-11-29 19:00:05 +08:00
const newLine = isLinux ? "\n" : "\r\n" ;
2025-04-15 23:43:01 +08:00
if ( isArray ( script ) ) {
2024-09-05 00:04:31 +08:00
script = script as Array < string > ;
2024-11-29 19:00:05 +08:00
script = script . join ( newLine ) ;
2024-09-05 00:04:31 +08:00
}
2024-10-10 16:18:37 +08:00
if ( envScripts . length > 0 ) {
2024-11-29 19:00:05 +08:00
script = envScripts . join ( newLine ) + newLine + script ;
2024-10-10 16:18:37 +08:00
}
2024-09-05 00:04:31 +08:00
}
2025-07-11 17:37:33 +08:00
2025-09-09 18:14:14 +08:00
if ( isLinux ) {
if ( options . connectConf . scriptType == "bash" ) {
script = "#!/usr/bin/env bash \n" + script ;
} else if ( options . connectConf . scriptType == "sh" ) {
script = "#!/bin/sh\n" + script ;
}
if ( options . connectConf . scriptType != "fish" && options . stopOnError !== false ) {
script = "set -e\n" + script ;
}
2025-07-15 16:41:15 +08:00
}
2025-07-09 15:43:25 +08:00
return await conn . exec ( script as string , { throwOnStdErr } ) ;
2024-06-25 12:25:57 +08:00
} ,
2022-11-07 23:31:20 +08:00
} ) ;
}
2024-11-30 17:36:47 +08:00
async shell ( options : { connectConf : SshAccess ; script : string | Array < string > } ) : Promise < string > {
2024-09-04 18:29:39 +08:00
let { script } = options ;
const { connectConf } = options ;
2025-04-15 23:43:01 +08:00
if ( isArray ( script ) ) {
2024-09-04 18:29:39 +08:00
script = script as Array < string > ;
if ( connectConf . windows ) {
2024-11-29 19:00:05 +08:00
script = script . join ( "\r\n" ) ;
2024-09-04 18:29:39 +08:00
} else {
2024-11-29 19:00:05 +08:00
script = script . join ( "\n" ) ;
2024-09-04 18:29:39 +08:00
}
} else {
if ( connectConf . windows ) {
2024-11-29 19:00:05 +08:00
//@ts-ignore
script = script . replaceAll ( "\n" , "\r\n" ) ;
2024-09-04 18:29:39 +08:00
}
}
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-11-30 17:36:47 +08:00
async _call < T = any > ( options : { connectConf : SshAccess ; callable : ( conn : AsyncSsh2Client ) = > Promise < T > } ) : Promise < T > {
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 ) {
2024-11-29 19:00:05 +08:00
if ( e . message ? . indexOf ( "All configured authentication methods failed" ) > - 1 ) {
2024-09-20 11:11:25 +08:00
this . logger . error ( e ) ;
2024-11-29 19:00:05 +08:00
throw new Error ( "登录失败,请检查用户名/密码/密钥是否正确" ) ;
2024-09-20 11:11:25 +08:00
}
throw e ;
}
2025-09-05 21:16:09 +08:00
let timeoutId = null ;
2024-06-25 12:25:57 +08:00
try {
2025-09-05 21:16:09 +08:00
timeoutId = setTimeout ( ( ) = > {
this . logger . info ( "执行超时,断开连接" ) ;
conn . end ( ) ;
2025-09-05 21:17:15 +08:00
} , 1000 * ( connectConf . timeout || 1800 ) ) ;
2024-06-25 12:25:57 +08:00
return await callable ( conn ) ;
} finally {
2025-09-05 21:16:09 +08:00
clearTimeout ( timeoutId ) ;
2024-06-25 12:25:57 +08:00
conn . end ( ) ;
}
2022-11-07 23:31:20 +08:00
}
2025-04-27 01:31:46 +08:00
async listDir ( param : { connectConf : any ; dir : string } ) {
return await this . _call < any > ( {
connectConf : param.connectConf ,
callable : async ( conn : AsyncSsh2Client ) = > {
const sftp = await conn . getSftp ( ) ;
return await conn . listDir ( {
sftp ,
remotePath : param.dir ,
} ) ;
} ,
} ) ;
}
async download ( param : { connectConf : any ; filePath : string ; savePath : string } ) {
return await this . _call < any > ( {
connectConf : param.connectConf ,
callable : async ( conn : AsyncSsh2Client ) = > {
const sftp = await conn . getSftp ( ) ;
return await conn . download ( {
sftp ,
remotePath : param.filePath ,
savePath : param.savePath ,
} ) ;
} ,
} ) ;
}
2022-11-07 23:31:20 +08:00
}