181 lines
6.7 KiB
Python
181 lines
6.7 KiB
Python
import errno
|
|
import sys
|
|
import re
|
|
try:
|
|
import httplib
|
|
except ImportError:
|
|
import http.client as httplib
|
|
from webob.compat import url_quote
|
|
import socket
|
|
from webob import exc
|
|
from webob.compat import PY2
|
|
|
|
__all__ = ['send_request_app', 'SendRequest']
|
|
|
|
class SendRequest:
|
|
"""
|
|
Sends the request, as described by the environ, over actual HTTP.
|
|
All controls about how it is sent are contained in the request
|
|
environ itself.
|
|
|
|
This connects to the server given in SERVER_NAME:SERVER_PORT, and
|
|
sends the Host header in HTTP_HOST -- they do not have to match.
|
|
You can send requests to servers despite what DNS says.
|
|
|
|
Set ``environ['webob.client.timeout'] = 10`` to set the timeout on
|
|
the request (to, for example, 10 seconds).
|
|
|
|
Does not add X-Forwarded-For or other standard headers
|
|
|
|
If you use ``send_request_app`` then simple ``httplib``
|
|
connections will be used.
|
|
"""
|
|
|
|
def __init__(self, HTTPConnection=httplib.HTTPConnection,
|
|
HTTPSConnection=httplib.HTTPSConnection):
|
|
self.HTTPConnection = HTTPConnection
|
|
self.HTTPSConnection = HTTPSConnection
|
|
|
|
def __call__(self, environ, start_response):
|
|
scheme = environ['wsgi.url_scheme']
|
|
if scheme == 'http':
|
|
ConnClass = self.HTTPConnection
|
|
elif scheme == 'https':
|
|
ConnClass = self.HTTPSConnection
|
|
else:
|
|
raise ValueError(
|
|
"Unknown scheme: %r" % scheme)
|
|
if 'SERVER_NAME' not in environ:
|
|
host = environ.get('HTTP_HOST')
|
|
if not host:
|
|
raise ValueError(
|
|
"environ contains neither SERVER_NAME nor HTTP_HOST")
|
|
if ':' in host:
|
|
host, port = host.split(':', 1)
|
|
else:
|
|
if scheme == 'http':
|
|
port = '80'
|
|
else:
|
|
port = '443'
|
|
environ['SERVER_NAME'] = host
|
|
environ['SERVER_PORT'] = port
|
|
kw = {}
|
|
if ('webob.client.timeout' in environ and
|
|
self._timeout_supported(ConnClass) ):
|
|
kw['timeout'] = environ['webob.client.timeout']
|
|
conn = ConnClass('%(SERVER_NAME)s:%(SERVER_PORT)s' % environ, **kw)
|
|
headers = {}
|
|
for key, value in environ.items():
|
|
if key.startswith('HTTP_'):
|
|
key = key[5:].replace('_', '-').title()
|
|
headers[key] = value
|
|
path = (url_quote(environ.get('SCRIPT_NAME', ''))
|
|
+ url_quote(environ.get('PATH_INFO', '')))
|
|
if environ.get('QUERY_STRING'):
|
|
path += '?' + environ['QUERY_STRING']
|
|
try:
|
|
content_length = int(environ.get('CONTENT_LENGTH', '0'))
|
|
except ValueError:
|
|
content_length = 0
|
|
## FIXME: there is no streaming of the body, and that might be useful
|
|
## in some cases
|
|
if content_length:
|
|
body = environ['wsgi.input'].read(content_length)
|
|
else:
|
|
body = ''
|
|
headers['Content-Length'] = content_length
|
|
if environ.get('CONTENT_TYPE'):
|
|
headers['Content-Type'] = environ['CONTENT_TYPE']
|
|
if not path.startswith("/"):
|
|
path = "/" + path
|
|
try:
|
|
conn.request(environ['REQUEST_METHOD'],
|
|
path, body, headers)
|
|
res = conn.getresponse()
|
|
except socket.timeout:
|
|
resp = exc.HTTPGatewayTimeout()
|
|
return resp(environ, start_response)
|
|
except (socket.error, socket.gaierror) as e:
|
|
if ((isinstance(e, socket.error) and e.args[0] == -2) or
|
|
(isinstance(e, socket.gaierror) and e.args[0] == 8)):
|
|
# Name or service not known
|
|
resp = exc.HTTPBadGateway(
|
|
"Name or service not known (bad domain name: %s)"
|
|
% environ['SERVER_NAME'])
|
|
return resp(environ, start_response)
|
|
elif e.args[0] in _e_refused: # pragma: no cover
|
|
# Connection refused
|
|
resp = exc.HTTPBadGateway("Connection refused")
|
|
return resp(environ, start_response)
|
|
raise
|
|
headers_out = self.parse_headers(res.msg)
|
|
status = '%s %s' % (res.status, res.reason)
|
|
start_response(status, headers_out)
|
|
length = res.getheader('content-length')
|
|
# FIXME: This shouldn't really read in all the content at once
|
|
if length is not None:
|
|
body = res.read(int(length))
|
|
else:
|
|
body = res.read()
|
|
conn.close()
|
|
return [body]
|
|
|
|
# Remove these headers from response (specify lower case header
|
|
# names):
|
|
filtered_headers = (
|
|
'transfer-encoding',
|
|
)
|
|
|
|
MULTILINE_RE = re.compile(r'\r?\n\s*')
|
|
|
|
def parse_headers(self, message):
|
|
"""
|
|
Turn a Message object into a list of WSGI-style headers.
|
|
"""
|
|
headers_out = []
|
|
if not PY2:
|
|
headers = message._headers
|
|
else:
|
|
headers = message.headers
|
|
for full_header in headers:
|
|
if not full_header: # pragma: no cover
|
|
# Shouldn't happen, but we'll just ignore
|
|
continue
|
|
if full_header[0].isspace(): # pragma: no cover
|
|
# Continuation line, add to the last header
|
|
if not headers_out:
|
|
raise ValueError(
|
|
"First header starts with a space (%r)" % full_header)
|
|
last_header, last_value = headers_out.pop()
|
|
value = last_value + ', ' + full_header.strip()
|
|
headers_out.append((last_header, value))
|
|
continue
|
|
if isinstance(full_header, tuple): # pragma: no cover
|
|
header, value = full_header
|
|
else: # pragma: no cover
|
|
try:
|
|
header, value = full_header.split(':', 1)
|
|
except:
|
|
raise ValueError("Invalid header: %r" % (full_header,))
|
|
value = value.strip()
|
|
if '\n' in value or '\r\n' in value: # pragma: no cover
|
|
# Python 3 has multiline values for continuations, Python 2
|
|
# has two items in headers
|
|
value = self.MULTILINE_RE.sub(', ', value)
|
|
if header.lower() not in self.filtered_headers:
|
|
headers_out.append((header, value))
|
|
return headers_out
|
|
|
|
def _timeout_supported(self, ConnClass):
|
|
if sys.version_info < (2, 7) and ConnClass in (
|
|
httplib.HTTPConnection, httplib.HTTPSConnection): # pragma: no cover
|
|
return False
|
|
return True
|
|
|
|
|
|
send_request_app = SendRequest()
|
|
|
|
_e_refused = (errno.ECONNREFUSED,)
|
|
if hasattr(errno, 'ENODATA'): # pragma: no cover
|
|
_e_refused += (errno.ENODATA,)
|