mirror of
https://github.com/zulip/zulip.git
synced 2025-11-12 18:06:44 +00:00
integration: Generate dynamic name for BigBlueButton video calls.
The name for a BigBlueButton meeting is now generated from the stream name and topic name. The createTime option is used to have the user redirected to a link that is only valid for this meeting. Even if the same link in Zulip is used again, a new createTime parameter will be created, as the Meeting on the BigBlueButton server has to be recreated. Fixes #16498. Fixes #20509. Fixes #20804.
This commit is contained in:
@@ -27,6 +27,7 @@ set_global("ResizeObserver", function () {
|
||||
|
||||
const server_events_dispatch = zrequire("server_events_dispatch");
|
||||
const compose_ui = zrequire("compose_ui");
|
||||
const compose_closed = zrequire("compose_closed_ui");
|
||||
const compose = zrequire("compose");
|
||||
function stub_out_video_calls() {
|
||||
const $elem = $("#below-compose-content .video_link");
|
||||
@@ -210,8 +211,11 @@ test("videos", ({override, override_rewire}) => {
|
||||
page_params.realm_video_chat_provider =
|
||||
realm_available_video_chat_providers.big_blue_button.id;
|
||||
|
||||
compose_closed.get_recipient_label = () => "a";
|
||||
|
||||
channel.get = (options) => {
|
||||
assert.equal(options.url, "/json/calls/bigbluebutton/create");
|
||||
assert.equal(options.data.meeting_name, "a meeting");
|
||||
options.success({
|
||||
url: "/calls/bigbluebutton/join?meeting_id=%22zulip-1%22&password=%22AAAAAAAAAA%22&checksum=%2232702220bff2a22a44aee72e96cfdb4c4091752e%22",
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import _ from "lodash";
|
||||
import * as blueslip from "./blueslip";
|
||||
import * as channel from "./channel";
|
||||
import * as compose_actions from "./compose_actions";
|
||||
import {get_recipient_label} from "./compose_closed_ui";
|
||||
import * as compose_error from "./compose_error";
|
||||
import * as compose_fade from "./compose_fade";
|
||||
import * as compose_state from "./compose_state";
|
||||
@@ -616,8 +617,12 @@ export function initialize() {
|
||||
available_providers.big_blue_button &&
|
||||
page_params.realm_video_chat_provider === available_providers.big_blue_button.id
|
||||
) {
|
||||
const meeting_name = get_recipient_label() + " meeting";
|
||||
channel.get({
|
||||
url: "/json/calls/bigbluebutton/create",
|
||||
data: {
|
||||
meeting_name,
|
||||
},
|
||||
success(response) {
|
||||
insert_video_call_url(response.url, $target_textarea);
|
||||
},
|
||||
|
||||
@@ -13868,6 +13868,16 @@ paths:
|
||||
description: |
|
||||
Create a video call URL for a BigBlueButton video call.
|
||||
Requires BigBlueButton to be configured on the Zulip server.
|
||||
parameters:
|
||||
- in: query
|
||||
name: meeting_name
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
description: |
|
||||
Title to use for the BigBlueButton meeting.
|
||||
|
||||
A good choice is something like "{stream_name} meeting".
|
||||
responses:
|
||||
"200":
|
||||
description: Success.
|
||||
@@ -13885,12 +13895,12 @@ paths:
|
||||
description: |
|
||||
The URL for the BigBlueButton video call.
|
||||
type: string
|
||||
example: "/calls/bbb/join?meeting_id=%22zulip-something%22&password=%22something%22&checksum=%22somechecksum%22"
|
||||
example: "/calls/bigbluebutton/join?meeting_id=%22zulip-something%22&password=%22something%22&name=%22your_meeting_name%22&checksum=%22somechecksum%22"
|
||||
example:
|
||||
{
|
||||
"msg": "",
|
||||
"result": "success",
|
||||
"url": "/calls/bbb/join?meeting_id=%22zulip-something%22&password=%22something%22&checksum=%22somechecksum%22",
|
||||
"url": "/calls/bigbluebutton/join?meeting_id=%22zulip-something%22&password=%22something%22&checksum=%22somechecksum%22",
|
||||
}
|
||||
|
||||
components:
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from unittest import mock
|
||||
|
||||
import responses
|
||||
from django.core.signing import Signer
|
||||
from django.http import HttpResponseRedirect
|
||||
|
||||
from zerver.lib.test_classes import ZulipTestCase
|
||||
from zerver.lib.url_encoding import append_url_query_string
|
||||
|
||||
|
||||
class TestVideoCall(ZulipTestCase):
|
||||
@@ -11,6 +13,15 @@ class TestVideoCall(ZulipTestCase):
|
||||
super().setUp()
|
||||
self.user = self.example_user("hamlet")
|
||||
self.login_user(self.user)
|
||||
# Signing for bbb
|
||||
self.signer = Signer()
|
||||
self.signed_bbb_a_object = self.signer.sign_object(
|
||||
{
|
||||
"meeting_id": "a",
|
||||
"name": "a",
|
||||
"password": "a",
|
||||
}
|
||||
)
|
||||
|
||||
def test_register_video_request_no_settings(self) -> None:
|
||||
with self.settings(VIDEO_ZOOM_CLIENT_ID=None):
|
||||
@@ -168,46 +179,70 @@ class TestVideoCall(ZulipTestCase):
|
||||
|
||||
def test_create_bigbluebutton_link(self) -> None:
|
||||
with mock.patch("zerver.views.video_calls.random.randint", return_value="1"), mock.patch(
|
||||
"secrets.token_bytes", return_value=b"\x00" * 7
|
||||
"secrets.token_bytes", return_value=b"\x00" * 12
|
||||
):
|
||||
response = self.client_get("/json/calls/bigbluebutton/create")
|
||||
response = self.client_get(
|
||||
"/json/calls/bigbluebutton/create?meeting_name=general > meeting"
|
||||
)
|
||||
self.assert_json_success(response)
|
||||
self.assertEqual(
|
||||
response.json()["url"],
|
||||
"/calls/bigbluebutton/join?meeting_id=zulip-1&password=AAAAAAAAAA"
|
||||
"&checksum=d5eb2098bcd0e69a33caf2b18490991b843c8fa6be779316b4303c7990aca687",
|
||||
append_url_query_string(
|
||||
"/calls/bigbluebutton/join",
|
||||
"bigbluebutton="
|
||||
+ self.signer.sign_object(
|
||||
{
|
||||
"meeting_id": "zulip-1",
|
||||
"name": "general > meeting",
|
||||
"password": "AAAAAAAAAAAAAAAAAAAA",
|
||||
}
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@responses.activate
|
||||
def test_join_bigbluebutton_redirect(self) -> None:
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://bbb.example.com/bigbluebutton/api/create?meetingID=zulip-1&moderatorPW=a&attendeePW=aa&checksum=check",
|
||||
"<response><returncode>SUCCESS</returncode><messageKey/></response>",
|
||||
"https://bbb.example.com/bigbluebutton/api/create?meetingID=a&name=a"
|
||||
"&moderatorPW=a&attendeePW=a&checksum=131bdec35f62fc63d5436e6f791d6d7aed7cf79ef256c03597e51d320d042823",
|
||||
"<response><returncode>SUCCESS</returncode><messageKey/><createTime>0</createTime></response>",
|
||||
)
|
||||
response = self.client_get(
|
||||
"/calls/bigbluebutton/join",
|
||||
{"meeting_id": "zulip-1", "password": "a", "checksum": "check"},
|
||||
"/calls/bigbluebutton/join", {"bigbluebutton": self.signed_bbb_a_object}
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(isinstance(response, HttpResponseRedirect), True)
|
||||
self.assertEqual(
|
||||
response.url,
|
||||
"https://bbb.example.com/bigbluebutton/api/join?meetingID=zulip-1&password=a"
|
||||
"&fullName=King%20Hamlet&checksum=ca78d6d3c3e04918bfab9d7d6cbc6e50602ab2bdfe1365314570943346a71a00",
|
||||
"https://bbb.example.com/bigbluebutton/api/join?meetingID=a&"
|
||||
"password=a&fullName=King%20Hamlet&createTime=0&checksum=47ca959b4ff5c8047a5a56d6e99c07e17eac43dbf792afc0a2a9f6491ec0048b",
|
||||
)
|
||||
|
||||
@responses.activate
|
||||
def test_join_bigbluebutton_redirect_wrong_check(self) -> None:
|
||||
def test_join_bigbluebutton_invalid_signature(self) -> None:
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://bbb.example.com/bigbluebutton/api/create?meetingID=zulip-1&moderatorPW=a&attendeePW=aa&checksum=check",
|
||||
"https://bbb.example.com/bigbluebutton/api/create?meetingID=a&name=a"
|
||||
"&moderatorPW=a&attendeePW=a&checksum=131bdec35f62fc63d5436e6f791d6d7aed7cf79ef256c03597e51d320d042823",
|
||||
"<response><returncode>SUCCESS</returncode><messageKey/><createTime>0</createTime></response>",
|
||||
)
|
||||
response = self.client_get(
|
||||
"/calls/bigbluebutton/join", {"bigbluebutton": self.signed_bbb_a_object + "zoo"}
|
||||
)
|
||||
self.assert_json_error(response, "Invalid signature.")
|
||||
|
||||
@responses.activate
|
||||
def test_join_bigbluebutton_redirect_wrong_big_blue_button_checksum(self) -> None:
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://bbb.example.com/bigbluebutton/api/create?meetingID=a&name=a&moderatorPW=a&attendeePW=a&checksum=131bdec35f62fc63d5436e6f791d6d7aed7cf79ef256c03597e51d320d042823",
|
||||
"<response><returncode>FAILED</returncode><messageKey>checksumError</messageKey>"
|
||||
"<message>You did not pass the checksum security check</message></response>",
|
||||
)
|
||||
response = self.client_get(
|
||||
"/calls/bigbluebutton/join",
|
||||
{"meeting_id": "zulip-1", "password": "a", "checksum": "check"},
|
||||
{"bigbluebutton": self.signed_bbb_a_object},
|
||||
)
|
||||
self.assert_json_error(response, "Error authenticating to the BigBlueButton server.")
|
||||
|
||||
@@ -216,13 +251,13 @@ class TestVideoCall(ZulipTestCase):
|
||||
# Simulate bbb server error
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://bbb.example.com/bigbluebutton/api/create?meetingID=zulip-1&moderatorPW=a&attendeePW=aa&checksum=check",
|
||||
"https://bbb.example.com/bigbluebutton/api/create?meetingID=a&name=a&moderatorPW=a&attendeePW=a&checksum=131bdec35f62fc63d5436e6f791d6d7aed7cf79ef256c03597e51d320d042823",
|
||||
"",
|
||||
status=500,
|
||||
)
|
||||
response = self.client_get(
|
||||
"/calls/bigbluebutton/join",
|
||||
{"meeting_id": "zulip-1", "password": "a", "checksum": "check"},
|
||||
{"bigbluebutton": self.signed_bbb_a_object},
|
||||
)
|
||||
self.assert_json_error(response, "Error connecting to the BigBlueButton server.")
|
||||
|
||||
@@ -231,12 +266,12 @@ class TestVideoCall(ZulipTestCase):
|
||||
# Simulate bbb server error
|
||||
responses.add(
|
||||
responses.GET,
|
||||
"https://bbb.example.com/bigbluebutton/api/create?meetingID=zulip-1&moderatorPW=a&attendeePW=aa&checksum=check",
|
||||
"https://bbb.example.com/bigbluebutton/api/create?meetingID=a&name=a&moderatorPW=a&attendeePW=a&checksum=131bdec35f62fc63d5436e6f791d6d7aed7cf79ef256c03597e51d320d042823",
|
||||
"<response><returncode>FAILURE</returncode><messageKey>otherFailure</messageKey></response>",
|
||||
)
|
||||
response = self.client_get(
|
||||
"/calls/bigbluebutton/join",
|
||||
{"meeting_id": "zulip-1", "password": "a", "checksum": "check"},
|
||||
{"bigbluebutton": self.signed_bbb_a_object},
|
||||
)
|
||||
self.assert_json_error(response, "BigBlueButton server returned an unexpected error.")
|
||||
|
||||
@@ -244,6 +279,6 @@ class TestVideoCall(ZulipTestCase):
|
||||
with self.settings(BIG_BLUE_BUTTON_SECRET=None, BIG_BLUE_BUTTON_URL=None):
|
||||
response = self.client_get(
|
||||
"/calls/bigbluebutton/join",
|
||||
{"meeting_id": "zulip-1", "password": "a", "checksum": "check"},
|
||||
{"bigbluebutton": self.signed_bbb_a_object},
|
||||
)
|
||||
self.assert_json_error(response, "BigBlueButton is not configured.")
|
||||
|
||||
@@ -10,6 +10,7 @@ from urllib.parse import quote, urlencode, urljoin
|
||||
import requests
|
||||
from defusedxml import ElementTree
|
||||
from django.conf import settings
|
||||
from django.core.signing import Signer
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.middleware import csrf
|
||||
from django.shortcuts import redirect, render
|
||||
@@ -168,35 +169,27 @@ def deauthorize_zoom_user(request: HttpRequest) -> HttpResponse:
|
||||
return json_success(request)
|
||||
|
||||
|
||||
def get_bigbluebutton_url(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
|
||||
@has_request_variables
|
||||
def get_bigbluebutton_url(
|
||||
request: HttpRequest, user_profile: UserProfile, meeting_name: str = REQ()
|
||||
) -> HttpResponse:
|
||||
# https://docs.bigbluebutton.org/dev/api.html#create for reference on the API calls
|
||||
# https://docs.bigbluebutton.org/dev/api.html#usage for reference for checksum
|
||||
id = "zulip-" + str(random.randint(100000000000, 999999999999))
|
||||
password = b32encode(secrets.token_bytes(7))[:10].decode()
|
||||
checksum = hashlib.sha256(
|
||||
(
|
||||
"create"
|
||||
+ "meetingID="
|
||||
+ id
|
||||
+ "&moderatorPW="
|
||||
+ password
|
||||
+ "&attendeePW="
|
||||
+ password
|
||||
+ "a"
|
||||
+ settings.BIG_BLUE_BUTTON_SECRET
|
||||
).encode()
|
||||
).hexdigest()
|
||||
url = append_url_query_string(
|
||||
"/calls/bigbluebutton/join",
|
||||
urlencode(
|
||||
password = b32encode(secrets.token_bytes(7))[:20].decode()
|
||||
|
||||
# We sign our data here to ensure a Zulip user can not tamper with
|
||||
# the join link to gain access to other meetings that are on the
|
||||
# same bigbluebutton server.
|
||||
signed = Signer().sign_object(
|
||||
{
|
||||
"meeting_id": id,
|
||||
"name": meeting_name,
|
||||
"password": password,
|
||||
"checksum": checksum,
|
||||
}
|
||||
),
|
||||
)
|
||||
return json_success(request, data={"url": url})
|
||||
url = append_url_query_string("/calls/bigbluebutton/join", "bigbluebutton=" + signed)
|
||||
return json_success(request, {"url": url})
|
||||
|
||||
|
||||
# We use zulip_login_required here mainly to get access to the user's
|
||||
@@ -207,30 +200,42 @@ def get_bigbluebutton_url(request: HttpRequest, user_profile: UserProfile) -> Ht
|
||||
@zulip_login_required
|
||||
@never_cache
|
||||
@has_request_variables
|
||||
def join_bigbluebutton(
|
||||
request: HttpRequest,
|
||||
meeting_id: str = REQ(),
|
||||
password: str = REQ(),
|
||||
checksum: str = REQ(),
|
||||
) -> HttpResponse:
|
||||
def join_bigbluebutton(request: HttpRequest, bigbluebutton: str = REQ()) -> HttpResponse:
|
||||
assert request.user.is_authenticated
|
||||
|
||||
if settings.BIG_BLUE_BUTTON_URL is None or settings.BIG_BLUE_BUTTON_SECRET is None:
|
||||
raise JsonableError(_("BigBlueButton is not configured."))
|
||||
else:
|
||||
|
||||
try:
|
||||
bigbluebutton_data = Signer().unsign_object(bigbluebutton)
|
||||
except Exception:
|
||||
raise JsonableError(_("Invalid signature."))
|
||||
|
||||
create_params = urlencode(
|
||||
{
|
||||
"meetingID": bigbluebutton_data["meeting_id"],
|
||||
"name": bigbluebutton_data["name"],
|
||||
"moderatorPW": bigbluebutton_data["password"],
|
||||
# We generate the attendee password from moderatorPW,
|
||||
# because the BigBlueButton API requires a separate
|
||||
# password. This integration is designed to have all users
|
||||
# join as moderators, so we generate attendeePW by
|
||||
# truncating the moderatorPW while keeping it long enough
|
||||
# to not be vulnerable to brute force attacks.
|
||||
"attendeePW": bigbluebutton_data["password"][:16],
|
||||
},
|
||||
quote_via=quote,
|
||||
)
|
||||
|
||||
checksum = hashlib.sha256(
|
||||
("create" + create_params + settings.BIG_BLUE_BUTTON_SECRET).encode()
|
||||
).hexdigest()
|
||||
|
||||
try:
|
||||
response = VideoCallSession().get(
|
||||
append_url_query_string(
|
||||
settings.BIG_BLUE_BUTTON_URL + "api/create",
|
||||
urlencode(
|
||||
{
|
||||
"meetingID": meeting_id,
|
||||
"moderatorPW": password,
|
||||
"attendeePW": password + "a",
|
||||
"checksum": checksum,
|
||||
}
|
||||
),
|
||||
)
|
||||
append_url_query_string(settings.BIG_BLUE_BUTTON_URL + "api/create", create_params)
|
||||
+ "&checksum="
|
||||
+ checksum
|
||||
)
|
||||
response.raise_for_status()
|
||||
except requests.RequestException:
|
||||
@@ -245,9 +250,20 @@ def join_bigbluebutton(
|
||||
|
||||
join_params = urlencode(
|
||||
{
|
||||
"meetingID": meeting_id,
|
||||
"password": password,
|
||||
"meetingID": bigbluebutton_data["meeting_id"],
|
||||
# We use the moderator password here to grant ever user
|
||||
# full moderator permissions to the bigbluebutton session.
|
||||
"password": bigbluebutton_data["password"],
|
||||
"fullName": request.user.full_name,
|
||||
# https://docs.bigbluebutton.org/dev/api.html#create
|
||||
# The createTime option is used to have the user redirected to a link
|
||||
# that is only valid for this meeting.
|
||||
#
|
||||
# Even if the same link in Zulip is used again, a new
|
||||
# createTime parameter will be created, as the meeting on
|
||||
# the BigBlueButton server has to be recreated. (after a
|
||||
# few minutes)
|
||||
"createTime": payload.find("createTime").text,
|
||||
},
|
||||
quote_via=quote,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user