ssh2-sftp-client使用介绍

最近在做WebShell,除了sz/rz命令方式实现上传下载之外,需要GUI方式实现基本的文件操作,比如拉取文件列表,上传下载。

调研后决定使用基于sftp/ssh实现的ssh2-sftp-client

这里总结下使用中遇到的问题

服务禁用/开启设定

每个Linux机器默认都有SFTP服务,毕竟底层是SSH协议,算是标配,但用户可以通过在服务端设置来选择开启和关闭sftp服务的。

因此在实际开发中要考虑到服务不可用场景下的处理

1
2
3
4
5
6
7
8
9
10
11
# vi /etc/ssh/sshd_config

# override default of no subsystems
Subsystem sftp /usr/libexec/openssh/sftp-server


After
# override default of no subsystems
# Subsystem sftp /usr/libexec/openssh/sftp-server

service sshd restart

如果服务没有正常开启,则异常错误码是ERR_GENERIC_CLIENT

https://static.1991421.cn/2022/2022-06-12-230743.jpeg

list

如果是想拉取文件列表,使用的是list方法,有几个问题需要注意

  1. 不支持~

    remotePath路径必须是相对或者绝对路径,不可以是路径别名,比如

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    async function main() {
    try {
    await sftp.connect(config);
    let fileList = await sftp.list(remotePath);
    console.log(fileList);
    await sftp.end();
    } catch (e) {
    console.error(e);
    }
    }
  2. 文件type

    返回结果中有type字段,表示文件类型,其中

    • d是文件夹
    • -是文件
    • l是软链接

针对链接类型文件,想知道具体是文件还是文件夹,只能再去单独查询判断,比如使用stat方法,返回值中isDirectory,isFile可以明确具体类别

  1. size单位为字节

    • 这点与一般shell命令显示单位一致
    • 文件夹类型也会返回size大小
  2. rights权限

    通过list接口可以拿到文件所属user/group,同时文件针对所属user/group/other权限,rights与longname信息等价,只是rights将权限信息结构化显示。owner字段表示归属userID,group表示归属groupID。通过信息还是无法直接判断当前用户对该文件是否有读写权限,为了判断需要SSH单独执行id username命令来获取当前用户userID和groupID

    举个例子如下,注意SSH2Client并不是sftpClient,这个需要使用ssh2下的client单独连接,sftp包不支持

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    const {Client: SSH2Client} = require('ssh2');

    const ssh2Client = new SSH2Client();

    await new Promise(resolve => ssh2Client.connect(config).on('ready', () => {
    return ssh2Client.exec(`id ${config.username}`, (err, stream) => {
    stream.on('data', (buf) => {
    const idRes = buf.toString();
    console.log(idRes); // uid=0(root) gid=0(root) groups=0(root)
    const rights = idRes.match(/\d+/g);
    console.log({
    uid: +rights[0],
    gid: +rights[1],
    });
    return resolve(idRes);
    });
    })
    }));

    当拿到权限信息后,结合list接口返回的信息即可判断。

    1
    2
    3
    4
    5
    const rights = idRes.match(/\d+/g);
    console.log({
    uid: +rights[0],
    gid: +rights[1],
    });

filter

list方法list(path, filter) ==> Array[object]第二个参数filter可以对列表文件进行过滤。比如隐藏文件不显示^[^.],但注意Windows文件名并没有规范要求一定是.开头文件,因此Windows下无法通过该方法过滤隐藏文件的显s。解决办法是比如通过执行命令获取文件隐藏属性进一步确定。

返回数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"type": "-", // 文件类型
"name": "admin1.pem", // 文件名
"size": 418, // 文件体积,字节数
"modifyTime": 1651807713000,
"accessTime": 1651807715000,
"rights": {
"user": "rw",
"group": "r",
"other": "r"
},
"owner": 0,
"group": 0
}

put/fastPut

  1. 上传文件时可以使用这两个方法,区别主要在于是否支持流。比如我这里的设计是用户从网页发送的请求是走了node服务,node服务再发起SSH连接到目标机器,如果使用fastPut方法就一定存在临时文件,这样第一是速度慢,且实现上传进度的话,也并不准确了,毕竟是先分片上传到服务端,然后再分片上传到服务器,两个过程是割裂的。因此像我这里的场景,更适合使用put方法。

  2. put支持的流写法确保是可读流即可

  3. 这里举个例子是网页发起的WS传输数据构造的可读流,这样前端发出的数据片,直接流化发送到目标机器,本身在node服务端不做任何其它处理

  4. 上传文件默认会有权限,这里建议设置为0o644 ,该值参考的FileZilla-SFTP上传后给予的权限设定。

    • 644即rw-r--r--,用户自己有读写权限,组/其他只有读权限

get/fastGet

与上传类似,如果需要中转到前台,可以使用get方法,构建可写流。

限速

有时为了安全,需要对上传下载进行限速,做法如下。注意,限速需要流化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Throttle = require('throttle');

const throttleStream = new Throttle(1024 * 1024); // 1MB
this.conn
.put(throttleStream.pipe(stream), data.path, {
writeStreamOptions: {
autoClose: false,
mode: 0o644,
},
readStreamOptions: {
autoClose: false,
},
pipeOptions: {
end: false,
},
})

取消

在上传下载过程中,突然想取消了,做法是直接断开SFTP连接,然后重新连接。

断点下载/上传

针对断点下载,远程文件可读流直接控制起始位置即可。

1
2
3
4
5
sftp.get(remoteFile, fileWtr, {
readStreamOptions: {
start: 10,
},
})

针对断点上传,可以走append方法即可。

downloadDir/uploadDir

上下载文件夹可以使用该方法,但缺点明显,比如无法流化,这样比如存在WebShell服务端中转,如果使用该方法,中转文件就必然存在,想要的上下载进度就无法准确。

因此可以使用get/put来解决,本质就是递归调用来实现文件夹中所有文件的读写操作,其中创建文件夹的话可以走mkdir解决。

SFTP协议介绍

https://www.ssh.com/academy/ssh/sftp

写在最后

done