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.
16 import testserver_base
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.
29 def http_response(self
, req
, response
):
32 def https_response(self
, req
, response
):
36 class RequestForwarder(BaseHTTPServer
.BaseHTTPRequestHandler
):
37 """Handles requests received by forwarding them to the another server."""
40 """Forwards GET requests."""
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.
51 body: The request body. This should be |None| for GET requests.
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],
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':
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()<>@,:\\"/[\]?={} ]+=',
89 self
.send_header(key
, value
[start
:end
])
91 self
.send_header(key
, value
[start
:])
93 self
.send_header(key
, value
)
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.
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
119 self
.private_key
= tlslite
.api
.parsePEMKey(pem_cert_and_key
,
121 implementations
=['python'])
123 testserver_base
.StoppableHTTPServer
.__init
__(self
,
125 request_hander_class
)
127 def handshake(self
, tlsConnection
):
128 """Performs the SSL handshake for an https connection.
131 tlsConnection: The https connection.
133 Whether the SSL handshake succeeded.
136 self
.tlsConnection
= tlsConnection
137 tlsConnection
.handshakeServer(certChain
=self
.cert_chain
,
138 privateKey
=self
.private_key
)
139 tlsConnection
.ignoreAbruptClose
= True
145 class ServerRunner(testserver_base
.TestServerRunner
):
146 """Runner that starts an https server which forwards requests to another
150 def create_server(self
, server_data
):
151 """Performs the SSL handshake for an https connection.
154 server_data: Dictionary that holds information about the 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
,
168 server
= MultiThreadedHTTPSServer((host
, port
),
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
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 '
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())