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 6 server that supports http only to be accessed over https. 7 """ 8 9 import BaseHTTPServer 10 import os 11 import SocketServer 12 import sys 13 import urllib2 14 import urlparse 15 import testserver_base 16 import tlslite.api 17 18 19 class 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 35 class 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 self.send_header(key, value) 68 self.end_headers() 69 self.wfile.write(forward.read()) 70 71 72 class MultiThreadedHTTPSServer(SocketServer.ThreadingMixIn, 73 tlslite.api.TLSSocketServerMixIn, 74 testserver_base.ClientRestrictingServerMixIn, 75 testserver_base.BrokenPipeHandlerMixIn, 76 testserver_base.StoppableHTTPServer): 77 """A multi-threaded version of testserver.HTTPSServer.""" 78 79 def __init__(self, server_address, request_hander_class, pem_cert_and_key): 80 """Initializes the server. 81 82 Args: 83 server_address: Server host and port. 84 request_hander_class: The class that will handle requests to the server. 85 pem_cert_and_key: Path to file containing the https cert and private key. 86 """ 87 self.cert_chain = tlslite.api.X509CertChain() 88 self.cert_chain.parsePemList(pem_cert_and_key) 89 # Force using only python implementation - otherwise behavior is different 90 # depending on whether m2crypto Python module is present (error is thrown 91 # when it is). m2crypto uses a C (based on OpenSSL) implementation under 92 # the hood. 93 self.private_key = tlslite.api.parsePEMKey(pem_cert_and_key, 94 private=True, 95 implementations=['python']) 96 97 testserver_base.StoppableHTTPServer.__init__(self, 98 server_address, 99 request_hander_class) 100 101 def handshake(self, tlsConnection): 102 """Performs the SSL handshake for an https connection. 103 104 Args: 105 tlsConnection: The https connection. 106 Returns: 107 Whether the SSL handshake succeeded. 108 """ 109 try: 110 self.tlsConnection = tlsConnection 111 tlsConnection.handshakeServer(certChain=self.cert_chain, 112 privateKey=self.private_key) 113 tlsConnection.ignoreAbruptClose = True 114 return True 115 except: 116 return False 117 118 119 class ServerRunner(testserver_base.TestServerRunner): 120 """Runner that starts an https server which forwards requests to another 121 server. 122 """ 123 124 def create_server(self, server_data): 125 """Performs the SSL handshake for an https connection. 126 127 Args: 128 server_data: Dictionary that holds information about the server. 129 Returns: 130 The started server. 131 """ 132 port = self.options.port 133 host = self.options.host 134 135 if not os.path.isfile(self.options.cert_and_key_file): 136 raise testserver_base.OptionError( 137 'Specified server cert file not found: ' + 138 self.options.cert_and_key_file) 139 pem_cert_and_key = open(self.options.cert_and_key_file).read() 140 141 server = MultiThreadedHTTPSServer((host, port), 142 RequestForwarder, 143 pem_cert_and_key) 144 print 'HTTPS server started on %s:%d...' % (host, server.server_port) 145 146 forward_target = urlparse.urlparse(self.options.forward_target) 147 server.forward_scheme = forward_target[0] 148 server.forward_netloc = forward_target[1] 149 server.forward_path = forward_target[2].rstrip('/') 150 server.forward_host = forward_target.hostname 151 if forward_target.port: 152 server.forward_host += ':' + str(forward_target.port) 153 server_data['port'] = server.server_port 154 return server 155 156 def add_options(self): 157 """Specifies the command-line options understood by the server.""" 158 testserver_base.TestServerRunner.add_options(self) 159 self.option_parser.add_option('--https', action='store_true', 160 help='Ignored (provided for compatibility ' 161 'only).') 162 self.option_parser.add_option('--cert-and-key-file', help='The path to the ' 163 'file containing the certificate and private ' 164 'key for the server in PEM format.') 165 self.option_parser.add_option('--forward-target', help='The URL prefix to ' 166 'which requests will be forwarded.') 167 168 169 if __name__ == '__main__': 170 sys.exit(ServerRunner().main()) 171