当前位置:网站首页>01. SSH Remote terminal and websocket of go language

01. SSH Remote terminal and websocket of go language

2020-11-06 01:28:28 you-men

Crypto/ssh brief introduction

Use

download
 go get "github.com/mitchellh/go-homedir"
 go get "golang.org/x/crypto/ssh"
Connect using password authentication

The connection contains Authentication , have access to password perhaps sshkey Two ways to authenticate , The following uses password authentication to complete the connection

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 perhaps key
	//sshKeyPath := "" // ssh id_rsa.id route 
	sshPort := 22

	//  establish ssh Login configuration 
	config := &ssh.ClientConfig{
		Timeout: time.Second, // ssh Connect time out Time one second , If ssh Validation errors return in one second 
		User: sshUser,
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),  //  This one can , But it's not safe enough 
		//HostKeyCallback: hostKeyCallBackFunc(h.Host),
	}
	if sshType == "password" {
		config.Auth = []ssh.AuthMethod{ssh.Password(sshPasswrod)}
	} else {
		//config.Auth = []ssh.AuthMethod(publicKeyAuthFunc(sshKeyPath))
		return
	}

	// dial  obtain ssh client
	addr := fmt.Sprintf("%s:%d",sshHost,sshPort)
	sshClient,err := ssh.Dial("tcp",addr,config)
	if err != nil {
		log.Fatal(" establish ssh client  Failure ",err)
	}
	defer sshClient.Close()

	//  establish ssh-session
	session,err := sshClient.NewSession()
	if err != nil {
		log.Fatal(" establish ssh session Failure ",err)
	}

	defer session.Close()

	//  Execute remote command 
	combo,err := session.CombinedOutput("whoami; cd /; ls -al;")
	if err != nil {
		log.Fatal(" Remote execution cmd Failure ",err)
	}
	log.Println(" Command output :",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)
//}

Code reading

//  To configure ssh.ClientConfig
/*
		 Suggest TimeOut Customize a comparison time 
		 Customize HostKeyCallback If it's like easy to use ssh.InsecureIgnoreHostKey Will bring oh , It's not very safe in this way 
		publicKeyAuthFunc  If you use key Login needs to use which function to read id_rsa Private key ,  Of course, you can also customize this access to support strings .
*/

// ssh.Dial establish ssh client 
/*
		 Concatenate strings to get ssh Link address , And don't forget defer client.Close()
*/

// sshClient.NewSession Create a session 
/*
		 You can customize stdin,stdout
		 You can create pty
		 Sure SetEnv
*/

//  Carry out orders CombinnedOutput run...
go run main.go
2020/11/06 00:07:31  Command output : 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

Excerpt from above

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

WebSocket brief introduction

HTML5 A network technology for duplex communication between browser and server is provided , Belongs to the application layer protocol , It's based on TCP Transfer protocol , And reuse HTTP The handshake channel :

For the most part web For developers , The above description is a bit boring , Just a few of the following three points

/*
		1. WebSocket It can be used in a browser 
		2.  Support two-way communication 
		3.  It's easy to use 
*/
advantage

contrast HTTP agreement , Generally speaking, it is : Support two-way communication , More flexible , More efficient , Better scalability

/*
		1.  Support two-way communication , More real time 
		2.  Better binary support 
		3.  Less control overhead , After the connection is created , Data exchange between client and server , Protocol controlled packet header is small , Without the head ,
				 The service end to the client's header is 2-10 byte ( Depends on the packet length ),  From client to server , Need to add extra 4 Mask of bytes ,
				 and HTTP Every time in the same year, hi tech needs to carry a complete head 
		4.  Support extended ,ws The protocol defines the extension ,  The user can extend the protocol ,  Or implement the custom sub protocol 
*/

be based on Web Of Terminal Terminal console

Finish such a Web Terminal The main purpose of this paper is to solve several problems :

/*
		1.  To a certain extent, replace xshell,secureRT,putty etc. ssh terminal 
		2.  Can facilitate identity authentication ,  Access control 
		3.  Easy to use ,  Not affected by computer environment 
*/

To realize the function of remote login , The data flow direction is probably

/*
		 browser  <-->  WebSocket  <---> SSH <---> Linux OS
*/
Implementation process
  1. The browser will host the information (ip, user name , password , The terminal size of the request, etc ) To encrypt , To the backstage , And pass HTTP Request to negotiate the upgrade agreement with the background . After the upgrade of the agreement , The subsequent data exchange follows web Socket The agreement .
  2. Backstage will HTTP Request upgrade to web Socket agreement , Get a connection channel to exchange data with the browser
  3. The background decrypts the data to get the host information , Create a SSH client , With the remote host SSH The server negotiates encryption , Mutual authentication , And then build a SSH Channel
  4. Background and remote host have communication channel , Then the background will the size of the terminal and other information through SSH Channel Request the remote host to create a pty( Pseudo terminal ), And request to start the current user's default shell
  5. Backstage through Socket Connect channels to get user input , Re pass SSH Channel Pass input to pty, pty The data will be sent to the remote host for processing and then output to... According to the terminal standard specified above SSH Channel in , Keyboard input is also sent to SSH Channel
  6. Backstage from SSH Channel Get the standard output according to the size of the terminal and then pass Socket The connection returns the output to the browser , Thus, the realization of Web Terminal


According to the above usage process, explain how to implement based on the code

upgrade HTTP Agreement for WebSocket
var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	CheckOrigin: func(r *http.Request) bool {
		return true
	},
}
Upgrade the agreement and get socket Connect
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
    c.Error(err)
    return
}

conn Namely socket Connection channel , Next, the communication between the background and browser will be based on this channel

The background gets the host information , establish ssh client

ssh Client structure

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
}

// Create a new ssh When the client ,  The default user name is root,  Port is 22
func NewSSHClient() SSHClient {
	client := SSHClient{}
	client.Username = "root"
	client.Port = 22
	return client
}

When initializing, we only have the host information , and Session, client, channel Are empty , Now the gentleman is really 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) Create a connection and return to the client , If the host information is not correct or there are other problems, this will directly fail

adopt ssh Client creation ssh channel, And ask for a pty Pseudo terminal , Request the user's default session

If the host information is verified by , Can pass ssh client Create a channel :

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

ssh When the channel is created , Request a standard output terminal , And turn on the user's default 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
}
Real time data exchange between remote host and browser

So far, two channels have been established , One is websocket, One is ssh channel, There will be two major collaborations in the background , One after another from websocket The channel reads the user's input , And pass ssh channel To the remote host :

// The first coprocessor here takes the user's input 
go func() {
    for {
        // p Enter... For the user 
        _, p, err := ws.ReadMessage()
        if err != nil {
            return
        }
        _, err = this.channel.Write(p)
        if err != nil {
            return
        }
    }
}()

The second host passes data from the remote host to the browser , In this process, there will be another one , gaining ssh channel And pass the data to a channel created inside the background , The main coroutine has an endless loop , Read data from internal channels at regular intervals , And pass it through websocket To the browser , So data transmission is not really real-time , But there is an interval between , My default is 100 Microsecond , This basically does not feel the delay , And it reduces consumption , Sometimes when a browser enters a command to get a large amount of data , You will feel that the data will appear one meal at a time because an interval has been set :

// The second coprocessor returns the result of the remote host to the user 
go func() {
    br := bufio.NewReader(this.channel)
    buf := []byte{}
    t := time.NewTimer(time.Microsecond * 100)
    defer t.Stop()
    //  Building a channel ,  One end writes data from the remote host to ,  A section of read data is written to ws
    r := make(chan rune)

    //  Start another program ,  An endless loop of reading ssh channel The data of ,  And to the r Channel until the connection is disconnected 
    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 Connection closed !\033[0m"))
                ws.Close()
                return
            }
            if size > 0 {
                r <- x
            }
        }
    }()

    //  Main circulation 
    for {
        select {
        //  every other 100 Microsecond ,  as long as buf The length is not 0 Just write the data to ws,  And reset the time and 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)
        //  I've already ssh channel The data read in is written to the created channel r,  Read the data here ,  Increasing buf The length of ,  In the setting  100 microsecond After that, we can determine whether the length returns the data 
        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 The backstage has been set up

front end

I chose to use the front end vue frame ( In fact, such a small project is totally unnecessary vue), The terminal tool uses xterm, vscode The built-in terminal is also used xterm. Here's a key piece of code , Front end project address

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

Back end item address

版权声明
本文为[you-men]所创,转载请带上原文链接,感谢