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