diff --git a/docs/changelog.md b/docs/changelog.md index 29898ab9..d9de4321 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -7,6 +7,7 @@ - Ability to set locks from config `NEKO_LOCKS=control login`. - Added control protection - users can gain control only if at least one admin is in the room `NEKO_CONTROL_PROTECTION=true`. - Emotes sending on mouse down holding. +- Include `banned`, `locked`, `server_started_at`, `last_admin_left_at`, `last_user_left_at`, `control_protection` data in stats. ### Misc - ARM-based images not bound to Raspberry Pi only. diff --git a/server/internal/types/websocket.go b/server/internal/types/websocket.go index a2b6e6e5..9000e9ba 100644 --- a/server/internal/types/websocket.go +++ b/server/internal/types/websocket.go @@ -1,11 +1,23 @@ package types -import "net/http" +import ( + "net/http" + "time" +) type Stats struct { Connections uint32 `json:"connections"` Host string `json:"host"` Members []*Member `json:"members"` + + Banned map[string]string `json:"banned"` // IP -> session ID (that banned it) + Locked map[string]string `json:"locked"` // resource name -> session ID (that locked it) + + ServerStartedAt time.Time `json:"server_started_at"` + LastAdminLeftAt *time.Time `json:"last_admin_left_at"` + LastUserLeftAt *time.Time `json:"last_user_left_at"` + + ControlProtection bool `json:"control_protection"` } type WebSocket interface { diff --git a/server/internal/websocket/admin.go b/server/internal/websocket/admin.go index be6c3de8..b7786b9f 100644 --- a/server/internal/websocket/admin.go +++ b/server/internal/websocket/admin.go @@ -292,8 +292,7 @@ func (h *MessageHandler) adminBan(id string, session types.Session, payload *mes } h.logger.Debug().Str("address", remote).Msg("adding address to banned") - - h.banned[address[0]] = true + h.banned[address[0]] = id if err := target.Kick("banned"); err != nil { return err diff --git a/server/internal/websocket/handler.go b/server/internal/websocket/handler.go index 1d290390..693e572e 100644 --- a/server/internal/websocket/handler.go +++ b/server/internal/websocket/handler.go @@ -18,8 +18,8 @@ type MessageHandler struct { webrtc types.WebRTCManager remote types.RemoteManager broadcast types.BroadcastManager - banned map[string]bool - locked map[string]string + banned map[string]string // IP -> session ID (that banned it) + locked map[string]string // resource name -> session ID (that locked it) } func (h *MessageHandler) Connected(admin bool, socket *WebSocket) (bool, string) { @@ -27,8 +27,8 @@ func (h *MessageHandler) Connected(admin bool, socket *WebSocket) (bool, string) if address == "" { h.logger.Debug().Msg("no remote address") } else { - ok, banned := h.banned[address] - if ok && banned { + _, ok := h.banned[address] + if ok { h.logger.Debug().Str("address", address).Msg("banned") return false, "banned" } diff --git a/server/internal/websocket/websocket.go b/server/internal/websocket/websocket.go index 2da6eb1d..bbb37750 100644 --- a/server/internal/websocket/websocket.go +++ b/server/internal/websocket/websocket.go @@ -18,12 +18,20 @@ import ( "m1k1o/neko/internal/utils" ) -const CONTROL_PROTECTION_SESSION = "control_protection" +const CONTROL_PROTECTION_SESSION = "by_control_protection" func New(sessions types.SessionManager, remote types.RemoteManager, broadcast types.BroadcastManager, webrtc types.WebRTCManager, conf *config.WebSocket) *WebSocketHandler { logger := log.With().Str("module", "websocket").Logger() locks := make(map[string]string) + + // if control protection is enabled + if conf.ControlProtection { + locks["control"] = CONTROL_PROTECTION_SESSION + logger.Info().Msgf("control locked on behalf of control protection") + } + + // apply default locks for _, lock := range conf.Locks { locks[lock] = "" // empty session ID } @@ -32,12 +40,6 @@ func New(sessions types.SessionManager, remote types.RemoteManager, broadcast ty logger.Info().Msgf("locked resources: %+v", conf.Locks) } - // if control protection is enabled - if conf.ControlProtection { - locks["control"] = CONTROL_PROTECTION_SESSION - logger.Info().Msgf("control locked on behalf of control protection") - } - return &WebSocketHandler{ logger: logger, shutdown: make(chan interface{}), @@ -55,10 +57,10 @@ func New(sessions types.SessionManager, remote types.RemoteManager, broadcast ty broadcast: broadcast, sessions: sessions, webrtc: webrtc, - banned: make(map[string]bool), + banned: make(map[string]string), locked: locks, }, - conns: 0, + serverStartedAt: time.Now(), } } @@ -74,7 +76,12 @@ type WebSocketHandler struct { remote types.RemoteManager conf *config.WebSocket handler *MessageHandler - conns uint32 + + // stats + conns uint32 + serverStartedAt time.Time + lastAdminLeftAt *time.Time + lastUserLeftAt *time.Time } func (ws *WebSocketHandler) Start() { @@ -109,6 +116,13 @@ func (ws *WebSocketHandler) Start() { ws.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_UNLOCK) } } + + // remove outdated stats + if session.Admin() { + ws.lastAdminLeftAt = nil + } else { + ws.lastUserLeftAt = nil + } }) ws.sessions.OnDestroy(func(id string, session types.Session) { @@ -118,8 +132,13 @@ func (ws *WebSocketHandler) Start() { ws.logger.Debug().Str("id", id).Msg("session destroyed") } + membersCount := len(ws.sessions.Members()) + adminCount := len(ws.sessions.Admins()) + // if control protection is enabled and no admin - if ws.conf.ControlProtection && len(ws.sessions.Admins()) == 0 { + // and room is not locked, lock + _, ok := ws.handler.locked["control"] + if !ok && ws.conf.ControlProtection && adminCount == 0 { ws.handler.locked["control"] = CONTROL_PROTECTION_SESSION ws.logger.Info().Msgf("control locked and released on behalf of control protection") ws.handler.adminRelease(id, session) @@ -133,6 +152,18 @@ func (ws *WebSocketHandler) Start() { ws.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_LOCK) } } + + // if this was the last admin + if session.Admin() && adminCount == 0 { + now := time.Now() + ws.lastAdminLeftAt = &now + } + + // if this was the last user + if !session.Admin() && membersCount-adminCount == 0 { + now := time.Now() + ws.lastUserLeftAt = &now + } }) ws.wg.Add(1) @@ -274,6 +305,15 @@ func (ws *WebSocketHandler) Stats() types.Stats { Connections: atomic.LoadUint32(&ws.conns), Host: host, Members: ws.sessions.Members(), + + Banned: ws.handler.banned, + Locked: ws.handler.locked, + + ServerStartedAt: ws.serverStartedAt, + LastAdminLeftAt: ws.lastAdminLeftAt, + LastUserLeftAt: ws.lastUserLeftAt, + + ControlProtection: ws.conf.ControlProtection, } }