一、使用密码认证连接 连接包含了认证,可以使用password或者sshkey 两种方式认证,下面采用密码认证方式完成连接
package mainimport ( "fmt" "github.com/mitchellh/go-homedir" "golang.org/x/crypto/ssh" "io/ioutil" "log" "time" ) func main () { sshHost := "home" sshUser := "x" sshPassword := "xxx" sshType := "password" sshKeyPath := "" sshPort := 22 config := &ssh.ClientConfig{ Timeout: time.Second, User: sshUser, HostKeyCallback: ssh.InsecureIgnoreHostKey(), } if sshType == "password" { config.Auth = []ssh.AuthMethod{ssh.Password(sshPassword)} } else { config.Auth = []ssh.AuthMethod{publicKeyAuthFunc(sshKeyPath)} } 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() session, err := sshClient.NewSession() if err != nil { log.Fatal("创建ssh session 失败" , err) } defer session.Close() combo, err := session.CombinedOutput("whoami; cd /; ls -la" ) 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
创建session会话
可以自定义stdin,stdout
可以创建pty
可以SetEnv
执行命令CombinedOutput run ...
go run main.go 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
二、WebSocket简介 HTML5开始提供的一种浏览器与服务器进行双工通讯的网络技术,属于应用层协议,它基于TCP传输协议,并复用HTTP的握手通道:
对大部分web开发者来说,上面描述有点枯燥,只需要记下以下三点
1. WebSocket可以在浏览器里使用 2. 支持双向通信 3. 使用很简单
对比HTTP协议的话,概括的说就是: 支持双向通信,更灵活,更高效,可扩展性更好
1. 支持双向通信,实时性更强 2. 更好的二进制支持 3. 较少的控制开销,连接创建后,客户端和服务端进行数据交换时,协议控制的数据包头部较小,在不包含头部的情况下,服务端到客户端的包头只有2-10字节(取决于数据包长度), 客户端到服务端的话,需要加上额外4字节的掩码,而HTTP每次同年高新都需要携带完整的头部 4. 支持扩展,ws协议定义了扩展, 用户可以扩展协议, 或者实现自定义的子协议
三、基于Web的Terminal终端控制台 1、解决问题 完成这样一个Web Terminal的目的主要是解决几个问题:
1.一定程度上取代xshell,secureRT,putty等ssh终端 2.方便身份认证,访问控制 3.方便使用,不受电脑环境的影响
2、数据流向 要实现远程登录的功能,其数据流向大概为
浏览器 <--> WebSocket <---> SSH <---> Linux OS
实现流程
浏览器将主机的信息(ip, 用户名, 密码, 请求的终端大小等)进行加密,传给后台, 并通过HTTP请求与后台协商升级协议。协议升级完成后,后续的数据交换则遵照web Socket的协议。
后台将HTTP请求升级为web Socket协议,得到一个和浏览器数据交换的连接通道
后台将数据进行解密拿到主机信息,创建一个SSH 客户端,与远程主机的SSH 服务端协商加密,互相认证, 然后建立一个SSH Channel
后台和远程主机有了通讯的信道, 然后后台将终端的大小等信息通过SSH Channel请求远程主机创建一个 pty(伪终端),并请求启动当前用户的默认 shell
后台通过 Socket连接通道拿到用户输入,再通过SSH Channel将输入传给pty,pty将这些数据交给远程主机处理后按照前面指定的终端标准输出到SSH Channel中,同时键盘输入也会发送给SSH Channel
后台从SSH Channel中拿到按照终端大小的标准输出后又通过Socket连接将输出返回给浏览器,由此变实现了Web Terminal
3、代码实现 照上面的使用流程基于代码解释如何实现
(1)升级HTTP协议为WebSocket var upgrader = websocket.Upgrader{ ReadBufferSize: 1024 , WriteBufferSize: 1024 , CheckOrigin: func (r *http.Request) bool { return true }, }
(2)升级协议并获得socket连接 conn就是socket连接通道,接下来后台和浏览器之间的通讯都将基于这个通道
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil ) if err != nil { c.Error(err) return }
(3)后台拿到主机信息,建立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 } 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)
创建连接并返回客户端,如果主机信息不对或其它问题这里将直接失败
(4)通过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 }
(5)远程主机与浏览器实时数据交换 现在为止建立了两个通道,一个是websocket,一个是ssh channel,后台将起两个主要的协程,一个不停的从websocket通道里读取用户的输入,并通过ssh channel传给远程主机:
go fun() { _, 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() r := make (chan rune ) 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 { 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 ) 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 ("@" )...) } } } }()
4、web-terminal (1)前端 git clone https://github.com/chengjoey/web-terminal-client.git cd web-terminal-clientnpm install npm run dev // 需要的话就执行,不需要的话运行npm run dev即可启动前端了 npm run build npm run build --report
(2)后端 go get github.com/chengjoey/web-terminal Linux: web-terminal Windows: web-terminal.exe
(3)使用 访问http://127.0.0.1:5001/ ,即可进行连接和shell操作
四、参考链接 https://www.cnblogs.com/you-men/p/13934845.html
https://mojotv.cn/2019/05/22/golang-ssh-session