Popular sites on the NTP: check that experiment group StartsWith (rather than IS...
[chromium-blink-merge.git] / chrome / browser / chromeos / login / test / https_forwarder.py
blob4470dbe02d43375a8b192683580e284d96f277f0
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.
7 """
9 import BaseHTTPServer
10 import minica
11 import re
12 import SocketServer
13 import sys
14 import urllib2
15 import urlparse
16 import testserver_base
17 import tlslite.api
20 class RedirectSuppressor(urllib2.HTTPErrorProcessor):
21 """Prevents urllib2 from following http redirects.
23 If this class is placed in an urllib2.OpenerDirector's handler chain before
24 the default urllib2.HTTPRedirectHandler, it will terminate the processing of
25 responses containing redirect codes (301, 302, 303, 307) before they reach the
26 default redirect handler.
27 """
29 def http_response(self, req, response):
30 return response
32 def https_response(self, req, response):
33 return response
36 class RequestForwarder(BaseHTTPServer.BaseHTTPRequestHandler):
37 """Handles requests received by forwarding them to the another server."""
39 def do_GET(self):
40 """Forwards GET requests."""
41 self._forward(None)
43 def do_POST(self):
44 """Forwards POST requests."""
45 self._forward(self.rfile.read(int(self.headers['Content-Length'])))
47 def _forward(self, body):
48 """Forwards a GET or POST request to another server.
50 Args:
51 body: The request body. This should be |None| for GET requests.
52 """
53 request_url = urlparse.urlparse(self.path)
54 url = urlparse.urlunparse((self.server.forward_scheme,
55 self.server.forward_netloc,
56 self.server.forward_path + request_url[2],
57 request_url[3],
58 request_url[4],
59 request_url[5]))
61 headers = dict((key, value) for key, value in dict(self.headers).iteritems()
62 if key.lower() != 'host')
63 opener = urllib2.build_opener(RedirectSuppressor)
64 forward = opener.open(urllib2.Request(url, body, headers))
66 self.send_response(forward.getcode())
67 for key, value in dict(forward.info()).iteritems():
68 # RFC 6265 states in section 3:
70 # Origin servers SHOULD NOT fold multiple Set-Cookie header fields into
71 # a single header field.
73 # Python 2 does not obey this requirement and folds multiple Set-Cookie
74 # header fields into one. The following code undoes this folding by
75 # splitting cookies into separate fields again. Note that this is a hack
76 # because the code cannot reliably distinguish between commas inserted by
77 # Python while folding multiple headers and commas that were part of the
78 # original Set-Cookie headers. The code uses a heuristic that splits at
79 # every comma followed by a space, a token and an equals sign.
80 if key == 'set-cookie':
81 start = 0
82 # Find the next occurrence of a comma followed by a space, a token
83 # (defined by RFC 2616 section 2.2 as one or more ASCII characters
84 # except the characters listed in the regex below) and an equals sign.
85 for match in re.finditer(r', [^\000-\037\177()<>@,:\\"/[\]?={} ]+=',
86 value):
87 end = match.start()
88 if end > start:
89 self.send_header(key, value[start:end])
90 start = end + 2
91 self.send_header(key, value[start:])
92 else:
93 self.send_header(key, value)
94 self.end_headers()
95 self.wfile.write(forward.read())
98 class MultiThreadedHTTPSServer(SocketServer.ThreadingMixIn,
99 tlslite.api.TLSSocketServerMixIn,
100 testserver_base.ClientRestrictingServerMixIn,
101 testserver_base.BrokenPipeHandlerMixIn,
102 testserver_base.StoppableHTTPServer):
103 """A multi-threaded version of testserver.HTTPSServer."""
105 def __init__(self, server_address, request_hander_class, pem_cert_and_key):
106 """Initializes the server.
108 Args:
109 server_address: Server host and port.
110 request_hander_class: The class that will handle requests to the server.
111 pem_cert_and_key: Path to file containing the https cert and private key.
113 self.cert_chain = tlslite.api.X509CertChain()
114 self.cert_chain.parsePemList(pem_cert_and_key)
115 # Force using only python implementation - otherwise behavior is different
116 # depending on whether m2crypto Python module is present (error is thrown
117 # when it is). m2crypto uses a C (based on OpenSSL) implementation under
118 # the hood.
119 self.private_key = tlslite.api.parsePEMKey(pem_cert_and_key,
120 private=True,
121 implementations=['python'])
123 testserver_base.StoppableHTTPServer.__init__(self,
124 server_address,
125 request_hander_class)
127 def handshake(self, tlsConnection):
128 """Performs the SSL handshake for an https connection.
130 Args:
131 tlsConnection: The https connection.
132 Returns:
133 Whether the SSL handshake succeeded.
135 try:
136 self.tlsConnection = tlsConnection
137 tlsConnection.handshakeServer(certChain=self.cert_chain,
138 privateKey=self.private_key)
139 tlsConnection.ignoreAbruptClose = True
140 return True
141 except:
142 return False
145 class ServerRunner(testserver_base.TestServerRunner):
146 """Runner that starts an https server which forwards requests to another
147 server.
150 def create_server(self, server_data):
151 """Performs the SSL handshake for an https connection.
153 Args:
154 server_data: Dictionary that holds information about the server.
155 Returns:
156 The started server.
158 # The server binds to |host:port| but the certificate is issued to
159 # |ssl_host| instead.
160 port = self.options.port
161 host = self.options.host
162 ssl_host = self.options.ssl_host
164 (pem_cert_and_key, ocsp_der) = minica.GenerateCertKeyAndOCSP(
165 subject = self.options.ssl_host,
166 ocsp_url = None)
168 server = MultiThreadedHTTPSServer((host, port),
169 RequestForwarder,
170 pem_cert_and_key)
171 print 'HTTPS server started on %s:%d...' % (host, server.server_port)
173 forward_target = urlparse.urlparse(self.options.forward_target)
174 server.forward_scheme = forward_target[0]
175 server.forward_netloc = forward_target[1]
176 server.forward_path = forward_target[2].rstrip('/')
177 server.forward_host = forward_target.hostname
178 if forward_target.port:
179 server.forward_host += ':' + str(forward_target.port)
180 server_data['port'] = server.server_port
181 return server
183 def add_options(self):
184 """Specifies the command-line options understood by the server."""
185 testserver_base.TestServerRunner.add_options(self)
186 self.option_parser.add_option('--https', action='store_true',
187 help='Ignored (provided for compatibility '
188 'only).')
189 self.option_parser.add_option('--ocsp', help='Ignored (provided for'
190 'compatibility only).')
191 self.option_parser.add_option('--ssl-host', help='The host name that the '
192 'certificate should be issued to.')
193 self.option_parser.add_option('--forward-target', help='The URL prefix to '
194 'which requests will be forwarded.')
197 if __name__ == '__main__':
198 sys.exit(ServerRunner().main())