From 7ffa6b4dbd77b8f816e93b6f96af69b53edccd30 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Mon, 4 Sep 2023 13:36:12 -0700 Subject: [PATCH] More thorough blocking behavior --- Protocol.md | 25 ++++++++++- docs/Configuration.md | 89 +++++++++++++++++++++++++++------------- pkg/handlers.go | 32 ++++++++++++++- pkg/messages/messages.go | 1 + pkg/websocket.go | 46 +++++++++++++++++++-- 5 files changed, 159 insertions(+), 34 deletions(-) diff --git a/Protocol.md b/Protocol.md index e19eb6e..ffe0907 100644 --- a/Protocol.md +++ b/Protocol.md @@ -343,11 +343,32 @@ The `unmute` action does the opposite and removes the mute status: } ``` +## Block + +Sent by: Client. + +The block command places a hard block between the current user and the target. + +When either user blocks the other: + +* They do not see each other in the Who's Online list at all. +* They can not see each other's messages, including presence messages. + +**Note:** the chat page currently does not have a front-end button to block a user. This feature is currently used by the Blocklist feature to apply a block to a set of users at once upon join. + +```javascript +// Client Block +{ + "action": "block", + "username": "target" +} +``` + ## Blocklist Sent by: Client. -The blocklist command is basically a bulk mute for (potentially) many usernames at once. +The blocklist command is basically a bulk block for (potentially) many usernames at once. ```javascript // Client blocklist @@ -359,7 +380,7 @@ The blocklist command is basically a bulk mute for (potentially) many usernames How this works: if you have an existing website and use JWT authentication to sign users into chat, your site can pre-emptively sync the user's block list **before** the user enters the room, using the `/api/blocklist` endpoint (see the README.md for BareRTC). -The chat server holds onto blocklists temporarily in memory: when that user loads the chat room (with a JWT token!), the front-end page receives the cached blocklist. As part of the "on connected" handler, the chat page sends the `blocklist` command over WebSocket to perform a mass mute on these users in one go. +The chat server holds onto blocklists temporarily in memory: when that user loads the chat room (with a JWT token!), the front-end page receives the cached blocklist. As part of the "on connected" handler, the chat page sends the `blocklist` command over WebSocket to perform a mass block on these users in one go. The reason for this workflow is in case the chat server is rebooted _while_ the user is in the room. The cached blocklist pushed by your website is forgotten by the chat server back-end, but the client's page was still open with the cached blocklist already, and it will send the `blocklist` command to the server when it reconnects, eliminating any gaps. diff --git a/docs/Configuration.md b/docs/Configuration.md index 3a63acf..ebead51 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -3,21 +3,28 @@ On first run it will create the default settings.toml file for you which you may then customize to your liking: ```toml -Version = 2 +Version = 7 Title = "BareRTC" Branding = "BareRTC" -WebsiteURL = "https://www.example.com" -CORSHosts = ["https://www.example.com"] +WebsiteURL = "http://www.example.com" +CORSHosts = ["http://www.example.com"] +AdminAPIKey = "e635e463-7987-4788-94f3-671a5c2a589f" PermitNSFW = true -UseXForwardedFor = true -WebSocketReadLimit = 41943040 +UseXForwardedFor = false +WebSocketReadLimit = 40971520 MaxImageWidth = 1280 PreviewImageWidth = 360 [JWT] - Enabled = false + Enabled = true Strict = true - SecretKey = "" + SecretKey = "05c45344-1c52-430b-beb9-c3f64ff7ed12" + LandingPageURL = "https://www.example.com/enter-chat" + +[TURN] + URLs = ["stun:stun.l.google.com:19302"] + Username = "" + Credential = "" [[PublicChannels]] ID = "lobby" @@ -29,28 +36,54 @@ PreviewImageWidth = 360 ID = "offtopic" Name = "Off Topic" WelcomeMessages = ["Welcome to the Off Topic channel!"] + +[VIP] + Name = "VIP" + Branding = "VIP Members" + Icon = "fa fa-circle" + MutuallySecret = false ``` A description of the config directives includes: -* Website settings: - * **Title** goes in the title bar of the chat page. - * **Branding** is the title shown in the corner of the page. HTML is permitted here! You may write an `` tag to embed an image or use custom markup to color and prettify your logo. - * **WebsiteURL** is the base URL of your actual website which is used in a couple of places: - * The About page will link to your website. - * If using [JWT authentication](#authentication), avatar and profile URLs may be relative (beginning with a "/") and will append to your website URL to safe space on the JWT token size! - * **UseXForwardedFor**: set it to true and (for logging) the user's remote IP will use the X-Real-IP header or the first address in X-Forwarded-For. Set this if you run the app behind a proxy like nginx if you want IPs not to be all localhost. - * **CORSHosts**: your website's domain names that will be allowed to access [JSON APIs](#JSON APIs), like `/api/statistics`. - * **PermitNSFW**: for user webcam streams, expressly permit "NSFW" content if the user opts in to mark their feed as such. Setting this will enable pop-up modals regarding NSFW video and give broadcasters an opt-in button, which will warn other users before they click in to watch. - * **WebSocketReadLimit**: sets a size limit for WebSocket messages - it essentially also caps the max upload size for shared images (add a buffer as images will be base64 encoded on upload). - * **MaxImageWidth**: for pictures shared in chat the server will resize them down to no larger than this width for the full size view. - * **PreviewImageWidth**: to not flood the chat, the image in chat is this wide and users can click it to see the MaxImageWidth in a lightbox modal. -* **JWT**: settings for JWT [Authentication](#authentication). - * Enabled (bool): activate the JWT token authentication feature. - * Strict (bool): if true, **only** valid signed JWT tokens may log in. If false, users with no/invalid token can enter their own username without authentication. - * SecretKey (string): the JWT signing secret shared with your back-end app. -* **PublicChannels**: list the public channels and their configuration. The default channel will be the first one listed. - * ID (string): an arbitrary 'username' for the chat channel, like "lobby". - * Name (string): the user friendly name for the channel, like "Off Topic" - * Icon (string, optional): CSS class names for FontAwesome icon for the channel, like "fa fa-message" - * WelcomeMessages ([]string, optional): messages that are delivered by ChatServer to the user when they connect to the server. Useful to give an introduction to each channel, list its rules, etc. +## Website Settings + +* **Version** number for the settings file itself. When new features are added, the Version will increment and your settings.toml will be written to disk with sensible defaults filled in for the new options. +* **Title** goes in the title bar of the chat page. +* **Branding** is the title shown in the corner of the page. HTML is permitted here! You may write an `` tag to embed an image or use custom markup to color and prettify your logo. +* **WebsiteURL** is the base URL of your actual website which is used in a couple of places: + * The About page will link to your website. + * If using [JWT authentication](#authentication), avatar and profile URLs may be relative (beginning with a "/") and will append to your website URL to safe space on the JWT token size! +* **CORSHosts** names HTTP hosts for Cross Origin Resource Sharing. Usually, this will be the same as your WebsiteURL. This feature is used with the [Web API](API.md) if your front-end page needs to call e.g. the /api/statistics endpoint on BareRTC. +* **AdminAPIKey** is a shared secret authentication key for the admin API endpoints. +* **PermitNSFW**: for user webcam streams, expressly permit "NSFW" content if the user opts in to mark their feed as such. Setting this will enable pop-up modals regarding NSFW video and give broadcasters an opt-in button, which will warn other users before they click in to watch. +* **UseXForwardedFor**: set it to true and (for logging) the user's remote IP will use the X-Real-IP header or the first address in X-Forwarded-For. Set this if you run the app behind a proxy like nginx if you want IPs not to be all localhost. +* **WebSocketReadLimit**: sets a size limit for WebSocket messages - it essentially also caps the max upload size for shared images (add a buffer as images will be base64 encoded on upload). +* **MaxImageWidth**: for pictures shared in chat the server will resize them down to no larger than this width for the full size view. +* **PreviewImageWidth**: to not flood the chat, the image in chat is this wide and users can click it to see the MaxImageWidth in a lightbox modal. + +## JWT Authentication + +Settings for JWT [Authentication](#authentication): + +* **Enabled** (bool): activate the JWT token authentication feature. +* **Strict** (bool): if true, **only** valid signed JWT tokens may log in. If false, users with no/invalid token can enter their own username without authentication. +* **SecretKey** (string): the JWT signing secret shared with your back-end app. + +## Public Channels + +Settings for the default public text channels of your room. + +* **ID** (string): an arbitrary 'username' for the chat channel, like "lobby". +* **Name** (string): the user friendly name for the channel, like "Off Topic" +* **Icon** (string, optional): CSS class names for FontAwesome icon for the channel, like "fa fa-message" +* **WelcomeMessages** ([]string, optional): messages that are delivered by ChatServer to the user when they connect to the server. Useful to give an introduction to each channel, list its rules, etc. + +## VIP Status + +If using JWT authentication, your website can mark some users as VIPs when sending them over to the chat. The `[VIP]` section of settings.toml lets you customize the branding and behavior in BareRTC: + +* **Name** (string): what you call your VIP users, used in mouse-over tooltips. +* **Branding** (string): HTML supported, this will appear in webcam sharing modals to "make my cam only visible to fellow VIP users" +* **Icon** (string): icon CSS name from Font Awesome. +* **MutuallySecret** (bool): if true, the VIP features are hidden and only visible to people who are, themselves, VIP. For example, the icon on the Who List will only show to VIP users but non-VIP will not see the icon. \ No newline at end of file diff --git a/pkg/handlers.go b/pkg/handlers.go index 88ab71c..149e655 100644 --- a/pkg/handlers.go +++ b/pkg/handlers.go @@ -179,6 +179,11 @@ func (s *Server) OnMessage(sub *Subscriber, msg messages.Message) { return } + // If there is blocking happening, do not send. + if sub.Blocks(rcpt) { + return + } + if err := s.SendTo(msg.Channel, message); err != nil { sub.ChatServer("Your message could not be delivered: %s", err) } @@ -289,6 +294,11 @@ func (s *Server) OnFile(sub *Subscriber, msg messages.Message) { return } + // If there is blocking happening, do not send. + if sub.Blocks(rcpt) { + return + } + if err := s.SendTo(msg.Channel, message); err != nil { sub.ChatServer("Your message could not be delivered: %s", err) } @@ -417,13 +427,33 @@ func (s *Server) OnMute(sub *Subscriber, msg messages.Message, mute bool) { s.SendWhoList() } +// OnBlock is a user placing a hard block (hide from) another user. +func (s *Server) OnBlock(sub *Subscriber, msg messages.Message) { + log.Info("%s blocks %s: %v", sub.Username, msg.Username) + + // If the subject of the block is an admin, return an error. + if other, err := s.GetSubscriber(msg.Username); err == nil && other.IsAdmin() { + sub.ChatServer( + "You are not allowed to block a chat operator.", + ) + return + } + + sub.muteMu.Lock() + sub.blocked[msg.Username] = struct{}{} + sub.muteMu.Unlock() + + // Send the Who List so the blocker/blockee can disappear from each other's list. + s.SendWhoList() +} + // OnBlocklist is a bulk user mute from the CachedBlocklist sent by the website. func (s *Server) OnBlocklist(sub *Subscriber, msg messages.Message) { log.Info("%s syncs their blocklist: %s", sub.Username, msg.Usernames) sub.muteMu.Lock() for _, username := range msg.Usernames { - sub.muted[username] = struct{}{} + sub.blocked[username] = struct{}{} } sub.muteMu.Unlock() diff --git a/pkg/messages/messages.go b/pkg/messages/messages.go index aed4526..b783e96 100644 --- a/pkg/messages/messages.go +++ b/pkg/messages/messages.go @@ -70,6 +70,7 @@ const ( ActionBoot = "boot" // boot a user off your video feed ActionMute = "mute" // mute a user's chat messages ActionUnmute = "unmute" + ActionBlock = "block" // hard block another user ActionBlocklist = "blocklist" // mute in bulk for usernames ActionReport = "report" // user reports a message diff --git a/pkg/websocket.go b/pkg/websocket.go index d97f5b3..aa03dbc 100644 --- a/pkg/websocket.go +++ b/pkg/websocket.go @@ -36,9 +36,10 @@ type Subscriber struct { messages chan []byte closeSlow func() - muteMu sync.RWMutex - booted map[string]struct{} // usernames booted off your camera - muted map[string]struct{} // usernames you muted + muteMu sync.RWMutex + booted map[string]struct{} // usernames booted off your camera + blocked map[string]struct{} // usernames you have blocked + muted map[string]struct{} // usernames you muted // Record which message IDs belong to this user. midMu sync.Mutex @@ -98,6 +99,8 @@ func (sub *Subscriber) ReadLoop(s *Server) { s.OnBoot(sub, msg) case messages.ActionMute, messages.ActionUnmute: s.OnMute(sub, msg, msg.Action == messages.ActionMute) + case messages.ActionBlock: + s.OnBlock(sub, msg) case messages.ActionBlocklist: s.OnBlocklist(sub, msg) case messages.ActionCandidate: @@ -193,6 +196,7 @@ func (s *Server) WebSocket() http.HandlerFunc { }, booted: make(map[string]struct{}), muted: make(map[string]struct{}), + blocked: make(map[string]struct{}), messageIDs: make(map[int]struct{}), ChatStatus: "online", } @@ -321,6 +325,13 @@ func (s *Server) Broadcast(msg messages.Message) { log.Debug("Broadcast: %+v", msg) } + // Get the sender of this message. + sender, err := s.GetSubscriber(msg.Username) + if err != nil { + log.Error("Broadcast: sender name %s not found as a current subscriber!", msg.Username) + sender = nil + } + // Get the list of users who are online NOW, so we don't hold the mutex lock too long. // Example: sending a fat GIF to a large audience could hang up the server for a long // time until every copy of the GIF has been sent. @@ -336,6 +347,12 @@ func (s *Server) Broadcast(msg messages.Message) { continue } + // Don't deliver it if there is any blocking between sender and receiver. + if sender != nil && sender.Blocks(sub) { + log.Debug("Do not broadcast message to %s: blocking between them and %s", msg.Username, sub.Username) + continue + } + // VIP channels: only deliver to subscribed VIP users. if ch, ok := config.Current.GetChannel(msg.Channel); ok && ch.VIP && !sub.IsVIP() { log.Debug("Do not broadcast message to %s: VIP channel and they are not VIP", sub.Username) @@ -403,6 +420,12 @@ func (s *Server) SendWhoList() { continue } + // Blocking: hide the presence of both people from the Who List. + if user.Blocks(sub) { + log.Debug("WhoList: hide %s from %s (blocking)", user.Username, sub.Username) + continue + } + who := messages.WhoList{ Username: user.Username, Status: user.ChatStatus, @@ -470,6 +493,23 @@ func (s *Subscriber) Mutes(username string) bool { return ok } +// Blocks checks whether the subscriber blocks the username, or vice versa (blocking goes both directions). +func (s *Subscriber) Blocks(other *Subscriber) bool { + s.muteMu.RLock() + defer s.muteMu.RUnlock() + + // Forward block? + if _, ok := s.blocked[other.Username]; ok { + return true + } + + // Reverse block? + other.muteMu.RLock() + defer other.muteMu.RUnlock() + _, ok := other.blocked[s.Username] + return ok +} + func writeTimeout(ctx context.Context, timeout time.Duration, c *websocket.Conn, msg []byte) error { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel()