Crypto/ssh简介

使用

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

Example

package main

import (
	"fmt"
	"golang.org/x/crypto/ssh"
	"log"
	"time"
)

func main()  {
	sshHost := "39.108.140.0"
	sshUser := "root"
	sshPasswrod := "youmen"
	sshType := "password"  // password或者key
	//sshKeyPath := "" // ssh id_rsa.id路径
	sshPort := 22

	// 创建ssh登录配置
	config := &ssh.ClientConfig{
		Timeout: time.Second, // ssh连接time out时间一秒钟,如果ssh验证错误会在一秒钟返回
		User: sshUser,
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),  // 这个可以,但是不够安全
		//HostKeyCallback: hostKeyCallBackFunc(h.Host),
	}
	if sshType == "password" {
		config.Auth = []ssh.AuthMethod{ssh.Password(sshPasswrod)}
	} else {
		//config.Auth = []ssh.AuthMethod(publicKeyAuthFunc(sshKeyPath))
		return
	}

	// dial 获取ssh client
	addr := fmt.Sprintf("%s:%d",sshHost,sshPort)
	sshClient,err := ssh.Dial("tcp",addr,config)
	if err != nil {
		log.Fatal("创建ssh client 失败",err)
	}
	defer sshClient.Close()

	// 创建ssh-session
	session,err := sshClient.NewSession()
	if err != nil {
		log.Fatal("创建ssh session失败",err)
	}

	defer session.Close()

	// 执行远程命令
	combo,err := session.CombinedOutput("whoami; cd /; ls -al;")
	if err != nil {
		log.Fatal("远程执行cmd失败",err)
	}
	log.Println("命令输出:",string(combo))
}

//func publicKeyAuthFunc(kPath string) ssh.AuthMethod  {
//	keyPath ,err := homedir.Expand(kPath)
//	if err != nil {
//		log.Fatal("find key's home dir failed",err)
//	}
//
//	key,err := ioutil.ReadFile(keyPath)
//	if err != nil {
//		log.Fatal("ssh key file read failed",err)
//	}
//
//	signer,err := ssh.ParsePrivateKey(key)
//	if err != nil {
//		log.Fatal("ssh key signer failed",err)
//	}
//	return ssh.PublicKeys(signer)
//}

代码解读

// 配置ssh.ClientConfig
/*
		建议TimeOut自定义一个比较端的时间
		自定义HostKeyCallback如果像简便就使用ssh.InsecureIgnoreHostKey会带哦,这种方式不是很安全
		publicKeyAuthFunc 如果使用key登录就需要用哪个这个函数量读取id_rsa私钥, 当然也可以自定义这个访问让他支持字符串.
*/

// ssh.Dial创建ssh客户端
/*
		拼接字符串得到ssh链接地址,同时不要忘记defer client.Close()
*/

// sshClient.NewSession创建会话
/*
		可以自定义stdin,stdout
		可以创建pty
		可以SetEnv
*/

// 执行命令CombinnedOutput run...
go run main.go
2020/11/06 00:07:31 命令输出: root
total 84
dr-xr-xr-x. 20 root  root   4096 Sep 28 09:38 .
dr-xr-xr-x. 20 root  root   4096 Sep 28 09:38 ..
-rw-r--r--   1 root  root      0 Aug 18  2017 .autorelabel
lrwxrwxrwx.  1 root  root      7 Aug 18  2017 bin -> usr/bin
dr-xr-xr-x.  4 root  root   4096 Sep 12  2017 boot
drwxrwxr-x   2 rsync rsync  4096 Jul 29 23:37 data
drwxr-xr-x  19 root  root   2980 Jul 28 13:29 dev
drwxr-xr-x. 95 root  root  12288 Nov  5 23:46 etc
drwxr-xr-x.  5 root  root   4096 Nov  3 16:11 home
lrwxrwxrwx.  1 root  root      7 Aug 18  2017 lib -> usr/lib
lrwxrwxrwx.  1 root  root      9 Aug 18  2017 lib64 -> usr/lib64
drwx------.  2 root  root  16384 Aug 18  2017 lost+found
drwxr-xr-x.  2 root  root   4096 Nov  5  2016 media
drwxr-xr-x.  3 root  root   4096 Jul 28 21:01 mnt
drwxr-xr-x   4 root  root   4096 Sep 28 09:38 nginx_test
drwxr-xr-x.  8 root  root   4096 Nov  3 16:10 opt
dr-xr-xr-x  87 root  root      0 Jul 28 13:26 proc
dr-xr-x---. 18 root  root   4096 Nov  4 00:38 root
drwxr-xr-x  27 root  root    860 Nov  4 21:57 run
lrwxrwxrwx.  1 root  root      8 Aug 18  2017 sbin -> usr/sbin
drwxr-xr-x.  2 root  root   4096 Nov  5  2016 srv
dr-xr-xr-x  13 root  root      0 Jul 28 21:26 sys
drwxrwxrwt.  8 root  root   4096 Nov  5 03:09 tmp
drwxr-xr-x. 13 root  root   4096 Aug 18  2017 usr
drwxr-xr-x. 21 root  root   4096 Nov  3 16:10 var

以上内容摘自

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

WebSocket简介

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

基于Web的Terminal终端控制台

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

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

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

/*
		浏览器 <-->  WebSocket  <---> SSH <---> Linux OS
*/
实现流程

01 . Go语言实现SSH远程终端及WebSocket-LMLPHP
01 . Go语言实现SSH远程终端及WebSocket-LMLPHP

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

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

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

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

ssh客户端结构体

type SSHClient struct {
	Username  string `json:"username"`
	Password  string `json:"password"`
	IpAddress string `json:"ipaddress"`
	Port      int    `json:"port"`
	Session   *ssh.Session
	Client    *ssh.Client
	channel   ssh.Channel
}

//创建新的ssh客户端时, 默认用户名为root, 端口为22
func NewSSHClient() SSHClient {
	client := SSHClient{}
	client.Username = "root"
	client.Port = 22
	return client
}

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

func (this *SSHClient) GenerateClient() error {
	var (
		auth         []ssh.AuthMethod
		addr         string
		clientConfig *ssh.ClientConfig
		client       *ssh.Client
		config       ssh.Config
		err          error
	)
	auth = make([]ssh.AuthMethod, 0)
	auth = append(auth, ssh.Password(this.Password))
	config = ssh.Config{
		Ciphers: []string{"aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "arcfour256", "arcfour128", "aes128-cbc", "3des-cbc", "aes192-cbc", "aes256-cbc"},
	}
	clientConfig = &ssh.ClientConfig{
		User:    this.Username,
		Auth:    auth,
		Timeout: 5 * time.Second,
		Config:  config,
		HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
			return nil
		},
	}
	addr = fmt.Sprintf("%s:%d", this.IpAddress, this.Port)
	if client, err = ssh.Dial("tcp", addr, clientConfig); err != nil {
		return err
	}
	this.Client = client
	return nil
}

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

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

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

channel, inRequests, err := this.Client.OpenChannel("session", nil)
if err != nil {
    log.Println(err)
    return nil
}
this.channel = channel

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

ok, err := channel.SendRequest("pty-req", true, ssh.Marshal(&req))
if !ok || err != nil {
    log.Println(err)
    return nil
}
ok, err = channel.SendRequest("shell", true, nil)
if !ok || err != nil {
    log.Println(err)
    return nil
}
远程主机与浏览器实时数据交换
//这里第一个协程获取用户的输入
go func() {
    for {
        // p为用户输入
        _, p, err := ws.ReadMessage()
        if err != nil {
            return
        }
        _, err = this.channel.Write(p)
        if err != nil {
            return
        }
    }
}()
//第二个协程将远程主机的返回结果返回给用户
go func() {
    br := bufio.NewReader(this.channel)
    buf := []byte{}
    t := time.NewTimer(time.Microsecond * 100)
    defer t.Stop()
    // 构建一个信道, 一端将数据远程主机的数据写入, 一段读取数据写入ws
    r := make(chan rune)

    // 另起一个协程, 一个死循环不断的读取ssh channel的数据, 并传给r信道直到连接断开
    go func() {
        defer this.Client.Close()
        defer this.Session.Close()

        for {
            x, size, err := br.ReadRune()
            if err != nil {
                log.Println(err)
                ws.WriteMessage(1, []byte("\033[31m已经关闭连接!\033[0m"))
                ws.Close()
                return
            }
            if size > 0 {
                r <- x
            }
        }
    }()

    // 主循环
    for {
        select {
        // 每隔100微秒, 只要buf的长度不为0就将数据写入ws, 并重置时间和buf
        case <-t.C:
            if len(buf) != 0 {
                err := ws.WriteMessage(websocket.TextMessage, buf)
                buf = []byte{}
                if err != nil {
                    log.Println(err)
                    return
                }
            }
            t.Reset(time.Microsecond * 100)
        // 前面已经将ssh channel里读取的数据写入创建的通道r, 这里读取数据, 不断增加buf的长度, 在设定的 100 microsecond后由上面判定长度是否返送数据
        case d := <-r:
            if d != utf8.RuneError {
                p := make([]byte, utf8.RuneLen(d))
                utf8.EncodeRune(p, d)
                buf = append(buf, p...)
            } else {
                buf = append(buf, []byte("@")...)
            }
        }
    }
}()

web terminal的后台建好了

前端
mounted () {
    var containerWidth = window.screen.height;
    var containerHeight = window.screen.width;
    var cols = Math.floor((containerWidth - 30) / 9);
    var rows = Math.floor(window.innerHeight/17) - 2;
    if (this.username === undefined){
        var url = (location.protocol === "http:" ? "ws" : "wss") + "://" + location.hostname + ":5001" + "/ws"+ "?" + "msg=" + this.msg + "&rows=" + rows + "&cols=" + cols;
    }else{
        var url = (location.protocol === "http:" ? "ws" : "wss") + "://" + location.hostname + ":5001" + "/ws"+ "?" + "msg=" + this.msg + "&rows=" + rows + "&cols=" + cols + "&username=" + this.username + "&password=" + this.password;
    }
    let terminalContainer = document.getElementById('terminal')
    this.term = new Terminal()
    this.term.open(terminalContainer)
    // open websocket
    this.terminalSocket = new WebSocket(url)
    this.terminalSocket.onopen = this.runRealTerminal
    this.terminalSocket.onclose = this.closeRealTerminal
    this.terminalSocket.onerror = this.errorRealTerminal
    this.term.attach(this.terminalSocket)
    this.term._initialized = true
    console.log('mounted is going on')
}

后端项目地址

11-06 12:35
查看更多