We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Use SHA-256 for BigBlueButton checksums
BigBlueButton's API uses SHA1 for its checksum; I marked is as dismissed for CodeQL since it's not something we can directly change without SHA256 support in BigBlueButton itself.  I note that https://github.com/bigbluebutton/bigbluebutton/issues/17135 and related issues suggest it might be possible to do so today, though apparently the way to do that may not be documented.  So probably not something we can change without testing with a running BigBlueButton instance.

Noticed by GitHub Actions in https://github.com/zulip/zulip/pull/19962; I disabled the CodeQL warning for this so CI remains happy.

@strifel FYI as the original author of our BigBlueButton plugin.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `zerver/views/video_calls.py`
Content:
```
1 import hashlib
2 import json
3 import random
4 import secrets
5 from base64 import b32encode
6 from functools import partial
7 from typing import Dict
8 from urllib.parse import quote, urlencode, urljoin
9 
10 import requests
11 from defusedxml import ElementTree
12 from django.conf import settings
13 from django.http import HttpRequest, HttpResponse
14 from django.middleware import csrf
15 from django.shortcuts import redirect, render
16 from django.utils.crypto import constant_time_compare, salted_hmac
17 from django.utils.translation import gettext as _
18 from django.views.decorators.cache import never_cache
19 from django.views.decorators.csrf import csrf_exempt
20 from django.views.decorators.http import require_POST
21 from oauthlib.oauth2 import OAuth2Error
22 from requests_oauthlib import OAuth2Session
23 
24 from zerver.decorator import zulip_login_required
25 from zerver.lib.actions import do_set_zoom_token
26 from zerver.lib.exceptions import ErrorCode, JsonableError
27 from zerver.lib.outgoing_http import OutgoingSession
28 from zerver.lib.pysa import mark_sanitized
29 from zerver.lib.request import REQ, has_request_variables
30 from zerver.lib.response import json_success
31 from zerver.lib.subdomains import get_subdomain
32 from zerver.lib.url_encoding import append_url_query_string
33 from zerver.lib.validator import check_dict, check_string
34 from zerver.models import UserProfile, get_realm
35 
36 
37 class VideoCallSession(OutgoingSession):
38     def __init__(self) -> None:
39         super().__init__(role="video_calls", timeout=5)
40 
41 
42 class InvalidZoomTokenError(JsonableError):
43     code = ErrorCode.INVALID_ZOOM_TOKEN
44 
45     def __init__(self) -> None:
46         super().__init__(_("Invalid Zoom access token"))
47 
48 
49 def get_zoom_session(user: UserProfile) -> OAuth2Session:
50     if settings.VIDEO_ZOOM_CLIENT_ID is None:
51         raise JsonableError(_("Zoom credentials have not been configured"))
52 
53     client_id = settings.VIDEO_ZOOM_CLIENT_ID
54     client_secret = settings.VIDEO_ZOOM_CLIENT_SECRET
55     if user.realm.string_id in settings.VIDEO_ZOOM_TESTING_REALMS:  # nocoverage
56         client_id = settings.VIDEO_ZOOM_TESTING_CLIENT_ID
57         client_secret = settings.VIDEO_ZOOM_TESTING_CLIENT_SECRET
58 
59     return OAuth2Session(
60         client_id,
61         redirect_uri=urljoin(settings.ROOT_DOMAIN_URI, "/calls/zoom/complete"),
62         auto_refresh_url="https://zoom.us/oauth/token",
63         auto_refresh_kwargs={
64             "client_id": client_id,
65             "client_secret": client_secret,
66         },
67         token=user.zoom_token,
68         token_updater=partial(do_set_zoom_token, user),
69     )
70 
71 
72 def get_zoom_sid(request: HttpRequest) -> str:
73     # This is used to prevent CSRF attacks on the Zoom OAuth
74     # authentication flow.  We want this value to be unpredictable and
75     # tied to the session, but we don’t want to expose the main CSRF
76     # token directly to the Zoom server.
77 
78     csrf.get_token(request)
79     # Use 'mark_sanitized' to cause Pysa to ignore the flow of user controlled
80     # data out of this function. 'request.META' is indeed user controlled, but
81     # post-HMAC output is no longer meaningfully controllable.
82     return mark_sanitized(
83         ""
84         if getattr(request, "_dont_enforce_csrf_checks", False)
85         else salted_hmac("Zulip Zoom sid", request.META["CSRF_COOKIE"]).hexdigest()
86     )
87 
88 
89 @zulip_login_required
90 @never_cache
91 def register_zoom_user(request: HttpRequest) -> HttpResponse:
92     assert request.user.is_authenticated
93 
94     oauth = get_zoom_session(request.user)
95     authorization_url, state = oauth.authorization_url(
96         "https://zoom.us/oauth/authorize",
97         state=json.dumps(
98             {"realm": get_subdomain(request), "sid": get_zoom_sid(request)},
99         ),
100     )
101     return redirect(authorization_url)
102 
103 
104 @never_cache
105 @has_request_variables
106 def complete_zoom_user(
107     request: HttpRequest,
108     state: Dict[str, str] = REQ(
109         json_validator=check_dict([("realm", check_string)], value_validator=check_string)
110     ),
111 ) -> HttpResponse:
112     if get_subdomain(request) != state["realm"]:
113         return redirect(urljoin(get_realm(state["realm"]).uri, request.get_full_path()))
114     return complete_zoom_user_in_realm(request)
115 
116 
117 @zulip_login_required
118 @has_request_variables
119 def complete_zoom_user_in_realm(
120     request: HttpRequest,
121     code: str = REQ(),
122     state: Dict[str, str] = REQ(
123         json_validator=check_dict([("sid", check_string)], value_validator=check_string)
124     ),
125 ) -> HttpResponse:
126     assert request.user.is_authenticated
127 
128     if not constant_time_compare(state["sid"], get_zoom_sid(request)):
129         raise JsonableError(_("Invalid Zoom session identifier"))
130 
131     client_secret = settings.VIDEO_ZOOM_CLIENT_SECRET
132     if request.user.realm.string_id in settings.VIDEO_ZOOM_TESTING_REALMS:  # nocoverage
133         client_secret = settings.VIDEO_ZOOM_TESTING_CLIENT_SECRET
134 
135     oauth = get_zoom_session(request.user)
136     try:
137         token = oauth.fetch_token(
138             "https://zoom.us/oauth/token",
139             code=code,
140             client_secret=client_secret,
141         )
142     except OAuth2Error:
143         raise JsonableError(_("Invalid Zoom credentials"))
144 
145     do_set_zoom_token(request.user, token)
146     return render(request, "zerver/close_window.html")
147 
148 
149 def make_zoom_video_call(request: HttpRequest, user: UserProfile) -> HttpResponse:
150     oauth = get_zoom_session(user)
151     if not oauth.authorized:
152         raise InvalidZoomTokenError
153 
154     try:
155         res = oauth.post("https://api.zoom.us/v2/users/me/meetings", json={})
156     except OAuth2Error:
157         do_set_zoom_token(user, None)
158         raise InvalidZoomTokenError
159 
160     if res.status_code == 401:
161         do_set_zoom_token(user, None)
162         raise InvalidZoomTokenError
163     elif not res.ok:
164         raise JsonableError(_("Failed to create Zoom call"))
165 
166     return json_success({"url": res.json()["join_url"]})
167 
168 
169 @csrf_exempt
170 @require_POST
171 @has_request_variables
172 def deauthorize_zoom_user(request: HttpRequest) -> HttpResponse:
173     return json_success()
174 
175 
176 def get_bigbluebutton_url(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
177     # https://docs.bigbluebutton.org/dev/api.html#create for reference on the API calls
178     # https://docs.bigbluebutton.org/dev/api.html#usage for reference for checksum
179     id = "zulip-" + str(random.randint(100000000000, 999999999999))
180     password = b32encode(secrets.token_bytes(7))[:10].decode()
181     checksum = hashlib.sha1(
182         (
183             "create"
184             + "meetingID="
185             + id
186             + "&moderatorPW="
187             + password
188             + "&attendeePW="
189             + password
190             + "a"
191             + settings.BIG_BLUE_BUTTON_SECRET
192         ).encode()
193     ).hexdigest()
194     url = append_url_query_string(
195         "/calls/bigbluebutton/join",
196         urlencode(
197             {
198                 "meeting_id": id,
199                 "password": password,
200                 "checksum": checksum,
201             }
202         ),
203     )
204     return json_success({"url": url})
205 
206 
207 # We use zulip_login_required here mainly to get access to the user's
208 # full name from Zulip to prepopulate the user's name in the
209 # BigBlueButton meeting.  Since the meeting's details are encoded in
210 # the link the user is clicking, there is no validation tying this
211 # meeting to the Zulip organization it was created in.
212 @zulip_login_required
213 @never_cache
214 @has_request_variables
215 def join_bigbluebutton(
216     request: HttpRequest,
217     meeting_id: str = REQ(),
218     password: str = REQ(),
219     checksum: str = REQ(),
220 ) -> HttpResponse:
221     assert request.user.is_authenticated
222 
223     if settings.BIG_BLUE_BUTTON_URL is None or settings.BIG_BLUE_BUTTON_SECRET is None:
224         raise JsonableError(_("BigBlueButton is not configured."))
225     else:
226         try:
227             response = VideoCallSession().get(
228                 append_url_query_string(
229                     settings.BIG_BLUE_BUTTON_URL + "api/create",
230                     urlencode(
231                         {
232                             "meetingID": meeting_id,
233                             "moderatorPW": password,
234                             "attendeePW": password + "a",
235                             "checksum": checksum,
236                         }
237                     ),
238                 )
239             )
240             response.raise_for_status()
241         except requests.RequestException:
242             raise JsonableError(_("Error connecting to the BigBlueButton server."))
243 
244         payload = ElementTree.fromstring(response.text)
245         if payload.find("messageKey").text == "checksumError":
246             raise JsonableError(_("Error authenticating to the BigBlueButton server."))
247 
248         if payload.find("returncode").text != "SUCCESS":
249             raise JsonableError(_("BigBlueButton server returned an unexpected error."))
250 
251         join_params = urlencode(  # type: ignore[type-var] # https://github.com/python/typeshed/issues/4234
252             {
253                 "meetingID": meeting_id,
254                 "password": password,
255                 "fullName": request.user.full_name,
256             },
257             quote_via=quote,
258         )
259 
260         checksum = hashlib.sha1(
261             ("join" + join_params + settings.BIG_BLUE_BUTTON_SECRET).encode()
262         ).hexdigest()
263         redirect_url_base = append_url_query_string(
264             settings.BIG_BLUE_BUTTON_URL + "api/join", join_params
265         )
266         return redirect(append_url_query_string(redirect_url_base, "checksum=" + checksum))
267
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

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
@@ -178,7 +178,7 @@
     # 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.sha1(
+    checksum = hashlib.sha256(
         (
             "create"
             + "meetingID="
@@ -257,7 +257,7 @@
             quote_via=quote,
         )
 
-        checksum = hashlib.sha1(
+        checksum = hashlib.sha256(
             ("join" + join_params + settings.BIG_BLUE_BUTTON_SECRET).encode()
         ).hexdigest()
         redirect_url_base = append_url_query_string(

{"golden_diff": "diff --git a/zerver/views/video_calls.py b/zerver/views/video_calls.py\n--- a/zerver/views/video_calls.py\n+++ b/zerver/views/video_calls.py\n@@ -178,7 +178,7 @@\n     # https://docs.bigbluebutton.org/dev/api.html#usage for reference for checksum\n     id = \"zulip-\" + str(random.randint(100000000000, 999999999999))\n     password = b32encode(secrets.token_bytes(7))[:10].decode()\n-    checksum = hashlib.sha1(\n+    checksum = hashlib.sha256(\n         (\n             \"create\"\n             + \"meetingID=\"\n@@ -257,7 +257,7 @@\n             quote_via=quote,\n         )\n \n-        checksum = hashlib.sha1(\n+        checksum = hashlib.sha256(\n             (\"join\" + join_params + settings.BIG_BLUE_BUTTON_SECRET).encode()\n         ).hexdigest()\n         redirect_url_base = append_url_query_string(\n", "issue": "Use SHA-256 for BigBlueButton checksums\nBigBlueButton's API uses SHA1 for its checksum; I marked is as dismissed for CodeQL since it's not something we can directly change without SHA256 support in BigBlueButton itself.  I note that https://github.com/bigbluebutton/bigbluebutton/issues/17135 and related issues suggest it might be possible to do so today, though apparently the way to do that may not be documented.  So probably not something we can change without testing with a running BigBlueButton instance.\r\n\r\nNoticed by GitHub Actions in https://github.com/zulip/zulip/pull/19962; I disabled the CodeQL warning for this so CI remains happy.\r\n\r\n@strifel FYI as the original author of our BigBlueButton plugin.\n", "before_files": [{"content": "import hashlib\nimport json\nimport random\nimport secrets\nfrom base64 import b32encode\nfrom functools import partial\nfrom typing import Dict\nfrom urllib.parse import quote, urlencode, urljoin\n\nimport requests\nfrom defusedxml import ElementTree\nfrom django.conf import settings\nfrom django.http import HttpRequest, HttpResponse\nfrom django.middleware import csrf\nfrom django.shortcuts import redirect, render\nfrom django.utils.crypto import constant_time_compare, salted_hmac\nfrom django.utils.translation import gettext as _\nfrom django.views.decorators.cache import never_cache\nfrom django.views.decorators.csrf import csrf_exempt\nfrom django.views.decorators.http import require_POST\nfrom oauthlib.oauth2 import OAuth2Error\nfrom requests_oauthlib import OAuth2Session\n\nfrom zerver.decorator import zulip_login_required\nfrom zerver.lib.actions import do_set_zoom_token\nfrom zerver.lib.exceptions import ErrorCode, JsonableError\nfrom zerver.lib.outgoing_http import OutgoingSession\nfrom zerver.lib.pysa import mark_sanitized\nfrom zerver.lib.request import REQ, has_request_variables\nfrom zerver.lib.response import json_success\nfrom zerver.lib.subdomains import get_subdomain\nfrom zerver.lib.url_encoding import append_url_query_string\nfrom zerver.lib.validator import check_dict, check_string\nfrom zerver.models import UserProfile, get_realm\n\n\nclass VideoCallSession(OutgoingSession):\n    def __init__(self) -> None:\n        super().__init__(role=\"video_calls\", timeout=5)\n\n\nclass InvalidZoomTokenError(JsonableError):\n    code = ErrorCode.INVALID_ZOOM_TOKEN\n\n    def __init__(self) -> None:\n        super().__init__(_(\"Invalid Zoom access token\"))\n\n\ndef get_zoom_session(user: UserProfile) -> OAuth2Session:\n    if settings.VIDEO_ZOOM_CLIENT_ID is None:\n        raise JsonableError(_(\"Zoom credentials have not been configured\"))\n\n    client_id = settings.VIDEO_ZOOM_CLIENT_ID\n    client_secret = settings.VIDEO_ZOOM_CLIENT_SECRET\n    if user.realm.string_id in settings.VIDEO_ZOOM_TESTING_REALMS:  # nocoverage\n        client_id = settings.VIDEO_ZOOM_TESTING_CLIENT_ID\n        client_secret = settings.VIDEO_ZOOM_TESTING_CLIENT_SECRET\n\n    return OAuth2Session(\n        client_id,\n        redirect_uri=urljoin(settings.ROOT_DOMAIN_URI, \"/calls/zoom/complete\"),\n        auto_refresh_url=\"https://zoom.us/oauth/token\",\n        auto_refresh_kwargs={\n            \"client_id\": client_id,\n            \"client_secret\": client_secret,\n        },\n        token=user.zoom_token,\n        token_updater=partial(do_set_zoom_token, user),\n    )\n\n\ndef get_zoom_sid(request: HttpRequest) -> str:\n    # This is used to prevent CSRF attacks on the Zoom OAuth\n    # authentication flow.  We want this value to be unpredictable and\n    # tied to the session, but we don\u2019t want to expose the main CSRF\n    # token directly to the Zoom server.\n\n    csrf.get_token(request)\n    # Use 'mark_sanitized' to cause Pysa to ignore the flow of user controlled\n    # data out of this function. 'request.META' is indeed user controlled, but\n    # post-HMAC output is no longer meaningfully controllable.\n    return mark_sanitized(\n        \"\"\n        if getattr(request, \"_dont_enforce_csrf_checks\", False)\n        else salted_hmac(\"Zulip Zoom sid\", request.META[\"CSRF_COOKIE\"]).hexdigest()\n    )\n\n\n@zulip_login_required\n@never_cache\ndef register_zoom_user(request: HttpRequest) -> HttpResponse:\n    assert request.user.is_authenticated\n\n    oauth = get_zoom_session(request.user)\n    authorization_url, state = oauth.authorization_url(\n        \"https://zoom.us/oauth/authorize\",\n        state=json.dumps(\n            {\"realm\": get_subdomain(request), \"sid\": get_zoom_sid(request)},\n        ),\n    )\n    return redirect(authorization_url)\n\n\n@never_cache\n@has_request_variables\ndef complete_zoom_user(\n    request: HttpRequest,\n    state: Dict[str, str] = REQ(\n        json_validator=check_dict([(\"realm\", check_string)], value_validator=check_string)\n    ),\n) -> HttpResponse:\n    if get_subdomain(request) != state[\"realm\"]:\n        return redirect(urljoin(get_realm(state[\"realm\"]).uri, request.get_full_path()))\n    return complete_zoom_user_in_realm(request)\n\n\n@zulip_login_required\n@has_request_variables\ndef complete_zoom_user_in_realm(\n    request: HttpRequest,\n    code: str = REQ(),\n    state: Dict[str, str] = REQ(\n        json_validator=check_dict([(\"sid\", check_string)], value_validator=check_string)\n    ),\n) -> HttpResponse:\n    assert request.user.is_authenticated\n\n    if not constant_time_compare(state[\"sid\"], get_zoom_sid(request)):\n        raise JsonableError(_(\"Invalid Zoom session identifier\"))\n\n    client_secret = settings.VIDEO_ZOOM_CLIENT_SECRET\n    if request.user.realm.string_id in settings.VIDEO_ZOOM_TESTING_REALMS:  # nocoverage\n        client_secret = settings.VIDEO_ZOOM_TESTING_CLIENT_SECRET\n\n    oauth = get_zoom_session(request.user)\n    try:\n        token = oauth.fetch_token(\n            \"https://zoom.us/oauth/token\",\n            code=code,\n            client_secret=client_secret,\n        )\n    except OAuth2Error:\n        raise JsonableError(_(\"Invalid Zoom credentials\"))\n\n    do_set_zoom_token(request.user, token)\n    return render(request, \"zerver/close_window.html\")\n\n\ndef make_zoom_video_call(request: HttpRequest, user: UserProfile) -> HttpResponse:\n    oauth = get_zoom_session(user)\n    if not oauth.authorized:\n        raise InvalidZoomTokenError\n\n    try:\n        res = oauth.post(\"https://api.zoom.us/v2/users/me/meetings\", json={})\n    except OAuth2Error:\n        do_set_zoom_token(user, None)\n        raise InvalidZoomTokenError\n\n    if res.status_code == 401:\n        do_set_zoom_token(user, None)\n        raise InvalidZoomTokenError\n    elif not res.ok:\n        raise JsonableError(_(\"Failed to create Zoom call\"))\n\n    return json_success({\"url\": res.json()[\"join_url\"]})\n\n\n@csrf_exempt\n@require_POST\n@has_request_variables\ndef deauthorize_zoom_user(request: HttpRequest) -> HttpResponse:\n    return json_success()\n\n\ndef get_bigbluebutton_url(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:\n    # https://docs.bigbluebutton.org/dev/api.html#create for reference on the API calls\n    # https://docs.bigbluebutton.org/dev/api.html#usage for reference for checksum\n    id = \"zulip-\" + str(random.randint(100000000000, 999999999999))\n    password = b32encode(secrets.token_bytes(7))[:10].decode()\n    checksum = hashlib.sha1(\n        (\n            \"create\"\n            + \"meetingID=\"\n            + id\n            + \"&moderatorPW=\"\n            + password\n            + \"&attendeePW=\"\n            + password\n            + \"a\"\n            + settings.BIG_BLUE_BUTTON_SECRET\n        ).encode()\n    ).hexdigest()\n    url = append_url_query_string(\n        \"/calls/bigbluebutton/join\",\n        urlencode(\n            {\n                \"meeting_id\": id,\n                \"password\": password,\n                \"checksum\": checksum,\n            }\n        ),\n    )\n    return json_success({\"url\": url})\n\n\n# We use zulip_login_required here mainly to get access to the user's\n# full name from Zulip to prepopulate the user's name in the\n# BigBlueButton meeting.  Since the meeting's details are encoded in\n# the link the user is clicking, there is no validation tying this\n# meeting to the Zulip organization it was created in.\n@zulip_login_required\n@never_cache\n@has_request_variables\ndef join_bigbluebutton(\n    request: HttpRequest,\n    meeting_id: str = REQ(),\n    password: str = REQ(),\n    checksum: str = REQ(),\n) -> HttpResponse:\n    assert request.user.is_authenticated\n\n    if settings.BIG_BLUE_BUTTON_URL is None or settings.BIG_BLUE_BUTTON_SECRET is None:\n        raise JsonableError(_(\"BigBlueButton is not configured.\"))\n    else:\n        try:\n            response = VideoCallSession().get(\n                append_url_query_string(\n                    settings.BIG_BLUE_BUTTON_URL + \"api/create\",\n                    urlencode(\n                        {\n                            \"meetingID\": meeting_id,\n                            \"moderatorPW\": password,\n                            \"attendeePW\": password + \"a\",\n                            \"checksum\": checksum,\n                        }\n                    ),\n                )\n            )\n            response.raise_for_status()\n        except requests.RequestException:\n            raise JsonableError(_(\"Error connecting to the BigBlueButton server.\"))\n\n        payload = ElementTree.fromstring(response.text)\n        if payload.find(\"messageKey\").text == \"checksumError\":\n            raise JsonableError(_(\"Error authenticating to the BigBlueButton server.\"))\n\n        if payload.find(\"returncode\").text != \"SUCCESS\":\n            raise JsonableError(_(\"BigBlueButton server returned an unexpected error.\"))\n\n        join_params = urlencode(  # type: ignore[type-var] # https://github.com/python/typeshed/issues/4234\n            {\n                \"meetingID\": meeting_id,\n                \"password\": password,\n                \"fullName\": request.user.full_name,\n            },\n            quote_via=quote,\n        )\n\n        checksum = hashlib.sha1(\n            (\"join\" + join_params + settings.BIG_BLUE_BUTTON_SECRET).encode()\n        ).hexdigest()\n        redirect_url_base = append_url_query_string(\n            settings.BIG_BLUE_BUTTON_URL + \"api/join\", join_params\n        )\n        return redirect(append_url_query_string(redirect_url_base, \"checksum=\" + checksum))\n", "path": "zerver/views/video_calls.py"}], "after_files": [{"content": "import hashlib\nimport json\nimport random\nimport secrets\nfrom base64 import b32encode\nfrom functools import partial\nfrom typing import Dict\nfrom urllib.parse import quote, urlencode, urljoin\n\nimport requests\nfrom defusedxml import ElementTree\nfrom django.conf import settings\nfrom django.http import HttpRequest, HttpResponse\nfrom django.middleware import csrf\nfrom django.shortcuts import redirect, render\nfrom django.utils.crypto import constant_time_compare, salted_hmac\nfrom django.utils.translation import gettext as _\nfrom django.views.decorators.cache import never_cache\nfrom django.views.decorators.csrf import csrf_exempt\nfrom django.views.decorators.http import require_POST\nfrom oauthlib.oauth2 import OAuth2Error\nfrom requests_oauthlib import OAuth2Session\n\nfrom zerver.decorator import zulip_login_required\nfrom zerver.lib.actions import do_set_zoom_token\nfrom zerver.lib.exceptions import ErrorCode, JsonableError\nfrom zerver.lib.outgoing_http import OutgoingSession\nfrom zerver.lib.pysa import mark_sanitized\nfrom zerver.lib.request import REQ, has_request_variables\nfrom zerver.lib.response import json_success\nfrom zerver.lib.subdomains import get_subdomain\nfrom zerver.lib.url_encoding import append_url_query_string\nfrom zerver.lib.validator import check_dict, check_string\nfrom zerver.models import UserProfile, get_realm\n\n\nclass VideoCallSession(OutgoingSession):\n    def __init__(self) -> None:\n        super().__init__(role=\"video_calls\", timeout=5)\n\n\nclass InvalidZoomTokenError(JsonableError):\n    code = ErrorCode.INVALID_ZOOM_TOKEN\n\n    def __init__(self) -> None:\n        super().__init__(_(\"Invalid Zoom access token\"))\n\n\ndef get_zoom_session(user: UserProfile) -> OAuth2Session:\n    if settings.VIDEO_ZOOM_CLIENT_ID is None:\n        raise JsonableError(_(\"Zoom credentials have not been configured\"))\n\n    client_id = settings.VIDEO_ZOOM_CLIENT_ID\n    client_secret = settings.VIDEO_ZOOM_CLIENT_SECRET\n    if user.realm.string_id in settings.VIDEO_ZOOM_TESTING_REALMS:  # nocoverage\n        client_id = settings.VIDEO_ZOOM_TESTING_CLIENT_ID\n        client_secret = settings.VIDEO_ZOOM_TESTING_CLIENT_SECRET\n\n    return OAuth2Session(\n        client_id,\n        redirect_uri=urljoin(settings.ROOT_DOMAIN_URI, \"/calls/zoom/complete\"),\n        auto_refresh_url=\"https://zoom.us/oauth/token\",\n        auto_refresh_kwargs={\n            \"client_id\": client_id,\n            \"client_secret\": client_secret,\n        },\n        token=user.zoom_token,\n        token_updater=partial(do_set_zoom_token, user),\n    )\n\n\ndef get_zoom_sid(request: HttpRequest) -> str:\n    # This is used to prevent CSRF attacks on the Zoom OAuth\n    # authentication flow.  We want this value to be unpredictable and\n    # tied to the session, but we don\u2019t want to expose the main CSRF\n    # token directly to the Zoom server.\n\n    csrf.get_token(request)\n    # Use 'mark_sanitized' to cause Pysa to ignore the flow of user controlled\n    # data out of this function. 'request.META' is indeed user controlled, but\n    # post-HMAC output is no longer meaningfully controllable.\n    return mark_sanitized(\n        \"\"\n        if getattr(request, \"_dont_enforce_csrf_checks\", False)\n        else salted_hmac(\"Zulip Zoom sid\", request.META[\"CSRF_COOKIE\"]).hexdigest()\n    )\n\n\n@zulip_login_required\n@never_cache\ndef register_zoom_user(request: HttpRequest) -> HttpResponse:\n    assert request.user.is_authenticated\n\n    oauth = get_zoom_session(request.user)\n    authorization_url, state = oauth.authorization_url(\n        \"https://zoom.us/oauth/authorize\",\n        state=json.dumps(\n            {\"realm\": get_subdomain(request), \"sid\": get_zoom_sid(request)},\n        ),\n    )\n    return redirect(authorization_url)\n\n\n@never_cache\n@has_request_variables\ndef complete_zoom_user(\n    request: HttpRequest,\n    state: Dict[str, str] = REQ(\n        json_validator=check_dict([(\"realm\", check_string)], value_validator=check_string)\n    ),\n) -> HttpResponse:\n    if get_subdomain(request) != state[\"realm\"]:\n        return redirect(urljoin(get_realm(state[\"realm\"]).uri, request.get_full_path()))\n    return complete_zoom_user_in_realm(request)\n\n\n@zulip_login_required\n@has_request_variables\ndef complete_zoom_user_in_realm(\n    request: HttpRequest,\n    code: str = REQ(),\n    state: Dict[str, str] = REQ(\n        json_validator=check_dict([(\"sid\", check_string)], value_validator=check_string)\n    ),\n) -> HttpResponse:\n    assert request.user.is_authenticated\n\n    if not constant_time_compare(state[\"sid\"], get_zoom_sid(request)):\n        raise JsonableError(_(\"Invalid Zoom session identifier\"))\n\n    client_secret = settings.VIDEO_ZOOM_CLIENT_SECRET\n    if request.user.realm.string_id in settings.VIDEO_ZOOM_TESTING_REALMS:  # nocoverage\n        client_secret = settings.VIDEO_ZOOM_TESTING_CLIENT_SECRET\n\n    oauth = get_zoom_session(request.user)\n    try:\n        token = oauth.fetch_token(\n            \"https://zoom.us/oauth/token\",\n            code=code,\n            client_secret=client_secret,\n        )\n    except OAuth2Error:\n        raise JsonableError(_(\"Invalid Zoom credentials\"))\n\n    do_set_zoom_token(request.user, token)\n    return render(request, \"zerver/close_window.html\")\n\n\ndef make_zoom_video_call(request: HttpRequest, user: UserProfile) -> HttpResponse:\n    oauth = get_zoom_session(user)\n    if not oauth.authorized:\n        raise InvalidZoomTokenError\n\n    try:\n        res = oauth.post(\"https://api.zoom.us/v2/users/me/meetings\", json={})\n    except OAuth2Error:\n        do_set_zoom_token(user, None)\n        raise InvalidZoomTokenError\n\n    if res.status_code == 401:\n        do_set_zoom_token(user, None)\n        raise InvalidZoomTokenError\n    elif not res.ok:\n        raise JsonableError(_(\"Failed to create Zoom call\"))\n\n    return json_success({\"url\": res.json()[\"join_url\"]})\n\n\n@csrf_exempt\n@require_POST\n@has_request_variables\ndef deauthorize_zoom_user(request: HttpRequest) -> HttpResponse:\n    return json_success()\n\n\ndef get_bigbluebutton_url(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:\n    # https://docs.bigbluebutton.org/dev/api.html#create for reference on the API calls\n    # https://docs.bigbluebutton.org/dev/api.html#usage for reference for checksum\n    id = \"zulip-\" + str(random.randint(100000000000, 999999999999))\n    password = b32encode(secrets.token_bytes(7))[:10].decode()\n    checksum = hashlib.sha256(\n        (\n            \"create\"\n            + \"meetingID=\"\n            + id\n            + \"&moderatorPW=\"\n            + password\n            + \"&attendeePW=\"\n            + password\n            + \"a\"\n            + settings.BIG_BLUE_BUTTON_SECRET\n        ).encode()\n    ).hexdigest()\n    url = append_url_query_string(\n        \"/calls/bigbluebutton/join\",\n        urlencode(\n            {\n                \"meeting_id\": id,\n                \"password\": password,\n                \"checksum\": checksum,\n            }\n        ),\n    )\n    return json_success({\"url\": url})\n\n\n# We use zulip_login_required here mainly to get access to the user's\n# full name from Zulip to prepopulate the user's name in the\n# BigBlueButton meeting.  Since the meeting's details are encoded in\n# the link the user is clicking, there is no validation tying this\n# meeting to the Zulip organization it was created in.\n@zulip_login_required\n@never_cache\n@has_request_variables\ndef join_bigbluebutton(\n    request: HttpRequest,\n    meeting_id: str = REQ(),\n    password: str = REQ(),\n    checksum: str = REQ(),\n) -> HttpResponse:\n    assert request.user.is_authenticated\n\n    if settings.BIG_BLUE_BUTTON_URL is None or settings.BIG_BLUE_BUTTON_SECRET is None:\n        raise JsonableError(_(\"BigBlueButton is not configured.\"))\n    else:\n        try:\n            response = VideoCallSession().get(\n                append_url_query_string(\n                    settings.BIG_BLUE_BUTTON_URL + \"api/create\",\n                    urlencode(\n                        {\n                            \"meetingID\": meeting_id,\n                            \"moderatorPW\": password,\n                            \"attendeePW\": password + \"a\",\n                            \"checksum\": checksum,\n                        }\n                    ),\n                )\n            )\n            response.raise_for_status()\n        except requests.RequestException:\n            raise JsonableError(_(\"Error connecting to the BigBlueButton server.\"))\n\n        payload = ElementTree.fromstring(response.text)\n        if payload.find(\"messageKey\").text == \"checksumError\":\n            raise JsonableError(_(\"Error authenticating to the BigBlueButton server.\"))\n\n        if payload.find(\"returncode\").text != \"SUCCESS\":\n            raise JsonableError(_(\"BigBlueButton server returned an unexpected error.\"))\n\n        join_params = urlencode(  # type: ignore[type-var] # https://github.com/python/typeshed/issues/4234\n            {\n                \"meetingID\": meeting_id,\n                \"password\": password,\n                \"fullName\": request.user.full_name,\n            },\n            quote_via=quote,\n        )\n\n        checksum = hashlib.sha256(\n            (\"join\" + join_params + settings.BIG_BLUE_BUTTON_SECRET).encode()\n        ).hexdigest()\n        redirect_url_base = append_url_query_string(\n            settings.BIG_BLUE_BUTTON_URL + \"api/join\", join_params\n        )\n        return redirect(append_url_query_string(redirect_url_base, \"checksum=\" + checksum))\n", "path": "zerver/views/video_calls.py"}]}