package protocols import ( "fmt" "strings" "time" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" "github.com/samber/lo" "github.com/spf13/cast" "go.uber.org/zap" "golang.org/x/sync/errgroup" "github.com/veops/oneterm/internal/guacd" "github.com/veops/oneterm/internal/model" "github.com/veops/oneterm/internal/service" gsession "github.com/veops/oneterm/internal/session" myErrors "github.com/veops/oneterm/pkg/errors" "github.com/veops/oneterm/pkg/logger" ) // ConnectGuacd connects to Guacamole server func ConnectGuacd(ctx *gin.Context, sess *gsession.Session, asset *model.Asset, account *model.Account, gateway *model.Gateway) (err error) { chs := sess.Chans defer func() { if err != nil { chs.ErrChan <- err } }() w, h, dpi := cast.ToInt(ctx.Query("w")), cast.ToInt(ctx.Query("h")), cast.ToInt(ctx.Query("dpi")) // Get permissions for guacd connection using batch check permissions := &guacd.PermissionInfo{} // Check all relevant permissions in one batch call batchResult, err := service.DefaultAuthService.HasAuthorizationV2(ctx, sess, model.ActionCopy, model.ActionPaste, model.ActionFileUpload, model.ActionFileDownload) if err != nil { logger.L().Warn("Failed to check permissions, using default settings", zap.Error(err)) // Continue with default (denied) permissions if check fails } else { // Extract individual permissions from batch result permissions.AllowCopy = batchResult.IsAllowed(model.ActionCopy) permissions.AllowPaste = batchResult.IsAllowed(model.ActionPaste) permissions.AllowFileUpload = batchResult.IsAllowed(model.ActionFileUpload) permissions.AllowFileDownload = batchResult.IsAllowed(model.ActionFileDownload) } // Clean protocol parameter - remove port number if present for guacd compatibility cleanProtocol := sess.Protocol if strings.Contains(sess.Protocol, ":") { cleanProtocol = strings.Split(sess.Protocol, ":")[0] } t, err := guacd.NewTunnel("", sess.SessionId, w, h, dpi, cleanProtocol, asset, account, gateway, permissions) if err != nil { logger.L().Error("guacd tunnel failed", zap.Error(err)) return } defer t.Close() sess.ConnectionId = t.ConnectionId sess.GuacdTunnel = t chs.ErrChan <- nil sess.G.Go(func() error { for { select { case <-sess.Gctx.Done(): return nil default: p, err := t.Read() if err != nil { return err } if len(p) <= 0 { continue } chs.OutChan <- p } } }) sess.G.Go(func() error { for { select { case <-sess.Gctx.Done(): return nil case <-chs.AwayChan: return fmt.Errorf("away") case in := <-chs.InChan: t.Write(in) } } }) sess.G.Wait() return } // HandleGuacd handles Guacamole sessions func HandleGuacd(sess *gsession.Session) (err error) { defer func() { sess.GuacdTunnel.Disconnect() sess.Status = model.SESSIONSTATUS_OFFLINE sess.ClosedAt = lo.ToPtr(time.Now()) if err = gsession.UpsertSession(sess); err != nil { logger.L().Error("offline guacd session failed", zap.Error(err)) return } }() chs := sess.Chans tk := time.NewTicker(time.Minute) assetService := service.NewAssetService() sess.G.Go(func() error { return Read(sess) }) sess.G.Go(func() error { for { select { case <-sess.Gctx.Done(): return nil case <-sess.IdleTk.C: return &myErrors.ApiError{Code: myErrors.ErrIdleTimeout, Data: map[string]any{"second": model.GlobalConfig.Load().Timeout}} case <-tk.C: asset, err := assetService.GetById(sess.Gctx, sess.AssetId) if err != nil { continue } if CheckTime(asset.AccessAuth) && (sess.ShareId == 0 || time.Now().Before(sess.ShareEnd)) { continue } return &myErrors.ApiError{Code: myErrors.ErrAccessTime} case closeBy := <-chs.CloseChan: return &myErrors.ApiError{Code: myErrors.ErrAdminClose, Data: map[string]any{"admin": closeBy}} case err := <-chs.ErrChan: return err case out := <-chs.OutChan: sess.Ws.WriteMessage(websocket.TextMessage, out) } } }) if err = sess.G.Wait(); err != nil { logger.L().Debug("sess wait end guacd", zap.String("id", sess.SessionId), zap.Error(err)) } return } // MonitGuacd handles monitoring of Guacamole sessions func MonitGuacd(ctx *gin.Context, sess *gsession.Session, chs *gsession.SessionChans, ws *websocket.Conn) (err error) { w, h, dpi := cast.ToInt(ctx.Query("w")), cast.ToInt(ctx.Query("h")), cast.ToInt(ctx.Query("dpi")) defer func() { chs.ErrChan <- err }() // For monitoring, no permissions needed since it's read-only t, err := guacd.NewTunnel(sess.ConnectionId, "", w, h, dpi, ":", nil, nil, nil, nil) if err != nil { logger.L().Error("guacd tunnel failed", zap.Error(err)) return } defer t.Disconnect() g, gctx := errgroup.WithContext(ctx) g.Go(func() error { for { select { case <-gctx.Done(): return nil default: p, err := t.Read() if err != nil { logger.L().Debug("read instruction failed", zap.Error(err)) return err } if len(p) <= 0 { continue } chs.OutChan <- p } } }) g.Go(func() error { for { select { case <-sess.Chans.AwayChan: err := fmt.Errorf("monitored session closed") ws.WriteMessage(websocket.TextMessage, NewInstruction("disconnect", err.Error()).Bytes()) return err case err := <-chs.ErrChan: return err case out := <-chs.OutChan: ws.WriteMessage(websocket.TextMessage, out) case in := <-chs.InChan: t.Write(in) } } }) if err = g.Wait(); err != nil { logger.L().Warn("monit guacd stopped", zap.Error(err)) } return }