Repository: zulip/zulip

Issue:
Adjusting ID and Title of Big Blue Button Meeting
We currently generate the ID of a Big Blue Button Meeting for Video Calls randomly.
As suggested by @GhaziTriki we should generate them the following way:
```
- meeting Id: 
 * private chat: zulip-deployment-id-owner-id-user
 * stream topic: zulip-deployment-id-stream-id-topic-id
- meeting name:
 * private chat: User Name Room
 * stream topic: stream - topic
```

The problem I see in that is that using the formats for the id that would mean that there can not be different meetings in one stream/pm chat. I think a time aspect should be included. (like appending the unix timestamp)
[Bug] Big Blue Button server returned an unexpected error.
I just installed a self-hosted Zulip instance. I encountered a bug when I tried to use the BigBlueButton Server of our organization as video call provider. I followed all the steps in https://zulip.com/integrations/doc/big-blue-button.

**Environment:**
Newest Zulip release (4.8) on a newly installed Ubuntu 20.04
I do not know the version of the BigBlueButton Server (not managed by myself) but it uses BBB's API version 2.0.

**Steps to reproduce:**
Follow steps in  https://zulip.com/integrations/doc/big-blue-button.

**Expected result:**
Zulip client should open a browser window and join the BBB meeting.

**Actual Result:**
Zulip opens a browser window but shows an error.

**Solution:**
I did not find anything in the logs but I was able to trace this down to the lines 239-240 in zerver/views/video_calls.py.

I then analyzed the messageKey and message of the payload from the BBB servers response which told me I had to
specify a name.
I then added a meeting name by changing the get request in the join_bigbluebutton function starting on line 208 in zerver/views/video_calls.py to something that is in line with BBB's API (https://docs.bigbluebutton.org/dev/api.html#create):
```
    response = requests.get(  
        add_query_to_redirect_url(   
            settings.BIG_BLUE_BUTTON_URL + "api/create",
            urlencode(
                {   
                "name": "Zulip-Meeting",
                "meetingID": meeting_id,
                "moderatorPW": password,
                "attendeePW": password + "a",
                "checksum": checksum,
                }   
            ),  
        )   
    )
```
i.e. I added a name to the meeting. Of course I also had to add this to the function generating the checksum in the get_bigbluebutton_url function starting on line 169 same file:

```
    checksum = hashlib.sha1(
        (
            "create"
            + "name=Zulip-Meeting&"
            + "meetingID="
            + id
            + "&moderatorPW="
            + password
            + "&attendeePW="
            + password
            + "a"
            + settings.BIG_BLUE_BUTTON_SECRET
        ).encode()
    ).hexdigest()
```
After adding the "name" parameter to the API call everything works as expected.


Looks like you have to specify a name for the meeting even though the API of BBB does not mention that "name" is required. At least, this is the case for BBB's API version 2.0 but the API documentation does not mention any changes to said parameter in other versions.
BigBlueButton integration not working since 2.4
The meeting name has now became mandatory !


Discussion:
@zulipbot claim
I started implementing it.
The problem I have is that in the request to generate the link we do not know which Stream/Private Message/Group Message the Meeting/Message that will be created is in.
I am not sure if the name and id should be generated client side.
I feel like we could generate the IDs still randomly and just configure the name based on the current compose box location at the time the request is made to create them?  
Hello @zulip/server-integrations members, this issue was labeled with the "area: integrations" label, so you may want to check it out!

<!-- areaLabelAddition -->
@strifel we can simply pass some JSON to the API call and write

```
data = json.loads(request.body.decode("utf-8"))
meeting_title = data['meeting_title']
```

in `zerver/views/video_calls.py:164:def get_bigbluebutton_url`



Resolving patch:
diff --git a/zerver/views/video_calls.py b/zerver/views/video_calls.py
--- a/zerver/views/video_calls.py
+++ b/zerver/views/video_calls.py
@@ -10,6 +10,7 @@
 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(
-            {
-                "meeting_id": id,
-                "password": password,
-                "checksum": checksum,
-            }
-        ),
+    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,
+        }
     )
-    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,55 +200,78 @@ 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:
-            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,
-                        }
-                    ),
-                )
-            )
-            response.raise_for_status()
-        except requests.RequestException:
-            raise JsonableError(_("Error connecting to the BigBlueButton server."))
-
-        payload = ElementTree.fromstring(response.text)
-        if payload.find("messageKey").text == "checksumError":
-            raise JsonableError(_("Error authenticating to the BigBlueButton server."))
-
-        if payload.find("returncode").text != "SUCCESS":
-            raise JsonableError(_("BigBlueButton server returned an unexpected error."))
-
-        join_params = urlencode(
-            {
-                "meetingID": meeting_id,
-                "password": password,
-                "fullName": request.user.full_name,
-            },
-            quote_via=quote,
-        )
 
-        checksum = hashlib.sha256(
-            ("join" + join_params + settings.BIG_BLUE_BUTTON_SECRET).encode()
-        ).hexdigest()
-        redirect_url_base = append_url_query_string(
-            settings.BIG_BLUE_BUTTON_URL + "api/join", join_params
+    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", create_params)
+            + "&checksum="
+            + checksum
         )
-        return redirect(append_url_query_string(redirect_url_base, "checksum=" + checksum))
+        response.raise_for_status()
+    except requests.RequestException:
+        raise JsonableError(_("Error connecting to the BigBlueButton server."))
+
+    payload = ElementTree.fromstring(response.text)
+    if payload.find("messageKey").text == "checksumError":
+        raise JsonableError(_("Error authenticating to the BigBlueButton server."))
+
+    if payload.find("returncode").text != "SUCCESS":
+        raise JsonableError(_("BigBlueButton server returned an unexpected error."))
+
+    join_params = urlencode(
+        {
+            "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,
+    )
+
+    checksum = hashlib.sha256(
+        ("join" + join_params + settings.BIG_BLUE_BUTTON_SECRET).encode()
+    ).hexdigest()
+    redirect_url_base = append_url_query_string(
+        settings.BIG_BLUE_BUTTON_URL + "api/join", join_params
+    )
+    return redirect(append_url_query_string(redirect_url_base, "checksum=" + checksum))


Test patch:
diff --git a/frontend_tests/node_tests/compose_video.js b/frontend_tests/node_tests/compose_video.js
--- a/frontend_tests/node_tests/compose_video.js
+++ b/frontend_tests/node_tests/compose_video.js
@@ -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",
             });
diff --git a/zerver/tests/test_create_video_call.py b/zerver/tests/test_create_video_call.py
--- a/zerver/tests/test_create_video_call.py
+++ b/zerver/tests/test_create_video_call.py
@@ -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 @@ def setUp(self) -> None:
         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 @@ def test_deauthorize_zoom_user(self) -> None:
 
     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_invalid_signature(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>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_check(self) -> None:
+    def test_join_bigbluebutton_redirect_wrong_big_blue_button_checksum(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>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 @@ def test_join_bigbluebutton_redirect_server_error(self) -> None:
         # 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 @@ def test_join_bigbluebutton_redirect_error_by_server(self) -> None:
         # 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 @@ def test_join_bigbluebutton_redirect_not_configured(self) -> None:
         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.")