Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / chrome / browser / chromeos / login / test / https_forwarder.py
blob5bde110935eafaa990ce0ac94d8e7ff13ff6e69b
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 SocketServer
12 import sys
13 import urllib2
14 import urlparse
15 import testserver_base
16 import tlslite.api
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.
26 """
28 def http_response(self, req, response):
29 return response
31 def https_response(self, req, response):
32 return response
35 class RequestForwarder(BaseHTTPServer.BaseHTTPRequestHandler):
36 """Handles requests received by forwarding them to the another server."""
38 def do_GET(self):
39 """Forwards GET requests."""
40 self._forward(None)
42 def do_POST(self):
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.
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]))
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 # RFC 6265 states in section 3:
69 # Origin servers SHOULD NOT fold multiple Set-Cookie header fields into
70 # a single header field.
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())
87 class 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."""
94 def __init__(self, server_address, request_hander_class, pem_cert_and_key):
95 """Initializes the server.
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.
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'])
112 testserver_base.StoppableHTTPServer.__init__(self,
113 server_address,
114 request_hander_class)
116 def handshake(self, tlsConnection):
117 """Performs the SSL handshake for an https connection.
119 Args:
120 tlsConnection: The https connection.
121 Returns:
122 Whether the SSL handshake succeeded.
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
134 class ServerRunner(testserver_base.TestServerRunner):
135 """Runner that starts an https server which forwards requests to another
136 server.
139 def create_server(self, server_data):
140 """Performs the SSL handshake for an https connection.
142 Args:
143 server_data: Dictionary that holds information about the server.
144 Returns:
145 The started server.
147 # The server binds to |host:port| but the certificate is issued to
148 # |ssl_host| instead.
149 port = self.options.port
150 host = self.options.host
151 ssl_host = self.options.ssl_host
153 (pem_cert_and_key, ocsp_der) = minica.GenerateCertKeyAndOCSP(
154 subject = self.options.ssl_host,
155 ocsp_url = None)
157 server = MultiThreadedHTTPSServer((host, port),
158 RequestForwarder,
159 pem_cert_and_key)
160 print 'HTTPS server started on %s:%d...' % (host, server.server_port)
162 forward_target = urlparse.urlparse(self.options.forward_target)
163 server.forward_scheme = forward_target[0]
164 server.forward_netloc = forward_target[1]
165 server.forward_path = forward_target[2].rstrip('/')
166 server.forward_host = forward_target.hostname
167 if forward_target.port:
168 server.forward_host += ':' + str(forward_target.port)
169 server_data['port'] = server.server_port
170 return server
172 def add_options(self):
173 """Specifies the command-line options understood by the server."""
174 testserver_base.TestServerRunner.add_options(self)
175 self.option_parser.add_option('--https', action='store_true',
176 help='Ignored (provided for compatibility '
177 'only).')
178 self.option_parser.add_option('--ocsp', help='Ignored (provided for'
179 'compatibility only).')
180 self.option_parser.add_option('--ssl-host', help='The host name that the '
181 'certificate should be issued to.')
182 self.option_parser.add_option('--forward-target', help='The URL prefix to '
183 'which requests will be forwarded.')
186 if __name__ == '__main__':
187 sys.exit(ServerRunner().main())