django: Use HttpRequest.headers.

Fixes #14769.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg 2022-05-11 21:54:12 -07:00 committed by Alex Vandiver
parent d98e3ecb0d
commit 0043c0b6b2
23 changed files with 54 additions and 57 deletions

View File

@ -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:

View File

@ -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`.

View File

@ -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.)

View File

@ -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:

View File

@ -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)

View File

@ -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(

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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",
}

View File

@ -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()

View File

@ -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"

View File

@ -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)

View File

@ -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
)

View File

@ -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<action>.*)$", 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

View File

@ -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)

View File

@ -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,

View File

@ -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")

View File

@ -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)

View File

@ -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,

View File

@ -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:

View File

@ -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"]

View File

@ -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