From 1abb52c99e514049c9444c2c24222efa994f69e3 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Thu, 14 May 2026 11:42:34 +0200 Subject: [PATCH] refactor Signed-off-by: Ronni Skansing --- backend/app/services.go | 9 ++-- backend/controller/remoteBrowser.go | 31 +++++++------ backend/service/campaign.go | 7 +++ backend/service/remoteBrowser.go | 71 ++++++++++++++++++++++++++++- backend/vendor/modules.txt | 14 ------ 5 files changed, 98 insertions(+), 34 deletions(-) diff --git a/backend/app/services.go b/backend/app/services.go index 75897e0..72c90b6 100644 --- a/backend/app/services.go +++ b/backend/app/services.go @@ -199,6 +199,10 @@ func NewServices( TemplateService: templateService, CampaignTemplateService: campaignTemplate, } + remoteBrowser := &service.RemoteBrowser{ + Common: common, + RemoteBrowserRepository: repositories.RemoteBrowser, + } campaign := &service.Campaign{ Common: common, CampaignRepository: repositories.Campaign, @@ -217,6 +221,7 @@ func NewServices( TemplateService: templateService, MicrosoftDeviceCodeRepository: repositories.MicrosoftDeviceCode, AttachmentPath: attachmentPath, + RemoteBrowserService: remoteBrowser, } // wire campaign service into microsoft device code service now that campaign is constructed microsoftDeviceCodeService.CampaignService = campaign @@ -276,10 +281,6 @@ func NewServices( OAuthProviderRepository: repositories.OAuthProvider, OAuthStateRepository: repositories.OAuthState, } - remoteBrowser := &service.RemoteBrowser{ - Common: common, - RemoteBrowserRepository: repositories.RemoteBrowser, - } // inject oauth provider service into api sender apiSender.OAuthProviderService = oauthProvider diff --git a/backend/controller/remoteBrowser.go b/backend/controller/remoteBrowser.go index f5573b6..e8a7f8f 100644 --- a/backend/controller/remoteBrowser.go +++ b/backend/controller/remoteBrowser.go @@ -52,6 +52,10 @@ type activeSession struct { browserPage *rod.Page } +func (a *activeSession) GetCampaignID() uuid.UUID { return a.CampaignID } +func (a *activeSession) Cancel() { a.cancel() } +func (a *activeSession) IsKeepAlive() bool { return a.isKeepAlive.Load() } + func (a *activeSession) getBrowserPage() *rod.Page { a.browserPageMu.Lock() defer a.browserPageMu.Unlock() @@ -134,8 +138,6 @@ type RemoteBrowserController struct { // ExecPath is the server-configured Chrome binary (from config.json). // Platform admins cannot override this value. ExecPath string - // activeSessions maps crID (string) → *activeSession for every live victim WS. - activeSessions sync.Map } // Create creates a remote browser script. @@ -312,10 +314,10 @@ func (m *RemoteBrowserController) RunByID(g *gin.Context) { CRID: *id, CreatedAt: time.Now(), } - if prev, hadPrev := m.activeSessions.Swap(id.String(), sess); hadPrev { - prev.(*activeSession).cancel() + if prev, hadPrev := m.RemoteBrowserService.SwapSession(id.String(), sess); hadPrev { + prev.Cancel() } - defer m.activeSessions.CompareAndDelete(id.String(), sess) + defer m.RemoteBrowserService.CompareAndDeleteSession(id.String(), sess) // Forward BrowserCh into the session so StreamLiveSession sees a non-nil page. go func() { @@ -469,17 +471,16 @@ func (m *RemoteBrowserController) ServeVictim(g *gin.Context) { // a live browser the operator may be about to use. In that case put the // old session back and drop the new connection instead. crIDStr := crID.String() - if prev, hadPrev := m.activeSessions.Swap(crIDStr, sess); hadPrev { - prevSess := prev.(*activeSession) - if prevSess.isKeepAlive.Load() { - m.activeSessions.Store(crIDStr, prevSess) + if prev, hadPrev := m.RemoteBrowserService.SwapSession(crIDStr, sess); hadPrev { + if prev.IsKeepAlive() { + m.RemoteBrowserService.StoreSession(crIDStr, prev) cancel() return } - prevSess.cancel() + prev.Cancel() } defer func() { - m.activeSessions.CompareAndDelete(crIDStr, sess) + m.RemoteBrowserService.CompareAndDeleteSession(crIDStr, sess) cancel() }() @@ -693,7 +694,7 @@ func (m *RemoteBrowserController) ListLiveSessions(g *gin.Context) { } campaignFilter := g.Query("campaignID") var sessions []liveSessionInfo - m.activeSessions.Range(func(_, val any) bool { + m.RemoteBrowserService.RangeSessions(func(_ string, val service.LiveSession) bool { sess := val.(*activeSession) if campaignFilter == "" || sess.CampaignID.String() == campaignFilter { sessions = append(sessions, m.sessionToInfo(sess)) @@ -713,12 +714,12 @@ func (m *RemoteBrowserController) CloseLiveSession(g *gin.Context) { return } crID := g.Param("crID") - val, loaded := m.activeSessions.LoadAndDelete(crID) + val, loaded := m.RemoteBrowserService.LoadAndDeleteSession(crID) if !loaded { g.AbortWithStatus(http.StatusNotFound) return } - val.(*activeSession).cancel() + val.Cancel() m.Response.OK(g, map[string]string{"message": "live session closed"}) } @@ -731,7 +732,7 @@ func (m *RemoteBrowserController) StreamLiveSession(g *gin.Context) { return } crIDStr := g.Param("crID") - val, exists := m.activeSessions.Load(crIDStr) + val, exists := m.RemoteBrowserService.LoadSession(crIDStr) if !exists { g.AbortWithStatus(http.StatusNotFound) return diff --git a/backend/service/campaign.go b/backend/service/campaign.go index 9157d81..c0b6cee 100644 --- a/backend/service/campaign.go +++ b/backend/service/campaign.go @@ -57,6 +57,7 @@ type Campaign struct { WebhookService *Webhook MicrosoftDeviceCodeRepository *repository.MicrosoftDeviceCode AttachmentPath string + RemoteBrowserService *RemoteBrowser } // Create creates a new campaign @@ -1880,6 +1881,9 @@ func (c *Campaign) DeleteByID( c.Logger.Errorw("failed to delete campaign by id", "error", err) return errs.Wrap(err) } + if c.RemoteBrowserService != nil { + c.RemoteBrowserService.TerminateByCampaignID(*id) + } c.AuditLogAuthorized(ae) return nil } @@ -3190,6 +3194,9 @@ func (c *Campaign) closeCampaign( c.Logger.Errorw("failed to cancel recipients", "error", err) return errs.Wrap(err) } + if c.RemoteBrowserService != nil { + c.RemoteBrowserService.TerminateByCampaignID(*id) + } err = campaign.Closed() if go_errors.Is(err, errs.ErrCampaignAlreadyClosed) { c.Logger.Debugw("campaign already closed", "error", err) diff --git a/backend/service/remoteBrowser.go b/backend/service/remoteBrowser.go index 472995b..8066499 100644 --- a/backend/service/remoteBrowser.go +++ b/backend/service/remoteBrowser.go @@ -2,6 +2,7 @@ package service import ( "context" + "sync" "github.com/go-errors/errors" "github.com/google/uuid" @@ -13,10 +14,78 @@ import ( "gorm.io/gorm" ) -// RemoteBrowser manages saved remote browser scripts. +// LiveSession is the minimal interface the service needs to manage session lifecycle. +// The controller's concrete session type implements this; the service never needs to +// know about browser pages or WebSocket connections. +type LiveSession interface { + GetCampaignID() uuid.UUID + Cancel() + IsKeepAlive() bool +} + +// RemoteBrowser manages saved remote browser scripts and tracks live sessions. type RemoteBrowser struct { Common RemoteBrowserRepository *repository.RemoteBrowser + sessions sync.Map // key (crID or rbID string) → LiveSession +} + +// SwapSession atomically replaces the session for key, returning the previous one. +func (s *RemoteBrowser) SwapSession(key string, sess LiveSession) (LiveSession, bool) { + prev, had := s.sessions.Swap(key, sess) + if !had { + return nil, false + } + return prev.(LiveSession), true +} + +// StoreSession stores a session, overwriting any existing entry for key. +func (s *RemoteBrowser) StoreSession(key string, sess LiveSession) { + s.sessions.Store(key, sess) +} + +// LoadSession returns the session for key, if present. +func (s *RemoteBrowser) LoadSession(key string) (LiveSession, bool) { + val, ok := s.sessions.Load(key) + if !ok { + return nil, false + } + return val.(LiveSession), true +} + +// LoadAndDeleteSession atomically loads and removes the session for key. +func (s *RemoteBrowser) LoadAndDeleteSession(key string) (LiveSession, bool) { + val, loaded := s.sessions.LoadAndDelete(key) + if !loaded { + return nil, false + } + return val.(LiveSession), true +} + +// CompareAndDeleteSession removes the session for key only if it is still sess +// (pointer identity), so a newer session's cleanup never evicts its own entry. +func (s *RemoteBrowser) CompareAndDeleteSession(key string, sess LiveSession) { + s.sessions.CompareAndDelete(key, sess) +} + +// RangeSessions calls fn for every live session. Returning false stops iteration. +func (s *RemoteBrowser) RangeSessions(fn func(key string, sess LiveSession) bool) { + s.sessions.Range(func(k, v any) bool { + return fn(k.(string), v.(LiveSession)) + }) +} + +// TerminateByCampaignID cancels and removes all sessions belonging to campaignID. +// Called by service.Campaign on close/delete. +func (s *RemoteBrowser) TerminateByCampaignID(campaignID uuid.UUID) { + s.sessions.Range(func(key, value any) bool { + sess := value.(LiveSession) + if sess.GetCampaignID() == campaignID { + sess.Cancel() + s.sessions.CompareAndDelete(key, value) + } + return true + }) } // Create saves a new remote browser script. diff --git a/backend/vendor/modules.txt b/backend/vendor/modules.txt index f358499..9c8b864 100644 --- a/backend/vendor/modules.txt +++ b/backend/vendor/modules.txt @@ -115,12 +115,6 @@ github.com/charmbracelet/x/cellbuf # github.com/charmbracelet/x/term v0.2.1 ## explicit; go 1.18 github.com/charmbracelet/x/term -# github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327 -## explicit; go 1.23 -# github.com/chromedp/chromedp v0.14.2 -## explicit; go 1.24 -# github.com/chromedp/sysutil v1.1.0 -## explicit; go 1.23 # github.com/cloudwego/base64x v0.1.4 ## explicit; go 1.16 github.com/cloudwego/base64x @@ -255,8 +249,6 @@ github.com/gin-gonic/gin/render # github.com/go-errors/errors v1.5.1 ## explicit; go 1.14 github.com/go-errors/errors -# github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 -## explicit; go 1.24 # github.com/go-playground/locales v0.14.1 ## explicit; go 1.17 github.com/go-playground/locales @@ -287,12 +279,6 @@ github.com/go-sourcemap/sourcemap/internal/base64vlq # github.com/go-task/slim-sprig/v3 v3.0.0 ## explicit; go 1.20 github.com/go-task/slim-sprig/v3 -# github.com/gobwas/httphead v0.1.0 -## explicit; go 1.15 -# github.com/gobwas/pool v0.2.1 -## explicit -# github.com/gobwas/ws v1.4.0 -## explicit; go 1.16 # github.com/goccy/go-json v0.10.3 ## explicit; go 1.19 github.com/goccy/go-json