proxy.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
  2. # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
  3. """
  4. An application that proxies WSGI requests to a remote server.
  5. TODO:
  6. * Send ``Via`` header? It's not clear to me this is a Via in the
  7. style of a typical proxy.
  8. * Other headers or metadata? I put in X-Forwarded-For, but that's it.
  9. * Signed data of non-HTTP keys? This would be for things like
  10. REMOTE_USER.
  11. * Something to indicate what the original URL was? The original host,
  12. scheme, and base path.
  13. * Rewriting ``Location`` headers? mod_proxy does this.
  14. * Rewriting body? (Probably not on this one -- that can be done with
  15. a different middleware that wraps this middleware)
  16. * Example::
  17. use = egg:Paste#proxy
  18. address = http://server3:8680/exist/rest/db/orgs/sch/config/
  19. allowed_request_methods = GET
  20. """
  21. from six.moves import http_client as httplib
  22. from six.moves.urllib import parse as urlparse
  23. from six.moves.urllib.parse import quote
  24. import six
  25. from paste import httpexceptions
  26. from paste.util.converters import aslist
  27. # Remove these headers from response (specify lower case header
  28. # names):
  29. filtered_headers = (
  30. 'transfer-encoding',
  31. 'connection',
  32. 'keep-alive',
  33. 'proxy-authenticate',
  34. 'proxy-authorization',
  35. 'te',
  36. 'trailers',
  37. 'upgrade',
  38. )
  39. class Proxy(object):
  40. def __init__(self, address, allowed_request_methods=(),
  41. suppress_http_headers=()):
  42. self.address = address
  43. self.parsed = urlparse.urlsplit(address)
  44. self.scheme = self.parsed[0].lower()
  45. self.host = self.parsed[1]
  46. self.path = self.parsed[2]
  47. self.allowed_request_methods = [
  48. x.lower() for x in allowed_request_methods if x]
  49. self.suppress_http_headers = [
  50. x.lower() for x in suppress_http_headers if x]
  51. def __call__(self, environ, start_response):
  52. if (self.allowed_request_methods and
  53. environ['REQUEST_METHOD'].lower() not in self.allowed_request_methods):
  54. return httpexceptions.HTTPBadRequest("Disallowed")(environ, start_response)
  55. if self.scheme == 'http':
  56. ConnClass = httplib.HTTPConnection
  57. elif self.scheme == 'https':
  58. ConnClass = httplib.HTTPSConnection
  59. else:
  60. raise ValueError(
  61. "Unknown scheme for %r: %r" % (self.address, self.scheme))
  62. conn = ConnClass(self.host)
  63. headers = {}
  64. for key, value in environ.items():
  65. if key.startswith('HTTP_'):
  66. key = key[5:].lower().replace('_', '-')
  67. if key == 'host' or key in self.suppress_http_headers:
  68. continue
  69. headers[key] = value
  70. headers['host'] = self.host
  71. if 'REMOTE_ADDR' in environ:
  72. headers['x-forwarded-for'] = environ['REMOTE_ADDR']
  73. if environ.get('CONTENT_TYPE'):
  74. headers['content-type'] = environ['CONTENT_TYPE']
  75. if environ.get('CONTENT_LENGTH'):
  76. if environ['CONTENT_LENGTH'] == '-1':
  77. # This is a special case, where the content length is basically undetermined
  78. body = environ['wsgi.input'].read(-1)
  79. headers['content-length'] = str(len(body))
  80. else:
  81. headers['content-length'] = environ['CONTENT_LENGTH']
  82. length = int(environ['CONTENT_LENGTH'])
  83. body = environ['wsgi.input'].read(length)
  84. else:
  85. body = ''
  86. path_info = quote(environ['PATH_INFO'])
  87. if self.path:
  88. request_path = path_info
  89. if request_path and request_path[0] == '/':
  90. request_path = request_path[1:]
  91. path = urlparse.urljoin(self.path, request_path)
  92. else:
  93. path = path_info
  94. if environ.get('QUERY_STRING'):
  95. path += '?' + environ['QUERY_STRING']
  96. conn.request(environ['REQUEST_METHOD'],
  97. path,
  98. body, headers)
  99. res = conn.getresponse()
  100. headers_out = parse_headers(res.msg)
  101. status = '%s %s' % (res.status, res.reason)
  102. start_response(status, headers_out)
  103. # @@: Default?
  104. length = res.getheader('content-length')
  105. if length is not None:
  106. body = res.read(int(length))
  107. else:
  108. body = res.read()
  109. conn.close()
  110. return [body]
  111. def make_proxy(global_conf, address, allowed_request_methods="",
  112. suppress_http_headers=""):
  113. """
  114. Make a WSGI application that proxies to another address:
  115. ``address``
  116. the full URL ending with a trailing ``/``
  117. ``allowed_request_methods``:
  118. a space seperated list of request methods (e.g., ``GET POST``)
  119. ``suppress_http_headers``
  120. a space seperated list of http headers (lower case, without
  121. the leading ``http_``) that should not be passed on to target
  122. host
  123. """
  124. allowed_request_methods = aslist(allowed_request_methods)
  125. suppress_http_headers = aslist(suppress_http_headers)
  126. return Proxy(
  127. address,
  128. allowed_request_methods=allowed_request_methods,
  129. suppress_http_headers=suppress_http_headers)
  130. class TransparentProxy(object):
  131. """
  132. A proxy that sends the request just as it was given, including
  133. respecting HTTP_HOST, wsgi.url_scheme, etc.
  134. This is a way of translating WSGI requests directly to real HTTP
  135. requests. All information goes in the environment; modify it to
  136. modify the way the request is made.
  137. If you specify ``force_host`` (and optionally ``force_scheme``)
  138. then HTTP_HOST won't be used to determine where to connect to;
  139. instead a specific host will be connected to, but the ``Host``
  140. header in the request will remain intact.
  141. """
  142. def __init__(self, force_host=None,
  143. force_scheme='http'):
  144. self.force_host = force_host
  145. self.force_scheme = force_scheme
  146. def __repr__(self):
  147. return '<%s %s force_host=%r force_scheme=%r>' % (
  148. self.__class__.__name__,
  149. hex(id(self)),
  150. self.force_host, self.force_scheme)
  151. def __call__(self, environ, start_response):
  152. scheme = environ['wsgi.url_scheme']
  153. if self.force_host is None:
  154. conn_scheme = scheme
  155. else:
  156. conn_scheme = self.force_scheme
  157. if conn_scheme == 'http':
  158. ConnClass = httplib.HTTPConnection
  159. elif conn_scheme == 'https':
  160. ConnClass = httplib.HTTPSConnection
  161. else:
  162. raise ValueError(
  163. "Unknown scheme %r" % scheme)
  164. if 'HTTP_HOST' not in environ:
  165. raise ValueError(
  166. "WSGI environ must contain an HTTP_HOST key")
  167. host = environ['HTTP_HOST']
  168. if self.force_host is None:
  169. conn_host = host
  170. else:
  171. conn_host = self.force_host
  172. conn = ConnClass(conn_host)
  173. headers = {}
  174. for key, value in environ.items():
  175. if key.startswith('HTTP_'):
  176. key = key[5:].lower().replace('_', '-')
  177. headers[key] = value
  178. headers['host'] = host
  179. if 'REMOTE_ADDR' in environ and 'HTTP_X_FORWARDED_FOR' not in environ:
  180. headers['x-forwarded-for'] = environ['REMOTE_ADDR']
  181. if environ.get('CONTENT_TYPE'):
  182. headers['content-type'] = environ['CONTENT_TYPE']
  183. if environ.get('CONTENT_LENGTH'):
  184. length = int(environ['CONTENT_LENGTH'])
  185. body = environ['wsgi.input'].read(length)
  186. if length == -1:
  187. environ['CONTENT_LENGTH'] = str(len(body))
  188. elif 'CONTENT_LENGTH' not in environ:
  189. body = ''
  190. length = 0
  191. else:
  192. body = ''
  193. length = 0
  194. path = (environ.get('SCRIPT_NAME', '')
  195. + environ.get('PATH_INFO', ''))
  196. path = quote(path)
  197. if 'QUERY_STRING' in environ:
  198. path += '?' + environ['QUERY_STRING']
  199. conn.request(environ['REQUEST_METHOD'],
  200. path, body, headers)
  201. res = conn.getresponse()
  202. headers_out = parse_headers(res.msg)
  203. status = '%s %s' % (res.status, res.reason)
  204. start_response(status, headers_out)
  205. # @@: Default?
  206. length = res.getheader('content-length')
  207. if length is not None:
  208. body = res.read(int(length))
  209. else:
  210. body = res.read()
  211. conn.close()
  212. return [body]
  213. def parse_headers(message):
  214. """
  215. Turn a Message object into a list of WSGI-style headers.
  216. """
  217. headers_out = []
  218. if six.PY3:
  219. for header, value in message.items():
  220. if header.lower() not in filtered_headers:
  221. headers_out.append((header, value))
  222. else:
  223. for full_header in message.headers:
  224. if not full_header:
  225. # Shouldn't happen, but we'll just ignore
  226. continue
  227. if full_header[0].isspace():
  228. # Continuation line, add to the last header
  229. if not headers_out:
  230. raise ValueError(
  231. "First header starts with a space (%r)" % full_header)
  232. last_header, last_value = headers_out.pop()
  233. value = last_value + ' ' + full_header.strip()
  234. headers_out.append((last_header, value))
  235. continue
  236. try:
  237. header, value = full_header.split(':', 1)
  238. except:
  239. raise ValueError("Invalid header: %r" % full_header)
  240. value = value.strip()
  241. if header.lower() not in filtered_headers:
  242. headers_out.append((header, value))
  243. return headers_out
  244. def make_transparent_proxy(
  245. global_conf, force_host=None, force_scheme='http'):
  246. """
  247. Create a proxy that connects to a specific host, but does
  248. absolutely no other filtering, including the Host header.
  249. """
  250. return TransparentProxy(force_host=force_host,
  251. force_scheme=force_scheme)