package core import ( "bytes" "encoding/json" "github.com/gorilla/websocket" "github.com/pkg/sftp" "golang.org/x/crypto/ssh" "io" "log" "sync" "time" ) // copy data from WebSocket to ssh server // and copy data from ssh server to WebSocket // write data to WebSocket // the data comes from ssh server. type wsBufferWriter struct { buffer bytes.Buffer mu sync.Mutex } // implement Write interface to write bytes from ssh server into bytes.Buffer. func (w *wsBufferWriter) Write(p []byte) (int, error) { w.mu.Lock() defer w.mu.Unlock() return w.buffer.Write(p) } const ( wsMsgCmd = "cmd" wsMsgResize = "resize" ) type wsMsg struct { Type string `json:"type"` Cmd string `json:"cmd"` Cols int `json:"cols"` Rows int `json:"rows"` } // connect to ssh server using ssh session. type SshConn struct { // calling Write() to write data into ssh server StdinPipe io.WriteCloser // Write() be called to receive data from ssh server ComboOutput *wsBufferWriter Session *ssh.Session SftpClient *sftp.Client } //flushComboOutput flush ssh.session combine output into websocket response func flushComboOutput(w *wsBufferWriter, wsConn *websocket.Conn) error { if w.buffer.Len() != 0 { err := wsConn.WriteMessage(websocket.BinaryMessage, w.buffer.Bytes()) if err != nil { return err } w.buffer.Reset() } return nil } // setup ssh shell session // set Session and StdinPipe here, // and the Session.Stdout and Session.Sdterr are also set. func NewSshConn(cols, rows int, sshClient *ssh.Client) (*SshConn, error) { sshSession, err := sshClient.NewSession() if err != nil { return nil, err } // we set stdin, then we can write data to ssh server via this stdin. // but, as for reading data from ssh server, we can set Session.Stdout and Session.Stderr // to receive data from ssh server, and write back to somewhere. stdinP, err := sshSession.StdinPipe() if err != nil { return nil, err } comboWriter := new(wsBufferWriter) //ssh.stdout and stderr will write output into comboWriter sshSession.Stdout = comboWriter sshSession.Stderr = comboWriter modes := ssh.TerminalModes{ ssh.ECHO: 1, // disable echo ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud } // Request pseudo terminal if err := sshSession.RequestPty("xterm", rows, cols, modes); err != nil { return nil, err } // Start remote shell if err := sshSession.Shell(); err != nil { return nil, err } sftpclient, err := sftp.NewClient(sshClient) //创建一个sftp客户端 if err != nil { return nil, err } return &SshConn{StdinPipe: stdinP, ComboOutput: comboWriter, Session: sshSession, SftpClient: sftpclient}, nil } func (s *SshConn) Close() { if s.Session != nil { s.Session.Close() } if s.SftpClient != nil{ s.SftpClient.Close() } } //ReceiveWsMsg receive websocket msg do some handling then write into ssh.session.stdin func (ssConn *SshConn) ReceiveWsMsg(wsConn *websocket.Conn, exitCh chan bool) { //tells other go routine quit defer setQuit(exitCh) for { select { case <-exitCh: return default: //read websocket msg _, wsData, err := wsConn.ReadMessage() if err != nil { log.Println(err.Error()) //logrus.WithError(err).Error("reading webSocket message failed") return } //unmashal bytes into struct msgObj := wsMsg{ Type: "cmd", Cmd: "", Rows: 50, Cols: 180, } if err := json.Unmarshal(wsData, &msgObj); err != nil { log.Println("unmarshal websocket message failed:", string(wsData)) continue } switch msgObj.Type { case wsMsgResize: //handle xterm.js size change if msgObj.Cols > 0 && msgObj.Rows > 0 { if err := ssConn.Session.WindowChange(msgObj.Rows, msgObj.Cols); err != nil { log.Println("ssh pty change windows size failed") continue } } case wsMsgCmd: //handle xterm.js stdin //decodeBytes, err := base64.StdEncoding.DecodeString(msgObj.Cmd) decodeBytes := []byte(msgObj.Cmd) if err != nil { log.Println("websock cmd string base64 decoding failed") continue } if _, err := ssConn.StdinPipe.Write(decodeBytes); err != nil { log.Println("ws cmd bytes write to ssh.stdin pipe failed") return } } } } } func (ssConn *SshConn) SendComboOutput(wsConn *websocket.Conn, exitCh chan bool) { //tells other go routine quit defer setQuit(exitCh) //every 120ms write combine output bytes into websocket response tick := time.NewTicker(time.Millisecond * time.Duration(120)) //for range time.Tick(120 * time.Millisecond){} defer tick.Stop() for { select { case <-tick.C: //write combine output bytes into websocket response if err := flushComboOutput(ssConn.ComboOutput, wsConn); err != nil { log.Println(err.Error()) //logrus.WithError(err).Error("ssh sending combo output to webSocket failed") return } case <-exitCh: return } } } func (ssConn *SshConn) SessionWait(quitChan chan bool) { //if err := ssConn.Session.Wait(); err != nil { // log.Println("ssh session wait failed") // setQuit(quitChan) //} timer := time.NewTicker(time.Second * 5) defer timer.Stop() for { select { case <-timer.C: { if _, err := ssConn.StdinPipe.Write([]byte{32, 127}); err != nil { log.Println("ws cmd bytes write to ssh.stdin pipe failed") return } } } } } func setQuit(ch chan bool) { ch <- true }