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