golang实践录:ssh及scp的实现(代码片段)

李迟 李迟     2022-11-29     193

关键词:

本文介绍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的scp实现和使用。问题提出工作中经常要查询日志,一般情况下需使用堡垒机登陆到远程机器,确认日志位置、文件名称,再用winscp软件下载,这过程比较繁琐,为节省时间,考虑用golang实现... 查看详情

golang实践录:ssh及scp实现的优化(代码片段)

本文对上文的实现的优化。问题提出上一文章中,基本上已经达到使用了,但为了适应更多场合,需要对上传、下载功能进行优化,本文实现对目录的传输。设计思路主要框架和上文相同,不再赘述。对于目... 查看详情

golang实践录:获取系统信息(代码片段)

...库也要往这方向考虑,比如线程库和socket库。当接触golang后,因 查看详情

golang实践录:使用gin框架实现转发功能:一些负载均衡算法的实现(代码片段)

近段时间需要实现一个转发post请求到指定后端服务的小工具,由于一直想学习gin框架,所以就使用这个框架进行尝试,预计会产生几篇文章。本文研究一些负载均衡算法的实现。概述本文实现的负载均衡纯粹是为了... 查看详情

golang实践录:使用gin实现httpbasic认证(代码片段)

本文介绍使用Golang语言实现httpbasic认证,并给出测试。起因接前文,按要求,接入内网环境的服务应用不能提供无任何验证方式的post接口,线上系统有相关的模块,但过于复杂。经讨论确定,使用httpbasic... 查看详情

golang实践录:使用gin框架实现转发功能:利用nginx转发(代码片段)

近段时间需要实现一个转发post请求到指定后端服务的小工具,由于一直想学习gin框架,所以就使用这个框架进行尝试,预计会产生几篇文章。本文研究如何利用nginx容器和后端服务进行转发工具的测试。概述转发的工... 查看详情

golang实践录:使用gin框架实现文件上传转发功能(代码片段)

近段时间需要实现一个转发post请求到指定后端服务的小工具,由于一直想学习gin框架,所以就使用这个框架进行尝试,预计会产生几篇文章。本文先研究如何在gin框架中实现上传和转发功能。问题提出一后台web服务&#... 查看详情

golang实践录:map的几个使用示例(代码片段)

本文针对Golang的Map实现几个简单示例。这些都是在实际工程中使用到的。基本使用map是一种无序的基于key-value的数据结构,Golang的map是引用类型,因此必须初始化才能使用。下面给出几种初始化形式示例:varmmap[string]... 查看详情

golang实践录:map的几个使用示例(代码片段)

本文针对Golang的Map实现几个简单示例。这些都是在实际工程中使用到的。基本使用map是一种无序的基于key-value的数据结构,Golang的map是引用类型,因此必须初始化才能使用。下面给出几种初始化形式示例:varmmap[string]... 查看详情

golang实践录:使用gin框架实现转发功能:上传文件并转(代码片段)

近段时间需要实现一个转发post请求到指定后端服务的小工具,由于一直想学习gin框架,所以就使用这个框架进行尝试,预计会产生几篇文章。本文先研究如何在gin框架中实现上传和转发功能。问题提出一后台web服务&#... 查看详情

golang实践录:使用gin框架实现转发功能:管理后端服务(代码片段)

近段时间需要实现一个转发post请求到指定后端服务的小工具,由于一直想学习gin框架,所以就使用这个框架进行尝试,预计会产生几篇文章。本文研究如何管理后端服务。思路在启动gin服务前,先启动所有的后端... 查看详情

golang实践录:获取目录文件列表(代码片段)

获取目录下匹配某种规则的文件,返回文件列表,在开发中比较常用。本文实现此功能,并做了些扩展。起因笔者开发的内部工具,需要查找各式文件,比如:数据文件,以csv结尾;信息文件,... 查看详情

golang实践录:获取目录文件列表(代码片段)

获取目录下匹配某种规则的文件,返回文件列表,在开发中比较常用。本文实现此功能,并做了些扩展。起因笔者开发的内部工具,需要查找各式文件,比如:数据文件,以csv结尾;信息文件,... 查看详情

golang实践录:使用gin框架实现转发功能:利用nginx转发(代码片段)

近段时间需要实现一个转发post请求到指定后端服务的小工具,由于一直想学习gin框架,所以就使用这个框架进行尝试,预计会产生几篇文章。本文研究如何利用nginx容器和后端服务进行转发工具的测试。概述转发的工... 查看详情

golang实践录:查询数据表的几种方式(代码片段)

...涉及的数据表是生产环境的,所以不敢越雷池半步。golang的sq 查看详情

golang实践录:使用gin框架实现转发功能:一些负载均衡算法的实现(代码片段)

近段时间需要实现一个转发post请求到指定后端服务的小工具,由于一直想学习gin框架,所以就使用这个框架进行尝试,预计会产生几篇文章。本文研究一些负载均衡算法的实现。概述本文实现的负载均衡纯粹是为了... 查看详情

golang实践录:使用gin实现cas单点登录(代码片段)

本文介绍使用Golang语言实现cas单点登录。起因新年伊始,上班第一天收到消息。原来那台用于部署内部网页工具的服务器因安全问题被停止使用,需更新服务器部署,但从中带出一个问题,那个应用服务程序必须... 查看详情