2019-06-05 15:12:34 +02:00
|
|
|
import importlib
|
2018-11-06 17:07:04 +01:00
|
|
|
from urllib.parse import unquote
|
|
|
|
|
2018-03-13 23:36:11 +01:00
|
|
|
from django.http import HttpRequest
|
2018-04-24 20:22:38 +02:00
|
|
|
from django.utils.translation import ugettext as _
|
2019-06-21 00:24:23 +02:00
|
|
|
from typing import Optional, Dict, Union, Any, Callable
|
2018-03-13 23:36:11 +01:00
|
|
|
|
|
|
|
from zerver.lib.actions import check_send_stream_message, \
|
2018-04-24 20:22:38 +02:00
|
|
|
check_send_private_message, send_rate_limited_pm_notification_to_bot_owner
|
|
|
|
from zerver.lib.exceptions import StreamDoesNotExistError, JsonableError, \
|
|
|
|
ErrorCode
|
2018-03-13 23:36:11 +01:00
|
|
|
from zerver.lib.request import REQ, has_request_variables
|
2018-04-24 20:22:38 +02:00
|
|
|
from zerver.lib.send_email import FromAddress
|
2019-02-02 23:53:55 +01:00
|
|
|
from zerver.models import UserProfile
|
2018-03-13 23:36:11 +01:00
|
|
|
|
2018-03-22 21:43:28 +01:00
|
|
|
|
2018-04-24 20:22:38 +02:00
|
|
|
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!
|
|
|
|
"""
|
|
|
|
|
2018-11-15 05:31:34 +01:00
|
|
|
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!
|
|
|
|
"""
|
|
|
|
|
2018-04-24 20:22:38 +02:00
|
|
|
# Django prefixes all custom HTTP headers with `HTTP_`
|
|
|
|
DJANGO_HTTP_PREFIX = "HTTP_"
|
|
|
|
|
2018-11-15 05:31:34 +01:00
|
|
|
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()
|
|
|
|
)
|
|
|
|
|
2018-05-22 16:45:21 +02:00
|
|
|
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")
|
|
|
|
|
2018-04-24 20:22:38 +02:00
|
|
|
class MissingHTTPEventHeader(JsonableError):
|
|
|
|
code = ErrorCode.MISSING_HTTP_EVENT_HEADER
|
|
|
|
data_fields = ['header']
|
|
|
|
|
2018-05-11 01:40:23 +02:00
|
|
|
def __init__(self, header: str) -> None:
|
2018-04-24 20:22:38 +02:00
|
|
|
self.header = header
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def msg_format() -> str:
|
|
|
|
return _("Missing the HTTP event header '{header}'")
|
|
|
|
|
2018-03-13 23:36:11 +01:00
|
|
|
@has_request_variables
|
|
|
|
def check_send_webhook_message(
|
|
|
|
request: HttpRequest, user_profile: UserProfile,
|
2018-05-11 01:40:23 +02:00
|
|
|
topic: str, body: str, stream: Optional[str]=REQ(default=None),
|
2018-11-06 17:07:04 +01:00
|
|
|
user_specified_topic: Optional[str]=REQ("topic", default=None),
|
2018-12-04 01:59:39 +01:00
|
|
|
unquote_url_parameters: Optional[bool]=False
|
2018-03-13 23:36:11 +01:00
|
|
|
) -> 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:
|
2018-11-06 17:07:04 +01:00
|
|
|
# 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
|
2018-12-04 01:59:39 +01:00
|
|
|
# that the URL parameters are decoded on our end.
|
|
|
|
if unquote_url_parameters:
|
2018-11-06 17:07:04 +01:00
|
|
|
stream = unquote(stream)
|
|
|
|
|
2018-03-13 23:36:11 +01:00
|
|
|
if user_specified_topic is not None:
|
|
|
|
topic = user_specified_topic
|
2018-12-04 01:59:39 +01:00
|
|
|
if unquote_url_parameters:
|
|
|
|
topic = unquote(topic)
|
2018-03-22 21:43:28 +01:00
|
|
|
|
|
|
|
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
|
2018-04-24 20:22:38 +02:00
|
|
|
|
2019-06-21 04:41:30 +02:00
|
|
|
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:
|
2019-06-05 15:12:34 +02:00
|
|
|
return {}
|
2019-06-21 04:41:30 +02:00
|
|
|
|
|
|
|
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
|
2019-06-05 15:12:34 +02:00
|
|
|
|
2018-05-11 01:40:23 +02:00
|
|
|
def validate_extract_webhook_http_header(request: HttpRequest, header: str,
|
2019-02-01 02:35:10 +01:00
|
|
|
integration_name: str,
|
|
|
|
fatal: Optional[bool]=True) -> Optional[str]:
|
2018-04-24 20:22:38 +02:00
|
|
|
extracted_header = request.META.get(DJANGO_HTTP_PREFIX + header)
|
2019-02-01 02:35:10 +01:00
|
|
|
if extracted_header is None and fatal:
|
2018-04-24 20:22:38 +02:00
|
|
|
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
|
2019-06-05 15:12:34 +02:00
|
|
|
|
|
|
|
|
|
|
|
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)
|
2019-06-21 00:24:23 +02:00
|
|
|
|
|
|
|
|
|
|
|
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
|