You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

common.py 7.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import re
  2. from urllib.parse import urlparse
  3. from django.conf import settings
  4. from django.core.exceptions import PermissionDenied
  5. from django.core.mail import mail_managers
  6. from django.http import HttpResponsePermanentRedirect
  7. from django.urls import is_valid_path
  8. from django.utils.deprecation import MiddlewareMixin
  9. from django.utils.http import escape_leading_slashes
  10. class CommonMiddleware(MiddlewareMixin):
  11. """
  12. "Common" middleware for taking care of some basic operations:
  13. - Forbid access to User-Agents in settings.DISALLOWED_USER_AGENTS
  14. - URL rewriting: Based on the APPEND_SLASH and PREPEND_WWW settings,
  15. append missing slashes and/or prepends missing "www."s.
  16. - If APPEND_SLASH is set and the initial URL doesn't end with a
  17. slash, and it is not found in urlpatterns, form a new URL by
  18. appending a slash at the end. If this new URL is found in
  19. urlpatterns, return an HTTP redirect to this new URL; otherwise
  20. process the initial URL as usual.
  21. This behavior can be customized by subclassing CommonMiddleware and
  22. overriding the response_redirect_class attribute.
  23. """
  24. response_redirect_class = HttpResponsePermanentRedirect
  25. def process_request(self, request):
  26. """
  27. Check for denied User-Agents and rewrite the URL based on
  28. settings.APPEND_SLASH and settings.PREPEND_WWW
  29. """
  30. # Check for denied User-Agents
  31. if 'HTTP_USER_AGENT' in request.META:
  32. for user_agent_regex in settings.DISALLOWED_USER_AGENTS:
  33. if user_agent_regex.search(request.META['HTTP_USER_AGENT']):
  34. raise PermissionDenied('Forbidden user agent')
  35. # Check for a redirect based on settings.PREPEND_WWW
  36. host = request.get_host()
  37. must_prepend = settings.PREPEND_WWW and host and not host.startswith('www.')
  38. redirect_url = ('%s://www.%s' % (request.scheme, host)) if must_prepend else ''
  39. # Check if a slash should be appended
  40. if self.should_redirect_with_slash(request):
  41. path = self.get_full_path_with_slash(request)
  42. else:
  43. path = request.get_full_path()
  44. # Return a redirect if necessary
  45. if redirect_url or path != request.get_full_path():
  46. redirect_url += path
  47. return self.response_redirect_class(redirect_url)
  48. def should_redirect_with_slash(self, request):
  49. """
  50. Return True if settings.APPEND_SLASH is True and appending a slash to
  51. the request path turns an invalid path into a valid one.
  52. """
  53. if settings.APPEND_SLASH and not request.path_info.endswith('/'):
  54. urlconf = getattr(request, 'urlconf', None)
  55. return (
  56. not is_valid_path(request.path_info, urlconf) and
  57. is_valid_path('%s/' % request.path_info, urlconf)
  58. )
  59. return False
  60. def get_full_path_with_slash(self, request):
  61. """
  62. Return the full path of the request with a trailing slash appended.
  63. Raise a RuntimeError if settings.DEBUG is True and request.method is
  64. POST, PUT, or PATCH.
  65. """
  66. new_path = request.get_full_path(force_append_slash=True)
  67. # Prevent construction of scheme relative urls.
  68. new_path = escape_leading_slashes(new_path)
  69. if settings.DEBUG and request.method in ('POST', 'PUT', 'PATCH'):
  70. raise RuntimeError(
  71. "You called this URL via %(method)s, but the URL doesn't end "
  72. "in a slash and you have APPEND_SLASH set. Django can't "
  73. "redirect to the slash URL while maintaining %(method)s data. "
  74. "Change your form to point to %(url)s (note the trailing "
  75. "slash), or set APPEND_SLASH=False in your Django settings." % {
  76. 'method': request.method,
  77. 'url': request.get_host() + new_path,
  78. }
  79. )
  80. return new_path
  81. def process_response(self, request, response):
  82. """
  83. When the status code of the response is 404, it may redirect to a path
  84. with an appended slash if should_redirect_with_slash() returns True.
  85. """
  86. # If the given URL is "Not Found", then check if we should redirect to
  87. # a path with a slash appended.
  88. if response.status_code == 404:
  89. if self.should_redirect_with_slash(request):
  90. return self.response_redirect_class(self.get_full_path_with_slash(request))
  91. # Add the Content-Length header to non-streaming responses if not
  92. # already set.
  93. if not response.streaming and not response.has_header('Content-Length'):
  94. response['Content-Length'] = str(len(response.content))
  95. return response
  96. class BrokenLinkEmailsMiddleware(MiddlewareMixin):
  97. def process_response(self, request, response):
  98. """Send broken link emails for relevant 404 NOT FOUND responses."""
  99. if response.status_code == 404 and not settings.DEBUG:
  100. domain = request.get_host()
  101. path = request.get_full_path()
  102. referer = request.META.get('HTTP_REFERER', '')
  103. if not self.is_ignorable_request(request, path, domain, referer):
  104. ua = request.META.get('HTTP_USER_AGENT', '<none>')
  105. ip = request.META.get('REMOTE_ADDR', '<none>')
  106. mail_managers(
  107. "Broken %slink on %s" % (
  108. ('INTERNAL ' if self.is_internal_request(domain, referer) else ''),
  109. domain
  110. ),
  111. "Referrer: %s\nRequested URL: %s\nUser agent: %s\n"
  112. "IP address: %s\n" % (referer, path, ua, ip),
  113. fail_silently=True,
  114. )
  115. return response
  116. def is_internal_request(self, domain, referer):
  117. """
  118. Return True if the referring URL is the same domain as the current
  119. request.
  120. """
  121. # Different subdomains are treated as different domains.
  122. return bool(re.match("^https?://%s/" % re.escape(domain), referer))
  123. def is_ignorable_request(self, request, uri, domain, referer):
  124. """
  125. Return True if the given request *shouldn't* notify the site managers
  126. according to project settings or in situations outlined by the inline
  127. comments.
  128. """
  129. # The referer is empty.
  130. if not referer:
  131. return True
  132. # APPEND_SLASH is enabled and the referer is equal to the current URL
  133. # without a trailing slash indicating an internal redirect.
  134. if settings.APPEND_SLASH and uri.endswith('/') and referer == uri[:-1]:
  135. return True
  136. # A '?' in referer is identified as a search engine source.
  137. if not self.is_internal_request(domain, referer) and '?' in referer:
  138. return True
  139. # The referer is equal to the current URL, ignoring the scheme (assumed
  140. # to be a poorly implemented bot).
  141. parsed_referer = urlparse(referer)
  142. if parsed_referer.netloc in ['', domain] and parsed_referer.path == uri:
  143. return True
  144. return any(pattern.search(uri) for pattern in settings.IGNORABLE_404_URLS)