From 50e9413aad4a982b773955ab1b7ce42a20b74e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BC=D1=8F=D0=BD=20=D0=9C=D0=B8=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2?= Date: Fri, 15 Nov 2024 15:08:26 -0600 Subject: [PATCH] feat(recording): Shows notification when you try to start recording too quick. (#15311) * feat(recording): Shows notification when you try to start recording too quick. * squash: separate values ip and room. * chore(deps) lib-jitsi-meet@latest https://github.com/jitsi/lib-jitsi-meet/compare/v1886.0.0+bc446e99...v1887.0.0+9652999d * squash: text adjust --- lang/main.json | 2 + package-lock.json | 10 ++--- package.json | 2 +- react/features/recording/middleware.ts | 6 +++ .../prosody-plugins/mod_filter_iq_jibri.lua | 43 +++++++++++++++++-- resources/prosody-plugins/mod_rate_limit.lua | 14 ++---- resources/prosody-plugins/util.lib.lua | 15 +++++++ 7 files changed, 71 insertions(+), 21 deletions(-) diff --git a/lang/main.json b/lang/main.json index ff7c8be21822..3f7624cf5519 100644 --- a/lang/main.json +++ b/lang/main.json @@ -641,6 +641,7 @@ "on": "Live Streaming started", "onBy": "{{name}} started the live streaming", "pending": "Starting Live Stream…", + "policyError": "You tried to start a live stream too quickly. Please try again later!", "serviceName": "Live Streaming service", "sessionAlreadyActive": "This session is already being recorded or live streamed.", "signIn": "Sign in with Google", @@ -1055,6 +1056,7 @@ "onBy": "{{name}} started the recording", "onlyRecordSelf": "Record only my audio and video streams", "pending": "Preparing to record the meeting…", + "policyError": "You tried to start a recording too quickly. Please try again later!", "recordAudioAndVideo": "Record audio and video", "recordTranscription": "Record transcription", "saveLocalRecording": "Save recording file locally (Beta)", diff --git a/package-lock.json b/package-lock.json index 0d992556fc84..d98e50658b67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,7 +62,7 @@ "js-md5": "0.6.1", "js-sha512": "0.8.0", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1886.0.0+bc446e99/lib-jitsi-meet.tgz", + "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1887.0.0+9652999d/lib-jitsi-meet.tgz", "lodash-es": "4.17.21", "moment": "2.29.4", "moment-duration-format": "2.2.2", @@ -15970,8 +15970,8 @@ }, "node_modules/lib-jitsi-meet": { "version": "0.0.0", - "resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1886.0.0+bc446e99/lib-jitsi-meet.tgz", - "integrity": "sha512-/XTGm2r3cgKZBMyPS5LHaX9DCZVpY6omWSnh7xkwYtSQtJXIbSKI07Mgmah6o0p8Y3/XsC7xUMfS0qOKn8TlYQ==", + "resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1887.0.0+9652999d/lib-jitsi-meet.tgz", + "integrity": "sha512-WQ4sF0B+K0V+nqTr8ldFuQkvNxp+2DTUIu+ix0cE6L+NsS1yorRus/mG5lMlaQU9So0W83fnsl38kIj1RuymRQ==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -35316,8 +35316,8 @@ } }, "lib-jitsi-meet": { - "version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1886.0.0+bc446e99/lib-jitsi-meet.tgz", - "integrity": "sha512-/XTGm2r3cgKZBMyPS5LHaX9DCZVpY6omWSnh7xkwYtSQtJXIbSKI07Mgmah6o0p8Y3/XsC7xUMfS0qOKn8TlYQ==", + "version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1887.0.0+9652999d/lib-jitsi-meet.tgz", + "integrity": "sha512-WQ4sF0B+K0V+nqTr8ldFuQkvNxp+2DTUIu+ix0cE6L+NsS1yorRus/mG5lMlaQU9So0W83fnsl38kIj1RuymRQ==", "requires": { "@jitsi/js-utils": "2.2.1", "@jitsi/logger": "2.0.2", diff --git a/package.json b/package.json index 3b4b8f6b63a6..ab612566d597 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "js-md5": "0.6.1", "js-sha512": "0.8.0", "jwt-decode": "2.2.0", - "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1886.0.0+bc446e99/lib-jitsi-meet.tgz", + "lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1887.0.0+9652999d/lib-jitsi-meet.tgz", "lodash-es": "4.17.21", "moment": "2.29.4", "moment-duration-format": "2.2.2", diff --git a/react/features/recording/middleware.ts b/react/features/recording/middleware.ts index bc95aa5ad1f3..ddc50bdbb057 100644 --- a/react/features/recording/middleware.ts +++ b/react/features/recording/middleware.ts @@ -364,6 +364,12 @@ function _showRecordingErrorNotification(session: any, dispatch: IStore['dispatc titleKey: isStreamMode ? 'liveStreaming.inProgress' : 'recording.inProgress' })); break; + case JitsiMeetJS.constants.recording.error.POLICY_VIOLATION: + dispatch(showRecordingWarning({ + descriptionKey: isStreamMode ? 'liveStreaming.policyError' : 'recording.policyError', + titleKey: isStreamMode ? 'liveStreaming.failedToStart' : 'recording.failedToStart' + })); + break; default: dispatch(showRecordingError({ descriptionKey: isStreamMode diff --git a/resources/prosody-plugins/mod_filter_iq_jibri.lua b/resources/prosody-plugins/mod_filter_iq_jibri.lua index be3d82339de5..8e299172af36 100644 --- a/resources/prosody-plugins/mod_filter_iq_jibri.lua +++ b/resources/prosody-plugins/mod_filter_iq_jibri.lua @@ -1,11 +1,28 @@ -- This module is enabled under the main virtual host +local cache = require 'util.cache'; +local new_throttle = require 'util.throttle'.create; local st = require "util.stanza"; local jid_bare = require "util.jid".bare; local util = module:require 'util'; local is_feature_allowed = util.is_feature_allowed; +local get_ip = util.get_ip; local get_room_from_jid = util.get_room_from_jid; local room_jid_match_rewrite = util.room_jid_match_rewrite; +local limit_jibri_reach_ip_attempts; +local limit_jibri_reach_room_attempts; +local rates_per_ip; +local function load_config() + limit_jibri_reach_ip_attempts = module:get_option_number("max_number_ip_attempts_per_minute", 9); + limit_jibri_reach_room_attempts = module:get_option_number("max_number_room_attempts_per_minute", 3); + -- The size of the cache that saves state for IP addresses + cache_size = module:get_option_number("jibri_rate_limit_cache_size", 10000); + + -- Maps an IP address to a util.throttle which keeps the rate of attempts to reach jibri events from that IP. + rates_per_ip = cache.new(cache_size); +end +load_config(); + -- filters jibri iq in case of requested from jwt authenticated session that -- has features in the user context, but without feature for recording module:hook("pre-iq/full", function(event) @@ -24,10 +41,28 @@ module:hook("pre-iq/full", function(event) session.granted_jitsi_meet_context_features, occupant.role == 'moderator'); - if jibri.attr.action == 'start' and not is_allowed then - module:log('info', 'Filtering jibri start recording, stanza:%s', tostring(stanza)); - session.send(st.error_reply(stanza, 'auth', 'forbidden')); - return true; + if jibri.attr.action == 'start' then + if not is_allowed then + module:log('info', 'Filtering jibri start recording, stanza:%s', tostring(stanza)); + session.send(st.error_reply(stanza, 'auth', 'forbidden')); + return true; + end + + local ip = get_ip(session); + if not rates_per_ip:get(ip) then + rates_per_ip:set(ip, new_throttle(limit_jibri_reach_ip_attempts, 60)); + end + + if not room.jibri_throttle then + room.jibri_throttle = new_throttle(limit_jibri_reach_room_attempts, 60); + end + + if not rates_per_ip:get(ip):poll(1) or not room.jibri_throttle:poll(1) then + module:log('warn', 'Filtering jibri start recording, ip:%s, room:%s stanza:%s', + ip, room.jid, tostring(stanza)); + session.send(st.error_reply(stanza, 'wait', 'policy-violation')); + return true; + end end end end diff --git a/resources/prosody-plugins/mod_rate_limit.lua b/resources/prosody-plugins/mod_rate_limit.lua index 7d07d53d7113..c0256debcaf0 100644 --- a/resources/prosody-plugins/mod_rate_limit.lua +++ b/resources/prosody-plugins/mod_rate_limit.lua @@ -14,6 +14,7 @@ local ip_util = require "util.ip"; local new_ip = ip_util.new_ip; local match_ip = ip_util.match; local parse_cidr = ip_util.parse_cidr; +local get_ip = module:require "util".get_ip; local config = {}; local limits_resolution = 1; @@ -76,14 +77,6 @@ local function is_whitelisted_host(h) return config.whitelist_hosts:contains(h); end --- Discover real remote IP of a session --- Note: http_server.get_request_from_conn() was added in Prosody 0.12.3, --- this code provides backwards compatibility with older versions -local get_request_from_conn = http_server.get_request_from_conn or function (conn) - local response = conn and conn._http_open_response; - return response and response.request or nil; -end; - -- Add an IP to the set of limied IPs local function limit_ip(ip) module:log("info", "Limiting %s due to login/join rate exceeded.", ip); @@ -192,9 +185,8 @@ local function filter_hook(session) return; end - local request = get_request_from_conn(session.conn); - local ip = request and request.ip or session.ip; - module:log("debug", "New session from %s", ip); + local ip = get_ip(session); + module:log("debug", "New session from %s", ip); if is_whitelisted(ip) or is_whitelisted_host(session.host) then return; end diff --git a/resources/prosody-plugins/util.lib.lua b/resources/prosody-plugins/util.lib.lua index 47ae6e19444f..1878cb75fb26 100644 --- a/resources/prosody-plugins/util.lib.lua +++ b/resources/prosody-plugins/util.lib.lua @@ -1,3 +1,4 @@ +local http_server = require "net.http.server"; local jid = require "util.jid"; local st = require 'util.stanza'; local timer = require "util.timer"; @@ -578,6 +579,19 @@ function respond_iq_result(origin, stanza) })); end +-- Note: http_server.get_request_from_conn() was added in Prosody 0.12.3, +-- this code provides backwards compatibility with older versions +local get_request_from_conn = http_server.get_request_from_conn or function (conn) + local response = conn and conn._http_open_response; + return response and response.request or nil; +end; + +-- Discover real remote IP of a session +function get_ip(session) + local request = get_request_from_conn(session.conn); + return request and request.ip or session.ip; +end + return { OUTBOUND_SIP_JIBRI_PREFIXES = OUTBOUND_SIP_JIBRI_PREFIXES; INBOUND_SIP_JIBRI_PREFIXES = INBOUND_SIP_JIBRI_PREFIXES; @@ -590,6 +604,7 @@ return { is_transcriber_jigasi = is_transcriber_jigasi; is_vpaas = is_vpaas; get_focus_occupant = get_focus_occupant; + get_ip = get_ip; get_room_from_jid = get_room_from_jid; get_room_by_name_and_subdomain = get_room_by_name_and_subdomain; get_sip_jibri_email_prefix = get_sip_jibri_email_prefix;