From 616526541795ea9036ec1b5ffe589f2157c89e21 Mon Sep 17 00:00:00 2001 From: dushixiang Date: Sun, 13 Feb 2022 14:42:22 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=9B=91=E6=8E=A7=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/api/guacamole.go | 243 ++++++++++-------- server/api/term.go | 4 +- server/app/server.go | 6 +- web/src/components/access/AccessMonitor.js | 11 +- web/src/components/access/BatchCommandTerm.js | 2 +- web/src/components/access/Term.js | 3 +- web/src/components/access/TermMonitor.js | 3 +- web/src/components/session/OnlineSession.js | 3 +- 8 files changed, 152 insertions(+), 123 deletions(-) diff --git a/server/api/guacamole.go b/server/api/guacamole.go index 618defc..8455087 100644 --- a/server/api/guacamole.go +++ b/server/api/guacamole.go @@ -2,7 +2,6 @@ package api import ( "context" - "errors" "fmt" "net/http" "path" @@ -54,7 +53,6 @@ func (api GuacamoleApi) Guacamole(c echo.Context) error { height := c.QueryParam("height") dpi := c.QueryParam("dpi") sessionId := c.Param("id") - connectionId := c.QueryParam("connectionId") intWidth, _ := strconv.Atoi(width) intHeight, _ := strconv.Atoi(height) @@ -63,65 +61,48 @@ func (api GuacamoleApi) Guacamole(c echo.Context) error { propertyMap := repository.PropertyRepository.FindAllMap(ctx) - var s model.Session - - if len(connectionId) > 0 { - s, err = repository.SessionRepository.FindByConnectionId(ctx, connectionId) + configuration.SetParameter("width", width) + configuration.SetParameter("height", height) + configuration.SetParameter("dpi", dpi) + s, err := service.SessionService.FindByIdAndDecrypt(ctx, sessionId) + if err != nil { + return err + } + api.setConfig(propertyMap, s, configuration) + var ( + ip = s.IP + port = s.Port + ) + if s.AccessGatewayId != "" && s.AccessGatewayId != "-" { + g, err := service.GatewayService.GetGatewayAndReconnectById(s.AccessGatewayId) if err != nil { - return err + utils.Disconnect(ws, AccessGatewayUnAvailable, "获取接入网关失败:"+err.Error()) + return nil } - if s.Status != constant.Connected { - return errors.New("会话未在线") + if !g.Connected { + utils.Disconnect(ws, AccessGatewayUnAvailable, "接入网关不可用:"+g.Message) + return nil } - configuration.ConnectionID = connectionId - sessionId = s.ID - configuration.SetParameter("width", strconv.Itoa(s.Width)) - configuration.SetParameter("height", strconv.Itoa(s.Height)) - configuration.SetParameter("dpi", "96") - } else { - configuration.SetParameter("width", width) - configuration.SetParameter("height", height) - configuration.SetParameter("dpi", dpi) - s, err = service.SessionService.FindByIdAndDecrypt(ctx, sessionId) + exposedIP, exposedPort, err := g.OpenSshTunnel(s.ID, ip, port) if err != nil { - return err - } - api.setConfig(propertyMap, s, configuration) - var ( - ip = s.IP - port = s.Port - ) - if s.AccessGatewayId != "" && s.AccessGatewayId != "-" { - g, err := service.GatewayService.GetGatewayAndReconnectById(s.AccessGatewayId) - if err != nil { - utils.Disconnect(ws, AccessGatewayUnAvailable, "获取接入网关失败:"+err.Error()) - return nil - } - if !g.Connected { - utils.Disconnect(ws, AccessGatewayUnAvailable, "接入网关不可用:"+g.Message) - return nil - } - exposedIP, exposedPort, err := g.OpenSshTunnel(s.ID, ip, port) - if err != nil { - utils.Disconnect(ws, AccessGatewayCreateError, "创建SSH隧道失败:"+err.Error()) - return nil - } - ip = exposedIP - port = exposedPort - defer g.CloseSshTunnel(s.ID) + utils.Disconnect(ws, AccessGatewayCreateError, "创建SSH隧道失败:"+err.Error()) + return nil } + ip = exposedIP + port = exposedPort + defer g.CloseSshTunnel(s.ID) + } - configuration.SetParameter("hostname", ip) - configuration.SetParameter("port", strconv.Itoa(port)) + configuration.SetParameter("hostname", ip) + configuration.SetParameter("port", strconv.Itoa(port)) - // 加载资产配置的属性,优先级比全局配置的高,因此最后加载,覆盖掉全局配置 - attributes, err := repository.AssetRepository.FindAssetAttrMapByAssetId(ctx, s.AssetId) - if err != nil { - return err - } - if len(attributes) > 0 { - api.setAssetConfig(attributes, s, configuration) - } + // 加载资产配置的属性,优先级比全局配置的高,因此最后加载,覆盖掉全局配置 + attributes, err := repository.AssetRepository.FindAssetAttrMapByAssetId(ctx, s.AssetId) + if err != nil { + return err + } + if len(attributes) > 0 { + api.setAssetConfig(attributes, s, configuration) } for name := range configuration.Parameters { // 替换数据库空格字符串占位符为真正的空格 @@ -136,9 +117,7 @@ func (api GuacamoleApi) Guacamole(c echo.Context) error { guacdTunnel, err := guacd.NewTunnel(addr, configuration) if err != nil { - if connectionId == "" { - utils.Disconnect(ws, NewTunnelError, err.Error()) - } + utils.Disconnect(ws, NewTunnelError, err.Error()) log.Printf("[%v] 建立连接失败: %v", sessionId, err.Error()) return err } @@ -151,43 +130,31 @@ func (api GuacamoleApi) Guacamole(c echo.Context) error { GuacdTunnel: guacdTunnel, } - if connectionId == "" { - if configuration.Protocol == constant.SSH { - nextTerminal, err := CreateNextTerminalBySession(s) - if err == nil { - nextSession.NextTerminal = nextTerminal - } + if configuration.Protocol == constant.SSH { + nextTerminal, err := CreateNextTerminalBySession(s) + if err == nil { + nextSession.NextTerminal = nextTerminal } + } - nextSession.Observer = session.NewObserver(sessionId) - session.GlobalSessionManager.Add <- nextSession - go nextSession.Observer.Start() - sess := model.Session{ - ConnectionId: guacdTunnel.UUID, - Width: intWidth, - Height: intHeight, - Status: constant.Connecting, - Recording: configuration.GetParameter(guacd.RecordingPath), - } - if sess.Recording == "" { - // 未录屏时无需审计 - sess.Reviewed = true - } - // 创建新会话 - log.Debugf("[%v] 新建会话成功: %v", sessionId, sess.ConnectionId) - if err := repository.SessionRepository.UpdateById(ctx, &sess, sessionId); err != nil { - return err - } - } else { - // 要监控会话 - forObsSession := session.GlobalSessionManager.GetById(sessionId) - if forObsSession == nil { - utils.Disconnect(ws, NotFoundSession, "获取会话失败") - return nil - } - nextSession.ID = utils.UUID() - forObsSession.Observer.Add <- nextSession - log.Debugf("[%v:%v] 观察者[%v]加入会话[%v]", sessionId, connectionId, nextSession.ID, s.ConnectionId) + nextSession.Observer = session.NewObserver(sessionId) + session.GlobalSessionManager.Add <- nextSession + go nextSession.Observer.Start() + sess := model.Session{ + ConnectionId: guacdTunnel.UUID, + Width: intWidth, + Height: intHeight, + Status: constant.Connecting, + Recording: configuration.GetParameter(guacd.RecordingPath), + } + if sess.Recording == "" { + // 未录屏时无需审计 + sess.Reviewed = true + } + // 创建新会话 + log.Debugf("[%v] 新建会话成功: %v", sessionId, sess.ConnectionId) + if err := repository.SessionRepository.UpdateById(ctx, &sess, sessionId); err != nil { + return err } guacamoleHandler := NewGuacamoleHandler(ws, guacdTunnel) @@ -196,21 +163,11 @@ func (api GuacamoleApi) Guacamole(c echo.Context) error { for { _, message, err := ws.ReadMessage() if err != nil { - log.Debugf("[%v:%v] WebSocket已关闭, %v", sessionId, connectionId, err.Error()) + log.Debugf("[%v] WebSocket已关闭, %v", sessionId, err.Error()) // guacdTunnel.Read() 会阻塞,所以要先把guacdTunnel客户端关闭,才能退出Guacd循环 _ = guacdTunnel.Close() - if connectionId != "" { - observerId := nextSession.ID - forObsSession := session.GlobalSessionManager.GetById(sessionId) - if forObsSession != nil { - // 移除会话中保存的观察者信息 - forObsSession.Observer.Del <- observerId - log.Debugf("[%v:%v] 观察者[%v]退出会话", sessionId, connectionId, observerId) - } - } else { - service.SessionService.CloseSessionById(sessionId, Normal, "用户正常退出") - } + service.SessionService.CloseSessionById(sessionId, Normal, "用户正常退出") guacamoleHandler.Stop() return nil } @@ -245,6 +202,84 @@ func (api GuacamoleApi) setAssetConfig(attributes map[string]string, s model.Ses } } +func (api GuacamoleApi) GuacamoleMonitor(c echo.Context) error { + ws, err := UpGrader.Upgrade(c.Response().Writer, c.Request(), nil) + if err != nil { + log.Errorf("升级为WebSocket协议失败:%v", err.Error()) + return err + } + ctx := context.TODO() + sessionId := c.Param("id") + + s, err := repository.SessionRepository.FindById(ctx, sessionId) + if err != nil { + return err + } + if s.Status != constant.Connected { + utils.Disconnect(ws, AssetNotActive, "会话离线") + return nil + } + connectionId := s.ConnectionId + configuration := guacd.NewConfiguration() + configuration.ConnectionID = connectionId + sessionId = s.ID + configuration.SetParameter("width", strconv.Itoa(s.Width)) + configuration.SetParameter("height", strconv.Itoa(s.Height)) + configuration.SetParameter("dpi", "96") + + addr := config.GlobalCfg.Guacd.Hostname + ":" + strconv.Itoa(config.GlobalCfg.Guacd.Port) + asset := fmt.Sprintf("%s:%s", configuration.GetParameter("hostname"), configuration.GetParameter("port")) + log.Debugf("[%v] 新建 guacd 会话, guacd=%v, asset=%v", sessionId, addr, asset) + + guacdTunnel, err := guacd.NewTunnel(addr, configuration) + if err != nil { + utils.Disconnect(ws, NewTunnelError, err.Error()) + log.Printf("[%v] 建立连接失败: %v", sessionId, err.Error()) + return err + } + + nextSession := &session.Session{ + ID: sessionId, + Protocol: s.Protocol, + Mode: s.Mode, + WebSocket: ws, + GuacdTunnel: guacdTunnel, + } + + // 要监控会话 + forObsSession := session.GlobalSessionManager.GetById(sessionId) + if forObsSession == nil { + utils.Disconnect(ws, NotFoundSession, "获取会话失败") + return nil + } + nextSession.ID = utils.UUID() + forObsSession.Observer.Add <- nextSession + log.Debugf("[%v:%v] 观察者[%v]加入会话[%v]", sessionId, connectionId, nextSession.ID, s.ConnectionId) + + guacamoleHandler := NewGuacamoleHandler(ws, guacdTunnel) + guacamoleHandler.Start() + + for { + _, message, err := ws.ReadMessage() + if err != nil { + log.Debugf("[%v:%v] WebSocket已关闭, %v", sessionId, connectionId, err.Error()) + // guacdTunnel.Read() 会阻塞,所以要先把guacdTunnel客户端关闭,才能退出Guacd循环 + _ = guacdTunnel.Close() + + observerId := nextSession.ID + forObsSession.Observer.Del <- observerId + log.Debugf("[%v:%v] 观察者[%v]退出会话", sessionId, connectionId, observerId) + guacamoleHandler.Stop() + return nil + } + _, err = guacdTunnel.WriteAndFlush(message) + if err != nil { + service.SessionService.CloseSessionById(sessionId, TunnelClosed, "远程连接已关闭") + return nil + } + } +} + func (api GuacamoleApi) setConfig(propertyMap map[string]string, s model.Session, configuration *guacd.Configuration) { if propertyMap[guacd.EnableRecording] == "true" { configuration.SetParameter(guacd.RecordingPath, path.Join(config.GlobalCfg.Guacd.Recording, s.ID)) diff --git a/server/api/term.go b/server/api/term.go index 355fa46..1b463f7 100644 --- a/server/api/term.go +++ b/server/api/term.go @@ -47,7 +47,7 @@ func (api WebTerminalApi) SshEndpoint(c echo.Context) error { }() ctx := context.TODO() - sessionId := c.QueryParam("sessionId") + sessionId := c.Param("id") cols, _ := strconv.Atoi(c.QueryParam("cols")) rows, _ := strconv.Atoi(c.QueryParam("rows")) @@ -222,7 +222,7 @@ func (api WebTerminalApi) SshMonitorEndpoint(c echo.Context) error { }() ctx := context.TODO() - sessionId := c.QueryParam("sessionId") + sessionId := c.Param("id") s, err := repository.SessionRepository.FindById(ctx, sessionId) if err != nil { return WriteMessage(ws, dto.NewMessage(Closed, "获取会话失败")) diff --git a/server/app/server.go b/server/app/server.go index 137925e..a16d494 100644 --- a/server/app/server.go +++ b/server/app/server.go @@ -85,9 +85,6 @@ func setupRoutes() *echo.Echo { e.POST("/login", accountApi.LoginEndpoint) e.POST("/loginWithTotp", accountApi.LoginWithTotpEndpoint) - e.GET("/ssh", webTerminalApi.SshEndpoint) - e.GET("/ssh-monitor", webTerminalApi.SshMonitorEndpoint) - account := e.Group("/account") { account.GET("/info", accountApi.InfoEndpoint) @@ -175,6 +172,9 @@ func setupRoutes() *echo.Echo { sessions.POST("", SessionApi.SessionCreateEndpoint) sessions.POST("/:id/connect", SessionApi.SessionConnectEndpoint) sessions.GET("/:id/tunnel", guacamoleApi.Guacamole) + sessions.GET("/:id/tunnel-monitor", guacamoleApi.GuacamoleMonitor) + sessions.GET("/:id/ssh", webTerminalApi.SshEndpoint) + sessions.GET("/:id/ssh-monitor", webTerminalApi.SshMonitorEndpoint) sessions.POST("/:id/resize", SessionApi.SessionResizeEndpoint) sessions.GET("/:id/stats", SessionApi.SessionStatsEndpoint) diff --git a/web/src/components/access/AccessMonitor.js b/web/src/components/access/AccessMonitor.js index 1da1992..6ced443 100644 --- a/web/src/components/access/AccessMonitor.js +++ b/web/src/components/access/AccessMonitor.js @@ -15,8 +15,6 @@ const STATE_DISCONNECTED = 5; class AccessMonitor extends Component { - formRef = React.createRef() - state = { client: {}, containerOverflow: 'hidden', @@ -29,7 +27,7 @@ class AccessMonitor extends Component { }; async componentDidMount() { - const connectionId = this.props.connectionId; + const sessionId = this.props.sessionId; let rate = this.props.rate; let protocol = this.props.protocol; let width = this.props.width; @@ -45,7 +43,7 @@ class AccessMonitor extends Component { height: height * rate, rate: rate, }) - this.renderDisplay(connectionId); + this.renderDisplay(sessionId); } componentWillUnmount() { @@ -110,9 +108,9 @@ class AccessMonitor extends Component { }); } - async renderDisplay(connectionId, protocol) { + async renderDisplay(sessionId, protocol) { - let tunnel = new Guacamole.WebSocketTunnel(wsServer + '/tunnel'); + let tunnel = new Guacamole.WebSocketTunnel(`${wsServer}/sessions/${sessionId}/tunnel-monitor`); tunnel.onstatechange = this.onTunnelStateChange; let client = new Guacamole.Client(tunnel); @@ -128,7 +126,6 @@ class AccessMonitor extends Component { let token = getToken(); let params = { - 'connectionId': connectionId, 'X-Auth-Token': token }; diff --git a/web/src/components/access/BatchCommandTerm.js b/web/src/components/access/BatchCommandTerm.js index 367c6df..f3a2e2c 100644 --- a/web/src/components/access/BatchCommandTerm.js +++ b/web/src/components/access/BatchCommandTerm.js @@ -62,7 +62,7 @@ class BatchCommandTerm extends Component { let paramStr = qs.stringify(params); - let webSocket = new WebSocket(wsServer + '/ssh?' + paramStr); + let webSocket = new WebSocket(`${wsServer}/sessions/${sessionId}/ssh?${paramStr}`); this.props.appendWebsocket({'id': assetId, 'ws': webSocket}); diff --git a/web/src/components/access/Term.js b/web/src/components/access/Term.js index bd2be47..5bb4002 100644 --- a/web/src/components/access/Term.js +++ b/web/src/components/access/Term.js @@ -113,13 +113,12 @@ class Term extends Component { let params = { 'cols': term.cols, 'rows': term.rows, - 'sessionId': sessionId, 'X-Auth-Token': token }; let paramStr = qs.stringify(params); - let webSocket = new WebSocket(wsServer + '/ssh?' + paramStr); + let webSocket = new WebSocket(`${wsServer}/sessions/${sessionId}/ssh?${paramStr}`); let pingInterval; webSocket.onopen = (e => { diff --git a/web/src/components/access/TermMonitor.js b/web/src/components/access/TermMonitor.js index 5029edf..da914b0 100644 --- a/web/src/components/access/TermMonitor.js +++ b/web/src/components/access/TermMonitor.js @@ -33,13 +33,12 @@ class TermMonitor extends Component { let token = getToken(); let params = { - 'sessionId': sessionId, 'X-Auth-Token': token }; let paramStr = qs.stringify(params); - let webSocket = new WebSocket(wsServer + '/ssh-monitor?' + paramStr); + let webSocket = new WebSocket(`${wsServer}/sessions/${sessionId}/ssh-monitor?${paramStr}`); webSocket.onmessage = (e) => { let msg = Message.parse(e.data); switch (msg['type']) { diff --git a/web/src/components/session/OnlineSession.js b/web/src/components/session/OnlineSession.js index b91ae63..6121458 100644 --- a/web/src/components/session/OnlineSession.js +++ b/web/src/components/session/OnlineSession.js @@ -202,7 +202,6 @@ class OnlineSession extends Component { showMonitor = (record) => { this.setState({ - connectionId: record.connectionId, sessionId: record.id, sessionProtocol: record.protocol, sessionMode: record.mode, @@ -477,7 +476,7 @@ class OnlineSession extends Component { > { this.state.sessionMode === 'guacd' ? -