proxy.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import io
  2. import logging
  3. import zlib
  4. from django.conf import settings
  5. from django.core.exceptions import RequestDataTooBig
  6. try:
  7. import uwsgi
  8. has_uwsgi = True
  9. except ImportError:
  10. has_uwsgi = False
  11. from django.conf import settings
  12. logger = logging.getLogger(__name__)
  13. Z_CHUNK = 1024 * 8
  14. if has_uwsgi:
  15. class UWsgiChunkedInput(io.RawIOBase):
  16. def __init__(self):
  17. self._internal_buffer = b""
  18. def readable(self):
  19. return True
  20. def readinto(self, buf):
  21. if not self._internal_buffer:
  22. self._internal_buffer = uwsgi.chunked_read()
  23. n = min(len(buf), len(self._internal_buffer))
  24. if n > 0:
  25. buf[:n] = self._internal_buffer[:n]
  26. self._internal_buffer = self._internal_buffer[n:]
  27. return n
  28. class ZDecoder(io.RawIOBase):
  29. """
  30. Base class for HTTP content decoders based on zlib
  31. See: https://github.com/eBay/wextracto/blob/9c789b1c98d95a1e87dbedfd1541a8688d128f5c/wex/http_decoder.py
  32. """
  33. def __init__(self, fp, z=None):
  34. self.fp = fp
  35. self.z = z
  36. self.flushed = None
  37. self.counter = 0
  38. def readable(self):
  39. return True
  40. def readinto(self, buf):
  41. if self.z is None:
  42. self.z = zlib.decompressobj()
  43. retry = True
  44. else:
  45. retry = False
  46. n = 0
  47. max_length = len(buf)
  48. # DOS mitigation - block unzipped payloads larger than max allowed size
  49. self.counter += 1
  50. if self.counter * max_length > settings.GLITCHTIP_MAX_UNZIPPED_PAYLOAD_SIZE:
  51. raise RequestDataTooBig()
  52. while max_length > 0:
  53. if self.flushed is None:
  54. chunk = self.fp.read(Z_CHUNK)
  55. compressed = self.z.unconsumed_tail + chunk
  56. try:
  57. decompressed = self.z.decompress(compressed, max_length)
  58. except zlib.error:
  59. if not retry:
  60. raise
  61. self.z = zlib.decompressobj(-zlib.MAX_WBITS)
  62. retry = False
  63. decompressed = self.z.decompress(compressed, max_length)
  64. if not chunk:
  65. self.flushed = self.z.flush()
  66. else:
  67. if not self.flushed:
  68. return n
  69. decompressed = self.flushed[:max_length]
  70. self.flushed = self.flushed[max_length:]
  71. buf[n : n + len(decompressed)] = decompressed
  72. n += len(decompressed)
  73. max_length = len(buf) - n
  74. return n
  75. class DeflateDecoder(ZDecoder):
  76. """
  77. Decoding for "content-encoding: deflate"
  78. """
  79. class GzipDecoder(ZDecoder):
  80. """
  81. Decoding for "content-encoding: gzip"
  82. """
  83. def __init__(self, fp):
  84. ZDecoder.__init__(self, fp, zlib.decompressobj(16 + zlib.MAX_WBITS))
  85. class SetRemoteAddrFromForwardedFor(object):
  86. def __init__(self):
  87. if not getattr(settings, "SENTRY_USE_X_FORWARDED_FOR", True):
  88. from django.core.exceptions import MiddlewareNotUsed
  89. raise MiddlewareNotUsed
  90. def _remove_port_number(self, ip_address):
  91. if "[" in ip_address and "]" in ip_address:
  92. # IPv6 address with brackets, possibly with a port number
  93. return ip_address[ip_address.find("[") + 1 : ip_address.find("]")]
  94. if "." in ip_address and ip_address.rfind(":") > ip_address.rfind("."):
  95. # IPv4 address with port number
  96. # the last condition excludes IPv4-mapped IPv6 addresses
  97. return ip_address.rsplit(":", 1)[0]
  98. return ip_address
  99. def process_request(self, request):
  100. try:
  101. real_ip = request.META["HTTP_X_FORWARDED_FOR"]
  102. except KeyError:
  103. pass
  104. else:
  105. # HTTP_X_FORWARDED_FOR can be a comma-separated list of IPs.
  106. # Take just the first one.
  107. real_ip = real_ip.split(",")[0].strip()
  108. real_ip = self._remove_port_number(real_ip)
  109. request.META["REMOTE_ADDR"] = real_ip
  110. class ChunkedMiddleware(object):
  111. def __init__(self):
  112. if not has_uwsgi:
  113. from django.core.exceptions import MiddlewareNotUsed
  114. raise MiddlewareNotUsed
  115. def process_request(self, request):
  116. # If we are dealing with chunked data and we have uwsgi we assume
  117. # that we can read to the end of the input stream so we can bypass
  118. # the default limited stream. We set the content length reasonably
  119. # high so that the reads generally succeed. This is ugly but with
  120. # Django 1.6 it seems to be the best we can easily do.
  121. if "HTTP_TRANSFER_ENCODING" not in request.META:
  122. return
  123. if request.META["HTTP_TRANSFER_ENCODING"].lower() == "chunked":
  124. request._stream = io.BufferedReader(UWsgiChunkedInput())
  125. request.META["CONTENT_LENGTH"] = "4294967295" # 0xffffffff
  126. class DecompressBodyMiddleware(object):
  127. def __init__(self, get_response):
  128. self.get_response = get_response
  129. def __call__(self, request):
  130. decode = False
  131. encoding = request.META.get("HTTP_CONTENT_ENCODING", "").lower()
  132. if encoding == "gzip":
  133. request._stream = GzipDecoder(request._stream)
  134. decode = True
  135. if encoding == "deflate":
  136. request._stream = DeflateDecoder(request._stream)
  137. decode = True
  138. if decode:
  139. # Since we don't know the original content length ahead of time, we
  140. # need to set the content length reasonably high so read generally
  141. # succeeds. This seems to be the only easy way for Django 1.6.
  142. request.META["CONTENT_LENGTH"] = "4294967295" # 0xffffffff
  143. # The original content encoding is no longer valid, so we have to
  144. # remove the header. Otherwise, LazyData will attempt to re-decode
  145. # the body.
  146. del request.META["HTTP_CONTENT_ENCODING"]
  147. return self.get_response(request)
  148. class ContentLengthHeaderMiddleware(object):
  149. """
  150. Ensure that we have a proper Content-Length/Transfer-Encoding header
  151. """
  152. def process_response(self, request, response):
  153. if "Transfer-Encoding" in response or "Content-Length" in response:
  154. return response
  155. if not response.streaming:
  156. response["Content-Length"] = str(len(response.content))
  157. return response