proxy_fix.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. """
  2. X-Forwarded-For Proxy Fix
  3. =========================
  4. This module provides a middleware that adjusts the WSGI environ based on
  5. ``X-Forwarded-`` headers that proxies in front of an application may
  6. set.
  7. When an application is running behind a proxy server, WSGI may see the
  8. request as coming from that server rather than the real client. Proxies
  9. set various headers to track where the request actually came from.
  10. This middleware should only be applied if the application is actually
  11. behind such a proxy, and should be configured with the number of proxies
  12. that are chained in front of it. Not all proxies set all the headers.
  13. Since incoming headers can be faked, you must set how many proxies are
  14. setting each header so the middleware knows what to trust.
  15. .. autoclass:: ProxyFix
  16. :copyright: 2007 Pallets
  17. :license: BSD-3-Clause
  18. """
  19. import warnings
  20. class ProxyFix(object):
  21. """Adjust the WSGI environ based on ``X-Forwarded-`` that proxies in
  22. front of the application may set.
  23. - ``X-Forwarded-For`` sets ``REMOTE_ADDR``.
  24. - ``X-Forwarded-Proto`` sets ``wsgi.url_scheme``.
  25. - ``X-Forwarded-Host`` sets ``HTTP_HOST``, ``SERVER_NAME``, and
  26. ``SERVER_PORT``.
  27. - ``X-Forwarded-Port`` sets ``HTTP_HOST`` and ``SERVER_PORT``.
  28. - ``X-Forwarded-Prefix`` sets ``SCRIPT_NAME``.
  29. You must tell the middleware how many proxies set each header so it
  30. knows what values to trust. It is a security issue to trust values
  31. that came from the client rather than a proxy.
  32. The original values of the headers are stored in the WSGI
  33. environ as ``werkzeug.proxy_fix.orig``, a dict.
  34. :param app: The WSGI application to wrap.
  35. :param x_for: Number of values to trust for ``X-Forwarded-For``.
  36. :param x_proto: Number of values to trust for ``X-Forwarded-Proto``.
  37. :param x_host: Number of values to trust for ``X-Forwarded-Host``.
  38. :param x_port: Number of values to trust for ``X-Forwarded-Port``.
  39. :param x_prefix: Number of values to trust for
  40. ``X-Forwarded-Prefix``.
  41. :param num_proxies: Deprecated, use ``x_for`` instead.
  42. .. code-block:: python
  43. from werkzeug.middleware.proxy_fix import ProxyFix
  44. # App is behind one proxy that sets the -For and -Host headers.
  45. app = ProxyFix(app, x_for=1, x_host=1)
  46. .. versionchanged:: 0.15
  47. All headers support multiple values. The ``num_proxies``
  48. argument is deprecated. Each header is configured with a
  49. separate number of trusted proxies.
  50. .. versionchanged:: 0.15
  51. Original WSGI environ values are stored in the
  52. ``werkzeug.proxy_fix.orig`` dict. ``orig_remote_addr``,
  53. ``orig_wsgi_url_scheme``, and ``orig_http_host`` are deprecated
  54. and will be removed in 1.0.
  55. .. versionchanged:: 0.15
  56. Support ``X-Forwarded-Port`` and ``X-Forwarded-Prefix``.
  57. .. versionchanged:: 0.15
  58. ``X-Fowarded-Host`` and ``X-Forwarded-Port`` modify
  59. ``SERVER_NAME`` and ``SERVER_PORT``.
  60. """
  61. def __init__(
  62. self, app, num_proxies=None, x_for=1, x_proto=1, x_host=0, x_port=0, x_prefix=0
  63. ):
  64. self.app = app
  65. self.x_for = x_for
  66. self.x_proto = x_proto
  67. self.x_host = x_host
  68. self.x_port = x_port
  69. self.x_prefix = x_prefix
  70. self.num_proxies = num_proxies
  71. @property
  72. def num_proxies(self):
  73. """The number of proxies setting ``X-Forwarded-For`` in front
  74. of the application.
  75. .. deprecated:: 0.15
  76. A separate number of trusted proxies is configured for each
  77. header. ``num_proxies`` maps to ``x_for``. This method will
  78. be removed in 1.0.
  79. :internal:
  80. """
  81. warnings.warn(
  82. "'num_proxies' is deprecated as of version 0.15 and will be"
  83. " removed in version 1.0. Use 'x_for' instead.",
  84. DeprecationWarning,
  85. stacklevel=2,
  86. )
  87. return self.x_for
  88. @num_proxies.setter
  89. def num_proxies(self, value):
  90. if value is not None:
  91. warnings.warn(
  92. "'num_proxies' is deprecated as of version 0.15 and"
  93. " will be removed in version 1.0. Use"
  94. " 'x_for={value}, x_proto={value}, x_host={value}'"
  95. " instead.".format(value=value),
  96. DeprecationWarning,
  97. stacklevel=2,
  98. )
  99. self.x_for = value
  100. self.x_proto = value
  101. self.x_host = value
  102. def get_remote_addr(self, forwarded_for):
  103. """Get the real ``remote_addr`` by looking backwards ``x_for``
  104. number of values in the ``X-Forwarded-For`` header.
  105. :param forwarded_for: List of values parsed from the
  106. ``X-Forwarded-For`` header.
  107. :return: The real ``remote_addr``, or ``None`` if there were not
  108. at least ``x_for`` values.
  109. .. deprecated:: 0.15
  110. This is handled internally for each header. This method will
  111. be removed in 1.0.
  112. .. versionchanged:: 0.9
  113. Use ``num_proxies`` instead of always picking the first
  114. value.
  115. .. versionadded:: 0.8
  116. """
  117. warnings.warn(
  118. "'get_remote_addr' is deprecated as of version 0.15 and"
  119. " will be removed in version 1.0. It is now handled"
  120. " internally for each header.",
  121. DeprecationWarning,
  122. )
  123. return self._get_trusted_comma(self.x_for, ",".join(forwarded_for))
  124. def _get_trusted_comma(self, trusted, value):
  125. """Get the real value from a comma-separated header based on the
  126. configured number of trusted proxies.
  127. :param trusted: Number of values to trust in the header.
  128. :param value: Header value to parse.
  129. :return: The real value, or ``None`` if there are fewer values
  130. than the number of trusted proxies.
  131. .. versionadded:: 0.15
  132. """
  133. if not (trusted and value):
  134. return
  135. values = [x.strip() for x in value.split(",")]
  136. if len(values) >= trusted:
  137. return values[-trusted]
  138. def __call__(self, environ, start_response):
  139. """Modify the WSGI environ based on the various ``Forwarded``
  140. headers before calling the wrapped application. Store the
  141. original environ values in ``werkzeug.proxy_fix.orig_{key}``.
  142. """
  143. environ_get = environ.get
  144. orig_remote_addr = environ_get("REMOTE_ADDR")
  145. orig_wsgi_url_scheme = environ_get("wsgi.url_scheme")
  146. orig_http_host = environ_get("HTTP_HOST")
  147. environ.update(
  148. {
  149. "werkzeug.proxy_fix.orig": {
  150. "REMOTE_ADDR": orig_remote_addr,
  151. "wsgi.url_scheme": orig_wsgi_url_scheme,
  152. "HTTP_HOST": orig_http_host,
  153. "SERVER_NAME": environ_get("SERVER_NAME"),
  154. "SERVER_PORT": environ_get("SERVER_PORT"),
  155. "SCRIPT_NAME": environ_get("SCRIPT_NAME"),
  156. },
  157. # todo: remove deprecated keys
  158. "werkzeug.proxy_fix.orig_remote_addr": orig_remote_addr,
  159. "werkzeug.proxy_fix.orig_wsgi_url_scheme": orig_wsgi_url_scheme,
  160. "werkzeug.proxy_fix.orig_http_host": orig_http_host,
  161. }
  162. )
  163. x_for = self._get_trusted_comma(self.x_for, environ_get("HTTP_X_FORWARDED_FOR"))
  164. if x_for:
  165. environ["REMOTE_ADDR"] = x_for
  166. x_proto = self._get_trusted_comma(
  167. self.x_proto, environ_get("HTTP_X_FORWARDED_PROTO")
  168. )
  169. if x_proto:
  170. environ["wsgi.url_scheme"] = x_proto
  171. x_host = self._get_trusted_comma(
  172. self.x_host, environ_get("HTTP_X_FORWARDED_HOST")
  173. )
  174. if x_host:
  175. environ["HTTP_HOST"] = x_host
  176. parts = x_host.split(":", 1)
  177. environ["SERVER_NAME"] = parts[0]
  178. if len(parts) == 2:
  179. environ["SERVER_PORT"] = parts[1]
  180. x_port = self._get_trusted_comma(
  181. self.x_port, environ_get("HTTP_X_FORWARDED_PORT")
  182. )
  183. if x_port:
  184. host = environ.get("HTTP_HOST")
  185. if host:
  186. parts = host.split(":", 1)
  187. host = parts[0] if len(parts) == 2 else host
  188. environ["HTTP_HOST"] = "%s:%s" % (host, x_port)
  189. environ["SERVER_PORT"] = x_port
  190. x_prefix = self._get_trusted_comma(
  191. self.x_prefix, environ_get("HTTP_X_FORWARDED_PREFIX")
  192. )
  193. if x_prefix:
  194. environ["SCRIPT_NAME"] = x_prefix
  195. return self.app(environ, start_response)