在最近一次需求里,需要实现一个webSSH的功能,就是把terminal搬到web中来。要实现这个功能,可以采用websocket+ssh来说实现
1.第一步实现websocket
websocket主要是ws或wss协议,其原理就是http协议升级成ws协议,即ws是建立在http上的,所有路由正常写http的路由,然后处理一下websocket升级。
注:我用的echo框架:
路由:
backendApi.GET("/tools/ssh/ws", tools.WebSSH).Name = "webssh"
handler
func WebSSH(e echo.Context) error {var param tools.WebSShReqif err := utils.BindAndValidate(e, ¶m); err != nil {return err}if err := tools.Upgrade(e.Response().Writer, e.Request(), param); err != nil {return err}return e.JSON(http.StatusOK, "'")
}
:
websocket协议升级:
var upgrader = websocket.Upgrader{ReadBufferSize: 1024,WriteBufferSize: 1024,CheckOrigin: func(r *http.Request) bool {return true},
}func Upgrade(w http.ResponseWriter, r *http.Request, param WebSShReq) (err error) {conn, err := upgrader.Upgrade(w, r, nil)if err != nil {return}client := NewSSHClient(param)client.Ws = connerr = client.GenerateClient()if err != nil {fmt.Println("链接ssh错误", err)conn.WriteMessage(1, []byte(err.Error()))conn.Close()return err}go client.Write()return nil
}
2.第一版ssh实现
type SSHClient struct {Username string `json:"username"`Password string `json:"password"`IpAddress string `json:"ipaddress"`Port int `json:"port"`Client *ssh.ClientWs *websocket.ConnSession *ssh.Session
}// NewSSHClient 创建新的ssh客户端时
func NewSSHClient(param WebSShReq) SSHClient {client := SSHClient{}client.Username = param.Usernameclient.Port = param.Portclient.IpAddress = param.IpAddressclient.Password = param.Passwordreturn client
}
func (t *SSHClient) GenerateClient() error {var (auth []ssh.AuthMethodaddr stringclientConfig *ssh.ClientConfigclient *ssh.Clientconfig ssh.Configerr error)auth = make([]ssh.AuthMethod, 0)auth = append(auth, ssh.Password(t.Password))config = ssh.Config{Ciphers: []string{"aes128-ctr", "aes192-ctr", "aes256-ctr", "arcfour256", "arcfour128", "aes128-cbc", "3des-cbc", "aes192-cbc", "aes256-cbc"},}clientConfig = &ssh.ClientConfig{User: t.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", t.IpAddress, t.Port)if client, err = ssh.Dial("tcp", addr, clientConfig); err != nil {return err}t.Client = clientreturn nil
}func (t *SSHClient) send(out, stderr *bytes.Buffer, cmd []byte) error {session, err := t.Client.NewSession()if err != nil {return errors.New("ssh session创建失败")}defer session.Close()session.Stdout = outsession.Stderr = stderrreturn session.Run(string(cmd))
}
func (t *SSHClient) Write() {defer t.Client.Close()for {// p为用户输入_, p, err := t.Ws.ReadMessage()if err != nil {return}fmt.Println("webssh:", string(p))go func(data []byte) {var (out bytes.BufferstdEr bytes.Buffer)err2 := t.send(&out, &stdEr, data)if err2 != nil {t.Ws.WriteMessage(1, stdEr.Bytes())return}t.Ws.WriteMessage(1, out.Bytes())}(p)}
}
这样能实现正常指令,但是有个问题;不能切换目录,不完善
3.改进
使用终端交互模式,做到真正的webssh, 直接上代码
改进方法:
func (t *SSHClient) RunTerminal(stdout, stderr io.Writer, stdin io.Reader) error {session, err := t.Client.NewSession()if err != nil {return err}defer session.Close()session.Stdout = io.MultiWriter(os.Stdout, stdout)session.Stderr = io.MultiWriter(os.Stderr, stderr)session.Stdin = stdinmodes := ssh.TerminalModes{ssh.ECHO: 0,ssh.TTY_OP_ISPEED: 14400,ssh.TTY_OP_OSPEED: 14400,}if err := session.RequestPty("xterm", 32, 160, modes); err != nil {return err}if err = session.Shell(); err != nil {log.Fatalf("start shell error: %s", err.Error())}if err = session.Wait(); err != nil {log.Fatalf("return error: %s", err.Error())}return nil
}
此方法需要输入,输出,和错误,使用标准的输入及标准输出,能实现交互,但是我是需要接收websocket发的消息,及返回websocket输出。
故,需要实现io.writer和io.reader接口
type stdout struct {ws *websocket.Conn
}func (s *stdout) Write(p []byte) (n int, err error) {// fmt.Println("SSH output:", string(p))err = s.ws.WriteMessage(1, p)return len(p), err
}type stdIn struct {ws *websocket.Conn
}func (s *stdIn) Read(p []byte) (n int, err error) {_, p2, err := s.ws.ReadMessage()// 指令需要加一个回车if err != nil {return 0, err}n = copy(p, []byte(fmt.Sprintf("%s\n", string(p2))))// fmt.Println("####", string(p))return n, err
}
func (s *stdIn) Close() error {return nil
}
注: 实现read方法时,注意加个回车,不然指令是不会执行的,我在这里就卡了很久......
最后,将upgrade方法中的,
go client.Write()
换成
go client.RunTerminal(&stdout{conn}, &stdout{conn}, &stdIn{conn})
就ok了。
4.最终效果如图: