proxy.py 6.1 KB

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