https_forwarder.py revision a02191e04bc25c4935f804f2c080ae28663d096d
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      self.send_header(key, value)
68    self.end_headers()
69    self.wfile.write(forward.read())
70
71
72class 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
119class 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
169if __name__ == '__main__':
170  sys.exit(ServerRunner().main())
171