Golang实践录:ssh及scp的实现

Posted 李迟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang实践录:ssh及scp的实现相关的知识,希望对你有一定的参考价值。

本文介绍golang的scp实现和使用。

问题提出

工作中经常要查询日志,一般情况下需使用堡垒机登陆到远程机器,确认日志位置、文件名称,再用winscp软件下载,这过程比较繁琐,为节省时间,考虑用 golang 实现 scp 功能,届时在远程机器部署web服务,使用浏览器即可下载日志文件。另外,也实现执行远程命令的功能,这样更方便远程操作。

设计思路

本文涉及到的所有接口,都在个人的工程库com包中实现,由于该包较庞杂,因些将ssh相关的函数封装成“类”。实现执行远程服务器命令、和远程服务器互传文件。

文件传输功能对标的是scp命令,该命令参数中的路径中包含了远程路径,根据路径位置的不同,实现了上传、下载文件,同时支持目录或多文件的传输。命令示例如下:

scp -r localdir latelee@172.18.18.168:/tmp
scp -r *.cpp latelee@172.18.18.168:/tmp
scp -r latelee@172.18.18.168:/home/latelee/project/foo/*.cpp .

对于接口的实现,方式稍有不同,但功能相同,即将上传、下载分别实现为不同的接口。如下:

1、实际运行远程脚本或命令接口,并返回运行结果(如可获取ls的结果)

2、实现上传文件接口。

3、实现下载文件接口。

sftp模块

sftp托管在 github 上,使用如下方式下载:

go get github.com/pkg/sftp
go get golang.org/x/crypto/ssh

注:为快速下载,建议设置golang.org代理。

在 sftp 包中有许多接口,比如ReadDir、Mkdir、Open、Create等,这些接口和常用的同名函数一样,但是针对sftp服务器的,比如用 Open 打开服务器文件,用 Create 在服务器上创建文件,等等。

另外,sftp 包也有服务端功能,后续根据需求,使用该模块实现sftp服务器。

代码片段

在设计上,将参数配置、连接、文件传输等分为不同函数。整体步骤如下:

  • 创建ssh.ClientConfig实例,指定User、Auth,本文使用密码登陆方式,另外要提供回调函数给 HostKeyCallback,本文使用ssh.InsecureIgnoreHostKey,网上有建议生产环境用ssh.FixedHostKey,但笔者无法调试通过,因此暂不用。
  • 用上一步骤创建的配置实例,调用ssh.Dial连接服务器。该函数返回客户端实例。此后可用该实例进行操作。
  • 执行远程命令:调用sshClient.NewSession创建会话,再调用CombinedOutput执行命令并返回结果。注意,一个会话中执行一次命令。
  • 下载文件:用sftpClient.Open打开服务器文件,再写回本地文件。
  • 上传文件:用sftpClient.Create在服务器上创建文件,再调用Write写文件。

初始化

type Cli struct 
    user       string
    pwd        string // TODO 目前是明文存储,如何加密?
    ip         string
    port       string
    sshClient  *ssh.Client
    sftpClient *sftp.Client

​
func NewSSHClient(user, pwd, ip, port string) Cli 
    return Cli
        user: user,
        pwd:  pwd,
        ip:   ip,
        port: port,
    

连接

// 不使用 HostKey, 使用密码
func (c *Cli) getConfig_nokey() *ssh.ClientConfig 
    config := &ssh.ClientConfig
        User: c.user,
        Auth: []ssh.AuthMethod
            ssh.Password(c.pwd),
        ,
        Timeout:         30 * time.Second,
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    
​
    return config

​
func (c *Cli) Connect() error 
    config := c.getConfig_nokey()
    client, err := ssh.Dial("tcp", c.ip+":"+c.port, config)
    if err != nil 
        return fmt.Errorf("connect server error: %w", err)
    
    sftp, err := sftp.NewClient(client)
    if err != nil 
        return fmt.Errorf("new sftp client error: %w", err)
    
​
    c.sshClient = client
    c.sftpClient = sftp
    return nil

执行远程命令

​
func (c Cli) Run(cmd string) (string, error) 
    if c.sshClient == nil 
        if err := c.Connect(); err != nil 
            return "", err
        
    
​
    session, err := c.sshClient.NewSession()
    if err != nil 
        return "", fmt.Errorf("create new session error: %w", err)
    
    defer session.Close()
​
    buf, err := session.CombinedOutput(cmd)
    return string(buf), err

下载文件

​
func (c Cli) DownloadFile(remoteFile, localFile string) (int, error) 
    if c.sshClient == nil 
        if err := c.Connect(); err != nil 
            return -1, err
        
    
    source, err := c.sftpClient.Open(remoteFile)
    if err != nil 
        return -1, fmt.Errorf("sftp client open file error: %w", err)
    
    defer source.Close()
​
    target, err := os.OpenFile(localFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
    if err != nil 
        return -1, fmt.Errorf("open local file error: %w", err)
    
    defer target.Close()
​
    n, err := io.Copy(target, source)
    if err != nil 
        return -1, fmt.Errorf("write file error: %w", err)
    
    return int(n), nil

上传文件

func (c Cli) UploadFile(localFile, remoteFileName string) (int, error) 
    if c.sshClient == nil 
        if err := c.Connect(); err != nil 
            return -1, err
        
    
    file, err := os.Open(localFile)
    if nil != err 
        return -1, fmt.Errorf("open local file failed: %w", err)
    
    defer file.Close()
​
    ftpFile, err := c.sftpClient.Create(remoteFileName)
    if nil != err 
        return -1, fmt.Errorf("Create remote path failed: %w", err)
    
    defer ftpFile.Close()
​
    fileByte, err := ioutil.ReadAll(file)
    if nil != err 
        return -1, fmt.Errorf("read local file failed: %w", err)
    
​
    ftpFile.Write(fileByte)
​
    return 0, nil

测试用例

测试用例描述如下:

  1. ssh命令执行:用ls命令显示默认目录的内容(一般是$HOME目录)。
  2. 创建一文件,填充内容,并上传到远程机器的/tmp目录。
  3. ssh命令执行:用cat显示该文件的内容。
  4. 下载文件到本地。
func TestSshSimple(t *testing.T) 
​
    username := "lijj"
    password := "ljjarm123"
​
    ip := "127.0.0.1"
    port := "22"
​
    client := NewSSHClient(username, password, ip, port)
​
    // 1.运行远程命令
    cmd := "ls"
    backinfo, err := client.Run(cmd)
    if err != nil 
        fmt.Printf("failed to run shell,err=[%v]\\n", err)
        return
    
    fmt.Printf("%v back info: \\n[%v]\\n", cmd, backinfo)
​
    // 2. 上传一文件
    filename := "foo.txt"
    WriteFile(filename, []byte("hello ssh\\r\\n"))
​
    // 上传
    n, err := client.UploadFile(filename, "/tmp/"+filename)
    if err != nil 
        fmt.Printf("upload failed: %v\\n", err)
        return
    
    // 3. 显示该文件
    cmd = "cat " + "/tmp/" + filename
    backinfo, err = client.Run(cmd)
    if err != nil 
        fmt.Printf("run cmd faild: %v\\n", err)
        return
    
    fmt.Printf("%v back info: \\n[%v]\\n", cmd, backinfo)
​
    // 4. 下载该文件到本地
    n, err = client.DownloadFile("/tmp/"+filename, "foo_new.txt")
    if err != nil 
        fmt.Printf("download failed: %v\\n", err)
        return
    
    fmt.Printf("download file[%v] ok, size=[%d]\\n", filename, n)

结果如下:

=== RUN   TestSsh
ls back info: 
[Desktop
Documents
Downloads
go
go_test
Music
project
Pictures
project
Public
Templates
tools
Videos
]
cat /tmp/foo.txt back info: 
[hello ssh
]
download file[foo.txt] ok, size=[0]
--- PASS: TestSsh (1.70s)
PASS
ok      goweb/pkg/com   1.710s

从结果看,达到预期。

其它测试

FixedHostKey的测试

参考官方示例测试FixedHostKey函数,但是失败了,提示如下:

connect server error: ssh: handshake failed: ssh: host key mismatch

经考虑暂不使用,即使调用ssh.InsecureIgnoreHostKey,也还需要账号和密码的验证,因此内部使用时,是可以接受的。

下载sftp服务器文件

在 windows 系统用 FreeSSHd 搭建sftp服务器,删除测试代码的ssh执行命令部分代码,修改远程目录文件。能正常下载、上传。

小结

本文基本达到可用目的,不过后续还可以再优化,但毕竟路要一步一步走,功能一个一个实现。

参考资料

https://lifelmy.github.io/post/2022_04_11_go_ssh_scp/

https://github.com/golang/crypto/blob/master/ssh/example_test.go

以上是关于Golang实践录:ssh及scp的实现的主要内容,如果未能解决你的问题,请参考以下文章

Golang实践录:ssh及scp的实现

Golang实践录:ssh及scp实现的优化

Golang实践录:ssh及scp实现的优化

Golang实践录:ssh及scp实现的优化

Golang实践录:一个字符串比较示例

Golang实践录:反射reflect的一些研究及代码汇总