ssh2-sftp-client使用介绍

· 4 min read

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

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

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

服务禁用/开启设定

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

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

# 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路径必须是相对或者绝对路径,不可以是路径别名,比如

    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包不支持

    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接口返回的信息即可判断。

     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。解决办法是比如通过执行命令获取文件隐藏属性进一步确定。

返回数据结构

{
  "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方法,构建可写流。

限速

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

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连接,然后重新连接。

断点下载/上传

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

sftp.get(remoteFile, fileWtr, {
        readStreamOptions: {
          start: 10,
        },
      })

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

downloadDir/uploadDir

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

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

SFTP协议介绍

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

写在最后

done