diff --git a/corporate/views/webhook.py b/corporate/views/webhook.py index ec2cc6e798..344b9483b8 100644 --- a/corporate/views/webhook.py +++ b/corporate/views/webhook.py @@ -29,7 +29,7 @@ def stripe_webhook(request: HttpRequest) -> HttpResponse: try: stripe_event = stripe.Webhook.construct_event( request.body, - request.META.get("HTTP_STRIPE_SIGNATURE"), + request.headers.get("Stripe-Signature"), stripe_webhook_endpoint_secret, ) except ValueError: diff --git a/templates/zerver/api/incoming-webhooks-walkthrough.md b/templates/zerver/api/incoming-webhooks-walkthrough.md index 0e9bfaac20..3fa68210af 100644 --- a/templates/zerver/api/incoming-webhooks-walkthrough.md +++ b/templates/zerver/api/incoming-webhooks-walkthrough.md @@ -610,7 +610,7 @@ event = validate_extract_webhook_http_header(request, header, integration_name) ``` `request` is the `HttpRequest` object passed to your main webhook function. `header` -is the name of the custom header you'd like to extract, such as `X_EVENT_KEY`, and +is the name of the custom header you'd like to extract, such as `X-Event-Key`, and `integration_name` is the name of the third-party service in question, such as `GitHub`. diff --git a/zerver/decorator.py b/zerver/decorator.py index d2f227262a..a34cc2e6bd 100644 --- a/zerver/decorator.py +++ b/zerver/decorator.py @@ -652,7 +652,7 @@ def authenticated_rest_api_view( try: # Grab the base64-encoded authentication string, decode it, and split it into # the email and API key - auth_type, credentials = request.META["HTTP_AUTHORIZATION"].split() + auth_type, credentials = request.headers["Authorization"].split() # case insensitive per RFC 1945 if auth_type.lower() != "basic": raise JsonableError(_("This endpoint requires HTTP basic authentication.")) @@ -716,7 +716,7 @@ def process_as_post(view_func: ViewFuncT) -> ViewFuncT: if not request.POST: # Only take action if POST is empty. - if request.META.get("CONTENT_TYPE", "").startswith("multipart"): + if request.content_type == "multipart/form-data": # Note that request._files is just the private attribute that backs the # FILES property, so we are essentially setting request.FILES here. (In # Django 1.5 FILES was still a read-only property.) diff --git a/zerver/lib/i18n.py b/zerver/lib/i18n.py index 300a867d1e..7aeba76dba 100644 --- a/zerver/lib/i18n.py +++ b/zerver/lib/i18n.py @@ -72,12 +72,12 @@ def get_and_set_request_language( def get_browser_language_code(request: HttpRequest) -> Optional[str]: - accept_lang_header = request.META.get("HTTP_ACCEPT_LANGUAGE") + accept_lang_header = request.headers.get("Accept-Language") if accept_lang_header is None: return None available_language_codes = get_available_language_codes() - for accept_lang, priority in parse_accept_lang_header(request.META.get("HTTP_ACCEPT_LANGUAGE")): + for accept_lang, priority in parse_accept_lang_header(accept_lang_header): if accept_lang == "*": return None if accept_lang in available_language_codes: diff --git a/zerver/lib/logging_util.py b/zerver/lib/logging_util.py index 9783e2584a..4f86f2ec9b 100644 --- a/zerver/lib/logging_util.py +++ b/zerver/lib/logging_util.py @@ -264,16 +264,12 @@ class ZulipWebhookFormatter(ZulipFormatter): except orjson.JSONDecodeError: pass - custom_header_template = "{header}: {value}\n" + header_text = "".join( + f"{header}: {value}\n" + for header, value in request.headers.items() + if header.lower().startswith("x-") + ) - header_text = "" - for header in request.META.keys(): - if header.lower().startswith("http_x"): - header_text += custom_header_template.format( - header=header, value=request.META[header] - ) - - header_message = header_text if header_text else None from zerver.lib.request import RequestNotes client = RequestNotes.get_notes(request).client @@ -283,7 +279,7 @@ class ZulipWebhookFormatter(ZulipFormatter): setattr(record, "client", client.name) setattr(record, "url", request.META.get("PATH_INFO", None)) setattr(record, "content_type", request.content_type) - setattr(record, "custom_headers", header_message) + setattr(record, "custom_headers", header_text or None) setattr(record, "payload", payload) return super().format(record) diff --git a/zerver/lib/rest.py b/zerver/lib/rest.py index 3888bc7a6c..1e3eb1a70f 100644 --- a/zerver/lib/rest.py +++ b/zerver/lib/rest.py @@ -113,10 +113,7 @@ def rest_dispatch(request: HttpRequest, **kwargs: Any) -> HttpResponse: # for some special views (e.g. serving a file that has been # uploaded), we support using the same URL for web and API clients. - if ( - "override_api_url_scheme" in view_flags - and request.META.get("HTTP_AUTHORIZATION", None) is not None - ): + if "override_api_url_scheme" in view_flags and "Authorization" in request.headers: # This request uses standard API based authentication. # For override_api_url_scheme views, we skip our normal # rate limiting, because there are good reasons clients @@ -126,7 +123,7 @@ def rest_dispatch(request: HttpRequest, **kwargs: Any) -> HttpResponse: elif "override_api_url_scheme" in view_flags and request.GET.get("api_key") is not None: # This request uses legacy API authentication. We # unfortunately need that in the React Native mobile apps, - # because there's no way to set HTTP_AUTHORIZATION in + # because there's no way to set the Authorization header in # React Native. See last block for rate limiting notes. target_function = authenticated_uploads_api_view(skip_rate_limiting=True)( target_function @@ -141,7 +138,7 @@ def rest_dispatch(request: HttpRequest, **kwargs: Any) -> HttpResponse: # most clients (mobile, bots, etc) use HTTP basic auth and REST calls, where instead of # username:password, we use email:apiKey - elif request.META.get("HTTP_AUTHORIZATION", None): + elif "Authorization" in request.headers: # Wrap function with decorator to authenticate the user before # proceeding target_function = authenticated_rest_api_view( diff --git a/zerver/lib/webhooks/common.py b/zerver/lib/webhooks/common.py index 4f220cb048..fe89839166 100644 --- a/zerver/lib/webhooks/common.py +++ b/zerver/lib/webhooks/common.py @@ -38,9 +38,6 @@ that this integration expects! SETUP_MESSAGE_TEMPLATE = "{integration} webhook has been successfully configured" SETUP_MESSAGE_USER_PART = " by {user_name}" -# Django prefixes all custom HTTP headers with `HTTP_` -DJANGO_HTTP_PREFIX = "HTTP_" - def get_setup_webhook_message(integration: str, user_name: Optional[str] = None) -> str: content = SETUP_MESSAGE_TEMPLATE.format(integration=integration) @@ -166,7 +163,7 @@ def validate_extract_webhook_http_header( ) -> Optional[str]: assert request.user.is_authenticated - extracted_header = request.META.get(DJANGO_HTTP_PREFIX + header) + extracted_header = request.headers.get(header) if extracted_header is None and fatal: message_body = MISSING_EVENT_HEADER_MESSAGE.format( bot_name=request.user.full_name, diff --git a/zerver/middleware.py b/zerver/middleware.py index 3f5a6845fc..803723e441 100644 --- a/zerver/middleware.py +++ b/zerver/middleware.py @@ -322,8 +322,8 @@ def parse_client( # USER_AGENT. if req_client is not None: return req_client, None - if "HTTP_USER_AGENT" in request.META: - user_agent: Optional[Dict[str, str]] = parse_user_agent(request.META["HTTP_USER_AGENT"]) + if "User-Agent" in request.headers: + user_agent: Optional[Dict[str, str]] = parse_user_agent(request.headers["User-Agent"]) else: user_agent = None if user_agent is None: @@ -453,7 +453,7 @@ class JsonErrorHandler(MiddlewareMixin): self, request: HttpRequest, exception: Exception ) -> Optional[HttpResponse]: if isinstance(exception, MissingAuthenticationError): - if "text/html" in request.META.get("HTTP_ACCEPT", ""): + if "text/html" in request.headers.get("Accept", ""): # If this looks like a request from a top-level page in a # browser, send the user to the login page. # @@ -632,9 +632,9 @@ class SetRemoteAddrFromRealIpHeader(MiddlewareMixin): def process_request(self, request: HttpRequest) -> None: try: - real_ip = request.META["HTTP_X_REAL_IP"] + real_ip = request.headers["X-Real-IP"] except KeyError: - return None + pass else: request.META["REMOTE_ADDR"] = real_ip diff --git a/zerver/signals.py b/zerver/signals.py index c7e96b3a6f..6c40048753 100644 --- a/zerver/signals.py +++ b/zerver/signals.py @@ -74,7 +74,7 @@ def email_on_new_login(sender: Any, user: UserProfile, request: Any, **kwargs: A if (timezone_now() - user.date_joined).total_seconds() <= JUST_CREATED_THRESHOLD: return - user_agent = request.META.get("HTTP_USER_AGENT", "").lower() + user_agent = request.headers.get("User-Agent", "").lower() context = common_context(user) context["user_email"] = user.delivery_email diff --git a/zerver/tests/test_integrations_dev_panel.py b/zerver/tests/test_integrations_dev_panel.py index 37e0421341..7888f480c8 100644 --- a/zerver/tests/test_integrations_dev_panel.py +++ b/zerver/tests/test_integrations_dev_panel.py @@ -80,7 +80,7 @@ class TestIntegrationsDevPanel(ZulipTestCase): data = { "url": url, "body": body, - "custom_headers": orjson.dumps({"X_GITHUB_EVENT": "ping"}).decode(), + "custom_headers": orjson.dumps({"X-GitHub-Event": "ping"}).decode(), "is_json": "true", } diff --git a/zerver/tests/test_webhooks_common.py b/zerver/tests/test_webhooks_common.py index 303e978915..90e3c6fc2c 100644 --- a/zerver/tests/test_webhooks_common.py +++ b/zerver/tests/test_webhooks_common.py @@ -31,7 +31,7 @@ class WebhooksCommonTestCase(ZulipTestCase): request.user = webhook_bot header_value = validate_extract_webhook_http_header( - request, "X_CUSTOM_HEADER", "test_webhook" + request, "X-Custom-Header", "test_webhook" ) self.assertEqual(header_value, "custom_value") @@ -45,15 +45,15 @@ class WebhooksCommonTestCase(ZulipTestCase): request.user = webhook_bot request.path = "some/random/path" - exception_msg = "Missing the HTTP event header 'X_CUSTOM_HEADER'" + exception_msg = "Missing the HTTP event header 'X-Custom-Header'" with self.assertRaisesRegex(MissingHTTPEventHeader, exception_msg): - validate_extract_webhook_http_header(request, "X_CUSTOM_HEADER", "test_webhook") + validate_extract_webhook_http_header(request, "X-Custom-Header", "test_webhook") msg = self.get_last_message() expected_message = MISSING_EVENT_HEADER_MESSAGE.format( bot_name=webhook_bot.full_name, request_path=request.path, - header_name="X_CUSTOM_HEADER", + header_name="X-Custom-Header", integration_name="test_webhook", support_email=FromAddress.SUPPORT, ).rstrip() @@ -190,7 +190,7 @@ class MissingEventHeaderTestCase(WebhookTestCase): self.get_body("ticket_state_changed"), content_type="application/x-www-form-urlencoded", ) - self.assert_json_error(result, "Missing the HTTP event header 'X_GROOVE_EVENT'") + self.assert_json_error(result, "Missing the HTTP event header 'X-Groove-Event'") realm = get_realm("zulip") webhook_bot = get_user("webhook-bot@zulip.com", realm) @@ -200,7 +200,7 @@ class MissingEventHeaderTestCase(WebhookTestCase): expected_message = MISSING_EVENT_HEADER_MESSAGE.format( bot_name=webhook_bot.full_name, request_path="/api/v1/external/groove", - header_name="X_GROOVE_EVENT", + header_name="X-Groove-Event", integration_name="Groove", support_email=FromAddress.SUPPORT, ).rstrip() diff --git a/zerver/views/auth.py b/zerver/views/auth.py index e14bc2e7cb..7a95fccaf3 100644 --- a/zerver/views/auth.py +++ b/zerver/views/auth.py @@ -538,7 +538,7 @@ def oauth_redirect_to_root( def handle_desktop_flow(func: ViewFuncT) -> ViewFuncT: @wraps(func) def wrapper(request: HttpRequest, *args: object, **kwargs: object) -> HttpResponse: - user_agent = parse_user_agent(request.META.get("HTTP_USER_AGENT", "Missing User-Agent")) + user_agent = parse_user_agent(request.headers.get("User-Agent", "Missing User-Agent")) if user_agent["name"] == "ZulipElectron": return render(request, "zerver/desktop_login.html") @@ -929,7 +929,7 @@ def get_auth_backends_data(request: HttpRequest) -> Dict[str, Any]: def check_server_incompatibility(request: HttpRequest) -> bool: - user_agent = parse_user_agent(request.META.get("HTTP_USER_AGENT", "Missing User-Agent")) + user_agent = parse_user_agent(request.headers.get("User-Agent", "Missing User-Agent")) return user_agent["name"] == "ZulipInvalid" diff --git a/zerver/views/compatibility.py b/zerver/views/compatibility.py index 97c080ce66..493a6f53ca 100644 --- a/zerver/views/compatibility.py +++ b/zerver/views/compatibility.py @@ -14,17 +14,17 @@ android_min_app_version = "16.2.96" def check_global_compatibility(request: HttpRequest) -> HttpResponse: - if request.META.get("HTTP_USER_AGENT") is None: + if "User-Agent" not in request.headers: raise JsonableError(_("User-Agent header missing from request")) # This string should not be tagged for translation, since old # clients are checking for an extra string. legacy_compatibility_error_message = "Client is too old" - user_agent = parse_user_agent(request.META["HTTP_USER_AGENT"]) + user_agent = parse_user_agent(request.headers["User-Agent"]) if user_agent["name"] == "ZulipInvalid": raise JsonableError(legacy_compatibility_error_message) if user_agent["name"] == "ZulipMobile": - user_os = find_mobile_os(request.META["HTTP_USER_AGENT"]) + user_os = find_mobile_os(request.headers["User-Agent"]) if user_os == "android" and version_lt(user_agent["version"], android_min_app_version): raise JsonableError(legacy_compatibility_error_message) return json_success(request) diff --git a/zerver/views/home.py b/zerver/views/home.py index f2d0d10865..ce6127170c 100644 --- a/zerver/views/home.py +++ b/zerver/views/home.py @@ -126,7 +126,7 @@ def home(request: HttpRequest) -> HttpResponse: def home_real(request: HttpRequest) -> HttpResponse: # Before we do any real work, check if the app is banned. - client_user_agent = request.META.get("HTTP_USER_AGENT", "") + client_user_agent = request.headers.get("User-Agent", "") (insecure_desktop_app, banned_desktop_app, auto_update_broken) = is_outdated_desktop_app( client_user_agent ) diff --git a/zerver/webhooks/bitbucket2/view.py b/zerver/webhooks/bitbucket2/view.py index f89249ec3a..83f8737564 100644 --- a/zerver/webhooks/bitbucket2/view.py +++ b/zerver/webhooks/bitbucket2/view.py @@ -183,7 +183,7 @@ def get_type(request: HttpRequest, payload: WildValue) -> str: pull_request_template = "pull_request_{}" # Note that we only need the HTTP header to determine pullrequest events. # We rely on the payload itself to determine the other ones. - event_key = validate_extract_webhook_http_header(request, "X_EVENT_KEY", "BitBucket") + event_key = validate_extract_webhook_http_header(request, "X-Event-Key", "BitBucket") assert event_key is not None action = re.match("pullrequest:(?P.*)$", event_key) if action: @@ -191,7 +191,7 @@ def get_type(request: HttpRequest, payload: WildValue) -> str: if action_group in PULL_REQUEST_SUPPORTED_ACTIONS: return pull_request_template.format(action_group) else: - event_key = validate_extract_webhook_http_header(request, "X_EVENT_KEY", "BitBucket") + event_key = validate_extract_webhook_http_header(request, "X-Event-Key", "BitBucket") if event_key == "repo:updated": return event_key diff --git a/zerver/webhooks/bitbucket3/view.py b/zerver/webhooks/bitbucket3/view.py index 774863ad9b..7eaf327db7 100644 --- a/zerver/webhooks/bitbucket3/view.py +++ b/zerver/webhooks/bitbucket3/view.py @@ -9,7 +9,10 @@ from zerver.lib.exceptions import UnsupportedWebhookEventType from zerver.lib.request import REQ, has_request_variables from zerver.lib.response import json_success from zerver.lib.validator import WildValue, check_int, check_none_or, check_string, to_wild_value -from zerver.lib.webhooks.common import check_send_webhook_message +from zerver.lib.webhooks.common import ( + check_send_webhook_message, + validate_extract_webhook_http_header, +) from zerver.lib.webhooks.git import ( CONTENT_MESSAGE_TEMPLATE, TOPIC_WITH_BRANCH_TEMPLATE, @@ -433,10 +436,14 @@ def api_bitbucket3_webhook( branches: Optional[str] = REQ(default=None), user_specified_topic: Optional[str] = REQ("topic", default=None), ) -> HttpResponse: + eventkey: Optional[str] if "eventKey" in payload: eventkey = payload["eventKey"].tame(check_string) else: - eventkey = request.META["HTTP_X_EVENT_KEY"] + eventkey = validate_extract_webhook_http_header( + request, "X-Event-Key", "BitBucket", fatal=True + ) + assert eventkey is not None handler = EVENT_HANDLER_MAP.get(eventkey) if handler is None: raise UnsupportedWebhookEventType(eventkey) diff --git a/zerver/webhooks/gitea/view.py b/zerver/webhooks/gitea/view.py index 5b1dd76682..f58deba9ab 100644 --- a/zerver/webhooks/gitea/view.py +++ b/zerver/webhooks/gitea/view.py @@ -56,7 +56,7 @@ def api_gitea_webhook( ) -> HttpResponse: return gogs_webhook_main( "Gitea", - "X_GITEA_EVENT", + "X-Gitea-Event", format_pull_request_event, request, user_profile, diff --git a/zerver/webhooks/github/view.py b/zerver/webhooks/github/view.py index 08f871ea76..50a0460a3a 100644 --- a/zerver/webhooks/github/view.py +++ b/zerver/webhooks/github/view.py @@ -746,10 +746,10 @@ def api_github_webhook( """ GitHub sends the event as an HTTP header. We have our own Zulip-specific concept of an event that often maps - directly to the X_GITHUB_EVENT header's event, but we sometimes + directly to the X-GitHub-Event header's event, but we sometimes refine it based on the payload. """ - header_event = validate_extract_webhook_http_header(request, "X_GITHUB_EVENT", "GitHub") + header_event = validate_extract_webhook_http_header(request, "X-GitHub-Event", "GitHub") if header_event is None: raise UnsupportedWebhookEventType("no header provided") diff --git a/zerver/webhooks/gitlab/view.py b/zerver/webhooks/gitlab/view.py index 03f7ddb77a..e0049bed68 100644 --- a/zerver/webhooks/gitlab/view.py +++ b/zerver/webhooks/gitlab/view.py @@ -493,7 +493,7 @@ def get_subject_based_on_event( def get_event(request: HttpRequest, payload: WildValue, branches: Optional[str]) -> Optional[str]: - event = validate_extract_webhook_http_header(request, "X_GITLAB_EVENT", "GitLab") + event = validate_extract_webhook_http_header(request, "X-GitLab-Event", "GitLab") if event == "System Hook": # Convert the event name to a GitLab event title event_name = payload.get("event_name", payload["object_kind"]).tame(check_string) diff --git a/zerver/webhooks/gogs/view.py b/zerver/webhooks/gogs/view.py index 99c607d918..d39a4bcd1d 100644 --- a/zerver/webhooks/gogs/view.py +++ b/zerver/webhooks/gogs/view.py @@ -160,7 +160,7 @@ def api_gogs_webhook( ) -> HttpResponse: return gogs_webhook_main( "Gogs", - "X_GOGS_EVENT", + "X-Gogs-Event", format_pull_request_event, request, user_profile, diff --git a/zerver/webhooks/groove/view.py b/zerver/webhooks/groove/view.py index bd1435e5d6..72d3da761a 100644 --- a/zerver/webhooks/groove/view.py +++ b/zerver/webhooks/groove/view.py @@ -97,7 +97,7 @@ def api_groove_webhook( user_profile: UserProfile, payload: Dict[str, Any] = REQ(argument_type="body"), ) -> HttpResponse: - event = validate_extract_webhook_http_header(request, "X_GROOVE_EVENT", "Groove") + event = validate_extract_webhook_http_header(request, "X-Groove-Event", "Groove") assert event is not None handler = EVENTS_FUNCTION_MAPPER.get(event) if handler is None: diff --git a/zerver/webhooks/netlify/view.py b/zerver/webhooks/netlify/view.py index 1da873d197..812f3f14da 100644 --- a/zerver/webhooks/netlify/view.py +++ b/zerver/webhooks/netlify/view.py @@ -51,7 +51,7 @@ def api_netlify_webhook( def get_template(request: HttpRequest, payload: Dict[str, Any]) -> Tuple[str, str]: message_template = "The build [{build_name}]({build_url}) on branch {branch_name} " - event = validate_extract_webhook_http_header(request, "X_NETLIFY_EVENT", "Netlify") + event = validate_extract_webhook_http_header(request, "X-Netlify-Event", "Netlify") if event == "deploy_failed": message_template += payload["error_message"] diff --git a/zerver/webhooks/reviewboard/view.py b/zerver/webhooks/reviewboard/view.py index f61083c63d..da108e9ae1 100644 --- a/zerver/webhooks/reviewboard/view.py +++ b/zerver/webhooks/reviewboard/view.py @@ -183,7 +183,7 @@ def api_reviewboard_webhook( payload: Dict[str, Sequence[Dict[str, Any]]] = REQ(argument_type="body"), ) -> HttpResponse: event_type = validate_extract_webhook_http_header( - request, "X_REVIEWBOARD_EVENT", "Review Board" + request, "X-ReviewBoard-Event", "Review Board" ) assert event_type is not None