Home | History | Annotate | Download | only in webob
      1 import errno
      2 import sys
      3 import re
      4 try:
      5     import httplib
      6 except ImportError: # pragma: no cover
      7     import http.client as httplib
      8 from webob.compat import url_quote
      9 import socket
     10 from webob import exc
     11 from webob.compat import PY3
     12 
     13 
     14 __all__ = ['send_request_app', 'SendRequest']
     15 
     16 class SendRequest:
     17     """
     18     Sends the request, as described by the environ, over actual HTTP.
     19     All controls about how it is sent are contained in the request
     20     environ itself.
     21 
     22     This connects to the server given in SERVER_NAME:SERVER_PORT, and
     23     sends the Host header in HTTP_HOST -- they do not have to match.
     24     You can send requests to servers despite what DNS says.
     25 
     26     Set ``environ['webob.client.timeout'] = 10`` to set the timeout on
     27     the request (to, for example, 10 seconds).
     28 
     29     Does not add X-Forwarded-For or other standard headers
     30 
     31     If you use ``send_request_app`` then simple ``httplib``
     32     connections will be used.
     33     """
     34 
     35     def __init__(self, HTTPConnection=httplib.HTTPConnection,
     36                  HTTPSConnection=httplib.HTTPSConnection):
     37         self.HTTPConnection = HTTPConnection
     38         self.HTTPSConnection = HTTPSConnection
     39 
     40     def __call__(self, environ, start_response):
     41         scheme = environ['wsgi.url_scheme']
     42         if scheme == 'http':
     43             ConnClass = self.HTTPConnection
     44         elif scheme == 'https':
     45             ConnClass = self.HTTPSConnection
     46         else:
     47             raise ValueError(
     48                 "Unknown scheme: %r" % scheme)
     49         if 'SERVER_NAME' not in environ:
     50             host = environ.get('HTTP_HOST')
     51             if not host:
     52                 raise ValueError(
     53                     "environ contains neither SERVER_NAME nor HTTP_HOST")
     54             if ':' in host:
     55                 host, port = host.split(':', 1)
     56             else:
     57                 if scheme == 'http':
     58                     port = '80'
     59                 else:
     60                     port = '443'
     61             environ['SERVER_NAME'] = host
     62             environ['SERVER_PORT'] = port
     63         kw = {}
     64         if ('webob.client.timeout' in environ and
     65             self._timeout_supported(ConnClass) ):
     66             kw['timeout'] = environ['webob.client.timeout']
     67         conn = ConnClass('%(SERVER_NAME)s:%(SERVER_PORT)s' % environ, **kw)
     68         headers = {}
     69         for key, value in environ.items():
     70             if key.startswith('HTTP_'):
     71                 key = key[5:].replace('_', '-').title()
     72                 headers[key] = value
     73         path = (url_quote(environ.get('SCRIPT_NAME', ''))
     74                 + url_quote(environ.get('PATH_INFO', '')))
     75         if environ.get('QUERY_STRING'):
     76             path += '?' + environ['QUERY_STRING']
     77         try:
     78             content_length = int(environ.get('CONTENT_LENGTH', '0'))
     79         except ValueError:
     80             content_length = 0
     81         ## FIXME: there is no streaming of the body, and that might be useful
     82         ## in some cases
     83         if content_length:
     84             body = environ['wsgi.input'].read(content_length)
     85         else:
     86             body = ''
     87         headers['Content-Length'] = content_length
     88         if environ.get('CONTENT_TYPE'):
     89             headers['Content-Type'] = environ['CONTENT_TYPE']
     90         if not path.startswith("/"):
     91             path = "/" + path
     92         try:
     93             conn.request(environ['REQUEST_METHOD'],
     94                          path, body, headers)
     95             res = conn.getresponse()
     96         except socket.timeout:
     97             resp = exc.HTTPGatewayTimeout()
     98             return resp(environ, start_response)
     99         except (socket.error, socket.gaierror) as e:
    100             if ((isinstance(e, socket.error) and e.args[0] == -2) or
    101                 (isinstance(e, socket.gaierror) and e.args[0] == 8)):
    102                 # Name or service not known
    103                 resp = exc.HTTPBadGateway(
    104                     "Name or service not known (bad domain name: %s)"
    105                     % environ['SERVER_NAME'])
    106                 return resp(environ, start_response)
    107             elif e.args[0] in _e_refused: # pragma: no cover
    108                 # Connection refused
    109                 resp = exc.HTTPBadGateway("Connection refused")
    110                 return resp(environ, start_response)
    111             raise
    112         headers_out = self.parse_headers(res.msg)
    113         status = '%s %s' % (res.status, res.reason)
    114         start_response(status, headers_out)
    115         length = res.getheader('content-length')
    116         # FIXME: This shouldn't really read in all the content at once
    117         if length is not None:
    118             body = res.read(int(length))
    119         else:
    120             body = res.read()
    121         conn.close()
    122         return [body]
    123 
    124     # Remove these headers from response (specify lower case header
    125     # names):
    126     filtered_headers = (
    127         'transfer-encoding',
    128     )
    129 
    130     MULTILINE_RE = re.compile(r'\r?\n\s*')
    131 
    132     def parse_headers(self, message):
    133         """
    134         Turn a Message object into a list of WSGI-style headers.
    135         """
    136         headers_out = []
    137         if PY3:  # pragma: no cover
    138             headers = message._headers
    139         else:  # pragma: no cover
    140             headers = message.headers
    141         for full_header in headers:
    142             if not full_header: # pragma: no cover
    143                 # Shouldn't happen, but we'll just ignore
    144                 continue
    145             if full_header[0].isspace():  # pragma: no cover
    146                 # Continuation line, add to the last header
    147                 if not headers_out:
    148                     raise ValueError(
    149                         "First header starts with a space (%r)" % full_header)
    150                 last_header, last_value = headers_out.pop()
    151                 value = last_value + ', ' + full_header.strip()
    152                 headers_out.append((last_header, value))
    153                 continue
    154             if isinstance(full_header, tuple):  # pragma: no cover
    155                 header, value = full_header
    156             else:  # pragma: no cover
    157                 try:
    158                     header, value = full_header.split(':', 1)
    159                 except:
    160                     raise ValueError("Invalid header: %r" % (full_header,))
    161             value = value.strip()
    162             if '\n' in value or '\r\n' in value:  # pragma: no cover
    163                 # Python 3 has multiline values for continuations, Python 2
    164                 # has two items in headers
    165                 value = self.MULTILINE_RE.sub(', ', value)
    166             if header.lower() not in self.filtered_headers:
    167                 headers_out.append((header, value))
    168         return headers_out
    169 
    170     def _timeout_supported(self, ConnClass):
    171         if sys.version_info < (2, 7) and ConnClass in (
    172             httplib.HTTPConnection, httplib.HTTPSConnection): # pragma: no cover
    173             return False
    174         return True
    175 
    176 
    177 send_request_app = SendRequest()
    178 
    179 _e_refused = (errno.ECONNREFUSED,)
    180 if hasattr(errno, 'ENODATA'): # pragma: no cover
    181     _e_refused += (errno.ENODATA,)
    182