diff --git a/internal/api/room/handler.go b/internal/api/room/handler.go index 4c425bf7..ef9cc525 100644 --- a/internal/api/room/handler.go +++ b/internal/api/room/handler.go @@ -4,6 +4,8 @@ import ( "context" "net/http" + "github.com/rs/zerolog/log" + "gitlab.com/demodesk/neko/server/pkg/auth" "gitlab.com/demodesk/neko/server/pkg/types" "gitlab.com/demodesk/neko/server/pkg/utils" @@ -13,6 +15,8 @@ type RoomHandler struct { sessions types.SessionManager desktop types.DesktopManager capture types.CaptureManager + + privateModeImage []byte } func New( @@ -20,13 +24,32 @@ func New( desktop types.DesktopManager, capture types.CaptureManager, ) *RoomHandler { - // Init - - return &RoomHandler{ + h := &RoomHandler{ sessions: sessions, desktop: desktop, capture: capture, } + + // generate fallback image for private mode when needed + sessions.OnPrivateModeChanged(func(isPrivateMode bool) { + if !isPrivateMode { + log.Debug().Msg("clearing private mode fallback image") + h.privateModeImage = nil + return + } + + img := h.desktop.GetScreenshotImage() + bytes, err := utils.CreateJPGImage(img, 90) + if err != nil { + log.Err(err).Msg("could not generate private mode fallback image") + return + } + + log.Debug().Msg("using private mode fallback image") + h.privateModeImage = bytes + }) + + return h } func (h *RoomHandler) Route(r types.Router) { diff --git a/internal/api/room/screen.go b/internal/api/room/screen.go index fc16ecc1..9be8c20b 100644 --- a/internal/api/room/screen.go +++ b/internal/api/room/screen.go @@ -4,6 +4,7 @@ import ( "net/http" "strconv" + "gitlab.com/demodesk/neko/server/pkg/auth" "gitlab.com/demodesk/neko/server/pkg/types" "gitlab.com/demodesk/neko/server/pkg/types/event" "gitlab.com/demodesk/neko/server/pkg/types/message" @@ -81,6 +82,19 @@ func (h *RoomHandler) screenShotGet(w http.ResponseWriter, r *http.Request) erro } func (h *RoomHandler) screenCastGet(w http.ResponseWriter, r *http.Request) error { + // display fallback image when private mode is enabled even if screencast is not + if session, ok := auth.GetSession(r); ok && session.PrivateModeEnabled() { + if h.privateModeImage != nil { + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Content-Type", "image/jpeg") + + _, err := w.Write(h.privateModeImage) + return err + } + + return utils.HttpBadRequest("private mode is enabled but no fallback image available") + } + screencast := h.capture.Screencast() if !screencast.Enabled() { return utils.HttpBadRequest("screencast pipeline is not enabled") diff --git a/internal/session/manager.go b/internal/session/manager.go index 5593521f..b0eed57a 100644 --- a/internal/session/manager.go +++ b/internal/session/manager.go @@ -56,6 +56,9 @@ type SessionManagerCtx struct { host types.Session hostMu sync.Mutex + privateMode bool + privateModeMu sync.Mutex + cursors map[types.Session][]types.Cursor cursorsMu sync.Mutex @@ -197,6 +200,39 @@ func (manager *SessionManagerCtx) ClearHost() { manager.SetHost(nil) } +// --- +// private mode +// --- + +func (manager *SessionManagerCtx) SetPrivateMode(isPrivateMode bool) { + manager.privateModeMu.Lock() + + // only if value changed + if manager.privateMode == isPrivateMode { + manager.privateModeMu.Unlock() + return + } + + // update webrtc paused state for all sessions + for _, session := range manager.List() { + if webrtcPeer := session.GetWebRTCPeer(); webrtcPeer != nil { + webrtcPeer.SetPaused(isPrivateMode && !session.Profile().IsAdmin) + } + } + + manager.privateMode = isPrivateMode + manager.privateModeMu.Unlock() + + manager.emmiter.Emit("private_mode_changed", isPrivateMode) +} + +func (manager *SessionManagerCtx) PrivateMode() bool { + manager.privateModeMu.Lock() + defer manager.privateModeMu.Unlock() + + return manager.privateMode +} + // --- // cursors // --- @@ -326,6 +362,12 @@ func (manager *SessionManagerCtx) OnHostChanged(listener func(session types.Sess }) } +func (manager *SessionManagerCtx) OnPrivateModeChanged(listener func(isPrivateMode bool)) { + manager.emmiter.On("private_mode_changed", func(payload ...interface{}) { + listener(payload[0].(bool)) + }) +} + // --- // config // --- diff --git a/internal/session/session.go b/internal/session/session.go index c885aeed..4e46c917 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -44,6 +44,11 @@ func (session *SessionCtx) profileChanged() { if (!session.profile.CanConnect || !session.profile.CanLogin) && session.state.IsConnected { session.GetWebSocketPeer().Destroy("profile changed") } + + // update webrtc paused state + if webrtcPeer := session.GetWebRTCPeer(); webrtcPeer != nil { + webrtcPeer.SetPaused(session.PrivateModeEnabled()) + } } func (session *SessionCtx) State() types.SessionState { @@ -54,6 +59,10 @@ func (session *SessionCtx) IsHost() bool { return session.manager.GetHost() == session } +func (session *SessionCtx) PrivateModeEnabled() bool { + return session.manager.PrivateMode() && !session.profile.IsAdmin +} + func (session *SessionCtx) SetCursor(cursor types.Cursor) { if session.manager.InactiveCursors() && session.profile.SendsInactiveCursor { session.manager.SetCursor(cursor, session) diff --git a/internal/webrtc/manager.go b/internal/webrtc/manager.go index 8684369c..4a121cd3 100644 --- a/internal/webrtc/manager.go +++ b/internal/webrtc/manager.go @@ -200,6 +200,12 @@ func (manager *WebRTCManagerCtx) CreatePeer(session types.Session, videoID strin return videoTrack.SetStream(videoStream) }, + setPaused: func(isPaused bool) { + videoTrack.SetPaused(isPaused) + audioTrack.SetPaused(isPaused) + + // TODO: Send fresh cursor position & image when unpausing. + }, iceTrickle: manager.config.ICETrickle, } @@ -314,12 +320,22 @@ func (manager *WebRTCManagerCtx) CreatePeer(session types.Session, videoID strin }) cursorImage := func(entry *cursor.ImageEntry) { + // TODO: Refactor. + if videoTrack.paused { + return + } + if err := peer.SendCursorImage(entry.Cursor, entry.Image); err != nil { logger.Err(err).Msg("could not send cursor image") } } cursorPosition := func(x, y int) { + // TODO: Refactor. + if videoTrack.paused { + return + } + if session.IsHost() { return } @@ -352,6 +368,11 @@ func (manager *WebRTCManagerCtx) CreatePeer(session types.Session, videoID strin }) dataChannel.OnMessage(func(message webrtc.DataChannelMessage) { + // TODO: Refactor. + if videoTrack.paused { + return + } + if err := manager.handle(message.Data, session); err != nil { logger.Err(err).Msg("data handle failed") } diff --git a/internal/webrtc/peer.go b/internal/webrtc/peer.go index e18a27bd..7bb3c668 100644 --- a/internal/webrtc/peer.go +++ b/internal/webrtc/peer.go @@ -15,6 +15,7 @@ type WebRTCPeerCtx struct { connection *webrtc.PeerConnection dataChannel *webrtc.DataChannel changeVideo func(videoID string) error + setPaused func(isPaused bool) iceTrickle bool } @@ -122,6 +123,19 @@ func (peer *WebRTCPeerCtx) SetVideoID(videoID string) error { return peer.changeVideo(videoID) } +func (peer *WebRTCPeerCtx) SetPaused(isPaused bool) error { + peer.mu.Lock() + defer peer.mu.Unlock() + + if peer.connection == nil { + return types.ErrWebRTCConnectionNotFound + } + + peer.logger.Info().Bool("is_paused", isPaused).Msg("set paused") + peer.setPaused(isPaused) + return nil +} + func (peer *WebRTCPeerCtx) Destroy() { peer.mu.Lock() defer peer.mu.Unlock() diff --git a/internal/webrtc/peerstreamtrack.go b/internal/webrtc/peerstreamtrack.go index a45e347d..b77cab2a 100644 --- a/internal/webrtc/peerstreamtrack.go +++ b/internal/webrtc/peerstreamtrack.go @@ -26,12 +26,17 @@ func (manager *WebRTCManagerCtx) newPeerStreamTrack(stream types.StreamSinkManag peer := &PeerStreamTrack{ logger: logger, track: track, - listener: func(sample types.Sample) { - err := track.WriteSample(media.Sample(sample)) - if err != nil && errors.Is(err, io.ErrClosedPipe) { - logger.Warn().Err(err).Msg("pipeline failed to write") - } - }, + } + + peer.listener = func(sample types.Sample) { + if peer.paused { + return + } + + err := track.WriteSample(media.Sample(sample)) + if err != nil && errors.Is(err, io.ErrClosedPipe) { + logger.Warn().Err(err).Msg("pipeline failed to write") + } } err = peer.SetStream(stream) @@ -41,6 +46,7 @@ func (manager *WebRTCManagerCtx) newPeerStreamTrack(stream types.StreamSinkManag type PeerStreamTrack struct { logger zerolog.Logger track *webrtc.TrackLocalStaticSample + paused bool listener func(sample types.Sample) stream types.StreamSinkManager @@ -92,3 +98,7 @@ func (peer *PeerStreamTrack) AddToConnection(connection *webrtc.PeerConnection) return nil } + +func (peer *PeerStreamTrack) SetPaused(paused bool) { + peer.paused = paused +} diff --git a/internal/websocket/handler/control.go b/internal/websocket/handler/control.go index 96a20de0..364852ea 100644 --- a/internal/websocket/handler/control.go +++ b/internal/websocket/handler/control.go @@ -17,7 +17,7 @@ var ( ) func (h *MessageHandlerCtx) controlRelease(session types.Session) error { - if !session.Profile().CanHost { + if !session.Profile().CanHost || session.PrivateModeEnabled() { return ErrIsNotAllowedToHost } @@ -32,7 +32,7 @@ func (h *MessageHandlerCtx) controlRelease(session types.Session) error { } func (h *MessageHandlerCtx) controlRequest(session types.Session) error { - if !session.Profile().CanHost { + if !session.Profile().CanHost || session.PrivateModeEnabled() { return ErrIsNotAllowedToHost } diff --git a/internal/websocket/handler/signal.go b/internal/websocket/handler/signal.go index f04b31a8..1b61e0ea 100644 --- a/internal/websocket/handler/signal.go +++ b/internal/websocket/handler/signal.go @@ -24,6 +24,11 @@ func (h *MessageHandlerCtx) signalRequest(session types.Session, payload *messag return err } + // set webrtc as paused if session has private mode enabled + if webrtcPeer := session.GetWebRTCPeer(); webrtcPeer != nil && session.PrivateModeEnabled() { + webrtcPeer.SetPaused(true) + } + session.Send( event.SIGNAL_PROVIDE, message.SignalProvide{ diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 9c558577..98620387 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -54,6 +54,10 @@ func CanHostOnly(w http.ResponseWriter, r *http.Request) (context.Context, error return nil, utils.HttpForbidden("session cannot host") } + if session.PrivateModeEnabled() { + return nil, utils.HttpUnprocessableEntity("private mode is enabled") + } + return nil, nil } diff --git a/pkg/types/session.go b/pkg/types/session.go index 8bc0f3a4..f3c3a39d 100644 --- a/pkg/types/session.go +++ b/pkg/types/session.go @@ -27,6 +27,7 @@ type Session interface { Profile() MemberProfile State() SessionState IsHost() bool + PrivateModeEnabled() bool // cursor SetCursor(cursor Cursor) @@ -55,6 +56,9 @@ type SessionManager interface { GetHost() Session ClearHost() + SetPrivateMode(isPrivateMode bool) + PrivateMode() bool + SetCursor(cursor Cursor, session Session) PopCursors() map[Session][]Cursor @@ -69,6 +73,7 @@ type SessionManager interface { OnProfileChanged(listener func(session Session)) OnStateChanged(listener func(session Session)) OnHostChanged(listener func(session Session)) + OnPrivateModeChanged(listener func(isPrivateMode bool)) ImplicitHosting() bool InactiveCursors() bool diff --git a/pkg/types/webrtc.go b/pkg/types/webrtc.go index 12c92973..ab323431 100644 --- a/pkg/types/webrtc.go +++ b/pkg/types/webrtc.go @@ -26,6 +26,8 @@ type WebRTCPeer interface { SetCandidate(candidate webrtc.ICECandidateInit) error SetVideoID(videoID string) error + SetPaused(isPaused bool) error + SendCursorPosition(x, y int) error SendCursorImage(cur *CursorImage, img []byte) error