Golang实现SSH、SFTP等相关操作

1.1 前言

在日常的一些开发场景中,我们需要去和远程服务器进行一些通信,执行一些相关命令操作,这个时候我们就可以使用SSH协议实现目标。SSH协议是建立在应用层上的安全协议,全称为Secure Shell,采用的是面向连接的TCP协议进行传输,也就意味着它是安全可靠的。需要注意的是文件传输并不能在SSH协议上完成,需要在下面提到的SFTP协议完成。

1.2 Go实现

Go官方为我们提供了用于实现SSH连接的package,位于golang.org/x/crypto下,通过在程序中调用包中提供的相关方法,便可以实现与其他机器进行通信。使用前我们需要使用go get导入相关的依赖包。

go get golang.org/x/crypto/ssh

1.2.1 配置相关参数

在进行通信之前,我们还需要配置一些用于配置一些用于建立连接的相关参数。ssh包下的ClientConfig结构体中,定义了建立SSH连接需要用到的一些配置项,部分项提供了默认参数,我们使用时可以不进行声明。

下面的代码段中,我们首先是声明了用户名和密码,连接超时时间设置为10秒钟,addr变量定义了目标机器的IP地址以及端口。

HostKeyCallback项,我们设置了忽略,这是因为SSH协议为客户端提供了两种安全验证方式,一种是基于口令的安全验证,也就是我们常常使用的账号密码形式,另外一种则是基于密钥的安全验证,相较于第一种,这种形式的校验方法极大的提升了安全等级,缺点则是时间损耗相对较长。

如果需要使用这种方式进行校验,首先我们需要在服务器上为自己创建一对密钥,作为客户端进行访问时,首先会向服务端发送安全验证请求,服务端收到请求后,首先会将机器上保存的公钥与客户端发送的公钥进行比较,如果一致,服务端则会向客户端响应加密质询,客户端接受到质询之后,使用私钥进行解密,然后再将解密结果发送给服务端,服务端进行校验后再返回响应结果,到这里就算是完成了一段密钥校验。

        //添加配置
        config := &ssh.ClientConfig{
                User: "root",
                Auth: []ssh.AuthMethod{ssh.Password("Password")},
                HostKeyCallback: ssh.InsecureIgnoreHostKey(),
                Timeout: 10 * time.Second,
            }
        }
        addr := fmt.Sprintf("%v:%v", IP, Port)

1.2.2 建立连接

在完成了所有的参数初始化之后,我们便可以调用Dial方法建立SSH连接。Dial方法一共有三个参数和两个返回值,第一个参数network为网络类型,这里我们使用面向连接的TCP协议,第二个参数addr则为目标机器的IP地址和端口号,第三个参数config则为前面我们生命的配置项。Dial会返回一个SSH连接和错误类型。

func Dial(network, addr string, config *ClientConfig) (*Client, error)

        //建立SSH连接
        sshClient, err := ssh.Dial("tcp", addr, config)
        if err != nil {
            log.Fatal("unable to create ssh conn")
        }

1.2.3 创建会话

在建立了与目标机器的SSH连接之后,我们就可以通过创建SSH会话来与目标机器进行通信。通过NewSession()方法便可以实现这一操作。

        //建立SSH会话
        sshSession, err := sshClient.NewSession()
        if err != nil {
           log.Fatal("unable to create ssh session")
        }

1.2.4 执行操作

与目标机器建立会话后,我们就可以通过执行命令等来操作远程服务器。Go目前为我们提供了五个用于操作远程机器的方法,分别是Run(), Start(), Output(), CombineOutpt(), Shell()

🦫其中 Output(), **CombineOutpt()**这两个方法是对Run()方法进行不同程度上的封装,校验了输出流,错误流等相关内容。

        // Output runs cmd on the remote host and returns its standard output.
        func (s *Session) Output(cmd string) ([]byte, error) {
           if s.Stdout != nil {
              return nil, errors.New("ssh: Stdout already set")
           }
           var b bytes.Buffer
           s.Stdout = &b
           err := s.Run(cmd)
           return b.Bytes(), err
        }
        
        
        // CombinedOutput runs cmd on the remote host and returns its combined
        // standard output and standard error.
        func (s *Session) CombinedOutput(cmd string) ([]byte, error) {
           if s.Stdout != nil {
              return nil, errors.New("ssh: Stdout already set")
           }
           if s.Stderr != nil {
              return nil, errors.New("ssh: Stderr already set")
           }
           var b singleWriter
           s.Stdout = &b
           s.Stderr = &b
           err := s.Run(cmd)
           return b.b.Bytes(), err
        }

Run()方法则是对Start()方法进行了封装,添加了Wait方法,用于校验远程服务器的退出指令。Wait()方法中有一个管道类型的变量exitStatus,它是用来保存每次执行命令后,机器返回的退出状态的。有兴趣的朋友可以去看看这块的代码,这里就不贴代码了。

这里面有一个坑,如果我们在远程机器上去运行一个永远不会停止的程序,这个时候我们的程序一直等待不到远程机器发送的退出指令,就会造成程序一直阻塞而无法正常返回。解决的办法是用一个协程去单独执行这一块的任务,或者是使用定时器来定时结束session会话,来正常返回。

Start()方法与Shell方法一致,都是返回一个error类型,在底层都是调用了start()方法和SendRequest方法,关于这两个方法的内容这里就不做详细介绍了,有兴趣的朋友可以自行去阅读。唯一的区别是Start()方法有一个string类型的参数,用于接收用户输入的参数,而Shell()方法是无参数的。

使用Shell()方法配合RequestPty()等方法可以在本地建立一个伪终端,可以直接通过输入命令的形式操作目标机器。下面都会做一个示例。

        //Run
        func (s *Session) Run(cmd string) error {
           err := s.Start(cmd)
           if err != nil {
              fmt.Println(err)
              return err
           }
           return s.Wait()
                }
                
                
        // Start runs cmd on the remote host. Typically, the remote
        // server passes cmd to the shell for interpretation.
        // A Session only accepts one call to Run, Start or Shell.
        func (s *Session) Start(cmd string) error {
           if s.started {
              return errors.New("ssh: session already started")
           }
           req := execMsg{
              Command: cmd,
           }
        
           ok, err := s.ch.SendRequest("exec", true, Marshal(&req))
           if err == nil && !ok {
              err = fmt.Errorf("ssh: command %v failed", cmd)
           }
           if err != nil {
              return err
           }
           return s.start()
        }

1.2.5 示例代码(执行命令)

这里我们使用Run()方法来演示一下如果去执行命令,其他方法类型就不做演示了。这里我们使用一个标准输出流、错误流来保存执行结果。

这里演示了一个简单的执行过程,使用了cd命令到/home/min目录下,在给helloworld程序添加可执行权限,最后运行程序。

        var stdoutBuf, stderrBuf bytes.Buffer
        session.Stdout = &stdoutBuf
        session.Stderr = &stderrBuf
    
        // cd /home/min
        // chmod +x helloworld
        // ./helloworld
        cmd := fmt.Sprintf("cd %v ; chmod +x %v ; %v &", "/home/min", "helloworld", ./helloworld)
        err := session.Run(cmd)
        if err != nil {
            log.Fatal("[ERROR]: ", session.Stderr, err)
        }

1.2.6(创建伪终端)

        // 设置Terminal Mode
	modes := ssh.TerminalModes{
		ssh.ECHO:          0,     // 关闭回显
		ssh.TTY_OP_ISPEED: 14400, // 设置传输速率
		ssh.TTY_OP_OSPEED: 14400,
	}
    
        // 请求伪终端
	err = session.RequestPty("linux", 32, 160, modes)
	if err != nil {
		log.Println(err)
		return
	}
    
        // 设置输入输出
	session.Stdout = os.Stdout
	session.Stdin = os.Stdin
	session.Stderr = os.Stderr
 
	session.Shell() // 启动shell
	session.Wait()  // 等待退出

1.2.7 完整代码


//机器平台信息
type Machine struct {
   IP       string
   Port     string
   Username string
   Password string
}

//建立SSH连接
func CreateSSHConn(m *model.Machine) error {
   //初始化连接信息
   config := &ssh.ClientConfig{
      User:            m.Username,
      Auth:            []ssh.AuthMethod{ssh.Password(m.Password)},
      HostKeyCallback: ssh.InsecureIgnoreHostKey(),
      Timeout:         10 * time.Second,
   }
   addr := fmt.Sprintf("%v:%v", m.IP, m.Port)

   //建立ssh连接
   sshClient, err := ssh.Dial("tcp", addr, config)
   if err != nil {
      fmt.Println("unable create ssh conn", err)
      return err
   }
   defer sshClient.Close()
   
   //建立ssh会话
   session, err := sshClient.NewSession()
   if err != nil {
      fmt.Println("unable create ssh conn", err)
      return err
   }
   defer session.Close()
   
   
   //执行命令
   var stdoutBuf, stderrBuf bytes.Buffer
   session.Stdout = &stdoutBuf
   session.Stderr = &stderrBuf
   
   cmd := fmt.Sprintf("cd %v ; chmod +x %v ; %v &", "/home/min", "helloworld", ./helloworld)
   if err := session.Run(cmd); err != nil {
       log.Fatal("[ERROR]: ", session.Stderr, err)
   }
  
   //创建伪终端
   // 设置Terminal Mode
   modes := ssh.TerminalModes{
           ssh.ECHO:          0,     // 关闭回显
           ssh.TTY_OP_ISPEED: 14400, // 设置传输速率
           ssh.TTY_OP_OSPEED: 14400,
   }
      
   // 请求伪终端
   err = session.RequestPty("linux", 32, 160, modes)
   if err != nil {
           log.Fatal(err)
   }
      
   // 设置输入输出
   session.Stdout = os.Stdout
   session.Stdin = os.Stdin
   session.Stderr = os.Stderr
   
   session.Shell() // 启动shell
   session.Wait()  // 等待退出
   
   return err
}
  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
实现 SSH 协议支持 RZ 命令需要使用 Go 语言中的 `golang.org/x/crypto/ssh` 包来建立 SSH 连接,同时还需要使用一个终端模拟器,如 `github.com/creack/termios` 包来模拟终端。在终端模拟器中,可以通过启动一个新的子进程来运行 rz 命令,等待用户选择要发送的文件。 关于唤醒终端模拟器的文件选择器,可以使用 Go 语言中的 `os/exec` 包来执行外部命令。具体实现步骤如下: 1. 从 SSH 服务器获取一个可用的终端,使用 `golang.org/x/crypto/ssh` 包中的 `NewSession` 函数创建一个终端会话。 2. 使用 `session.Stdin`、`session.Stdout` 和 `session.Stderr` 属性来与终端会话进行交互。 3. 使用 `github.com/creack/termios` 包中的 `MakeRaw` 函数将终端设置为原始模式,以便能够捕获所有输入和输出。 4. 启动一个新的子进程来运行 rz 命令,使用 `os/exec` 包中的 `Command` 函数创建一个新的命令。 5. 使用 `Start` 方法启动新的命令,并使用 `Wait` 方法等待命令完成。 6. 在 rz 命令结束后,使用 `github.com/creack/termios` 包中的 `Restore` 函数将终端设置回原始模式,并关闭终端会话。 下面是一个简单的示例代码,用于实现 SSH 协议支持 RZ 命令并唤醒终端模拟器文件选择器: ```go package main import ( "fmt" "os" "os/exec" "github.com/creack/termios" "golang.org/x/crypto/ssh" ) func main() { // 连接 SSH 服务器 config := &ssh.ClientConfig{ User: "username", Auth: []ssh.AuthMethod{ ssh.Password("password"), }, } client, err := ssh.Dial("tcp", "ssh.example.com:22", config) if err != nil { panic(err) } defer client.Close() // 打开一个新的终端会话 session, err := client.NewSession() if err != nil { panic(err) } defer session.Close() // 设置终端会话为原始模式 oldState, err := termios.MakeRaw(0) if err != nil { panic(err) } defer termios.Restore(0, oldState) // 启动 rz 命令 cmd := exec.Command("rz") cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { panic(err) } // 等待 rz 命令完成 if err := cmd.Wait(); err != nil { fmt.Println("Command finished with error:", err) } else { fmt.Println("Command finished successfully") } } ``` 在这个示例中,我们使用 `ssh.Dial` 函数连接到 SSH 服务器,并使用 `ssh.ClientConfig` 结构体来设置身份验证信息。然后,我们使用 `client.NewSession` 函数创建一个新的终端会话,并使用 `termios.MakeRaw` 函数将终端设置为原始模式。接下来,我们使用 `os/exec` 包中的 `Command` 函数创建一个新的命令,将其标准输入、标准输出和标准错误输出与当前进程关联,并使用 `cmd.Start` 方法启动新的命令。最后,我们使用 `cmd.Wait` 方法等待命令完成,并在命令结束后使用 `termios.Restore` 函数将终端设置回原始模式。 需要注意的是,如果您想要唤醒终端模拟器的文件选择器,需要在启动 rz 命令之前执行必要的操作。例如,您可以使用 `os/exec` 包中的 `Command` 函数来执行 `xdg-open` 命令(在基于 Linux 的系统上)或 `open` 命令(在基于 macOS 的系统上)来打开文件选择器。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值