Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2026-05-14 11:42:34 +02:00
parent 35bd34f57f
commit 1abb52c99e
5 changed files with 98 additions and 34 deletions
+5 -4
View File
@@ -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
+16 -15
View File
@@ -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
+7
View File
@@ -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)
+70 -1
View File
@@ -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.
-14
View File
@@ -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