Files
zulip/zerver/lib/webhooks/common.py
Hemanth V. Alluri 4691028097 webhooks: Extract the '__' method of mapping to headers.
In the GitHub integration we established that for many integrations,
we can directly map the fixture filename to the set of required
headers and by following a simple naming convention we can greatly
ease the logic involved in fixture_to_headers method required .

So to prevent the need for duplicating the logic used by the GitHub
integration, we created a method called `get_http_headers_from_filename`
which will take the name of the HTTP header (key) and then return a
corresponding method (in a decorator-like fashion) which could then be
equated to fixture_to_headers in headers.py.

The GitHub integration was modified to use this method and the docs
were updated to suggest using this when possible.
2019-06-26 10:32:34 -07:00

184 lines
7.6 KiB
Python

import importlib
from urllib.parse import unquote
from django.http import HttpRequest
from django.utils.translation import ugettext as _
from typing import Optional, Dict, Union, Any, Callable
from zerver.lib.actions import check_send_stream_message, \
check_send_private_message, send_rate_limited_pm_notification_to_bot_owner
from zerver.lib.exceptions import StreamDoesNotExistError, JsonableError, \
ErrorCode
from zerver.lib.request import REQ, has_request_variables
from zerver.lib.send_email import FromAddress
from zerver.models import UserProfile
MISSING_EVENT_HEADER_MESSAGE = """
Hi there! Your bot {bot_name} just sent an HTTP request to {request_path} that
is missing the HTTP {header_name} header. Because this header is how
{integration_name} indicates the event type, this usually indicates a configuration
issue, where you either entered the URL for a different integration, or are running
an older version of the third-party service that doesn't provide that header.
Contact {support_email} if you need help debugging!
"""
INVALID_JSON_MESSAGE = """
Hi there! It looks like you tried to setup the Zulip {webhook_name} integration,
but didn't correctly configure the webhook to send data in the JSON format
that this integration expects!
"""
# Django prefixes all custom HTTP headers with `HTTP_`
DJANGO_HTTP_PREFIX = "HTTP_"
def notify_bot_owner_about_invalid_json(user_profile: UserProfile,
webhook_client_name: str) -> None:
send_rate_limited_pm_notification_to_bot_owner(
user_profile, user_profile.realm,
INVALID_JSON_MESSAGE.format(webhook_name=webhook_client_name).strip()
)
class UnexpectedWebhookEventType(JsonableError):
code = ErrorCode.UNEXPECTED_WEBHOOK_EVENT_TYPE
data_fields = ['webhook_name', 'event_type']
def __init__(self, webhook_name: str, event_type: Optional[str]) -> None:
self.webhook_name = webhook_name
self.event_type = event_type
@staticmethod
def msg_format() -> str:
return _("The '{event_type}' event isn't currently supported by the {webhook_name} webhook")
class MissingHTTPEventHeader(JsonableError):
code = ErrorCode.MISSING_HTTP_EVENT_HEADER
data_fields = ['header']
def __init__(self, header: str) -> None:
self.header = header
@staticmethod
def msg_format() -> str:
return _("Missing the HTTP event header '{header}'")
@has_request_variables
def check_send_webhook_message(
request: HttpRequest, user_profile: UserProfile,
topic: str, body: str, stream: Optional[str]=REQ(default=None),
user_specified_topic: Optional[str]=REQ("topic", default=None),
unquote_url_parameters: Optional[bool]=False
) -> None:
if stream is None:
assert user_profile.bot_owner is not None
check_send_private_message(user_profile, request.client,
user_profile.bot_owner, body)
else:
# Some third-party websites (such as Atlassian's JIRA), tend to
# double escape their URLs in a manner that escaped space characters
# (%20) are never properly decoded. We work around that by making sure
# that the URL parameters are decoded on our end.
if unquote_url_parameters:
stream = unquote(stream)
if user_specified_topic is not None:
topic = user_specified_topic
if unquote_url_parameters:
topic = unquote(topic)
try:
check_send_stream_message(user_profile, request.client,
stream, topic, body)
except StreamDoesNotExistError:
# A PM will be sent to the bot_owner by check_message, notifying
# that the webhook bot just tried to send a message to a non-existent
# stream, so we don't need to re-raise it since it clutters up
# webhook-errors.log
pass
def standardize_headers(input_headers: Union[None, Dict[str, Any]]) -> Dict[str, str]:
""" This method can be used to standardize a dictionary of headers with
the standard format that Django expects. For reference, refer to:
https://docs.djangoproject.com/en/2.2/ref/request-response/#django.http.HttpRequest.headers
NOTE: Historically, Django's headers were not case-insensitive. We're still
capitalizing our headers to make it easier to compare/search later if required.
"""
canonical_headers = {}
if not input_headers:
return {}
for raw_header in input_headers:
polished_header = raw_header.upper().replace("-", "_")
if polished_header not in["CONTENT_TYPE", "CONTENT_LENGTH"]:
if not polished_header.startswith("HTTP_"):
polished_header = "HTTP_" + polished_header
canonical_headers[polished_header] = str(input_headers[raw_header])
return canonical_headers
def validate_extract_webhook_http_header(request: HttpRequest, header: str,
integration_name: str,
fatal: Optional[bool]=True) -> Optional[str]:
extracted_header = request.META.get(DJANGO_HTTP_PREFIX + header)
if extracted_header is None and fatal:
message_body = MISSING_EVENT_HEADER_MESSAGE.format(
bot_name=request.user.full_name,
request_path=request.path,
header_name=header,
integration_name=integration_name,
support_email=FromAddress.SUPPORT,
)
send_rate_limited_pm_notification_to_bot_owner(
request.user, request.user.realm, message_body)
raise MissingHTTPEventHeader(header)
return extracted_header
def get_fixture_http_headers(integration_name: str,
fixture_name: str) -> Dict["str", "str"]:
"""For integrations that require custom HTTP headers for some (or all)
of their test fixtures, this method will call a specially named
function from the target integration module to determine what set
of HTTP headers goes with the given test fixture.
"""
headers_module_name = "zerver.webhooks.{integration_name}.headers".format(
integration_name=integration_name)
try:
# Try to import the headers.py file. If it does not exist,
# then that means that the integration does not have any
# special http headers registered.
#
# TODO: We may want to migrate to a more explicit registration
# strategy for this behavior rather than a try/except import.
headers_module = importlib.import_module(headers_module_name)
except ImportError:
return {}
try:
fixture_to_headers = headers_module.fixture_to_headers # type: ignore # we do extra exception handling in case it does not exist below.
except AttributeError as e:
msg = ". The {module_name} module must contain a fixture_to_headers method.".format(
module_name=headers_module_name
)
raise AttributeError(str(e) + msg)
return fixture_to_headers(fixture_name)
def get_http_headers_from_filename(http_header_key: str) -> Callable[[str], Dict[str, str]]:
"""If an integration requires an event type kind of HTTP header which can
be easily (statically) determined, then name the fixtures in the format
of "header_value__other_details" or even "header_value" and the use this
method in the headers.py file for the integration."""
def fixture_to_headers(filename: str) -> Dict[str, str]:
if '__' in filename:
event_type = filename.split("__")[0]
else:
event_type = filename
return {http_header_key: event_type}
return fixture_to_headers