proxy.py 7.8 KB

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