golang实现WebSSH的功能

   在最近一次需求里,需要实现一个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.WebSShReq
	if err := utils.BindAndValidate(e, &param); 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 = conn
	err = 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.Client
	Ws        *websocket.Conn
    Session   *ssh.Session
}

// NewSSHClient 创建新的ssh客户端时
func NewSSHClient(param WebSShReq) SSHClient {
	client := SSHClient{}
	client.Username = param.Username
	client.Port = param.Port
	client.IpAddress = param.IpAddress
	client.Password = param.Password
	return client
}
func (t *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(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 = client
	return 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 = out
	session.Stderr = stderr
	return 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.Buffer
				stdEr 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 = stdin
	modes := 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.最终效果如图:

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值