blob: 90d3c881fc29b25552c312582c1adea72ba3e9cd [file] [log] [blame]
Guido van Rossumcd16bf62007-06-13 18:07:49 +00001#!/usr/bin/env python
2
Barry Warsaw820c1202008-06-12 04:06:45 +00003import email
Guido van Rossumcd16bf62007-06-13 18:07:49 +00004import threading
Jeremy Hylton1afc1692008-06-18 20:49:58 +00005import urllib.parse
6import urllib.request
Georg Brandl24420152008-05-26 16:32:26 +00007import http.server
Guido van Rossumcd16bf62007-06-13 18:07:49 +00008import unittest
9import hashlib
Benjamin Petersonee8712c2008-05-20 21:35:26 +000010from test import support
Guido van Rossumcd16bf62007-06-13 18:07:49 +000011
12# Loopback http server infrastructure
13
Georg Brandl24420152008-05-26 16:32:26 +000014class LoopbackHttpServer(http.server.HTTPServer):
Guido van Rossumcd16bf62007-06-13 18:07:49 +000015 """HTTP server w/ a few modifications that make it useful for
16 loopback testing purposes.
17 """
18
19 def __init__(self, server_address, RequestHandlerClass):
Georg Brandl24420152008-05-26 16:32:26 +000020 http.server.HTTPServer.__init__(self,
21 server_address,
22 RequestHandlerClass)
Guido van Rossumcd16bf62007-06-13 18:07:49 +000023
24 # Set the timeout of our listening socket really low so
25 # that we can stop the server easily.
26 self.socket.settimeout(1.0)
27
28 def get_request(self):
Georg Brandl24420152008-05-26 16:32:26 +000029 """HTTPServer method, overridden."""
Guido van Rossumcd16bf62007-06-13 18:07:49 +000030
31 request, client_address = self.socket.accept()
32
33 # It's a loopback connection, so setting the timeout
34 # really low shouldn't affect anything, but should make
35 # deadlocks less likely to occur.
36 request.settimeout(10.0)
37
38 return (request, client_address)
39
40class LoopbackHttpServerThread(threading.Thread):
41 """Stoppable thread that runs a loopback http server."""
42
Guido van Rossum806c2462007-08-06 23:33:07 +000043 def __init__(self, request_handler):
Guido van Rossumcd16bf62007-06-13 18:07:49 +000044 threading.Thread.__init__(self)
Guido van Rossum4566c712007-08-21 03:36:47 +000045 self._stop_server = False
Guido van Rossumcd16bf62007-06-13 18:07:49 +000046 self.ready = threading.Event()
Guido van Rossum806c2462007-08-06 23:33:07 +000047 request_handler.protocol_version = "HTTP/1.0"
Jeremy Hylton1afc1692008-06-18 20:49:58 +000048 self.httpd = LoopbackHttpServer(("127.0.0.1", 0),
Guido van Rossum806c2462007-08-06 23:33:07 +000049 request_handler)
50 #print "Serving HTTP on %s port %s" % (self.httpd.server_name,
51 # self.httpd.server_port)
52 self.port = self.httpd.server_port
Guido van Rossumcd16bf62007-06-13 18:07:49 +000053
54 def stop(self):
55 """Stops the webserver if it's currently running."""
56
57 # Set the stop flag.
Guido van Rossum4566c712007-08-21 03:36:47 +000058 self._stop_server = True
Guido van Rossumcd16bf62007-06-13 18:07:49 +000059
60 self.join()
61
62 def run(self):
Guido van Rossumcd16bf62007-06-13 18:07:49 +000063 self.ready.set()
Guido van Rossum4566c712007-08-21 03:36:47 +000064 while not self._stop_server:
Guido van Rossum806c2462007-08-06 23:33:07 +000065 self.httpd.handle_request()
Guido van Rossumcd16bf62007-06-13 18:07:49 +000066
67# Authentication infrastructure
68
69class DigestAuthHandler:
70 """Handler for performing digest authentication."""
71
72 def __init__(self):
73 self._request_num = 0
74 self._nonces = []
75 self._users = {}
76 self._realm_name = "Test Realm"
77 self._qop = "auth"
78
79 def set_qop(self, qop):
80 self._qop = qop
81
82 def set_users(self, users):
83 assert isinstance(users, dict)
84 self._users = users
85
86 def set_realm(self, realm):
87 self._realm_name = realm
88
89 def _generate_nonce(self):
90 self._request_num += 1
Guido van Rossum81360142007-08-29 14:26:52 +000091 nonce = hashlib.md5(str(self._request_num).encode("ascii")).hexdigest()
Guido van Rossumcd16bf62007-06-13 18:07:49 +000092 self._nonces.append(nonce)
93 return nonce
94
95 def _create_auth_dict(self, auth_str):
96 first_space_index = auth_str.find(" ")
97 auth_str = auth_str[first_space_index+1:]
98
99 parts = auth_str.split(",")
100
101 auth_dict = {}
102 for part in parts:
103 name, value = part.split("=")
104 name = name.strip()
105 if value[0] == '"' and value[-1] == '"':
106 value = value[1:-1]
107 else:
108 value = value.strip()
109 auth_dict[name] = value
110 return auth_dict
111
112 def _validate_auth(self, auth_dict, password, method, uri):
113 final_dict = {}
114 final_dict.update(auth_dict)
115 final_dict["password"] = password
116 final_dict["method"] = method
117 final_dict["uri"] = uri
118 HA1_str = "%(username)s:%(realm)s:%(password)s" % final_dict
Guido van Rossum81360142007-08-29 14:26:52 +0000119 HA1 = hashlib.md5(HA1_str.encode("ascii")).hexdigest()
Guido van Rossumcd16bf62007-06-13 18:07:49 +0000120 HA2_str = "%(method)s:%(uri)s" % final_dict
Guido van Rossum81360142007-08-29 14:26:52 +0000121 HA2 = hashlib.md5(HA2_str.encode("ascii")).hexdigest()
Guido van Rossumcd16bf62007-06-13 18:07:49 +0000122 final_dict["HA1"] = HA1
123 final_dict["HA2"] = HA2
124 response_str = "%(HA1)s:%(nonce)s:%(nc)s:" \
125 "%(cnonce)s:%(qop)s:%(HA2)s" % final_dict
Guido van Rossum81360142007-08-29 14:26:52 +0000126 response = hashlib.md5(response_str.encode("ascii")).hexdigest()
Guido van Rossumcd16bf62007-06-13 18:07:49 +0000127
128 return response == auth_dict["response"]
129
130 def _return_auth_challenge(self, request_handler):
131 request_handler.send_response(407, "Proxy Authentication Required")
132 request_handler.send_header("Content-Type", "text/html")
133 request_handler.send_header(
134 'Proxy-Authenticate', 'Digest realm="%s", '
135 'qop="%s",'
136 'nonce="%s", ' % \
137 (self._realm_name, self._qop, self._generate_nonce()))
138 # XXX: Not sure if we're supposed to add this next header or
139 # not.
140 #request_handler.send_header('Connection', 'close')
141 request_handler.end_headers()
Guido van Rossum8a392d72007-11-21 22:09:45 +0000142 request_handler.wfile.write(b"Proxy Authentication Required.")
Guido van Rossumcd16bf62007-06-13 18:07:49 +0000143 return False
144
145 def handle_request(self, request_handler):
146 """Performs digest authentication on the given HTTP request
147 handler. Returns True if authentication was successful, False
148 otherwise.
149
150 If no users have been set, then digest auth is effectively
151 disabled and this method will always return True.
152 """
153
154 if len(self._users) == 0:
155 return True
156
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000157 if "Proxy-Authorization" not in request_handler.headers:
Guido van Rossumcd16bf62007-06-13 18:07:49 +0000158 return self._return_auth_challenge(request_handler)
159 else:
160 auth_dict = self._create_auth_dict(
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000161 request_handler.headers["Proxy-Authorization"]
Guido van Rossumcd16bf62007-06-13 18:07:49 +0000162 )
163 if auth_dict["username"] in self._users:
164 password = self._users[ auth_dict["username"] ]
165 else:
166 return self._return_auth_challenge(request_handler)
167 if not auth_dict.get("nonce") in self._nonces:
168 return self._return_auth_challenge(request_handler)
169 else:
170 self._nonces.remove(auth_dict["nonce"])
171
172 auth_validated = False
173
174 # MSIE uses short_path in its validation, but Python's
175 # urllib2 uses the full path, so we're going to see if
176 # either of them works here.
177
178 for path in [request_handler.path, request_handler.short_path]:
179 if self._validate_auth(auth_dict,
180 password,
181 request_handler.command,
182 path):
183 auth_validated = True
184
185 if not auth_validated:
186 return self._return_auth_challenge(request_handler)
187 return True
188
189# Proxy test infrastructure
190
Georg Brandl24420152008-05-26 16:32:26 +0000191class FakeProxyHandler(http.server.BaseHTTPRequestHandler):
Guido van Rossumcd16bf62007-06-13 18:07:49 +0000192 """This is a 'fake proxy' that makes it look like the entire
193 internet has gone down due to a sudden zombie invasion. It main
194 utility is in providing us with authentication support for
195 testing.
196 """
197
198 digest_auth_handler = DigestAuthHandler()
199
200 def log_message(self, format, *args):
201 # Uncomment the next line for debugging.
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000202 # sys.stderr.write(format % args)
Guido van Rossumcd16bf62007-06-13 18:07:49 +0000203 pass
204
205 def do_GET(self):
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000206 (scm, netloc, path, params, query, fragment) = urllib.parse.urlparse(
207 self.path, "http")
Guido van Rossumcd16bf62007-06-13 18:07:49 +0000208 self.short_path = path
209 if self.digest_auth_handler.handle_request(self):
210 self.send_response(200, "OK")
211 self.send_header("Content-Type", "text/html")
212 self.end_headers()
Guido van Rossum8a392d72007-11-21 22:09:45 +0000213 self.wfile.write(bytes("You've reached %s!<BR>" % self.path,
214 "ascii"))
215 self.wfile.write(b"Our apologies, but our server is down due to "
216 b"a sudden zombie invasion.")
Guido van Rossumcd16bf62007-06-13 18:07:49 +0000217
218# Test cases
219
220class ProxyAuthTests(unittest.TestCase):
Christian Heimesbbe741d2008-03-28 10:53:29 +0000221 URL = "http://localhost"
Guido van Rossumcd16bf62007-06-13 18:07:49 +0000222
Guido van Rossumcd16bf62007-06-13 18:07:49 +0000223 USER = "tester"
224 PASSWD = "test123"
225 REALM = "TestRealm"
226
Guido van Rossumcd16bf62007-06-13 18:07:49 +0000227 def setUp(self):
228 FakeProxyHandler.digest_auth_handler.set_users({
229 self.USER : self.PASSWD
230 })
231 FakeProxyHandler.digest_auth_handler.set_realm(self.REALM)
232
Guido van Rossum806c2462007-08-06 23:33:07 +0000233 self.server = LoopbackHttpServerThread(FakeProxyHandler)
Guido van Rossumcd16bf62007-06-13 18:07:49 +0000234 self.server.start()
235 self.server.ready.wait()
Guido van Rossum806c2462007-08-06 23:33:07 +0000236 proxy_url = "http://127.0.0.1:%d" % self.server.port
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000237 handler = urllib.request.ProxyHandler({"http" : proxy_url})
238 self._digest_auth_handler = urllib.request.ProxyDigestAuthHandler()
239 self.opener = urllib.request.build_opener(
240 handler, self._digest_auth_handler)
Guido van Rossumcd16bf62007-06-13 18:07:49 +0000241
242 def tearDown(self):
243 self.server.stop()
244
245 def test_proxy_with_bad_password_raises_httperror(self):
246 self._digest_auth_handler.add_password(self.REALM, self.URL,
247 self.USER, self.PASSWD+"bad")
248 FakeProxyHandler.digest_auth_handler.set_qop("auth")
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000249 self.assertRaises(urllib.error.HTTPError,
Guido van Rossumcd16bf62007-06-13 18:07:49 +0000250 self.opener.open,
251 self.URL)
252
253 def test_proxy_with_no_password_raises_httperror(self):
254 FakeProxyHandler.digest_auth_handler.set_qop("auth")
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000255 self.assertRaises(urllib.error.HTTPError,
Guido van Rossumcd16bf62007-06-13 18:07:49 +0000256 self.opener.open,
257 self.URL)
258
259 def test_proxy_qop_auth_works(self):
260 self._digest_auth_handler.add_password(self.REALM, self.URL,
261 self.USER, self.PASSWD)
262 FakeProxyHandler.digest_auth_handler.set_qop("auth")
263 result = self.opener.open(self.URL)
264 while result.read():
265 pass
266 result.close()
267
268 def test_proxy_qop_auth_int_works_or_throws_urlerror(self):
269 self._digest_auth_handler.add_password(self.REALM, self.URL,
270 self.USER, self.PASSWD)
271 FakeProxyHandler.digest_auth_handler.set_qop("auth-int")
272 try:
273 result = self.opener.open(self.URL)
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000274 except urllib.error.URLError:
Guido van Rossumcd16bf62007-06-13 18:07:49 +0000275 # It's okay if we don't support auth-int, but we certainly
276 # shouldn't receive any kind of exception here other than
277 # a URLError.
278 result = None
279 if result:
280 while result.read():
281 pass
282 result.close()
283
Christian Heimesbbe741d2008-03-28 10:53:29 +0000284
285def GetRequestHandler(responses):
286
Georg Brandl24420152008-05-26 16:32:26 +0000287 class FakeHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
Christian Heimesbbe741d2008-03-28 10:53:29 +0000288
289 server_version = "TestHTTP/"
290 requests = []
291 headers_received = []
292 port = 80
293
294 def do_GET(self):
295 body = self.send_head()
296 if body:
297 self.wfile.write(body)
298
299 def do_POST(self):
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000300 content_length = self.headers["Content-Length"]
Christian Heimesbbe741d2008-03-28 10:53:29 +0000301 post_data = self.rfile.read(int(content_length))
302 self.do_GET()
303 self.requests.append(post_data)
304
305 def send_head(self):
306 FakeHTTPRequestHandler.headers_received = self.headers
307 self.requests.append(self.path)
308 response_code, headers, body = responses.pop(0)
309
310 self.send_response(response_code)
311
312 for (header, value) in headers:
Antoine Pitroub353c122009-02-11 00:39:14 +0000313 self.send_header(header, value % {'port':self.port})
Christian Heimesbbe741d2008-03-28 10:53:29 +0000314 if body:
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000315 self.send_header("Content-type", "text/plain")
Christian Heimesbbe741d2008-03-28 10:53:29 +0000316 self.end_headers()
317 return body
318 self.end_headers()
319
320 def log_message(self, *args):
321 pass
322
323
324 return FakeHTTPRequestHandler
325
326
327class TestUrlopen(unittest.TestCase):
328 """Tests urllib2.urlopen using the network.
329
330 These tests are not exhaustive. Assuming that testing using files does a
331 good job overall of some of the basic interface features. There are no
332 tests exercising the optional 'data' and 'proxies' arguments. No tests
333 for transparent redirection have been written.
334 """
335
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000336 def setUp(self):
337 self.server = None
338
339 def tearDown(self):
340 if self.server is not None:
341 self.server.stop()
342
343 def urlopen(self, url, data=None):
Antoine Pitroub353c122009-02-11 00:39:14 +0000344 l = []
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000345 f = urllib.request.urlopen(url, data)
Antoine Pitroub353c122009-02-11 00:39:14 +0000346 try:
347 # Exercise various methods
348 l.extend(f.readlines(200))
349 l.append(f.readline())
350 l.append(f.read(1024))
351 l.append(f.read())
352 finally:
353 f.close()
354 return b"".join(l)
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000355
356 def start_server(self, responses=None):
357 if responses is None:
358 responses = [(200, [], b"we don't care")]
Christian Heimesbbe741d2008-03-28 10:53:29 +0000359 handler = GetRequestHandler(responses)
360
361 self.server = LoopbackHttpServerThread(handler)
362 self.server.start()
363 self.server.ready.wait()
364 port = self.server.port
365 handler.port = port
366 return handler
367
Christian Heimesbbe741d2008-03-28 10:53:29 +0000368 def test_redirection(self):
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000369 expected_response = b"We got here..."
Christian Heimesbbe741d2008-03-28 10:53:29 +0000370 responses = [
Antoine Pitroub353c122009-02-11 00:39:14 +0000371 (302, [("Location", "http://localhost:%(port)s/somewhere_else")],
372 ""),
Christian Heimesbbe741d2008-03-28 10:53:29 +0000373 (200, [], expected_response)
374 ]
375
376 handler = self.start_server(responses)
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000377 data = self.urlopen("http://localhost:%s/" % handler.port)
378 self.assertEquals(data, expected_response)
379 self.assertEquals(handler.requests, ["/", "/somewhere_else"])
Christian Heimesbbe741d2008-03-28 10:53:29 +0000380
Antoine Pitroub353c122009-02-11 00:39:14 +0000381 def test_chunked(self):
382 expected_response = b"hello world"
383 chunked_start = (
384 b'a\r\n'
385 b'hello worl\r\n'
386 b'1\r\n'
387 b'd\r\n'
388 b'0\r\n'
389 )
390 response = [(200, [("Transfer-Encoding", "chunked")], chunked_start)]
391 handler = self.start_server(response)
392 data = self.urlopen("http://localhost:%s/" % handler.port)
393 self.assertEquals(data, expected_response)
394
Christian Heimesbbe741d2008-03-28 10:53:29 +0000395 def test_404(self):
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000396 expected_response = b"Bad bad bad..."
Christian Heimesbbe741d2008-03-28 10:53:29 +0000397 handler = self.start_server([(404, [], expected_response)])
398
399 try:
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000400 self.urlopen("http://localhost:%s/weeble" % handler.port)
401 except urllib.error.URLError as f:
402 data = f.read()
403 f.close()
404 else:
405 self.fail("404 should raise URLError")
Christian Heimesbbe741d2008-03-28 10:53:29 +0000406
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000407 self.assertEquals(data, expected_response)
408 self.assertEquals(handler.requests, ["/weeble"])
Christian Heimesbbe741d2008-03-28 10:53:29 +0000409
410 def test_200(self):
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000411 expected_response = b"pycon 2008..."
Christian Heimesbbe741d2008-03-28 10:53:29 +0000412 handler = self.start_server([(200, [], expected_response)])
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000413 data = self.urlopen("http://localhost:%s/bizarre" % handler.port)
414 self.assertEquals(data, expected_response)
415 self.assertEquals(handler.requests, ["/bizarre"])
Christian Heimesbbe741d2008-03-28 10:53:29 +0000416
417 def test_200_with_parameters(self):
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000418 expected_response = b"pycon 2008..."
Christian Heimesbbe741d2008-03-28 10:53:29 +0000419 handler = self.start_server([(200, [], expected_response)])
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000420 data = self.urlopen("http://localhost:%s/bizarre" % handler.port,
421 b"get=with_feeling")
422 self.assertEquals(data, expected_response)
423 self.assertEquals(handler.requests, ["/bizarre", b"get=with_feeling"])
Christian Heimesbbe741d2008-03-28 10:53:29 +0000424
425 def test_sending_headers(self):
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000426 handler = self.start_server()
427 req = urllib.request.Request("http://localhost:%s/" % handler.port,
428 headers={"Range": "bytes=20-39"})
429 urllib.request.urlopen(req)
430 self.assertEqual(handler.headers_received["Range"], "bytes=20-39")
Christian Heimesbbe741d2008-03-28 10:53:29 +0000431
432 def test_basic(self):
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000433 handler = self.start_server()
434 open_url = urllib.request.urlopen("http://localhost:%s" % handler.port)
435 for attr in ("read", "close", "info", "geturl"):
436 self.assert_(hasattr(open_url, attr), "object returned from "
437 "urlopen lacks the %s attribute" % attr)
Christian Heimesbbe741d2008-03-28 10:53:29 +0000438 try:
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000439 self.assert_(open_url.read(), "calling 'read' failed")
Christian Heimesbbe741d2008-03-28 10:53:29 +0000440 finally:
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000441 open_url.close()
Christian Heimesbbe741d2008-03-28 10:53:29 +0000442
443 def test_info(self):
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000444 handler = self.start_server()
Christian Heimesbbe741d2008-03-28 10:53:29 +0000445 try:
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000446 open_url = urllib.request.urlopen(
447 "http://localhost:%s" % handler.port)
Christian Heimesbbe741d2008-03-28 10:53:29 +0000448 info_obj = open_url.info()
Barry Warsaw820c1202008-06-12 04:06:45 +0000449 self.assert_(isinstance(info_obj, email.message.Message),
Christian Heimesbbe741d2008-03-28 10:53:29 +0000450 "object returned by 'info' is not an instance of "
Barry Warsaw820c1202008-06-12 04:06:45 +0000451 "email.message.Message")
452 self.assertEqual(info_obj.get_content_subtype(), "plain")
Christian Heimesbbe741d2008-03-28 10:53:29 +0000453 finally:
454 self.server.stop()
455
456 def test_geturl(self):
457 # Make sure same URL as opened is returned by geturl.
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000458 handler = self.start_server()
459 open_url = urllib.request.urlopen("http://localhost:%s" % handler.port)
460 url = open_url.geturl()
461 self.assertEqual(url, "http://localhost:%s" % handler.port)
Christian Heimesbbe741d2008-03-28 10:53:29 +0000462
463 def test_bad_address(self):
464 # Make sure proper exception is raised when connecting to a bogus
465 # address.
466 self.assertRaises(IOError,
467 # SF patch 809915: In Sep 2003, VeriSign started
468 # highjacking invalid .com and .net addresses to
469 # boost traffic to their own site. This test
470 # started failing then. One hopes the .invalid
471 # domain will be spared to serve its defined
472 # purpose.
Jeremy Hylton1afc1692008-06-18 20:49:58 +0000473 urllib.request.urlopen,
Antoine Pitrou8fd33d32008-12-15 13:08:55 +0000474 "http://sadflkjsasf.i.nvali.d/")
Christian Heimesbbe741d2008-03-28 10:53:29 +0000475
Guido van Rossumcd16bf62007-06-13 18:07:49 +0000476def test_main():
Benjamin Petersonee8712c2008-05-20 21:35:26 +0000477 support.run_unittest(ProxyAuthTests)
478 support.run_unittest(TestUrlopen)
Guido van Rossumcd16bf62007-06-13 18:07:49 +0000479
480if __name__ == "__main__":
481 test_main()