https_forwarder.py revision 5f1c94371a64b3196d4be9466099bb892df9b88e
1# Copyright 2014 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""An https server that forwards requests to another server. This allows a 6server that supports http only to be accessed over https. 7""" 8 9import BaseHTTPServer 10import os 11import SocketServer 12import sys 13import urllib2 14import urlparse 15import testserver_base 16import tlslite.api 17 18 19class RedirectSuppressor(urllib2.HTTPErrorProcessor): 20 """Prevents urllib2 from following http redirects. 21 22 If this class is placed in an urllib2.OpenerDirector's handler chain before 23 the default urllib2.HTTPRedirectHandler, it will terminate the processing of 24 responses containing redirect codes (301, 302, 303, 307) before they reach the 25 default redirect handler. 26 """ 27 28 def http_response(self, req, response): 29 return response 30 31 def https_response(self, req, response): 32 return response 33 34 35class RequestForwarder(BaseHTTPServer.BaseHTTPRequestHandler): 36 """Handles requests received by forwarding them to the another server.""" 37 38 def do_GET(self): 39 """Forwards GET requests.""" 40 self._forward(None) 41 42 def do_POST(self): 43 """Forwards POST requests.""" 44 self._forward(self.rfile.read(int(self.headers['Content-Length']))) 45 46 def _forward(self, body): 47 """Forwards a GET or POST request to another server. 48 49 Args: 50 body: The request body. This should be |None| for GET requests. 51 """ 52 request_url = urlparse.urlparse(self.path) 53 url = urlparse.urlunparse((self.server.forward_scheme, 54 self.server.forward_netloc, 55 self.server.forward_path + request_url[2], 56 request_url[3], 57 request_url[4], 58 request_url[5])) 59 60 headers = dict((key, value) for key, value in dict(self.headers).iteritems() 61 if key.lower() != 'host') 62 opener = urllib2.build_opener(RedirectSuppressor) 63 forward = opener.open(urllib2.Request(url, body, headers)) 64 65 self.send_response(forward.getcode()) 66 for key, value in dict(forward.info()).iteritems(): 67 # RFC 6265 states in section 3: 68 # 69 # Origin servers SHOULD NOT fold multiple Set-Cookie header fields into 70 # a single header field. 71 # 72 # Python 2 does not obey this requirement and folds multiple Set-Cookie 73 # header fields into one. The following code undoes this folding by 74 # splitting the Set-Cookie header field at each comma. Note that this is a 75 # hack because the code does not (and cannot reliably) distinguish between 76 # commas inserted by Python while folding multiple headers and commas that 77 # were part of the original Set-Cookie headers. 78 if key == 'set-cookie': 79 for cookie in value.split(','): 80 self.send_header(key, cookie) 81 else: 82 self.send_header(key, value) 83 self.end_headers() 84 self.wfile.write(forward.read()) 85 86 87class MultiThreadedHTTPSServer(SocketServer.ThreadingMixIn, 88 tlslite.api.TLSSocketServerMixIn, 89 testserver_base.ClientRestrictingServerMixIn, 90 testserver_base.BrokenPipeHandlerMixIn, 91 testserver_base.StoppableHTTPServer): 92 """A multi-threaded version of testserver.HTTPSServer.""" 93 94 def __init__(self, server_address, request_hander_class, pem_cert_and_key): 95 """Initializes the server. 96 97 Args: 98 server_address: Server host and port. 99 request_hander_class: The class that will handle requests to the server. 100 pem_cert_and_key: Path to file containing the https cert and private key. 101 """ 102 self.cert_chain = tlslite.api.X509CertChain() 103 self.cert_chain.parsePemList(pem_cert_and_key) 104 # Force using only python implementation - otherwise behavior is different 105 # depending on whether m2crypto Python module is present (error is thrown 106 # when it is). m2crypto uses a C (based on OpenSSL) implementation under 107 # the hood. 108 self.private_key = tlslite.api.parsePEMKey(pem_cert_and_key, 109 private=True, 110 implementations=['python']) 111 112 testserver_base.StoppableHTTPServer.__init__(self, 113 server_address, 114 request_hander_class) 115 116 def handshake(self, tlsConnection): 117 """Performs the SSL handshake for an https connection. 118 119 Args: 120 tlsConnection: The https connection. 121 Returns: 122 Whether the SSL handshake succeeded. 123 """ 124 try: 125 self.tlsConnection = tlsConnection 126 tlsConnection.handshakeServer(certChain=self.cert_chain, 127 privateKey=self.private_key) 128 tlsConnection.ignoreAbruptClose = True 129 return True 130 except: 131 return False 132 133 134class ServerRunner(testserver_base.TestServerRunner): 135 """Runner that starts an https server which forwards requests to another 136 server. 137 """ 138 139 def create_server(self, server_data): 140 """Performs the SSL handshake for an https connection. 141 142 Args: 143 server_data: Dictionary that holds information about the server. 144 Returns: 145 The started server. 146 """ 147 port = self.options.port 148 host = self.options.host 149 150 if not os.path.isfile(self.options.cert_and_key_file): 151 raise testserver_base.OptionError( 152 'Specified server cert file not found: ' + 153 self.options.cert_and_key_file) 154 pem_cert_and_key = open(self.options.cert_and_key_file).read() 155 156 server = MultiThreadedHTTPSServer((host, port), 157 RequestForwarder, 158 pem_cert_and_key) 159 print 'HTTPS server started on %s:%d...' % (host, server.server_port) 160 161 forward_target = urlparse.urlparse(self.options.forward_target) 162 server.forward_scheme = forward_target[0] 163 server.forward_netloc = forward_target[1] 164 server.forward_path = forward_target[2].rstrip('/') 165 server.forward_host = forward_target.hostname 166 if forward_target.port: 167 server.forward_host += ':' + str(forward_target.port) 168 server_data['port'] = server.server_port 169 return server 170 171 def add_options(self): 172 """Specifies the command-line options understood by the server.""" 173 testserver_base.TestServerRunner.add_options(self) 174 self.option_parser.add_option('--https', action='store_true', 175 help='Ignored (provided for compatibility ' 176 'only).') 177 self.option_parser.add_option('--cert-and-key-file', help='The path to the ' 178 'file containing the certificate and private ' 179 'key for the server in PEM format.') 180 self.option_parser.add_option('--forward-target', help='The URL prefix to ' 181 'which requests will be forwarded.') 182 183 184if __name__ == '__main__': 185 sys.exit(ServerRunner().main()) 186