Crypto/ssh简介

使用

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

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

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简介

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

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

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

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

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

基于Web的Terminal终端控制台

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

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

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

/*
浏览器 <--> WebSocket <---> SSH <---> Linux OS
*/
实现流程
  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
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
}
远程主机与浏览器实时数据交换

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

//这里第一个协程获取用户的输入
go func() {
for {
// p为用户输入
_, p, err := ws.ReadMessage()
if err != nil {
return
}
_, err = this.channel.Write(p)
if err != nil {
return
}
}
}()

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

//第二个协程将远程主机的返回结果返回给用户
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的后台建好了

前端

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

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')
}

后端项目地址

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. 【代码审计】JAVA代码审计

    分享一些Java安全相关文章,其中大部分都涉及到代码的分析与审计. 大家总是在找Java的代码审计的文章,但好像很多人选择性失明. 其实Java没有和PHP一样的简单,所以你觉得你看到的文章不是入门级 ...

  2. 达梦产品技术支持培训-day8-DM8数据库备份与还原-实操

    1.DM8的备份还原方法 Disql 工具:联机数据备份与还原,包括库备份.表空间备份与还原.表备份与还原:  DMRMAN 工具:脱机数据库备份还原与恢复: 客户端工具 MANAGER和CONSOL ...

  3. Vue结合Django-Rest-Frameword结合实现登录认证(二)

    作者:小土豆biubiubiu 博客园:https://www.cnblogs.com/HouJiao/ 掘金:https://juejin.im/user/2436173500265335 微信公众 ...

  4. MeteoInfoLab脚本示例:获取气团轨迹每个节点的气象数据

    读取HYSPLIT输出的轨迹数据文件和相应时间的气象数据文件,生成轨迹图层,循环每条轨迹的节点,读出该节点的经度.纬度.气压.时间,通过对气象数据插值获得该节点的气象数据.脚本程序: #------- ...

  5. MeteoInfoLab脚本示例:Trajectory

    示例读取HYSPLIT模式输出的气团轨迹数据文件,生成轨迹图层,并显示轨迹各节点的气压图.脚本程序: f = addfile_hytraj('D:/MyProgram/Distribution/jav ...

  6. MeteoInfoLab脚本示例:加载图片和透明图层

    MeteoInfoLab的georead函数提供了读取shape文件.image文件(JPG.PNG等,需要有相应的地理定位文件)文件生成图层的功能(事实上shaperead也是同样的功能,不过函数名 ...

  7. 【C语言程序设计】小游戏之俄罗斯方块(二)!适合初学者上手、练手!

    第二篇,主要实现俄罗斯方块中的主体部分,包括容器的数据结构以及容器的相关操作,特别是大方块和容器之间的交互逻辑,包括碰撞检测,消除检测等等. 1. 容器的表示 大方块的实现涉及到位运算,而容器同样如此 ...

  8. 【树形DP】JSOI BZOJ4472 salesman

    题目内容 vjudge链接 某售货员小T要到若干城镇去推销商品,由于该地区是交通不便的山区,任意两个城镇 之间都只有唯一的可能经过其它城镇的路线. 小T 可以准确地估计出在每个城镇停留的净收 益.这些 ...

  9. 第四章 Bash Shell 的简单应用

    一.Bash Shell 的简单介绍 1.什么是bash shell? 是一个命令解释器 它在操作系统的最外面 负责用户与内核进行交互的一种接口 将用户输入的命令翻译给操作系统,并将处理后的结果输出到 ...

  10. linux 环境搭建Jenkins

    这里提供一个本地搭建Jenkins的方法,基于wins 的 https://blog.csdn.net/u011541946/article/month/2017/09/2   下面讲的是在服务器上操 ...