Crypto/ssh简介

使用

下载
  1. go get "github.com/mitchellh/go-homedir"
  2. go get "golang.org/x/crypto/ssh"
使用密码认证连接

连接包含了认证,可以使用password或者sshkey 两种方式认证,下面采用密码认证方式完成连接

Example

  1. package main
  2. import (
  3. "fmt"
  4. "golang.org/x/crypto/ssh"
  5. "log"
  6. "time"
  7. )
  8. func main() {
  9. sshHost := "39.108.140.0"
  10. sshUser := "root"
  11. sshPasswrod := "youmen"
  12. sshType := "password" // password或者key
  13. //sshKeyPath := "" // ssh id_rsa.id路径
  14. sshPort := 22
  15. // 创建ssh登录配置
  16. config := &ssh.ClientConfig{
  17. Timeout: time.Second, // ssh连接time out时间一秒钟,如果ssh验证错误会在一秒钟返回
  18. User: sshUser,
  19. HostKeyCallback: ssh.InsecureIgnoreHostKey(), // 这个可以,但是不够安全
  20. //HostKeyCallback: hostKeyCallBackFunc(h.Host),
  21. }
  22. if sshType == "password" {
  23. config.Auth = []ssh.AuthMethod{ssh.Password(sshPasswrod)}
  24. } else {
  25. //config.Auth = []ssh.AuthMethod(publicKeyAuthFunc(sshKeyPath))
  26. return
  27. }
  28. // dial 获取ssh client
  29. addr := fmt.Sprintf("%s:%d",sshHost,sshPort)
  30. sshClient,err := ssh.Dial("tcp",addr,config)
  31. if err != nil {
  32. log.Fatal("创建ssh client 失败",err)
  33. }
  34. defer sshClient.Close()
  35. // 创建ssh-session
  36. session,err := sshClient.NewSession()
  37. if err != nil {
  38. log.Fatal("创建ssh session失败",err)
  39. }
  40. defer session.Close()
  41. // 执行远程命令
  42. combo,err := session.CombinedOutput("whoami; cd /; ls -al;")
  43. if err != nil {
  44. log.Fatal("远程执行cmd失败",err)
  45. }
  46. log.Println("命令输出:",string(combo))
  47. }
  48. //func publicKeyAuthFunc(kPath string) ssh.AuthMethod {
  49. // keyPath ,err := homedir.Expand(kPath)
  50. // if err != nil {
  51. // log.Fatal("find key's home dir failed",err)
  52. // }
  53. //
  54. // key,err := ioutil.ReadFile(keyPath)
  55. // if err != nil {
  56. // log.Fatal("ssh key file read failed",err)
  57. // }
  58. //
  59. // signer,err := ssh.ParsePrivateKey(key)
  60. // if err != nil {
  61. // log.Fatal("ssh key signer failed",err)
  62. // }
  63. // return ssh.PublicKeys(signer)
  64. //}

代码解读

  1. // 配置ssh.ClientConfig
  2. /*
  3. 建议TimeOut自定义一个比较端的时间
  4. 自定义HostKeyCallback如果像简便就使用ssh.InsecureIgnoreHostKey会带哦,这种方式不是很安全
  5. publicKeyAuthFunc 如果使用key登录就需要用哪个这个函数量读取id_rsa私钥, 当然也可以自定义这个访问让他支持字符串.
  6. */
  7. // ssh.Dial创建ssh客户端
  8. /*
  9. 拼接字符串得到ssh链接地址,同时不要忘记defer client.Close()
  10. */
  11. // sshClient.NewSession创建会话
  12. /*
  13. 可以自定义stdin,stdout
  14. 可以创建pty
  15. 可以SetEnv
  16. */
  17. // 执行命令CombinnedOutput run...
  18. go run main.go
  19. 2020/11/06 00:07:31 命令输出: root
  20. total 84
  21. dr-xr-xr-x. 20 root root 4096 Sep 28 09:38 .
  22. dr-xr-xr-x. 20 root root 4096 Sep 28 09:38 ..
  23. -rw-r--r-- 1 root root 0 Aug 18 2017 .autorelabel
  24. lrwxrwxrwx. 1 root root 7 Aug 18 2017 bin -> usr/bin
  25. dr-xr-xr-x. 4 root root 4096 Sep 12 2017 boot
  26. drwxrwxr-x 2 rsync rsync 4096 Jul 29 23:37 data
  27. drwxr-xr-x 19 root root 2980 Jul 28 13:29 dev
  28. drwxr-xr-x. 95 root root 12288 Nov 5 23:46 etc
  29. drwxr-xr-x. 5 root root 4096 Nov 3 16:11 home
  30. lrwxrwxrwx. 1 root root 7 Aug 18 2017 lib -> usr/lib
  31. lrwxrwxrwx. 1 root root 9 Aug 18 2017 lib64 -> usr/lib64
  32. drwx------. 2 root root 16384 Aug 18 2017 lost+found
  33. drwxr-xr-x. 2 root root 4096 Nov 5 2016 media
  34. drwxr-xr-x. 3 root root 4096 Jul 28 21:01 mnt
  35. drwxr-xr-x 4 root root 4096 Sep 28 09:38 nginx_test
  36. drwxr-xr-x. 8 root root 4096 Nov 3 16:10 opt
  37. dr-xr-xr-x 87 root root 0 Jul 28 13:26 proc
  38. dr-xr-x---. 18 root root 4096 Nov 4 00:38 root
  39. drwxr-xr-x 27 root root 860 Nov 4 21:57 run
  40. lrwxrwxrwx. 1 root root 8 Aug 18 2017 sbin -> usr/sbin
  41. drwxr-xr-x. 2 root root 4096 Nov 5 2016 srv
  42. dr-xr-xr-x 13 root root 0 Jul 28 21:26 sys
  43. drwxrwxrwt. 8 root root 4096 Nov 5 03:09 tmp
  44. drwxr-xr-x. 13 root root 4096 Aug 18 2017 usr
  45. drwxr-xr-x. 21 root root 4096 Nov 3 16:10 var

以上内容摘自

https://mojotv.cn/2019/05/22/golang-ssh-session

WebSocket简介

HTML5开始提供的一种浏览器与服务器进行双工通讯的网络技术,属于应用层协议,它基于TCP传输协议,并复用HTTP的握手通道:

对大部分web开发者来说,上面描述有点枯燥,只需要几下以下三点

  1. /*
  2. 1. WebSocket可以在浏览器里使用
  3. 2. 支持双向通信
  4. 3. 使用很简单
  5. */
优点

对比HTTP协议的话,概括的说就是: 支持双向通信,更灵活,更高效,可扩展性更好

  1. /*
  2. 1. 支持双向通信,实时性更强
  3. 2. 更好的二进制支持
  4. 3. 较少的控制开销,连接创建后,客户端和服务端进行数据交换时,协议控制的数据包头部较小,在不包含头部的情况下,
  5. 服务端到客户端的包头只有2-10字节(取决于数据包长度), 客户端到服务端的话,需要加上额外4字节的掩码,
  6. 而HTTP每次同年高新都需要携带完整的头部
  7. 4. 支持扩展,ws协议定义了扩展, 用户可以扩展协议, 或者实现自定义的子协议
  8. */

基于Web的Terminal终端控制台

完成这样一个Web Terminal的目的主要是解决几个问题:

  1. /*
  2. 1. 一定程度上取代xshell,secureRT,putty等ssh终端
  3. 2. 可以方便身份认证, 访问控制
  4. 3. 方便使用, 不受电脑环境的影响
  5. */

要实现远程登录的功能,其数据流向大概为

  1. /*
  2. 浏览器 <--> WebSocket <---> SSH <---> Linux OS
  3. */
实现流程
  1. 浏览器将主机的信息(ip, 用户名, 密码, 请求的终端大小等)进行加密, 传给后台, 并通过HTTP请求与后台协商升级协议. 协议升级完成后, 后续的数据交换则遵照web Socket的协议.
  2. 后台将HTTP请求升级为web Socket协议, 得到一个和浏览器数据交换的连接通道
  3. 后台将数据进行解密拿到主机信息, 创建一个SSH 客户端, 与远程主机的SSH 服务端协商加密, 互相认证, 然后建立一个SSH Channel
  4. 后台和远程主机有了通讯的信道, 然后后台将终端的大小等信息通过SSH Channel请求远程主机创建一个 pty(伪终端), 并请求启动当前用户的默认 shell
  5. 后台通过 Socket连接通道拿到用户输入, 再通过SSH Channel将输入传给pty, pty将这些数据交给远程主机处理后按照前面指定的终端标准输出到SSH Channel中, 同时键盘输入也会发送给SSH Channel
  6. 后台从SSH Channel中拿到按照终端大小的标准输出后又通过Socket连接将输出返回给浏览器, 由此变实现了Web Terminal



按照上面的使用流程基于代码解释如何实现

升级HTTP协议为WebSocket
  1. var upgrader = websocket.Upgrader{
  2. ReadBufferSize: 1024,
  3. WriteBufferSize: 1024,
  4. CheckOrigin: func(r *http.Request) bool {
  5. return true
  6. },
  7. }
升级协议并获得socket连接
  1. conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
  2. if err != nil {
  3. c.Error(err)
  4. return
  5. }

conn就是socket连接通道, 接下来后台和浏览器之间的通讯都将基于这个通道

后台拿到主机信息,建立ssh客户端

ssh客户端结构体

  1. type SSHClient struct {
  2. Username string `json:"username"`
  3. Password string `json:"password"`
  4. IpAddress string `json:"ipaddress"`
  5. Port int `json:"port"`
  6. Session *ssh.Session
  7. Client *ssh.Client
  8. channel ssh.Channel
  9. }
  10. //创建新的ssh客户端时, 默认用户名为root, 端口为22
  11. func NewSSHClient() SSHClient {
  12. client := SSHClient{}
  13. client.Username = "root"
  14. client.Port = 22
  15. return client
  16. }

初始化的时候我们只有主机的信息, 而Session, client, channel都是空的, 现在先生成真正的client:

  1. func (this *SSHClient) GenerateClient() error {
  2. var (
  3. auth []ssh.AuthMethod
  4. addr string
  5. clientConfig *ssh.ClientConfig
  6. client *ssh.Client
  7. config ssh.Config
  8. err error
  9. )
  10. auth = make([]ssh.AuthMethod, 0)
  11. auth = append(auth, ssh.Password(this.Password))
  12. config = ssh.Config{
  13. Ciphers: []string{"aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "arcfour256", "arcfour128", "aes128-cbc", "3des-cbc", "aes192-cbc", "aes256-cbc"},
  14. }
  15. clientConfig = &ssh.ClientConfig{
  16. User: this.Username,
  17. Auth: auth,
  18. Timeout: 5 * time.Second,
  19. Config: config,
  20. HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
  21. return nil
  22. },
  23. }
  24. addr = fmt.Sprintf("%s:%d", this.IpAddress, this.Port)
  25. if client, err = ssh.Dial("tcp", addr, clientConfig); err != nil {
  26. return err
  27. }
  28. this.Client = client
  29. return nil
  30. }

ssh.Dial(“tcp”, addr, clientConfig)创建连接并返回客户端, 如果主机信息不对或其它问题这里将直接失败

通过ssh客户端创建ssh channel,并请求一个pty伪终端,请求用户的默认会话

如果主机信息验证通过, 可以通过ssh client创建一个通道:

  1. channel, inRequests, err := this.Client.OpenChannel("session", nil)
  2. if err != nil {
  3. log.Println(err)
  4. return nil
  5. }
  6. this.channel = channel

ssh通道创建完成后, 请求一个标准输出的终端, 并开启用户的默认shell:

  1. ok, err := channel.SendRequest("pty-req", true, ssh.Marshal(&req))
  2. if !ok || err != nil {
  3. log.Println(err)
  4. return nil
  5. }
  6. ok, err = channel.SendRequest("shell", true, nil)
  7. if !ok || err != nil {
  8. log.Println(err)
  9. return nil
  10. }
远程主机与浏览器实时数据交换

现在为止建立了两个通道, 一个是websocket, 一个是ssh channel, 后台将起两个主要的协程, 一个不停的从websocket通道里读取用户的输入, 并通过ssh channel传给远程主机:

  1. //这里第一个协程获取用户的输入
  2. go func() {
  3. for {
  4. // p为用户输入
  5. _, p, err := ws.ReadMessage()
  6. if err != nil {
  7. return
  8. }
  9. _, err = this.channel.Write(p)
  10. if err != nil {
  11. return
  12. }
  13. }
  14. }()

第二个主协程将远程主机的数据传递给浏览器, 在这个协程里还将起一个协程, 不断获取ssh channel里的数据并传给后台内部创建的一个通道, 主协程则有一个死循环, 每隔一段时间从内部通道里读取数据, 并将其通过websocket传给浏览器, 所以数据传输并不是真正实时的,而是有一个间隔在, 我写的默认为100微秒, 这样基本感受不到延迟, 而且减少了消耗, 有时浏览器输入一个命令获取大量数据时, 会感觉数据出现会一顿一顿的便是因为设置了一个间隔:

  1. //第二个协程将远程主机的返回结果返回给用户
  2. go func() {
  3. br := bufio.NewReader(this.channel)
  4. buf := []byte{}
  5. t := time.NewTimer(time.Microsecond * 100)
  6. defer t.Stop()
  7. // 构建一个信道, 一端将数据远程主机的数据写入, 一段读取数据写入ws
  8. r := make(chan rune)
  9. // 另起一个协程, 一个死循环不断的读取ssh channel的数据, 并传给r信道直到连接断开
  10. go func() {
  11. defer this.Client.Close()
  12. defer this.Session.Close()
  13. for {
  14. x, size, err := br.ReadRune()
  15. if err != nil {
  16. log.Println(err)
  17. ws.WriteMessage(1, []byte("\033[31m已经关闭连接!\033[0m"))
  18. ws.Close()
  19. return
  20. }
  21. if size > 0 {
  22. r <- x
  23. }
  24. }
  25. }()
  26. // 主循环
  27. for {
  28. select {
  29. // 每隔100微秒, 只要buf的长度不为0就将数据写入ws, 并重置时间和buf
  30. case <-t.C:
  31. if len(buf) != 0 {
  32. err := ws.WriteMessage(websocket.TextMessage, buf)
  33. buf = []byte{}
  34. if err != nil {
  35. log.Println(err)
  36. return
  37. }
  38. }
  39. t.Reset(time.Microsecond * 100)
  40. // 前面已经将ssh channel里读取的数据写入创建的通道r, 这里读取数据, 不断增加buf的长度, 在设定的 100 microsecond后由上面判定长度是否返送数据
  41. case d := <-r:
  42. if d != utf8.RuneError {
  43. p := make([]byte, utf8.RuneLen(d))
  44. utf8.EncodeRune(p, d)
  45. buf = append(buf, p...)
  46. } else {
  47. buf = append(buf, []byte("@")...)
  48. }
  49. }
  50. }
  51. }()

web terminal的后台建好了

前端

前端我选择用了vue框架(其实这么小的项目完全不用vue), 终端工具用的是xterm, vscode内置的终端也是采用的xterm.这里贴一段关键代码, 前端项目地址

  1. mounted () {
  2. var containerWidth = window.screen.height;
  3. var containerHeight = window.screen.width;
  4. var cols = Math.floor((containerWidth - 30) / 9);
  5. var rows = Math.floor(window.innerHeight/17) - 2;
  6. if (this.username === undefined){
  7. var url = (location.protocol === "http:" ? "ws" : "wss") + "://" + location.hostname + ":5001" + "/ws"+ "?" + "msg=" + this.msg + "&rows=" + rows + "&cols=" + cols;
  8. }else{
  9. var url = (location.protocol === "http:" ? "ws" : "wss") + "://" + location.hostname + ":5001" + "/ws"+ "?" + "msg=" + this.msg + "&rows=" + rows + "&cols=" + cols + "&username=" + this.username + "&password=" + this.password;
  10. }
  11. let terminalContainer = document.getElementById('terminal')
  12. this.term = new Terminal()
  13. this.term.open(terminalContainer)
  14. // open websocket
  15. this.terminalSocket = new WebSocket(url)
  16. this.terminalSocket.onopen = this.runRealTerminal
  17. this.terminalSocket.onclose = this.closeRealTerminal
  18. this.terminalSocket.onerror = this.errorRealTerminal
  19. this.term.attach(this.terminalSocket)
  20. this.term._initialized = true
  21. console.log('mounted is going on')
  22. }

后端项目地址

01 . Go语言的SSH远程终端及WebSocket的更多相关文章

  1. 01 C语言程序设计--01 C语言基础--第1章 C语言概述&第2章 GCC和GDB

    走进嵌入式开发的世界,企业级项目课程让你达到企业嵌入式应用开发要求.名师在线答疑,解决疑难.科学评测体系,系统评估学习.核心项目实........ 30 门课程 241小时12分钟 824 人学习 学 ...

  2. 一个优秀的SSH远程终端工具

    SSH远程终端工具是一款在Windows界面下用来访问远端不同系统下的服务器,从而比较好的达到远程控制终端的目的.向我们操控集群的时候,如果每台机器都安装一个显示器和键盘也是一个不小的花费,而远程终端 ...

  3. 01 C语言程序设计--01 C语言基础--第3章 基本数据类型01

    01.1.3.1序言 00:02:17 01.1.3.2 C语言中的基本元素和常量的概念 00:08:54 01.1.3.3示例--常量 00:12:08 01.1.3.4变量的概念和命名规则 00: ...

  4. 01.C语言关于结构体的学习笔记

    我对于学习的C语言的结构体做一个小的学习总结,总结如下: 结构体:structure 结构体是一种用户自己建立的数据类型,由不同类型数据组成的组合型的数据结构.在其他高级语言中称为记录(record) ...

  5. 从头开始-01.C语言环境测试

    在Mac下编写C程序需要以下几步: 编写代码 a>编译:把C语言编译成0和1 b>工具:clang编译器 c>指令:cc -c 文件名.c      编译成功会生成一个. o目标文件 ...

  6. [01] Java语言的基本认识

    0.写在前面的话 我们都知道在计算机的底层,它是识别二进制的,也就是说,计算机只能认识0和1.这主要是因为电路的逻辑只有两种状态,所以只需要0和1两个数字就可以表示低电平和高电平.而计算机是由数不清的 ...

  7. 网络游戏开发-服务器(01)Asp.Net Core中的websocket,并封装一个简单的中间件

    先拉开MSDN的文档,大致读一遍 (https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/websockets) WebSocket 是一 ...

  8. C语言入门:01.C语言概述

    一.计算机和软件常识 1.计算机运行原理 (1)硬件基本组成:硬盘.内存.CPU (2)个部件之间的运作协调(下图)

  9. 01. Go 语言简介

    Go语言简介 引用原文地址:http://m.biancheng.net/golang/ Go语言也称 Golang,兼具效率.性能.安全.健壮等特性.这套Go语言教程(Golang教程)通俗易懂,深 ...

随机推荐

  1. Leetcode-剪枝

    51. N皇后 https://leetcode-cn.com/problems/n-queens/ n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击. ...

  2. 玩命学JVM(二)—类加载机制

    前言 Java程序运行图: 上一篇玩命学JVM(一)-认识JVM和字节码文件我们简单认识了 JVM 和字节码文件.那JVM是如何使用字节码文件的呢?从上图清晰地可以看到,JVM 通过类加载器完成了这一 ...

  3. Spring系列之事务的控制 注解实现+xml实现+事务的隔离等级

    Spring系列之事务的控制 注解实现+xml实现 在前面我写过一篇关于事务的文章,大家可以先去看看那一篇再看这一篇,学习起来会更加得心应手 链接:https://blog.csdn.net/pjh8 ...

  4. C 多态 RT-Thread

    // RT-Thread对象模型采用结构封装中使用指针的形式达到面向对象中多态的效果,例如: // 抽象父类 #include <stdio.h> #include <assert. ...

  5. 使用Maven那么久了,你对企业级Maven的核心配置了解多少?

    写在前面 相信从事Java工作的小伙伴们多多少少都会接触到Maven.使用Maven来搭建项目,能够极大的方便我们构建项目的依赖关系,对于项目中需要依赖的Jar包,也只是简单的在pom.xml中进行配 ...

  6. The comparison between object and constructor

    1.相似的地方 1.举个栗子:public struct Student{    string name;    int age;}public class bike{    int weight;  ...

  7. JAVA学习线路:day01面向对象(继承、抽象类)

    所有的文档和源代码都开源在GitHub: https://github.com/kun213/DailyCode上了.希望我们可以一起加油,一起学习,一起交流. day01面向对象[继承.抽象类] 今 ...

  8. c++ 通用单例类声明

    //单例类定义#define CLASS_INSTANCE_DEF(className) \public: \ static className* GetInstance() \ { \ static ...

  9. suse使用镜像源

    创建挂载目录,随便建个目录 mkdir /mnt/cdrom 光盘连上,挂载光盘 mount -t iso9660 /dev/sr0 /mnt/cdrom/#确保挂上了ls看看/mnt/cdrom是否 ...

  10. git学习(十一) idea git pull 解决冲突

    测试如下: 先将远程的代码修改,之后更新: 之后将工作区修改的代码(这里修改的代码跟远程修改的位置一样)提交到本地,之后拉取远程的代码,会发现有冲突: Accept Yours 就是直接选取本地的代码 ...