• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import errno
2import sys
3import re
4try:
5    import httplib
6except ImportError: # pragma: no cover
7    import http.client as httplib
8from webob.compat import url_quote
9import socket
10from webob import exc
11from webob.compat import PY3
12
13
14__all__ = ['send_request_app', 'SendRequest']
15
16class 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
177send_request_app = SendRequest()
178
179_e_refused = (errno.ECONNREFUSED,)
180if hasattr(errno, 'ENODATA'): # pragma: no cover
181    _e_refused += (errno.ENODATA,)
182