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.
5 """An https server that forwards requests to another server. This allows a
6 server that supports http only to be accessed over https.
15 import testserver_base
19 class RedirectSuppressor(urllib2.HTTPErrorProcessor):
20 """Prevents urllib2 from following http redirects.
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.
28 def http_response(self, req, response):
31 def https_response(self, req, response):
35 class RequestForwarder(BaseHTTPServer.BaseHTTPRequestHandler):
36 """Handles requests received by forwarding them to the another server."""
39 """Forwards GET requests."""
43 """Forwards POST requests."""
44 self._forward(self.rfile.read(int(self.headers['Content-Length'])))
46 def _forward(self, body):
47 """Forwards a GET or POST request to another server.
50 body: The request body. This should be |None| for GET requests.
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],
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))
65 self.send_response(forward.getcode())
66 for key, value in dict(forward.info()).iteritems():
67 self.send_header(key, value)
69 self.wfile.write(forward.read())
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."""
79 def __init__(self, server_address, request_hander_class, pem_cert_and_key):
80 """Initializes the server.
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.
87 self.cert_chain = tlslite.api.X509CertChain().parseChain(pem_cert_and_key)
88 # Force using only python implementation - otherwise behavior is different
89 # depending on whether m2crypto Python module is present (error is thrown
90 # when it is). m2crypto uses a C (based on OpenSSL) implementation under
92 self.private_key = tlslite.api.parsePEMKey(pem_cert_and_key,
94 implementations=['python'])
96 testserver_base.StoppableHTTPServer.__init__(self,
100 def handshake(self, tlsConnection):
101 """Performs the SSL handshake for an https connection.
104 tlsConnection: The https connection.
106 Whether the SSL handshake succeeded.
109 self.tlsConnection = tlsConnection
110 tlsConnection.handshakeServer(certChain=self.cert_chain,
111 privateKey=self.private_key)
112 tlsConnection.ignoreAbruptClose = True
118 class ServerRunner(testserver_base.TestServerRunner):
119 """Runner that starts an https server which forwards requests to another
123 def create_server(self, server_data):
124 """Performs the SSL handshake for an https connection.
127 server_data: Dictionary that holds information about the server.
131 port = self.options.port
132 host = self.options.host
134 if not os.path.isfile(self.options.cert_and_key_file):
135 raise testserver_base.OptionError(
136 'Specified server cert file not found: ' +
137 self.options.cert_and_key_file)
138 pem_cert_and_key = open(self.options.cert_and_key_file).read()
140 server = MultiThreadedHTTPSServer((host, port),
143 print 'HTTPS server started on %s:%d...' % (host, server.server_port)
145 forward_target = urlparse.urlparse(self.options.forward_target)
146 server.forward_scheme = forward_target[0]
147 server.forward_netloc = forward_target[1]
148 server.forward_path = forward_target[2].rstrip('/')
149 server.forward_host = forward_target.hostname
150 if forward_target.port:
151 server.forward_host += ':' + str(forward_target.port)
152 server_data['port'] = server.server_port
155 def add_options(self):
156 """Specifies the command-line options understood by the server."""
157 testserver_base.TestServerRunner.add_options(self)
158 self.option_parser.add_option('--https', action='store_true',
159 help='Ignored (provided for compatibility '
161 self.option_parser.add_option('--cert-and-key-file', help='The path to the '
162 'file containing the certificate and private '
163 'key for the server in PEM format.')
164 self.option_parser.add_option('--forward-target', help='The URL prefix to '
165 'which requests will be forwarded.')
168 if __name__ == '__main__':
169 sys.exit(ServerRunner().main())