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. Python练习题 018:打印星号菱形

    [Python练习题 018] 打印出如下图案(菱形): * *** ***** ******* ***** *** * --------------------------------------- ...

  2. 在IDEA创建类时自动创建作者日期等信息设定

    1.效果 1 package com.dream.test; 2 3 /* 4 * @author 匠人码农 5 * @date 2020/04/18 11:17 6 * 概要: 7 * XXXXX ...

  3. Win10系统中文显示乱码怎么解决

    来源:https://jingyan.baidu.com/article/d8072ac4ba20cfec94cefd48.html 简单的说是: 全部设置改为中国而且一定要重启系统,无论时间还是区域 ...

  4. JavaFX ImageView

    例子1:显示4个狗头.正常显示左上角.右下角的狗头:右上角的狗头旋转180°,并设置了透明度:左下角的狗头旋转90°,也设置了透明度. 1 import javafx.application.Appl ...

  5. MySQL的简单实用 手把手教学

    ------------恢复内容开始------------ MySQL的使用 1.登陆数据库 打开terminal 在终端根文件目录下输入/usr/local/mysql/bin/mysql -u ...

  6. Azure Cosmos DB (二) SQL API 操作

    一,引言 还记得国庆期间,我们学习了一下关于Azure Cosmos DB 的一些基础知识以及Azure Cosmos DB 的几种支持数据库类型.今天就开始分享一些实战操作,如何通过Azure Po ...

  7. 洛谷UVA524 素数环 Prime Ring Problem

    标签:搜索与回溯 题目: 从1到20这20个数摆成一个环,要求相邻的两个数的和是一个素数. 算法分析: 非常明显,这是一道回溯的题目.从1开始,每个空位有20种可能,只要填进去的数合法:与前面的数不相 ...

  8. 多测师讲解pyhon__hashlib_高级讲师肖sir

    一.加密,加密成16进制的字符串 # import hashlib # 导入hashlib模块# md = hashlib.md5() # 获取一个md5加密算法对象# md.update('需要加密 ...

  9. linux(centos8):使用cgroups做资源限制

    一,什么是cgroups? 1,cgroups是资源的控制组,它提供了一套机制用于控制一组特定进程对资源的使用.     cgroups绑定一个进程集合到一个或多个限制资源使用的子系统上. 2, cg ...

  10. centos8上安装ffmpeg4.2.2并做视频截图

    一,ffmpeg的作用: FFmpeg是一套可以用来记录.转换数字音频.视频,并能将其转化为流的开源计算机程序. 它提供了录制.转换以及流化音视频的完整解决方案.它包含了非常先进的音频/视频编解码库l ...