https_forwarder.py revision 5f1c94371a64b3196d4be9466099bb892df9b88e
15821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)# Copyright 2014 The Chromium Authors. All rights reserved. 25821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)# Use of this source code is governed by a BSD-style license that can be 35821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)# found in the LICENSE file. 45821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 55821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)"""An https server that forwards requests to another server. This allows a 65821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)server that supports http only to be accessed over https. 75821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)""" 85821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 95821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import BaseHTTPServer 105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import os 115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import SocketServer 125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import sys 135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import urllib2 145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import urlparse 155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import testserver_base 165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import tlslite.api 175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)class RedirectSuppressor(urllib2.HTTPErrorProcessor): 205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) """Prevents urllib2 from following http redirects. 215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) If this class is placed in an urllib2.OpenerDirector's handler chain before 235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) the default urllib2.HTTPRedirectHandler, it will terminate the processing of 245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) responses containing redirect codes (301, 302, 303, 307) before they reach the 255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) default redirect handler. 265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) """ 275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) def http_response(self, req, response): 295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) return response 305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) def https_response(self, req, response): 325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) return response 335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)class RequestForwarder(BaseHTTPServer.BaseHTTPRequestHandler): 365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) """Handles requests received by forwarding them to the another server.""" 375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) def do_GET(self): 395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) """Forwards GET requests.""" 405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) self._forward(None) 415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) def do_POST(self): 435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) """Forwards POST requests.""" 445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) self._forward(self.rfile.read(int(self.headers['Content-Length']))) 455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) def _forward(self, body): 475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) """Forwards a GET or POST request to another server. 485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) Args: 505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) body: The request body. This should be |None| for GET requests. 515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) """ 525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) request_url = urlparse.urlparse(self.path) 535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) url = urlparse.urlunparse((self.server.forward_scheme, 545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) self.server.forward_netloc, 555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) self.server.forward_path + request_url[2], 565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) request_url[3], 575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) request_url[4], 585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) request_url[5])) 595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) headers = dict((key, value) for key, value in dict(self.headers).iteritems() 615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) if key.lower() != 'host') 625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) opener = urllib2.build_opener(RedirectSuppressor) 635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) forward = opener.open(urllib2.Request(url, body, headers)) 645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) self.send_response(forward.getcode()) 665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) for key, value in dict(forward.info()).iteritems(): 675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) # RFC 6265 states in section 3: 685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) # 695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) # Origin servers SHOULD NOT fold multiple Set-Cookie header fields into 705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) # a single header field. 715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) # 725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) # Python 2 does not obey this requirement and folds multiple Set-Cookie 735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) # header fields into one. The following code undoes this folding by 745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) # splitting the Set-Cookie header field at each comma. Note that this is a 755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) # hack because the code does not (and cannot reliably) distinguish between 765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) # commas inserted by Python while folding multiple headers and commas that 775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) # were part of the original Set-Cookie headers. 785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) if key == 'set-cookie': 795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) for cookie in value.split(','): 805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) self.send_header(key, cookie) 815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) else: 825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) self.send_header(key, value) 835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) self.end_headers() 845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) self.wfile.write(forward.read()) 855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)class MultiThreadedHTTPSServer(SocketServer.ThreadingMixIn, 885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) tlslite.api.TLSSocketServerMixIn, 895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) testserver_base.ClientRestrictingServerMixIn, 905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) testserver_base.BrokenPipeHandlerMixIn, 915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) testserver_base.StoppableHTTPServer): 925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) """A multi-threaded version of testserver.HTTPSServer.""" 935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) def __init__(self, server_address, request_hander_class, pem_cert_and_key): 955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) """Initializes the server. 96a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) 97a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) Args: 98a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) server_address: Server host and port. 995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) request_hander_class: The class that will handle requests to the server. 100a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) pem_cert_and_key: Path to file containing the https cert and private key. 101a3f6a49ab37290eeeb8db0f41ec0f1cb74a68be7Torne (Richard Coles) """ 1025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) self.cert_chain = tlslite.api.X509CertChain() 1035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles) 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