2017-11-16 00:50:28 +01:00
|
|
|
import cProfile
|
|
|
|
import logging
|
|
|
|
import time
|
|
|
|
import traceback
|
2019-02-02 23:53:55 +01:00
|
|
|
from typing import Any, AnyStr, Dict, \
|
2018-05-11 01:39:17 +02:00
|
|
|
Iterable, List, MutableMapping, Optional
|
2013-04-23 18:51:17 +02:00
|
|
|
|
2013-02-11 23:33:47 +01:00
|
|
|
from django.conf import settings
|
2018-11-14 23:48:53 +01:00
|
|
|
from django.contrib.sessions.backends.base import UpdateError
|
2017-11-16 00:50:28 +01:00
|
|
|
from django.contrib.sessions.middleware import SessionMiddleware
|
2018-11-14 23:48:53 +01:00
|
|
|
from django.core.exceptions import DisallowedHost, SuspiciousOperation
|
2017-11-16 00:50:28 +01:00
|
|
|
from django.db import connection
|
|
|
|
from django.http import HttpRequest, HttpResponse, StreamingHttpResponse
|
2019-02-02 23:53:55 +01:00
|
|
|
from django.shortcuts import render
|
2017-11-16 00:50:28 +01:00
|
|
|
from django.utils.cache import patch_vary_headers
|
2017-05-18 11:56:03 +02:00
|
|
|
from django.utils.deprecation import MiddlewareMixin
|
2017-11-16 00:50:28 +01:00
|
|
|
from django.utils.http import cookie_date
|
|
|
|
from django.utils.translation import ugettext as _
|
|
|
|
from django.views.csrf import csrf_failure as html_csrf_failure
|
2016-05-25 15:02:02 +02:00
|
|
|
|
2017-11-16 00:50:28 +01:00
|
|
|
from zerver.lib.bugdown import get_bugdown_requests, get_bugdown_time
|
2019-04-24 02:50:25 +02:00
|
|
|
from zerver.lib.cache import get_remote_cache_requests, get_remote_cache_time
|
2017-10-04 01:29:53 +02:00
|
|
|
from zerver.lib.debug import maybe_tracemalloc_listen
|
2018-11-01 00:19:48 +01:00
|
|
|
from zerver.lib.db import reset_queries
|
2017-11-16 00:50:28 +01:00
|
|
|
from zerver.lib.exceptions import ErrorCode, JsonableError, RateLimited
|
2019-04-24 02:50:25 +02:00
|
|
|
from zerver.lib.html_to_text import get_content_description
|
2017-11-16 00:50:28 +01:00
|
|
|
from zerver.lib.queue import queue_json_publish
|
2017-07-21 02:19:52 +02:00
|
|
|
from zerver.lib.response import json_error, json_response_from_error
|
2017-10-19 07:21:57 +02:00
|
|
|
from zerver.lib.subdomains import get_subdomain
|
|
|
|
from zerver.lib.utils import statsd
|
2018-03-14 23:16:27 +01:00
|
|
|
from zerver.lib.types import ViewFuncT
|
2017-10-20 02:56:49 +02:00
|
|
|
from zerver.models import Realm, flush_per_request_caches, get_realm
|
2012-10-16 23:52:10 +02:00
|
|
|
|
2013-08-06 22:51:47 +02:00
|
|
|
logger = logging.getLogger('zulip.requests')
|
2012-10-16 23:52:10 +02:00
|
|
|
|
2017-11-27 07:33:05 +01:00
|
|
|
def record_request_stop_data(log_data: MutableMapping[str, Any]) -> None:
|
2013-11-08 21:40:44 +01:00
|
|
|
log_data['time_stopped'] = time.time()
|
2016-03-31 03:23:21 +02:00
|
|
|
log_data['remote_cache_time_stopped'] = get_remote_cache_time()
|
2016-03-31 03:24:05 +02:00
|
|
|
log_data['remote_cache_requests_stopped'] = get_remote_cache_requests()
|
2013-11-08 21:40:44 +01:00
|
|
|
log_data['bugdown_time_stopped'] = get_bugdown_time()
|
|
|
|
log_data['bugdown_requests_stopped'] = get_bugdown_requests()
|
2013-11-18 18:55:19 +01:00
|
|
|
if settings.PROFILE_ALL_REQUESTS:
|
|
|
|
log_data["prof"].disable()
|
2013-11-08 21:40:44 +01:00
|
|
|
|
2018-10-17 00:39:10 +02:00
|
|
|
def async_request_timer_stop(request: HttpRequest) -> None:
|
2013-11-08 21:40:44 +01:00
|
|
|
record_request_stop_data(request._log_data)
|
|
|
|
|
2017-11-27 07:33:05 +01:00
|
|
|
def record_request_restart_data(log_data: MutableMapping[str, Any]) -> None:
|
2013-11-18 18:55:19 +01:00
|
|
|
if settings.PROFILE_ALL_REQUESTS:
|
|
|
|
log_data["prof"].enable()
|
2013-11-08 21:40:44 +01:00
|
|
|
log_data['time_restarted'] = time.time()
|
2016-03-31 03:23:21 +02:00
|
|
|
log_data['remote_cache_time_restarted'] = get_remote_cache_time()
|
2016-03-31 03:24:05 +02:00
|
|
|
log_data['remote_cache_requests_restarted'] = get_remote_cache_requests()
|
2013-11-08 21:40:44 +01:00
|
|
|
log_data['bugdown_time_restarted'] = get_bugdown_time()
|
|
|
|
log_data['bugdown_requests_restarted'] = get_bugdown_requests()
|
2013-04-23 19:36:50 +02:00
|
|
|
|
2018-10-17 00:39:10 +02:00
|
|
|
def async_request_timer_restart(request: HttpRequest) -> None:
|
2013-12-12 18:59:02 +01:00
|
|
|
if "time_restarted" in request._log_data:
|
|
|
|
# Don't destroy data when being called from
|
|
|
|
# finish_current_handler
|
|
|
|
return
|
2013-11-08 21:40:44 +01:00
|
|
|
record_request_restart_data(request._log_data)
|
|
|
|
|
2017-11-27 07:33:05 +01:00
|
|
|
def record_request_start_data(log_data: MutableMapping[str, Any]) -> None:
|
2013-11-18 18:55:19 +01:00
|
|
|
if settings.PROFILE_ALL_REQUESTS:
|
|
|
|
log_data["prof"] = cProfile.Profile()
|
|
|
|
log_data["prof"].enable()
|
|
|
|
|
2018-11-01 00:19:48 +01:00
|
|
|
reset_queries()
|
2013-11-08 21:40:44 +01:00
|
|
|
log_data['time_started'] = time.time()
|
2016-03-31 03:23:21 +02:00
|
|
|
log_data['remote_cache_time_start'] = get_remote_cache_time()
|
2016-03-31 03:24:05 +02:00
|
|
|
log_data['remote_cache_requests_start'] = get_remote_cache_requests()
|
2013-11-08 21:40:44 +01:00
|
|
|
log_data['bugdown_time_start'] = get_bugdown_time()
|
|
|
|
log_data['bugdown_requests_start'] = get_bugdown_requests()
|
|
|
|
|
2017-11-27 07:33:05 +01:00
|
|
|
def timedelta_ms(timedelta: float) -> float:
|
2013-11-08 23:11:37 +01:00
|
|
|
return timedelta * 1000
|
2013-11-08 21:40:44 +01:00
|
|
|
|
2017-11-27 07:33:05 +01:00
|
|
|
def format_timedelta(timedelta: float) -> str:
|
2013-11-08 23:11:37 +01:00
|
|
|
if (timedelta >= 1):
|
2019-04-20 01:00:46 +02:00
|
|
|
return "%.1fs" % (timedelta,)
|
2013-11-08 23:11:37 +01:00
|
|
|
return "%.0fms" % (timedelta_ms(timedelta),)
|
2013-11-08 21:40:44 +01:00
|
|
|
|
2018-05-11 01:39:17 +02:00
|
|
|
def is_slow_query(time_delta: float, path: str) -> bool:
|
2013-12-26 15:16:49 +01:00
|
|
|
if time_delta < 1.2:
|
2013-12-26 15:13:00 +01:00
|
|
|
return False
|
|
|
|
is_exempt = \
|
2017-10-16 22:07:19 +02:00
|
|
|
path in ["/activity", "/json/report/error",
|
2013-12-26 15:13:00 +01:00
|
|
|
"/api/v1/deployments/report_error"] \
|
|
|
|
or path.startswith("/realm_activity/") \
|
|
|
|
or path.startswith("/user_activity/")
|
|
|
|
if is_exempt:
|
2013-12-26 15:23:18 +01:00
|
|
|
return time_delta >= 5
|
2013-12-26 15:20:59 +01:00
|
|
|
if 'webathena_kerberos' in path:
|
|
|
|
return time_delta >= 10
|
2013-12-26 15:13:00 +01:00
|
|
|
return True
|
2013-12-26 15:01:46 +01:00
|
|
|
|
2019-02-28 02:46:00 +01:00
|
|
|
statsd_blacklisted_requests = [
|
|
|
|
'do_confirm', 'signup_send_confirm', 'new_realm_send_confirm,'
|
|
|
|
'eventslast_event_id', 'webreq.content', 'avatar', 'user_uploads',
|
|
|
|
'password.reset', 'static', 'json.bots', 'json.users', 'json.streams',
|
|
|
|
'accounts.unsubscribe', 'apple-touch-icon', 'emoji', 'json.bots',
|
|
|
|
'upload_file', 'realm_activity', 'user_activity'
|
|
|
|
]
|
|
|
|
|
2018-05-11 01:39:17 +02:00
|
|
|
def write_log_line(log_data: MutableMapping[str, Any], path: str, method: str, remote_ip: str, email: str,
|
|
|
|
client_name: str, status_code: int=200, error_content: Optional[AnyStr]=None,
|
2017-12-08 17:42:02 +01:00
|
|
|
error_content_iter: Optional[Iterable[AnyStr]]=None) -> None:
|
2015-08-22 23:38:01 +02:00
|
|
|
assert error_content is None or error_content_iter is None
|
2015-08-22 23:18:31 +02:00
|
|
|
if error_content is not None:
|
|
|
|
error_content_iter = (error_content,)
|
|
|
|
|
2019-02-28 02:46:00 +01:00
|
|
|
if settings.STATSD_HOST != '':
|
|
|
|
# For statsd timer name
|
|
|
|
if path == '/':
|
|
|
|
statsd_path = u'webreq'
|
|
|
|
else:
|
|
|
|
statsd_path = u"webreq.%s" % (path[1:].replace('/', '.'),)
|
|
|
|
# Remove non-ascii chars from path (there should be none, if there are it's
|
|
|
|
# because someone manually entered a nonexistent path), as UTF-8 chars make
|
|
|
|
# statsd sad when it sends the key name over the socket
|
|
|
|
statsd_path = statsd_path.encode('ascii', errors='ignore').decode("ascii")
|
|
|
|
# TODO: This could probably be optimized to use a regular expression rather than a loop.
|
|
|
|
suppress_statsd = any((blacklisted in statsd_path for blacklisted in statsd_blacklisted_requests))
|
2013-11-08 21:40:44 +01:00
|
|
|
else:
|
2019-02-28 02:46:00 +01:00
|
|
|
suppress_statsd = True
|
|
|
|
statsd_path = ''
|
2013-11-08 21:40:44 +01:00
|
|
|
|
|
|
|
time_delta = -1
|
|
|
|
# A time duration of -1 means the StartLogRequests middleware
|
|
|
|
# didn't run for some reason
|
|
|
|
optional_orig_delta = ""
|
|
|
|
if 'time_started' in log_data:
|
|
|
|
time_delta = time.time() - log_data['time_started']
|
|
|
|
if 'time_stopped' in log_data:
|
|
|
|
orig_time_delta = time_delta
|
|
|
|
time_delta = ((log_data['time_stopped'] - log_data['time_started']) +
|
|
|
|
(time.time() - log_data['time_restarted']))
|
|
|
|
optional_orig_delta = " (lp: %s)" % (format_timedelta(orig_time_delta),)
|
2016-03-31 03:28:24 +02:00
|
|
|
remote_cache_output = ""
|
2016-03-31 03:23:21 +02:00
|
|
|
if 'remote_cache_time_start' in log_data:
|
|
|
|
remote_cache_time_delta = get_remote_cache_time() - log_data['remote_cache_time_start']
|
2016-03-31 03:26:47 +02:00
|
|
|
remote_cache_count_delta = get_remote_cache_requests() - log_data['remote_cache_requests_start']
|
2016-03-31 03:24:05 +02:00
|
|
|
if 'remote_cache_requests_stopped' in log_data:
|
2013-11-08 21:40:44 +01:00
|
|
|
# (now - restarted) + (stopped - start) = (now - start) + (stopped - restarted)
|
2016-03-31 03:23:21 +02:00
|
|
|
remote_cache_time_delta += (log_data['remote_cache_time_stopped'] -
|
2016-12-03 00:04:17 +01:00
|
|
|
log_data['remote_cache_time_restarted'])
|
2016-03-31 03:26:47 +02:00
|
|
|
remote_cache_count_delta += (log_data['remote_cache_requests_stopped'] -
|
2016-12-03 00:04:17 +01:00
|
|
|
log_data['remote_cache_requests_restarted'])
|
2013-11-08 21:40:44 +01:00
|
|
|
|
2016-03-31 03:23:21 +02:00
|
|
|
if (remote_cache_time_delta > 0.005):
|
2016-03-31 03:28:24 +02:00
|
|
|
remote_cache_output = " (mem: %s/%s)" % (format_timedelta(remote_cache_time_delta),
|
|
|
|
remote_cache_count_delta)
|
2013-11-08 21:40:44 +01:00
|
|
|
|
|
|
|
if not suppress_statsd:
|
2016-03-31 03:33:30 +02:00
|
|
|
statsd.timing("%s.remote_cache.time" % (statsd_path,), timedelta_ms(remote_cache_time_delta))
|
|
|
|
statsd.incr("%s.remote_cache.querycount" % (statsd_path,), remote_cache_count_delta)
|
2013-11-08 21:40:44 +01:00
|
|
|
|
2013-11-18 19:34:37 +01:00
|
|
|
startup_output = ""
|
|
|
|
if 'startup_time_delta' in log_data and log_data["startup_time_delta"] > 0.005:
|
2019-04-20 01:00:46 +02:00
|
|
|
startup_output = " (+start: %s)" % (format_timedelta(log_data["startup_time_delta"]),)
|
2013-11-18 19:34:37 +01:00
|
|
|
|
2013-11-08 21:40:44 +01:00
|
|
|
bugdown_output = ""
|
|
|
|
if 'bugdown_time_start' in log_data:
|
|
|
|
bugdown_time_delta = get_bugdown_time() - log_data['bugdown_time_start']
|
|
|
|
bugdown_count_delta = get_bugdown_requests() - log_data['bugdown_requests_start']
|
|
|
|
if 'bugdown_requests_stopped' in log_data:
|
|
|
|
# (now - restarted) + (stopped - start) = (now - start) + (stopped - restarted)
|
|
|
|
bugdown_time_delta += (log_data['bugdown_time_stopped'] -
|
|
|
|
log_data['bugdown_time_restarted'])
|
|
|
|
bugdown_count_delta += (log_data['bugdown_requests_stopped'] -
|
|
|
|
log_data['bugdown_requests_restarted'])
|
|
|
|
|
|
|
|
if (bugdown_time_delta > 0.005):
|
|
|
|
bugdown_output = " (md: %s/%s)" % (format_timedelta(bugdown_time_delta),
|
|
|
|
bugdown_count_delta)
|
|
|
|
|
|
|
|
if not suppress_statsd:
|
|
|
|
statsd.timing("%s.markdown.time" % (statsd_path,), timedelta_ms(bugdown_time_delta))
|
|
|
|
statsd.incr("%s.markdown.count" % (statsd_path,), bugdown_count_delta)
|
|
|
|
|
|
|
|
# Get the amount of time spent doing database queries
|
|
|
|
db_time_output = ""
|
2014-01-07 22:20:29 +01:00
|
|
|
queries = connection.connection.queries if connection.connection is not None else []
|
|
|
|
if len(queries) > 0:
|
|
|
|
query_time = sum(float(query.get('time', 0)) for query in queries)
|
2013-11-08 21:40:44 +01:00
|
|
|
db_time_output = " (db: %s/%sq)" % (format_timedelta(query_time),
|
2014-01-07 22:20:29 +01:00
|
|
|
len(queries))
|
2013-11-08 21:40:44 +01:00
|
|
|
|
|
|
|
if not suppress_statsd:
|
|
|
|
# Log ms, db ms, and num queries to statsd
|
|
|
|
statsd.timing("%s.dbtime" % (statsd_path,), timedelta_ms(query_time))
|
2016-05-04 23:16:27 +02:00
|
|
|
statsd.incr("%s.dbq" % (statsd_path,), len(queries))
|
2013-11-08 21:40:44 +01:00
|
|
|
statsd.timing("%s.total" % (statsd_path,), timedelta_ms(time_delta))
|
|
|
|
|
|
|
|
if 'extra' in log_data:
|
|
|
|
extra_request_data = " %s" % (log_data['extra'],)
|
|
|
|
else:
|
|
|
|
extra_request_data = ""
|
|
|
|
logger_client = "(%s via %s)" % (email, client_name)
|
2016-11-30 14:17:35 +01:00
|
|
|
logger_timing = ('%5s%s%s%s%s%s %s' %
|
2013-11-08 21:40:44 +01:00
|
|
|
(format_timedelta(time_delta), optional_orig_delta,
|
2016-03-31 03:28:24 +02:00
|
|
|
remote_cache_output, bugdown_output,
|
2016-11-30 14:17:35 +01:00
|
|
|
db_time_output, startup_output, path))
|
|
|
|
logger_line = ('%-15s %-7s %3d %s%s %s' %
|
|
|
|
(remote_ip, method, status_code,
|
|
|
|
logger_timing, extra_request_data, logger_client))
|
2015-12-14 06:54:38 +01:00
|
|
|
if (status_code in [200, 304] and method == "GET" and path.startswith("/static")):
|
|
|
|
logger.debug(logger_line)
|
|
|
|
else:
|
|
|
|
logger.info(logger_line)
|
2013-11-08 21:40:44 +01:00
|
|
|
|
2013-12-26 15:01:46 +01:00
|
|
|
if (is_slow_query(time_delta, path)):
|
2019-07-22 22:08:03 +02:00
|
|
|
queue_json_publish("slow_queries", dict(
|
|
|
|
query="%s (%s)" % (logger_line, email)))
|
2013-11-08 21:40:44 +01:00
|
|
|
|
2013-11-18 18:55:19 +01:00
|
|
|
if settings.PROFILE_ALL_REQUESTS:
|
|
|
|
log_data["prof"].disable()
|
|
|
|
profile_path = "/tmp/profile.data.%s.%s" % (path.split("/")[-1], int(time_delta * 1000),)
|
|
|
|
log_data["prof"].dump_stats(profile_path)
|
|
|
|
|
2013-11-08 21:40:44 +01:00
|
|
|
# Log some additional data whenever we return certain 40x errors
|
|
|
|
if 400 <= status_code < 500 and status_code not in [401, 404, 405]:
|
2017-02-11 05:26:24 +01:00
|
|
|
assert error_content_iter is not None
|
2016-07-10 00:15:36 +02:00
|
|
|
error_content_list = list(error_content_iter)
|
|
|
|
if error_content_list:
|
|
|
|
error_data = u''
|
2018-05-11 01:39:17 +02:00
|
|
|
elif isinstance(error_content_list[0], str):
|
2016-07-10 00:15:36 +02:00
|
|
|
error_data = u''.join(error_content_list)
|
2017-11-09 09:03:33 +01:00
|
|
|
elif isinstance(error_content_list[0], bytes):
|
2016-07-10 00:15:36 +02:00
|
|
|
error_data = repr(b''.join(error_content_list))
|
|
|
|
if len(error_data) > 100:
|
|
|
|
error_data = u"[content more than 100 characters]"
|
|
|
|
logger.info('status=%3d, data=%s, uid=%s' % (status_code, error_data, email))
|
2013-04-23 19:36:50 +02:00
|
|
|
|
2017-05-18 11:56:03 +02:00
|
|
|
class LogRequests(MiddlewareMixin):
|
2013-11-18 19:34:37 +01:00
|
|
|
# We primarily are doing logging using the process_view hook, but
|
|
|
|
# for some views, process_view isn't run, so we call the start
|
|
|
|
# method here too
|
2017-11-27 07:33:05 +01:00
|
|
|
def process_request(self, request: HttpRequest) -> None:
|
2017-10-04 01:29:53 +02:00
|
|
|
maybe_tracemalloc_listen()
|
2013-11-08 20:18:02 +01:00
|
|
|
request._log_data = dict()
|
2013-11-08 21:40:44 +01:00
|
|
|
record_request_start_data(request._log_data)
|
2012-11-14 21:00:26 +01:00
|
|
|
|
2018-03-14 23:16:27 +01:00
|
|
|
def process_view(self, request: HttpRequest, view_func: ViewFuncT,
|
2017-11-27 07:33:05 +01:00
|
|
|
args: List[str], kwargs: Dict[str, Any]) -> None:
|
2013-11-18 19:34:37 +01:00
|
|
|
# process_request was already run; we save the initialization
|
|
|
|
# time (i.e. the time between receiving the request and
|
|
|
|
# figuring out which view function to call, which is primarily
|
|
|
|
# importing modules on the first start)
|
|
|
|
request._log_data["startup_time_delta"] = time.time() - request._log_data["time_started"]
|
|
|
|
# And then completely reset our tracking to only cover work
|
|
|
|
# done as part of this request
|
|
|
|
record_request_start_data(request._log_data)
|
|
|
|
|
2017-11-27 07:33:05 +01:00
|
|
|
def process_response(self, request: HttpRequest,
|
|
|
|
response: StreamingHttpResponse) -> StreamingHttpResponse:
|
2012-10-16 23:52:10 +02:00
|
|
|
# The reverse proxy might have sent us the real external IP
|
|
|
|
remote_ip = request.META.get('HTTP_X_REAL_IP')
|
|
|
|
if remote_ip is None:
|
|
|
|
remote_ip = request.META['REMOTE_ADDR']
|
|
|
|
|
2013-03-15 18:57:58 +01:00
|
|
|
# Get the requestor's email address and client, if available.
|
2013-02-11 23:15:34 +01:00
|
|
|
try:
|
|
|
|
email = request._email
|
|
|
|
except Exception:
|
|
|
|
email = "unauth"
|
2013-03-15 18:57:58 +01:00
|
|
|
try:
|
2013-03-26 19:40:28 +01:00
|
|
|
client = request.client.name
|
2013-03-15 18:57:58 +01:00
|
|
|
except Exception:
|
|
|
|
client = "?"
|
2013-02-11 23:15:34 +01:00
|
|
|
|
2015-08-19 22:20:27 +02:00
|
|
|
if response.streaming:
|
|
|
|
content_iter = response.streaming_content
|
2015-08-22 23:18:31 +02:00
|
|
|
content = None
|
2015-08-19 22:20:27 +02:00
|
|
|
else:
|
2015-08-22 23:18:31 +02:00
|
|
|
content = response.content
|
2015-08-22 23:38:01 +02:00
|
|
|
content_iter = None
|
2015-08-19 22:20:27 +02:00
|
|
|
|
2013-11-08 21:40:44 +01:00
|
|
|
write_log_line(request._log_data, request.path, request.method,
|
2015-08-22 23:18:31 +02:00
|
|
|
remote_ip, email, client, status_code=response.status_code,
|
|
|
|
error_content=content, error_content_iter=content_iter)
|
2012-10-16 23:52:10 +02:00
|
|
|
return response
|
2012-12-19 20:19:46 +01:00
|
|
|
|
2017-05-18 11:56:03 +02:00
|
|
|
class JsonErrorHandler(MiddlewareMixin):
|
2017-11-27 07:33:05 +01:00
|
|
|
def process_exception(self, request: HttpRequest, exception: Exception) -> Optional[HttpResponse]:
|
2017-07-20 00:19:42 +02:00
|
|
|
if isinstance(exception, JsonableError):
|
2017-07-21 02:19:52 +02:00
|
|
|
return json_response_from_error(exception)
|
2013-12-17 22:50:49 +01:00
|
|
|
if request.error_format == "JSON":
|
2018-03-01 23:55:37 +01:00
|
|
|
logging.error(traceback.format_exc(), extra=dict(request=request))
|
2016-05-25 15:02:02 +02:00
|
|
|
return json_error(_("Internal server error"), status=500)
|
2012-12-19 20:19:46 +01:00
|
|
|
return None
|
2013-02-12 17:26:12 +01:00
|
|
|
|
2017-05-18 11:56:03 +02:00
|
|
|
class TagRequests(MiddlewareMixin):
|
2018-03-14 23:16:27 +01:00
|
|
|
def process_view(self, request: HttpRequest, view_func: ViewFuncT,
|
2017-11-27 07:33:05 +01:00
|
|
|
args: List[str], kwargs: Dict[str, Any]) -> None:
|
2013-12-17 22:18:13 +01:00
|
|
|
self.process_request(request)
|
2016-11-29 07:22:02 +01:00
|
|
|
|
2017-11-27 07:33:05 +01:00
|
|
|
def process_request(self, request: HttpRequest) -> None:
|
2013-12-17 22:18:13 +01:00
|
|
|
if request.path.startswith("/api/") or request.path.startswith("/json/"):
|
|
|
|
request.error_format = "JSON"
|
|
|
|
else:
|
|
|
|
request.error_format = "HTML"
|
|
|
|
|
2017-07-25 03:30:13 +02:00
|
|
|
class CsrfFailureError(JsonableError):
|
|
|
|
http_status_code = 403
|
|
|
|
code = ErrorCode.CSRF_FAILED
|
|
|
|
data_fields = ['reason']
|
|
|
|
|
2018-05-11 01:39:17 +02:00
|
|
|
def __init__(self, reason: str) -> None:
|
|
|
|
self.reason = reason # type: str
|
2017-07-25 03:30:13 +02:00
|
|
|
|
|
|
|
@staticmethod
|
2018-05-11 01:39:17 +02:00
|
|
|
def msg_format() -> str:
|
2017-07-25 03:30:13 +02:00
|
|
|
return _("CSRF Error: {reason}")
|
|
|
|
|
2018-05-11 01:39:17 +02:00
|
|
|
def csrf_failure(request: HttpRequest, reason: str="") -> HttpResponse:
|
2013-12-17 22:18:13 +01:00
|
|
|
if request.error_format == "JSON":
|
2017-07-25 03:30:13 +02:00
|
|
|
return json_response_from_error(CsrfFailureError(reason))
|
2013-12-17 22:18:13 +01:00
|
|
|
else:
|
|
|
|
return html_csrf_failure(request, reason)
|
|
|
|
|
2017-05-18 11:56:03 +02:00
|
|
|
class RateLimitMiddleware(MiddlewareMixin):
|
2017-11-27 07:33:05 +01:00
|
|
|
def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
|
2013-05-29 23:58:07 +02:00
|
|
|
if not settings.RATE_LIMITING:
|
|
|
|
return response
|
|
|
|
|
2017-07-31 08:08:47 +02:00
|
|
|
from zerver.lib.rate_limiter import max_api_calls, RateLimitedUser
|
2013-05-29 23:58:07 +02:00
|
|
|
# Add X-RateLimit-*** headers
|
2019-04-01 20:11:56 +02:00
|
|
|
if hasattr(request, '_ratelimit'):
|
|
|
|
# Right now, the only kind of limiting requests is user-based.
|
|
|
|
ratelimit_user_results = request._ratelimit['RateLimitedUser']
|
2017-07-31 08:08:47 +02:00
|
|
|
entity = RateLimitedUser(request.user)
|
|
|
|
response['X-RateLimit-Limit'] = str(max_api_calls(entity))
|
2019-04-01 20:11:56 +02:00
|
|
|
response['X-RateLimit-Reset'] = str(int(time.time() + ratelimit_user_results['secs_to_freedom']))
|
|
|
|
if 'remaining' in ratelimit_user_results:
|
|
|
|
response['X-RateLimit-Remaining'] = str(ratelimit_user_results['remaining'])
|
2013-05-29 23:58:07 +02:00
|
|
|
return response
|
|
|
|
|
2017-11-27 07:33:05 +01:00
|
|
|
def process_exception(self, request: HttpRequest, exception: Exception) -> Optional[HttpResponse]:
|
2015-11-01 17:10:16 +01:00
|
|
|
if isinstance(exception, RateLimited):
|
2019-04-01 20:11:56 +02:00
|
|
|
entity_type = str(exception) # entity type is passed to RateLimited when raising
|
2017-05-22 20:12:59 +02:00
|
|
|
resp = json_error(
|
|
|
|
_("API usage exceeded rate limit"),
|
2019-04-01 20:11:56 +02:00
|
|
|
data={'retry-after': request._ratelimit[entity_type]['secs_to_freedom']},
|
2017-05-22 20:12:59 +02:00
|
|
|
status=429
|
|
|
|
)
|
2019-04-01 20:11:56 +02:00
|
|
|
resp['Retry-After'] = request._ratelimit[entity_type]['secs_to_freedom']
|
2013-05-29 23:58:07 +02:00
|
|
|
return resp
|
2017-08-02 17:21:46 +02:00
|
|
|
return None
|
2013-11-08 21:13:34 +01:00
|
|
|
|
2017-05-18 11:56:03 +02:00
|
|
|
class FlushDisplayRecipientCache(MiddlewareMixin):
|
2017-11-27 07:33:05 +01:00
|
|
|
def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
|
2013-12-18 23:00:14 +01:00
|
|
|
# We flush the per-request caches after every request, so they
|
|
|
|
# are not shared at all between requests.
|
|
|
|
flush_per_request_caches()
|
2013-11-08 21:13:34 +01:00
|
|
|
return response
|
2015-01-16 05:59:20 +01:00
|
|
|
|
|
|
|
class SessionHostDomainMiddleware(SessionMiddleware):
|
2017-11-27 07:33:05 +01:00
|
|
|
def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
|
2017-01-30 05:17:48 +01:00
|
|
|
try:
|
|
|
|
request.get_host()
|
|
|
|
except DisallowedHost:
|
|
|
|
# If we get a DisallowedHost exception trying to access
|
|
|
|
# the host, (1) the request is failed anyway and so the
|
|
|
|
# below code will do nothing, and (2) the below will
|
|
|
|
# trigger a recursive exception, breaking things, so we
|
|
|
|
# just return here.
|
|
|
|
return response
|
|
|
|
|
2017-10-02 08:32:09 +02:00
|
|
|
if (not request.path.startswith("/static/") and not request.path.startswith("/api/") and
|
|
|
|
not request.path.startswith("/json/")):
|
|
|
|
subdomain = get_subdomain(request)
|
2017-10-20 02:56:49 +02:00
|
|
|
if subdomain != Realm.SUBDOMAIN_FOR_ROOT_DOMAIN:
|
2019-05-04 04:47:44 +02:00
|
|
|
try:
|
|
|
|
get_realm(subdomain)
|
|
|
|
except Realm.DoesNotExist:
|
2019-03-12 01:56:52 +01:00
|
|
|
return render(request, "zerver/invalid_realm.html", status=404)
|
2015-01-16 05:59:20 +01:00
|
|
|
"""
|
|
|
|
If request.session was modified, or if the configuration is to save the
|
2018-11-14 23:48:53 +01:00
|
|
|
session every time, save the changes and set a session cookie or delete
|
|
|
|
the session cookie if the session has been emptied.
|
2015-01-16 05:59:20 +01:00
|
|
|
"""
|
|
|
|
try:
|
|
|
|
accessed = request.session.accessed
|
|
|
|
modified = request.session.modified
|
2018-11-14 23:48:53 +01:00
|
|
|
empty = request.session.is_empty()
|
2015-01-16 05:59:20 +01:00
|
|
|
except AttributeError:
|
|
|
|
pass
|
|
|
|
else:
|
2018-11-14 23:48:53 +01:00
|
|
|
# First check if we need to delete this cookie.
|
|
|
|
# The session should be deleted only if the session is entirely empty
|
|
|
|
if settings.SESSION_COOKIE_NAME in request.COOKIES and empty:
|
|
|
|
response.delete_cookie(
|
|
|
|
settings.SESSION_COOKIE_NAME,
|
|
|
|
path=settings.SESSION_COOKIE_PATH,
|
|
|
|
domain=settings.SESSION_COOKIE_DOMAIN,
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
if accessed:
|
|
|
|
patch_vary_headers(response, ('Cookie',))
|
|
|
|
if (modified or settings.SESSION_SAVE_EVERY_REQUEST) and not empty:
|
|
|
|
if request.session.get_expire_at_browser_close():
|
|
|
|
max_age = None
|
|
|
|
expires = None
|
|
|
|
else:
|
|
|
|
max_age = request.session.get_expiry_age()
|
|
|
|
expires_time = time.time() + max_age
|
|
|
|
expires = cookie_date(expires_time)
|
|
|
|
# Save the session data and refresh the client cookie.
|
|
|
|
# Skip session save for 500 responses, refs #3881.
|
|
|
|
if response.status_code != 500:
|
|
|
|
try:
|
|
|
|
request.session.save()
|
|
|
|
except UpdateError:
|
|
|
|
raise SuspiciousOperation(
|
|
|
|
"The request's session was deleted before the "
|
|
|
|
"request completed. The user may have logged "
|
|
|
|
"out in a concurrent request, for example."
|
|
|
|
)
|
|
|
|
host = request.get_host().split(':')[0]
|
|
|
|
|
|
|
|
# The subdomains feature overrides the
|
|
|
|
# SESSION_COOKIE_DOMAIN setting, since the setting
|
|
|
|
# is a fixed value and with subdomains enabled,
|
|
|
|
# the session cookie domain has to vary with the
|
|
|
|
# subdomain.
|
|
|
|
session_cookie_domain = host
|
|
|
|
response.set_cookie(
|
|
|
|
settings.SESSION_COOKIE_NAME,
|
|
|
|
request.session.session_key, max_age=max_age,
|
|
|
|
expires=expires, domain=session_cookie_domain,
|
|
|
|
path=settings.SESSION_COOKIE_PATH,
|
|
|
|
secure=settings.SESSION_COOKIE_SECURE or None,
|
|
|
|
httponly=settings.SESSION_COOKIE_HTTPONLY or None,
|
|
|
|
)
|
2015-01-16 05:59:20 +01:00
|
|
|
return response
|
2017-01-30 23:19:38 +01:00
|
|
|
|
2017-05-18 11:56:03 +02:00
|
|
|
class SetRemoteAddrFromForwardedFor(MiddlewareMixin):
|
2017-01-30 23:19:38 +01:00
|
|
|
"""
|
|
|
|
Middleware that sets REMOTE_ADDR based on the HTTP_X_FORWARDED_FOR.
|
|
|
|
|
|
|
|
This middleware replicates Django's former SetRemoteAddrFromForwardedFor middleware.
|
|
|
|
Because Zulip sits behind a NGINX reverse proxy, if the HTTP_X_FORWARDED_FOR
|
|
|
|
is set in the request, then it has properly been set by NGINX.
|
|
|
|
Therefore HTTP_X_FORWARDED_FOR's value is trusted.
|
|
|
|
"""
|
2017-11-27 07:33:05 +01:00
|
|
|
def process_request(self, request: HttpRequest) -> None:
|
2017-01-30 23:19:38 +01:00
|
|
|
try:
|
|
|
|
real_ip = request.META['HTTP_X_FORWARDED_FOR']
|
|
|
|
except KeyError:
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
# HTTP_X_FORWARDED_FOR can be a comma-separated list of IPs.
|
|
|
|
# For NGINX reverse proxy servers, the client's IP will be the first one.
|
|
|
|
real_ip = real_ip.split(",")[0].strip()
|
|
|
|
request.META['REMOTE_ADDR'] = real_ip
|
2018-12-14 23:28:00 +01:00
|
|
|
|
2019-02-28 02:30:16 +01:00
|
|
|
def alter_content(request: HttpRequest, content: bytes) -> bytes:
|
|
|
|
first_paragraph_text = get_content_description(content, request)
|
|
|
|
return content.replace(request.placeholder_open_graph_description.encode("utf-8"),
|
2019-04-24 03:37:34 +02:00
|
|
|
first_paragraph_text.encode("utf-8"))
|
2018-12-14 23:28:00 +01:00
|
|
|
|
2019-02-28 02:30:16 +01:00
|
|
|
class FinalizeOpenGraphDescription(MiddlewareMixin):
|
|
|
|
def process_response(self, request: HttpRequest,
|
|
|
|
response: StreamingHttpResponse) -> StreamingHttpResponse:
|
2018-12-14 23:28:00 +01:00
|
|
|
|
|
|
|
if getattr(request, "placeholder_open_graph_description", None) is not None:
|
|
|
|
assert not response.streaming
|
2019-02-28 02:30:16 +01:00
|
|
|
response.content = alter_content(request, response.content)
|
2018-12-14 23:28:00 +01:00
|
|
|
return response
|