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.

wsgi.py 7.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import cgi
  2. import codecs
  3. import re
  4. from io import BytesIO
  5. from django.conf import settings
  6. from django.core import signals
  7. from django.core.handlers import base
  8. from django.http import HttpRequest, QueryDict, parse_cookie
  9. from django.urls import set_script_prefix
  10. from django.utils.encoding import repercent_broken_unicode
  11. from django.utils.functional import cached_property
  12. _slashes_re = re.compile(br'/+')
  13. class LimitedStream:
  14. """Wrap another stream to disallow reading it past a number of bytes."""
  15. def __init__(self, stream, limit, buf_size=64 * 1024 * 1024):
  16. self.stream = stream
  17. self.remaining = limit
  18. self.buffer = b''
  19. self.buf_size = buf_size
  20. def _read_limited(self, size=None):
  21. if size is None or size > self.remaining:
  22. size = self.remaining
  23. if size == 0:
  24. return b''
  25. result = self.stream.read(size)
  26. self.remaining -= len(result)
  27. return result
  28. def read(self, size=None):
  29. if size is None:
  30. result = self.buffer + self._read_limited()
  31. self.buffer = b''
  32. elif size < len(self.buffer):
  33. result = self.buffer[:size]
  34. self.buffer = self.buffer[size:]
  35. else: # size >= len(self.buffer)
  36. result = self.buffer + self._read_limited(size - len(self.buffer))
  37. self.buffer = b''
  38. return result
  39. def readline(self, size=None):
  40. while b'\n' not in self.buffer and \
  41. (size is None or len(self.buffer) < size):
  42. if size:
  43. # since size is not None here, len(self.buffer) < size
  44. chunk = self._read_limited(size - len(self.buffer))
  45. else:
  46. chunk = self._read_limited()
  47. if not chunk:
  48. break
  49. self.buffer += chunk
  50. sio = BytesIO(self.buffer)
  51. if size:
  52. line = sio.readline(size)
  53. else:
  54. line = sio.readline()
  55. self.buffer = sio.read()
  56. return line
  57. class WSGIRequest(HttpRequest):
  58. def __init__(self, environ):
  59. script_name = get_script_name(environ)
  60. # If PATH_INFO is empty (e.g. accessing the SCRIPT_NAME URL without a
  61. # trailing slash), operate as if '/' was requested.
  62. path_info = get_path_info(environ) or '/'
  63. self.environ = environ
  64. self.path_info = path_info
  65. # be careful to only replace the first slash in the path because of
  66. # http://test/something and http://test//something being different as
  67. # stated in https://www.ietf.org/rfc/rfc2396.txt
  68. self.path = '%s/%s' % (script_name.rstrip('/'),
  69. path_info.replace('/', '', 1))
  70. self.META = environ
  71. self.META['PATH_INFO'] = path_info
  72. self.META['SCRIPT_NAME'] = script_name
  73. self.method = environ['REQUEST_METHOD'].upper()
  74. self.content_type, self.content_params = cgi.parse_header(environ.get('CONTENT_TYPE', ''))
  75. if 'charset' in self.content_params:
  76. try:
  77. codecs.lookup(self.content_params['charset'])
  78. except LookupError:
  79. pass
  80. else:
  81. self.encoding = self.content_params['charset']
  82. try:
  83. content_length = int(environ.get('CONTENT_LENGTH'))
  84. except (ValueError, TypeError):
  85. content_length = 0
  86. self._stream = LimitedStream(self.environ['wsgi.input'], content_length)
  87. self._read_started = False
  88. self.resolver_match = None
  89. def _get_scheme(self):
  90. return self.environ.get('wsgi.url_scheme')
  91. @cached_property
  92. def GET(self):
  93. # The WSGI spec says 'QUERY_STRING' may be absent.
  94. raw_query_string = get_bytes_from_wsgi(self.environ, 'QUERY_STRING', '')
  95. return QueryDict(raw_query_string, encoding=self._encoding)
  96. def _get_post(self):
  97. if not hasattr(self, '_post'):
  98. self._load_post_and_files()
  99. return self._post
  100. def _set_post(self, post):
  101. self._post = post
  102. @cached_property
  103. def COOKIES(self):
  104. raw_cookie = get_str_from_wsgi(self.environ, 'HTTP_COOKIE', '')
  105. return parse_cookie(raw_cookie)
  106. @property
  107. def FILES(self):
  108. if not hasattr(self, '_files'):
  109. self._load_post_and_files()
  110. return self._files
  111. POST = property(_get_post, _set_post)
  112. class WSGIHandler(base.BaseHandler):
  113. request_class = WSGIRequest
  114. def __init__(self, *args, **kwargs):
  115. super().__init__(*args, **kwargs)
  116. self.load_middleware()
  117. def __call__(self, environ, start_response):
  118. set_script_prefix(get_script_name(environ))
  119. signals.request_started.send(sender=self.__class__, environ=environ)
  120. request = self.request_class(environ)
  121. response = self.get_response(request)
  122. response._handler_class = self.__class__
  123. status = '%d %s' % (response.status_code, response.reason_phrase)
  124. response_headers = [
  125. *response.items(),
  126. *(('Set-Cookie', c.output(header='')) for c in response.cookies.values()),
  127. ]
  128. start_response(status, response_headers)
  129. if getattr(response, 'file_to_stream', None) is not None and environ.get('wsgi.file_wrapper'):
  130. response = environ['wsgi.file_wrapper'](response.file_to_stream)
  131. return response
  132. def get_path_info(environ):
  133. """Return the HTTP request's PATH_INFO as a string."""
  134. path_info = get_bytes_from_wsgi(environ, 'PATH_INFO', '/')
  135. return repercent_broken_unicode(path_info).decode()
  136. def get_script_name(environ):
  137. """
  138. Return the equivalent of the HTTP request's SCRIPT_NAME environment
  139. variable. If Apache mod_rewrite is used, return what would have been
  140. the script name prior to any rewriting (so it's the script name as seen
  141. from the client's perspective), unless the FORCE_SCRIPT_NAME setting is
  142. set (to anything).
  143. """
  144. if settings.FORCE_SCRIPT_NAME is not None:
  145. return settings.FORCE_SCRIPT_NAME
  146. # If Apache's mod_rewrite had a whack at the URL, Apache set either
  147. # SCRIPT_URL or REDIRECT_URL to the full resource URL before applying any
  148. # rewrites. Unfortunately not every Web server (lighttpd!) passes this
  149. # information through all the time, so FORCE_SCRIPT_NAME, above, is still
  150. # needed.
  151. script_url = get_bytes_from_wsgi(environ, 'SCRIPT_URL', '') or get_bytes_from_wsgi(environ, 'REDIRECT_URL', '')
  152. if script_url:
  153. if b'//' in script_url:
  154. # mod_wsgi squashes multiple successive slashes in PATH_INFO,
  155. # do the same with script_url before manipulating paths (#17133).
  156. script_url = _slashes_re.sub(b'/', script_url)
  157. path_info = get_bytes_from_wsgi(environ, 'PATH_INFO', '')
  158. script_name = script_url[:-len(path_info)] if path_info else script_url
  159. else:
  160. script_name = get_bytes_from_wsgi(environ, 'SCRIPT_NAME', '')
  161. return script_name.decode()
  162. def get_bytes_from_wsgi(environ, key, default):
  163. """
  164. Get a value from the WSGI environ dictionary as bytes.
  165. key and default should be strings.
  166. """
  167. value = environ.get(key, default)
  168. # Non-ASCII values in the WSGI environ are arbitrarily decoded with
  169. # ISO-8859-1. This is wrong for Django websites where UTF-8 is the default.
  170. # Re-encode to recover the original bytestring.
  171. return value.encode('iso-8859-1')
  172. def get_str_from_wsgi(environ, key, default):
  173. """
  174. Get a value from the WSGI environ dictionary as str.
  175. key and default should be str objects.
  176. """
  177. value = get_bytes_from_wsgi(environ, key, default)
  178. return value.decode(errors='replace')