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
测试用例
测试用例描述如下:
- ssh命令执行:用
ls
命令显示默认目录的内容(一般是$HOME目录)。 - 创建一文件,填充内容,并上传到远程机器的
/tmp
目录。 - ssh命令执行:用
cat
显示该文件的内容。 - 下载文件到本地。
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的实现的主要内容,如果未能解决你的问题,请参考以下文章