http_proxy.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. """
  2. Basic HTTP Proxy
  3. ================
  4. .. autoclass:: ProxyMiddleware
  5. :copyright: 2007 Pallets
  6. :license: BSD-3-Clause
  7. """
  8. import socket
  9. from ..datastructures import EnvironHeaders
  10. from ..http import is_hop_by_hop_header
  11. from ..urls import url_parse
  12. from ..urls import url_quote
  13. from ..wsgi import get_input_stream
  14. try:
  15. from http import client
  16. except ImportError:
  17. import httplib as client
  18. class ProxyMiddleware(object):
  19. """Proxy requests under a path to an external server, routing other
  20. requests to the app.
  21. This middleware can only proxy HTTP requests, as that is the only
  22. protocol handled by the WSGI server. Other protocols, such as
  23. websocket requests, cannot be proxied at this layer. This should
  24. only be used for development, in production a real proxying server
  25. should be used.
  26. The middleware takes a dict that maps a path prefix to a dict
  27. describing the host to be proxied to::
  28. app = ProxyMiddleware(app, {
  29. "/static/": {
  30. "target": "http://127.0.0.1:5001/",
  31. }
  32. })
  33. Each host has the following options:
  34. ``target``:
  35. The target URL to dispatch to. This is required.
  36. ``remove_prefix``:
  37. Whether to remove the prefix from the URL before dispatching it
  38. to the target. The default is ``False``.
  39. ``host``:
  40. ``"<auto>"`` (default):
  41. The host header is automatically rewritten to the URL of the
  42. target.
  43. ``None``:
  44. The host header is unmodified from the client request.
  45. Any other value:
  46. The host header is overwritten with the value.
  47. ``headers``:
  48. A dictionary of headers to be sent with the request to the
  49. target. The default is ``{}``.
  50. ``ssl_context``:
  51. A :class:`ssl.SSLContext` defining how to verify requests if the
  52. target is HTTPS. The default is ``None``.
  53. In the example above, everything under ``"/static/"`` is proxied to
  54. the server on port 5001. The host header is rewritten to the target,
  55. and the ``"/static/"`` prefix is removed from the URLs.
  56. :param app: The WSGI application to wrap.
  57. :param targets: Proxy target configurations. See description above.
  58. :param chunk_size: Size of chunks to read from input stream and
  59. write to target.
  60. :param timeout: Seconds before an operation to a target fails.
  61. .. versionadded:: 0.14
  62. """
  63. def __init__(self, app, targets, chunk_size=2 << 13, timeout=10):
  64. def _set_defaults(opts):
  65. opts.setdefault("remove_prefix", False)
  66. opts.setdefault("host", "<auto>")
  67. opts.setdefault("headers", {})
  68. opts.setdefault("ssl_context", None)
  69. return opts
  70. self.app = app
  71. self.targets = dict(
  72. ("/%s/" % k.strip("/"), _set_defaults(v)) for k, v in targets.items()
  73. )
  74. self.chunk_size = chunk_size
  75. self.timeout = timeout
  76. def proxy_to(self, opts, path, prefix):
  77. target = url_parse(opts["target"])
  78. def application(environ, start_response):
  79. headers = list(EnvironHeaders(environ).items())
  80. headers[:] = [
  81. (k, v)
  82. for k, v in headers
  83. if not is_hop_by_hop_header(k)
  84. and k.lower() not in ("content-length", "host")
  85. ]
  86. headers.append(("Connection", "close"))
  87. if opts["host"] == "<auto>":
  88. headers.append(("Host", target.ascii_host))
  89. elif opts["host"] is None:
  90. headers.append(("Host", environ["HTTP_HOST"]))
  91. else:
  92. headers.append(("Host", opts["host"]))
  93. headers.extend(opts["headers"].items())
  94. remote_path = path
  95. if opts["remove_prefix"]:
  96. remote_path = "%s/%s" % (
  97. target.path.rstrip("/"),
  98. remote_path[len(prefix) :].lstrip("/"),
  99. )
  100. content_length = environ.get("CONTENT_LENGTH")
  101. chunked = False
  102. if content_length not in ("", None):
  103. headers.append(("Content-Length", content_length))
  104. elif content_length is not None:
  105. headers.append(("Transfer-Encoding", "chunked"))
  106. chunked = True
  107. try:
  108. if target.scheme == "http":
  109. con = client.HTTPConnection(
  110. target.ascii_host, target.port or 80, timeout=self.timeout
  111. )
  112. elif target.scheme == "https":
  113. con = client.HTTPSConnection(
  114. target.ascii_host,
  115. target.port or 443,
  116. timeout=self.timeout,
  117. context=opts["ssl_context"],
  118. )
  119. else:
  120. raise RuntimeError(
  121. "Target scheme must be 'http' or 'https', got '{}'.".format(
  122. target.scheme
  123. )
  124. )
  125. con.connect()
  126. remote_url = url_quote(remote_path)
  127. querystring = environ["QUERY_STRING"]
  128. if querystring:
  129. remote_url = remote_url + "?" + querystring
  130. con.putrequest(environ["REQUEST_METHOD"], remote_url, skip_host=True)
  131. for k, v in headers:
  132. if k.lower() == "connection":
  133. v = "close"
  134. con.putheader(k, v)
  135. con.endheaders()
  136. stream = get_input_stream(environ)
  137. while 1:
  138. data = stream.read(self.chunk_size)
  139. if not data:
  140. break
  141. if chunked:
  142. con.send(b"%x\r\n%s\r\n" % (len(data), data))
  143. else:
  144. con.send(data)
  145. resp = con.getresponse()
  146. except socket.error:
  147. from ..exceptions import BadGateway
  148. return BadGateway()(environ, start_response)
  149. start_response(
  150. "%d %s" % (resp.status, resp.reason),
  151. [
  152. (k.title(), v)
  153. for k, v in resp.getheaders()
  154. if not is_hop_by_hop_header(k)
  155. ],
  156. )
  157. def read():
  158. while 1:
  159. try:
  160. data = resp.read(self.chunk_size)
  161. except socket.error:
  162. break
  163. if not data:
  164. break
  165. yield data
  166. return read()
  167. return application
  168. def __call__(self, environ, start_response):
  169. path = environ["PATH_INFO"]
  170. app = self.app
  171. for prefix, opts in self.targets.items():
  172. if path.startswith(prefix):
  173. app = self.proxy_to(opts, path, prefix)
  174. break
  175. return app(environ, start_response)