blob: cf94d2f2c6cba0e6b807e769e33ebedd1d15cc7a [file] [log] [blame]
Guido van Rossume7b146f2000-02-04 15:28:42 +00001"""An extensible library for opening URLs using a variety of protocols
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +00002
3The simplest way to use this module is to call the urlopen function,
Tim Peterse1190062001-01-15 03:34:38 +00004which accepts a string containing a URL or a Request object (described
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +00005below). It opens the URL and returns the results as file-like
6object; the returned object has some extra methods described below.
7
8The OpenerDirectory manages a collection of Handler objects that do
Tim Peterse1190062001-01-15 03:34:38 +00009all the actual work. Each Handler implements a particular protocol or
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +000010option. The OpenerDirector is a composite object that invokes the
11Handlers needed to open the requested URL. For example, the
12HTTPHandler performs HTTP GET and POST requests and deals with
13non-error returns. The HTTPRedirectHandler automatically deals with
14HTTP 301 & 302 redirect errors, and the HTTPDigestAuthHandler deals
15with digest authentication.
16
17urlopen(url, data=None) -- basic usage is that same as original
18urllib. pass the url and optionally data to post to an HTTP URL, and
Tim Peterse1190062001-01-15 03:34:38 +000019get a file-like object back. One difference is that you can also pass
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +000020a Request instance instead of URL. Raises a URLError (subclass of
21IOError); for HTTP errors, raises an HTTPError, which can also be
22treated as a valid response.
23
24build_opener -- function that creates a new OpenerDirector instance.
25will install the default handlers. accepts one or more Handlers as
26arguments, either instances or Handler classes that it will
27instantiate. if one of the argument is a subclass of the default
28handler, the argument will be installed instead of the default.
29
30install_opener -- installs a new opener as the default opener.
31
32objects of interest:
33OpenerDirector --
34
35Request -- an object that encapsulates the state of a request. the
36state can be a simple as the URL. it can also include extra HTTP
37headers, e.g. a User-Agent.
38
39BaseHandler --
40
41exceptions:
42URLError-- a subclass of IOError, individual protocols have their own
43specific subclass
44
Tim Peterse1190062001-01-15 03:34:38 +000045HTTPError-- also a valid HTTP response, so you can treat an HTTP error
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +000046as an exceptional event or valid response
47
48internals:
49BaseHandler and parent
50_call_chain conventions
51
52Example usage:
53
54import urllib2
55
56# set up authentication info
57authinfo = urllib2.HTTPBasicAuthHandler()
58authinfo.add_password('realm', 'host', 'username', 'password')
59
Tim Peterse1190062001-01-15 03:34:38 +000060# build a new opener that adds authentication and caching FTP handlers
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +000061opener = urllib2.build_opener(authinfo, urllib2.CacheFTPHandler)
62
63# install it
64urllib2.install_opener(opener)
65
66f = urllib2.urlopen('http://www.python.org/')
67
68
69"""
70
71# XXX issues:
72# If an authentication error handler that tries to perform
73 # authentication for some reason but fails, how should the error be
74 # signalled? The client needs to know the HTTP error code. But if
75 # the handler knows that the problem was, e.g., that it didn't know
Tim Peterse1190062001-01-15 03:34:38 +000076 # that hash algo that requested in the challenge, it would be good to
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +000077 # pass that information along to the client, too.
78
79# XXX to do:
80# name!
81# documentation (getting there)
82# complex proxies
83# abstract factory for opener
84# ftp errors aren't handled cleanly
85# gopher can return a socket.error
86# check digest against correct (i.e. non-apache) implementation
87
88import string
89import socket
90import UserDict
91import httplib
92import re
93import base64
94import types
95import urlparse
96import os
97import md5
98import mimetypes
99import mimetools
100import ftplib
101import sys
102import time
103import gopherlib
104
105try:
106 from cStringIO import StringIO
107except ImportError:
108 from StringIO import StringIO
109
110try:
111 import sha
112except ImportError:
113 # need 1.5.2 final
114 sha = None
115
116# not sure how many of these need to be gotten rid of
117from urllib import unwrap, unquote, splittype, splithost, \
118 addinfourl, splitport, splitgophertype, splitquery, \
119 splitattr, ftpwrapper, noheaders
120
121# support for proxies via environment variables
122from urllib import getproxies
123
124# support for FileHandler
125from urllib import localhost, thishost, url2pathname, pathname2url
126
127# support for GopherHandler
128from urllib import splitgophertype, splitquery
129
130__version__ = "2.0a1"
131
132_opener = None
133def urlopen(url, data=None):
134 global _opener
135 if _opener is None:
136 _opener = build_opener()
137 return _opener.open(url, data)
138
139def install_opener(opener):
140 global _opener
141 _opener = opener
142
143# do these error classes make sense?
Tim Peterse1190062001-01-15 03:34:38 +0000144# make sure all of the IOError stuff is overridden. we just want to be
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000145 # subtypes.
146
147class URLError(IOError):
148 # URLError is a sub-type of IOError, but it doesn't share any of
149 # the implementation. need to override __init__ and __str__
150 def __init__(self, reason):
Fred Drake13a2c272000-02-10 17:17:14 +0000151 self.reason = reason
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000152
153 def __str__(self):
Fred Drake13a2c272000-02-10 17:17:14 +0000154 return '<urlopen error %s>' % self.reason
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000155
156class HTTPError(URLError, addinfourl):
157 """Raised when HTTP error occurs, but also acts like non-error return"""
Jeremy Hylton73574ee2000-10-12 18:54:18 +0000158 __super_init = addinfourl.__init__
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000159
160 def __init__(self, url, code, msg, hdrs, fp):
Jeremy Hylton73574ee2000-10-12 18:54:18 +0000161 self.__super_init(fp, hdrs, url)
Fred Drake13a2c272000-02-10 17:17:14 +0000162 self.code = code
163 self.msg = msg
164 self.hdrs = hdrs
165 self.fp = fp
166 # XXX
167 self.filename = url
Tim Peterse1190062001-01-15 03:34:38 +0000168
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000169 def __str__(self):
Fred Drake13a2c272000-02-10 17:17:14 +0000170 return 'HTTP Error %s: %s' % (self.code, self.msg)
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000171
172 def __del__(self):
Fred Drake13a2c272000-02-10 17:17:14 +0000173 # XXX is this safe? what if user catches exception, then
174 # extracts fp and discards exception?
Jeremy Hylton73574ee2000-10-12 18:54:18 +0000175 if self.fp:
176 self.fp.close()
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000177
178class GopherError(URLError):
179 pass
180
181class Request:
182 def __init__(self, url, data=None, headers={}):
Fred Drake13a2c272000-02-10 17:17:14 +0000183 # unwrap('<URL:type://host/path>') --> 'type://host/path'
184 self.__original = unwrap(url)
185 self.type = None
186 # self.__r_type is what's left after doing the splittype
187 self.host = None
188 self.port = None
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000189 self.data = data
Fred Drake13a2c272000-02-10 17:17:14 +0000190 self.headers = {}
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000191 self.headers.update(headers)
192
193 def __getattr__(self, attr):
Fred Drake13a2c272000-02-10 17:17:14 +0000194 # XXX this is a fallback mechanism to guard against these
Tim Peterse1190062001-01-15 03:34:38 +0000195 # methods getting called in a non-standard order. this may be
Fred Drake13a2c272000-02-10 17:17:14 +0000196 # too complicated and/or unnecessary.
197 # XXX should the __r_XXX attributes be public?
198 if attr[:12] == '_Request__r_':
199 name = attr[12:]
200 if hasattr(Request, 'get_' + name):
201 getattr(self, 'get_' + name)()
202 return getattr(self, attr)
203 raise AttributeError, attr
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000204
205 def add_data(self, data):
206 self.data = data
207
208 def has_data(self):
209 return self.data is not None
210
211 def get_data(self):
212 return self.data
213
214 def get_full_url(self):
215 return self.__original
216
217 def get_type(self):
Fred Drake13a2c272000-02-10 17:17:14 +0000218 if self.type is None:
219 self.type, self.__r_type = splittype(self.__original)
Jeremy Hylton73574ee2000-10-12 18:54:18 +0000220 assert self.type is not None, self.__original
Fred Drake13a2c272000-02-10 17:17:14 +0000221 return self.type
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000222
223 def get_host(self):
Fred Drake13a2c272000-02-10 17:17:14 +0000224 if self.host is None:
225 self.host, self.__r_host = splithost(self.__r_type)
226 if self.host:
227 self.host = unquote(self.host)
228 return self.host
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000229
230 def get_selector(self):
Fred Drake13a2c272000-02-10 17:17:14 +0000231 return self.__r_host
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000232
233 def set_proxy(self, proxy):
Fred Drake13a2c272000-02-10 17:17:14 +0000234 self.__proxy = proxy
235 # XXX this code is based on urllib, but it doesn't seem
236 # correct. specifically, if the proxy has a port number then
237 # splittype will return the hostname as the type and the port
238 # will be include with everything else
239 self.type, self.__r_type = splittype(self.__proxy)
240 self.host, XXX = splithost(self.__r_type)
241 self.host = unquote(self.host)
242 self.__r_host = self.__original
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000243
244 def add_header(self, key, val):
Fred Drake13a2c272000-02-10 17:17:14 +0000245 # useful for something like authentication
246 self.headers[key] = val
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000247
248class OpenerDirector:
249 def __init__(self):
250 server_version = "Python-urllib/%s" % __version__
251 self.addheaders = [('User-agent', server_version)]
252 # manage the individual handlers
253 self.handlers = []
254 self.handle_open = {}
255 self.handle_error = {}
256
257 def add_handler(self, handler):
258 added = 0
259 for meth in get_methods(handler):
260 if meth[-5:] == '_open':
261 protocol = meth[:-5]
Tim Peterse1190062001-01-15 03:34:38 +0000262 if self.handle_open.has_key(protocol):
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000263 self.handle_open[protocol].append(handler)
264 else:
265 self.handle_open[protocol] = [handler]
266 added = 1
267 continue
268 i = string.find(meth, '_')
269 j = string.find(meth[i+1:], '_') + i + 1
270 if j != -1 and meth[i+1:j] == 'error':
271 proto = meth[:i]
272 kind = meth[j+1:]
273 try:
274 kind = string.atoi(kind)
275 except ValueError:
276 pass
277 dict = self.handle_error.get(proto, {})
278 if dict.has_key(kind):
279 dict[kind].append(handler)
280 else:
281 dict[kind] = [handler]
282 self.handle_error[proto] = dict
283 added = 1
284 continue
285 if added:
286 self.handlers.append(handler)
287 handler.add_parent(self)
Tim Peterse1190062001-01-15 03:34:38 +0000288
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000289 def __del__(self):
290 self.close()
291
292 def close(self):
293 for handler in self.handlers:
294 handler.close()
295 self.handlers = []
296
297 def _call_chain(self, chain, kind, meth_name, *args):
298 # XXX raise an exception if no one else should try to handle
299 # this url. return None if you can't but someone else could.
300 handlers = chain.get(kind, ())
301 for handler in handlers:
302 func = getattr(handler, meth_name)
Jeremy Hylton73574ee2000-10-12 18:54:18 +0000303
304 result = func(*args)
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000305 if result is not None:
306 return result
307
308 def open(self, fullurl, data=None):
Fred Drake13a2c272000-02-10 17:17:14 +0000309 # accept a URL or a Request object
310 if type(fullurl) == types.StringType:
311 req = Request(fullurl, data)
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000312 else:
313 req = fullurl
314 if data is not None:
315 req.add_data(data)
Fred Drake13a2c272000-02-10 17:17:14 +0000316 assert isinstance(req, Request) # really only care about interface
Tim Peterse1190062001-01-15 03:34:38 +0000317
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000318 result = self._call_chain(self.handle_open, 'default',
Tim Peterse1190062001-01-15 03:34:38 +0000319 'default_open', req)
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000320 if result:
321 return result
322
Fred Drake13a2c272000-02-10 17:17:14 +0000323 type_ = req.get_type()
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000324 result = self._call_chain(self.handle_open, type_, type_ + \
Jeremy Hylton73574ee2000-10-12 18:54:18 +0000325 '_open', req)
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000326 if result:
327 return result
328
329 return self._call_chain(self.handle_open, 'unknown',
330 'unknown_open', req)
331
332 def error(self, proto, *args):
333 if proto == 'http':
334 # XXX http protocol is special cased
335 dict = self.handle_error[proto]
336 proto = args[2] # YUCK!
337 meth_name = 'http_error_%d' % proto
338 http_err = 1
339 orig_args = args
340 else:
341 dict = self.handle_error
342 meth_name = proto + '_error'
343 http_err = 0
344 args = (dict, proto, meth_name) + args
Jeremy Hylton73574ee2000-10-12 18:54:18 +0000345 result = self._call_chain(*args)
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000346 if result:
347 return result
348
349 if http_err:
350 args = (dict, 'default', 'http_error_default') + orig_args
Jeremy Hylton73574ee2000-10-12 18:54:18 +0000351 return self._call_chain(*args)
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000352
353def is_callable(obj):
354 # not quite like builtin callable (which I didn't know existed),
355 # not entirely sure it needs to be different
356 if type(obj) in (types.BuiltinFunctionType,
Fred Drake13a2c272000-02-10 17:17:14 +0000357 types.BuiltinMethodType, types.LambdaType,
358 types.MethodType):
359 return 1
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000360 if type(obj) == types.InstanceType:
Fred Drake13a2c272000-02-10 17:17:14 +0000361 return hasattr(obj, '__call__')
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000362 return 0
363
364def get_methods(inst):
365 methods = {}
366 classes = []
367 classes.append(inst.__class__)
368 while classes:
369 klass = classes[0]
370 del classes[0]
371 classes = classes + list(klass.__bases__)
372 for name in dir(klass):
373 attr = getattr(klass, name)
374 if type(attr) == types.UnboundMethodType:
375 methods[name] = 1
376 for name in dir(inst):
Fred Drake13a2c272000-02-10 17:17:14 +0000377 if is_callable(getattr(inst, name)):
378 methods[name] = 1
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000379 return methods.keys()
380
381# XXX probably also want an abstract factory that knows things like
382 # the fact that a ProxyHandler needs to get inserted first.
383# would also know when it makes sense to skip a superclass in favor of
Tim Peterse1190062001-01-15 03:34:38 +0000384 # a subclass and when it might make sense to include both
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000385
386def build_opener(*handlers):
387 """Create an opener object from a list of handlers.
388
389 The opener will use several default handlers, including support
390 for HTTP and FTP. If there is a ProxyHandler, it must be at the
391 front of the list of handlers. (Yuck.)
392
393 If any of the handlers passed as arguments are subclasses of the
394 default handlers, the default handlers will not be used.
395 """
Tim Peterse1190062001-01-15 03:34:38 +0000396
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000397 opener = OpenerDirector()
398 default_classes = [ProxyHandler, UnknownHandler, HTTPHandler,
399 HTTPDefaultErrorHandler, HTTPRedirectHandler,
400 FTPHandler, FileHandler]
401 skip = []
402 for klass in default_classes:
403 for check in handlers:
404 if type(check) == types.ClassType:
405 if issubclass(check, klass):
406 skip.append(klass)
407 elif type(check) == types.InstanceType:
408 if isinstance(check, klass):
409 skip.append(klass)
410 for klass in skip:
411 default_classes.remove(klass)
412
413 for klass in default_classes:
414 opener.add_handler(klass())
415
416 for h in handlers:
417 if type(h) == types.ClassType:
418 h = h()
419 opener.add_handler(h)
420 return opener
421
422class BaseHandler:
423 def add_parent(self, parent):
424 self.parent = parent
425 def close(self):
426 self.parent = None
427
428class HTTPDefaultErrorHandler(BaseHandler):
429 def http_error_default(self, req, fp, code, msg, hdrs):
Fred Drake13a2c272000-02-10 17:17:14 +0000430 raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000431
432class HTTPRedirectHandler(BaseHandler):
433 # Implementation note: To avoid the server sending us into an
434 # infinite loop, the request object needs to track what URLs we
435 # have already seen. Do this by adding a handler-specific
436 # attribute to the Request object.
437 def http_error_302(self, req, fp, code, msg, headers):
438 if headers.has_key('location'):
439 newurl = headers['location']
440 elif headers.has_key('uri'):
441 newurl = headers['uri']
442 else:
443 return
444 nil = fp.read()
445 fp.close()
446
Jeremy Hylton73574ee2000-10-12 18:54:18 +0000447 newurl = urlparse.urljoin(req.get_full_url(), newurl)
448
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000449 # XXX Probably want to forget about the state of the current
450 # request, although that might interact poorly with other
451 # handlers that also use handler-specific request attributes
452 new = Request(newurl, req.get_data())
453 new.error_302_dict = {}
454 if hasattr(req, 'error_302_dict'):
455 if req.error_302_dict.has_key(newurl):
456 raise HTTPError(req.get_full_url(), code,
457 self.inf_msg + msg, headers)
458 new.error_302_dict.update(req.error_302_dict)
459 new.error_302_dict[newurl] = newurl
460 return self.parent.open(new)
461
462 http_error_301 = http_error_302
463
464 inf_msg = "The HTTP server returned a redirect error that would" \
Thomas Wouters7e474022000-07-16 12:04:32 +0000465 "lead to an infinite loop.\n" \
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000466 "The last 302 error message was:\n"
467
468class ProxyHandler(BaseHandler):
469 def __init__(self, proxies=None):
Fred Drake13a2c272000-02-10 17:17:14 +0000470 if proxies is None:
471 proxies = getproxies()
472 assert hasattr(proxies, 'has_key'), "proxies must be a mapping"
473 self.proxies = proxies
474 for type, url in proxies.items():
Tim Peterse1190062001-01-15 03:34:38 +0000475 setattr(self, '%s_open' % type,
Fred Drake13a2c272000-02-10 17:17:14 +0000476 lambda r, proxy=url, type=type, meth=self.proxy_open: \
477 meth(r, proxy, type))
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000478
479 def proxy_open(self, req, proxy, type):
Fred Drake13a2c272000-02-10 17:17:14 +0000480 orig_type = req.get_type()
481 req.set_proxy(proxy)
482 if orig_type == type:
483 # let other handlers take care of it
484 # XXX this only makes sense if the proxy is before the
485 # other handlers
486 return None
487 else:
488 # need to start over, because the other handlers don't
489 # grok the proxy's URL type
490 return self.parent.open(req)
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000491
492# feature suggested by Duncan Booth
493# XXX custom is not a good name
494class CustomProxy:
495 # either pass a function to the constructor or override handle
496 def __init__(self, proto, func=None, proxy_addr=None):
Fred Drake13a2c272000-02-10 17:17:14 +0000497 self.proto = proto
498 self.func = func
499 self.addr = proxy_addr
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000500
501 def handle(self, req):
Fred Drake13a2c272000-02-10 17:17:14 +0000502 if self.func and self.func(req):
503 return 1
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000504
505 def get_proxy(self):
Fred Drake13a2c272000-02-10 17:17:14 +0000506 return self.addr
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000507
508class CustomProxyHandler(BaseHandler):
509 def __init__(self, *proxies):
Fred Drake13a2c272000-02-10 17:17:14 +0000510 self.proxies = {}
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000511
512 def proxy_open(self, req):
Fred Drake13a2c272000-02-10 17:17:14 +0000513 proto = req.get_type()
514 try:
515 proxies = self.proxies[proto]
516 except KeyError:
517 return None
518 for p in proxies:
519 if p.handle(req):
520 req.set_proxy(p.get_proxy())
521 return self.parent.open(req)
522 return None
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000523
524 def do_proxy(self, p, req):
Fred Drake13a2c272000-02-10 17:17:14 +0000525 p
526 return self.parent.open(req)
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000527
528 def add_proxy(self, cpo):
Fred Drake13a2c272000-02-10 17:17:14 +0000529 if self.proxies.has_key(cpo.proto):
530 self.proxies[cpo.proto].append(cpo)
531 else:
532 self.proxies[cpo.proto] = [cpo]
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000533
534class HTTPPasswordMgr:
535 def __init__(self):
Fred Drake13a2c272000-02-10 17:17:14 +0000536 self.passwd = {}
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000537
538 def add_password(self, realm, uri, user, passwd):
Fred Drake13a2c272000-02-10 17:17:14 +0000539 # uri could be a single URI or a sequence
540 if type(uri) == types.StringType:
541 uri = [uri]
542 uri = tuple(map(self.reduce_uri, uri))
543 if not self.passwd.has_key(realm):
544 self.passwd[realm] = {}
545 self.passwd[realm][uri] = (user, passwd)
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000546
547 def find_user_password(self, realm, authuri):
Fred Drake13a2c272000-02-10 17:17:14 +0000548 domains = self.passwd.get(realm, {})
549 authuri = self.reduce_uri(authuri)
550 for uris, authinfo in domains.items():
551 for uri in uris:
552 if self.is_suburi(uri, authuri):
553 return authinfo
554 return None, None
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000555
556 def reduce_uri(self, uri):
Fred Drake13a2c272000-02-10 17:17:14 +0000557 """Accept netloc or URI and extract only the netloc and path"""
558 parts = urlparse.urlparse(uri)
559 if parts[1]:
560 return parts[1], parts[2] or '/'
561 else:
562 return parts[2], '/'
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000563
564 def is_suburi(self, base, test):
Fred Drake13a2c272000-02-10 17:17:14 +0000565 """Check if test is below base in a URI tree
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000566
Fred Drake13a2c272000-02-10 17:17:14 +0000567 Both args must be URIs in reduced form.
568 """
569 if base == test:
570 return 1
571 if base[0] != test[0]:
572 return 0
573 common = os.path.commonprefix((base[1], test[1]))
574 if len(common) == len(base[1]):
575 return 1
576 return 0
Tim Peterse1190062001-01-15 03:34:38 +0000577
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000578
579class HTTPBasicAuthHandler(BaseHandler):
580 rx = re.compile('[ \t]*([^ \t]+)[ \t]+realm="([^"]*)"')
581
582 # XXX there can actually be multiple auth-schemes in a
583 # www-authenticate header. should probably be a lot more careful
584 # in parsing them to extract multiple alternatives
585
586 def __init__(self):
587 self.passwd = HTTPPasswordMgr()
Fred Drake13a2c272000-02-10 17:17:14 +0000588 self.add_password = self.passwd.add_password
589 self.__current_realm = None
590 # if __current_realm is not None, then the server must have
591 # refused our name/password and is asking for authorization
592 # again. must be careful to set it to None on successful
Tim Peterse1190062001-01-15 03:34:38 +0000593 # return.
594
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000595 def http_error_401(self, req, fp, code, msg, headers):
Fred Drake13a2c272000-02-10 17:17:14 +0000596 # XXX could be mult. headers
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000597 authreq = headers.get('www-authenticate', None)
598 if authreq:
599 mo = HTTPBasicAuthHandler.rx.match(authreq)
600 if mo:
601 scheme, realm = mo.groups()
602 if string.lower(scheme) == 'basic':
603 return self.retry_http_basic_auth(req, realm)
604
605 def retry_http_basic_auth(self, req, realm):
Fred Drake13a2c272000-02-10 17:17:14 +0000606 if self.__current_realm is None:
607 self.__current_realm = realm
608 else:
609 self.__current_realm = realm
610 return None
611 # XXX host isn't really the correct URI?
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000612 host = req.get_host()
613 user,pw = self.passwd.find_user_password(realm, host)
614 if pw:
Fred Drake13a2c272000-02-10 17:17:14 +0000615 raw = "%s:%s" % (user, pw)
616 auth = string.strip(base64.encodestring(raw))
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000617 req.add_header('Authorization', 'Basic %s' % auth)
618 resp = self.parent.open(req)
Fred Drake13a2c272000-02-10 17:17:14 +0000619 self.__current_realm = None
620 return resp
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000621 else:
Fred Drake13a2c272000-02-10 17:17:14 +0000622 self.__current_realm = None
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000623 return None
624
625class HTTPDigestAuthHandler(BaseHandler):
626 """An authentication protocol defined by RFC 2069
627
628 Digest authentication improves on basic authentication because it
629 does not transmit passwords in the clear.
630 """
631
632 def __init__(self):
Fred Drake13a2c272000-02-10 17:17:14 +0000633 self.passwd = HTTPPasswordMgr()
634 self.add_password = self.passwd.add_password
635 self.__current_realm = None
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000636
637 def http_error_401(self, req, fp, code, msg, headers):
Fred Drake13a2c272000-02-10 17:17:14 +0000638 # XXX could be mult. headers
639 authreq = headers.get('www-authenticate', None)
640 if authreq:
641 kind = string.split(authreq)[0]
642 if kind == 'Digest':
643 return self.retry_http_digest_auth(req, authreq)
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000644
645 def retry_http_digest_auth(self, req, auth):
Fred Drake13a2c272000-02-10 17:17:14 +0000646 token, challenge = string.split(auth, ' ', 1)
647 chal = parse_keqv_list(parse_http_list(challenge))
648 auth = self.get_authorization(req, chal)
649 if auth:
650 req.add_header('Authorization', 'Digest %s' % auth)
651 resp = self.parent.open(req)
652 self.__current_realm = None
653 return resp
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000654
655 def get_authorization(self, req, chal):
Fred Drake13a2c272000-02-10 17:17:14 +0000656 try:
657 realm = chal['realm']
658 nonce = chal['nonce']
659 algorithm = chal.get('algorithm', 'MD5')
660 # mod_digest doesn't send an opaque, even though it isn't
661 # supposed to be optional
662 opaque = chal.get('opaque', None)
663 except KeyError:
664 return None
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000665
Fred Drake13a2c272000-02-10 17:17:14 +0000666 if self.__current_realm is None:
667 self.__current_realm = realm
668 else:
669 self.__current_realm = realm
670 return None
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000671
Fred Drake13a2c272000-02-10 17:17:14 +0000672 H, KD = self.get_algorithm_impls(algorithm)
673 if H is None:
674 return None
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000675
Fred Drake13a2c272000-02-10 17:17:14 +0000676 user, pw = self.passwd.find_user_password(realm,
Tim Peterse1190062001-01-15 03:34:38 +0000677 req.get_full_url())
Fred Drake13a2c272000-02-10 17:17:14 +0000678 if user is None:
679 return None
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000680
Fred Drake13a2c272000-02-10 17:17:14 +0000681 # XXX not implemented yet
682 if req.has_data():
683 entdig = self.get_entity_digest(req.get_data(), chal)
684 else:
685 entdig = None
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000686
Fred Drake13a2c272000-02-10 17:17:14 +0000687 A1 = "%s:%s:%s" % (user, realm, pw)
688 A2 = "%s:%s" % (req.has_data() and 'POST' or 'GET',
689 # XXX selector: what about proxies and full urls
690 req.get_selector())
691 respdig = KD(H(A1), "%s:%s" % (nonce, H(A2)))
692 # XXX should the partial digests be encoded too?
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000693
Fred Drake13a2c272000-02-10 17:17:14 +0000694 base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \
695 'response="%s"' % (user, realm, nonce, req.get_selector(),
696 respdig)
697 if opaque:
698 base = base + ', opaque="%s"' % opaque
699 if entdig:
700 base = base + ', digest="%s"' % entdig
701 if algorithm != 'MD5':
702 base = base + ', algorithm="%s"' % algorithm
703 return base
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000704
705 def get_algorithm_impls(self, algorithm):
Fred Drake13a2c272000-02-10 17:17:14 +0000706 # lambdas assume digest modules are imported at the top level
707 if algorithm == 'MD5':
708 H = lambda x, e=encode_digest:e(md5.new(x).digest())
709 elif algorithm == 'SHA':
710 H = lambda x, e=encode_digest:e(sha.new(x).digest())
711 # XXX MD5-sess
712 KD = lambda s, d, H=H: H("%s:%s" % (s, d))
713 return H, KD
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000714
715 def get_entity_digest(self, data, chal):
Fred Drake13a2c272000-02-10 17:17:14 +0000716 # XXX not implemented yet
717 return None
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000718
719def encode_digest(digest):
720 hexrep = []
721 for c in digest:
Fred Drake13a2c272000-02-10 17:17:14 +0000722 n = (ord(c) >> 4) & 0xf
723 hexrep.append(hex(n)[-1])
724 n = ord(c) & 0xf
725 hexrep.append(hex(n)[-1])
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000726 return string.join(hexrep, '')
Tim Peterse1190062001-01-15 03:34:38 +0000727
728
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000729class HTTPHandler(BaseHandler):
730 def http_open(self, req):
731 # XXX devise a new mechanism to specify user/password
Fred Drake13a2c272000-02-10 17:17:14 +0000732 host = req.get_host()
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000733 if not host:
734 raise URLError('no host given')
735
Jeremy Hylton73574ee2000-10-12 18:54:18 +0000736 try:
737 h = httplib.HTTP(host) # will parse host:port
738 if req.has_data():
739 data = req.get_data()
740 h.putrequest('POST', req.get_selector())
741 h.putheader('Content-type',
742 'application/x-www-form-urlencoded')
743 h.putheader('Content-length', '%d' % len(data))
744 else:
745 h.putrequest('GET', req.get_selector())
746 except socket.error, err:
747 raise URLError(err)
Tim Peterse1190062001-01-15 03:34:38 +0000748
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000749 # XXX proxies would have different host here
750 h.putheader('Host', host)
751 for args in self.parent.addheaders:
Jeremy Hylton73574ee2000-10-12 18:54:18 +0000752 h.putheader(*args)
Fred Drake13a2c272000-02-10 17:17:14 +0000753 for k, v in req.headers.items():
754 h.putheader(k, v)
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000755 h.endheaders()
756 if req.has_data():
757 h.send(data + '\r\n')
758
759 code, msg, hdrs = h.getreply()
760 fp = h.getfile()
761 if code == 200:
762 return addinfourl(fp, hdrs, req.get_full_url())
763 else:
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000764 return self.parent.error('http', req, fp, code, msg, hdrs)
765
766class UnknownHandler(BaseHandler):
767 def unknown_open(self, req):
Fred Drake13a2c272000-02-10 17:17:14 +0000768 type = req.get_type()
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000769 raise URLError('unknown url type: %s' % type)
770
771def parse_keqv_list(l):
772 """Parse list of key=value strings where keys are not duplicated."""
773 parsed = {}
774 for elt in l:
Fred Drake13a2c272000-02-10 17:17:14 +0000775 k, v = string.split(elt, '=', 1)
776 if v[0] == '"' and v[-1] == '"':
777 v = v[1:-1]
778 parsed[k] = v
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000779 return parsed
780
781def parse_http_list(s):
782 """Parse lists as described by RFC 2068 Section 2.
783
784 In particular, parse comman-separated lists where the elements of
785 the list may include quoted-strings. A quoted-string could
786 contain a comma.
787 """
788 # XXX this function could probably use more testing
789
790 list = []
791 end = len(s)
792 i = 0
793 inquote = 0
794 start = 0
795 while i < end:
Fred Drake13a2c272000-02-10 17:17:14 +0000796 cur = s[i:]
797 c = string.find(cur, ',')
798 q = string.find(cur, '"')
799 if c == -1:
800 list.append(s[start:])
801 break
802 if q == -1:
803 if inquote:
804 raise ValueError, "unbalanced quotes"
805 else:
806 list.append(s[start:i+c])
807 i = i + c + 1
808 continue
809 if inquote:
810 if q < c:
811 list.append(s[start:i+c])
812 i = i + c + 1
813 start = i
814 inquote = 0
815 else:
Tim Peterse1190062001-01-15 03:34:38 +0000816 i = i + q
Fred Drake13a2c272000-02-10 17:17:14 +0000817 else:
818 if c < q:
819 list.append(s[start:i+c])
820 i = i + c + 1
821 start = i
822 else:
823 inquote = 1
824 i = i + q + 1
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000825 return map(string.strip, list)
826
827class FileHandler(BaseHandler):
828 # Use local file or FTP depending on form of URL
829 def file_open(self, req):
Fred Drake13a2c272000-02-10 17:17:14 +0000830 url = req.get_selector()
831 if url[:2] == '//' and url[2:3] != '/':
832 req.type = 'ftp'
833 return self.parent.open(req)
834 else:
835 return self.open_local_file(req)
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000836
837 # names for the localhost
838 names = None
839 def get_names(self):
Fred Drake13a2c272000-02-10 17:17:14 +0000840 if FileHandler.names is None:
Tim Peterse1190062001-01-15 03:34:38 +0000841 FileHandler.names = (socket.gethostbyname('localhost'),
Fred Drake13a2c272000-02-10 17:17:14 +0000842 socket.gethostbyname(socket.gethostname()))
843 return FileHandler.names
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000844
845 # not entirely sure what the rules are here
846 def open_local_file(self, req):
Fred Drake13a2c272000-02-10 17:17:14 +0000847 mtype = mimetypes.guess_type(req.get_selector())[0]
848 headers = mimetools.Message(StringIO('Content-Type: %s\n' \
849 % (mtype or 'text/plain')))
850 host = req.get_host()
851 file = req.get_selector()
852 if host:
853 host, port = splitport(host)
854 if not host or \
855 (not port and socket.gethostbyname(host) in self.get_names()):
856 return addinfourl(open(url2pathname(file), 'rb'),
857 headers, 'file:'+file)
858 raise URLError('file not on local host')
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000859
860class FTPHandler(BaseHandler):
861 def ftp_open(self, req):
Fred Drake13a2c272000-02-10 17:17:14 +0000862 host = req.get_host()
863 if not host:
864 raise IOError, ('ftp error', 'no host given')
865 # XXX handle custom username & password
Jeremy Hylton73574ee2000-10-12 18:54:18 +0000866 try:
867 host = socket.gethostbyname(host)
868 except socket.error, msg:
869 raise URLError(msg)
Fred Drake13a2c272000-02-10 17:17:14 +0000870 host, port = splitport(host)
871 if port is None:
872 port = ftplib.FTP_PORT
873 path, attrs = splitattr(req.get_selector())
874 path = unquote(path)
875 dirs = string.splitfields(path, '/')
876 dirs, file = dirs[:-1], dirs[-1]
877 if dirs and not dirs[0]:
878 dirs = dirs[1:]
879 user = passwd = '' # XXX
880 try:
881 fw = self.connect_ftp(user, passwd, host, port, dirs)
882 type = file and 'I' or 'D'
883 for attr in attrs:
884 attr, value = splitattr(attr)
885 if string.lower(attr) == 'type' and \
886 value in ('a', 'A', 'i', 'I', 'd', 'D'):
887 type = string.upper(value)
888 fp, retrlen = fw.retrfile(file, type)
889 if retrlen is not None and retrlen >= 0:
890 sf = StringIO('Content-Length: %d\n' % retrlen)
891 headers = mimetools.Message(sf)
892 else:
893 headers = noheaders()
894 return addinfourl(fp, headers, req.get_full_url())
895 except ftplib.all_errors, msg:
896 raise IOError, ('ftp error', msg), sys.exc_info()[2]
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000897
898 def connect_ftp(self, user, passwd, host, port, dirs):
899 fw = ftpwrapper(user, passwd, host, port, dirs)
900## fw.ftp.set_debuglevel(1)
901 return fw
902
903class CacheFTPHandler(FTPHandler):
904 # XXX would be nice to have pluggable cache strategies
905 # XXX this stuff is definitely not thread safe
906 def __init__(self):
907 self.cache = {}
908 self.timeout = {}
909 self.soonest = 0
910 self.delay = 60
Fred Drake13a2c272000-02-10 17:17:14 +0000911 self.max_conns = 16
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000912
913 def setTimeout(self, t):
914 self.delay = t
915
916 def setMaxConns(self, m):
Fred Drake13a2c272000-02-10 17:17:14 +0000917 self.max_conns = m
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000918
919 def connect_ftp(self, user, passwd, host, port, dirs):
920 key = user, passwd, host, port
921 if self.cache.has_key(key):
922 self.timeout[key] = time.time() + self.delay
923 else:
924 self.cache[key] = ftpwrapper(user, passwd, host, port, dirs)
925 self.timeout[key] = time.time() + self.delay
Fred Drake13a2c272000-02-10 17:17:14 +0000926 self.check_cache()
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000927 return self.cache[key]
928
929 def check_cache(self):
Fred Drake13a2c272000-02-10 17:17:14 +0000930 # first check for old ones
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000931 t = time.time()
932 if self.soonest <= t:
933 for k, v in self.timeout.items():
934 if v < t:
935 self.cache[k].close()
936 del self.cache[k]
937 del self.timeout[k]
938 self.soonest = min(self.timeout.values())
939
940 # then check the size
Fred Drake13a2c272000-02-10 17:17:14 +0000941 if len(self.cache) == self.max_conns:
942 for k, v in self.timeout.items():
943 if v == self.soonest:
944 del self.cache[k]
945 del self.timeout[k]
946 break
947 self.soonest = min(self.timeout.values())
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000948
949class GopherHandler(BaseHandler):
950 def gopher_open(self, req):
Fred Drake13a2c272000-02-10 17:17:14 +0000951 host = req.get_host()
952 if not host:
953 raise GopherError('no host given')
954 host = unquote(host)
955 selector = req.get_selector()
956 type, selector = splitgophertype(selector)
957 selector, query = splitquery(selector)
958 selector = unquote(selector)
959 if query:
960 query = unquote(query)
961 fp = gopherlib.send_query(selector, query, host)
962 else:
963 fp = gopherlib.send_selector(selector, host)
964 return addinfourl(fp, noheaders(), req.get_full_url())
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000965
966#bleck! don't use this yet
967class OpenerFactory:
968
969 default_handlers = [UnknownHandler, HTTPHandler,
Tim Peterse1190062001-01-15 03:34:38 +0000970 HTTPDefaultErrorHandler, HTTPRedirectHandler,
Fred Drake13a2c272000-02-10 17:17:14 +0000971 FTPHandler, FileHandler]
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000972 proxy_handlers = [ProxyHandler]
973 handlers = []
974 replacement_handlers = []
975
976 def add_proxy_handler(self, ph):
Fred Drake13a2c272000-02-10 17:17:14 +0000977 self.proxy_handlers = self.proxy_handlers + [ph]
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000978
979 def add_handler(self, h):
Fred Drake13a2c272000-02-10 17:17:14 +0000980 self.handlers = self.handlers + [h]
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000981
982 def replace_handler(self, h):
Fred Drake13a2c272000-02-10 17:17:14 +0000983 pass
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000984
985 def build_opener(self):
Fred Drake13a2c272000-02-10 17:17:14 +0000986 opener = OpenerDirectory()
987 for ph in self.proxy_handlers:
988 if type(ph) == types.ClassType:
989 ph = ph()
990 opener.add_handler(ph)
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000991
992if __name__ == "__main__":
Tim Peterse1190062001-01-15 03:34:38 +0000993 # XXX some of the test code depends on machine configurations that
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000994 # are internal to CNRI. Need to set up a public server with the
995 # right authentication configuration for test purposes.
996 if socket.gethostname() == 'bitdiddle':
997 localhost = 'bitdiddle.cnri.reston.va.us'
Jeremy Hylton73574ee2000-10-12 18:54:18 +0000998 elif socket.gethostname() == 'bitdiddle.concentric.net':
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +0000999 localhost = 'localhost'
1000 else:
1001 localhost = None
1002 urls = [
Fred Drake13a2c272000-02-10 17:17:14 +00001003 # Thanks to Fred for finding these!
1004 'gopher://gopher.lib.ncsu.edu/11/library/stacks/Alex',
1005 'gopher://gopher.vt.edu:10010/10/33',
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +00001006
Fred Drake13a2c272000-02-10 17:17:14 +00001007 'file:/etc/passwd',
1008 'file://nonsensename/etc/passwd',
1009 'ftp://www.python.org/pub/tmp/httplib.py',
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +00001010 'ftp://www.python.org/pub/tmp/imageop.c',
1011 'ftp://www.python.org/pub/tmp/blat',
Fred Drake13a2c272000-02-10 17:17:14 +00001012 'http://www.espn.com/', # redirect
1013 'http://www.python.org/Spanish/Inquistion/',
1014 ('http://grail.cnri.reston.va.us/cgi-bin/faqw.py',
1015 'query=pythonistas&querytype=simple&casefold=yes&req=search'),
1016 'http://www.python.org/',
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +00001017 'ftp://prep.ai.mit.edu/welcome.msg',
1018 'ftp://www.python.org/pub/tmp/figure.prn',
1019 'ftp://www.python.org/pub/tmp/interp.pl',
Fred Drake13a2c272000-02-10 17:17:14 +00001020 'http://checkproxy.cnri.reston.va.us/test/test.html',
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +00001021 ]
1022
1023 if localhost is not None:
1024 urls = urls + [
1025 'file://%s/etc/passwd' % localhost,
1026 'http://%s/simple/' % localhost,
1027 'http://%s/digest/' % localhost,
1028 'http://%s/not/found.h' % localhost,
1029 ]
1030
1031 bauth = HTTPBasicAuthHandler()
1032 bauth.add_password('basic_test_realm', localhost, 'jhylton',
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +00001033 'password')
Tim Peterse1190062001-01-15 03:34:38 +00001034 dauth = HTTPDigestAuthHandler()
1035 dauth.add_password('digest_test_realm', localhost, 'jhylton',
1036 'password')
1037
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +00001038
1039 cfh = CacheFTPHandler()
1040 cfh.setTimeout(1)
1041
1042 # XXX try out some custom proxy objects too!
1043 def at_cnri(req):
Fred Drake13a2c272000-02-10 17:17:14 +00001044 host = req.get_host()
1045 print host
1046 if host[-18:] == '.cnri.reston.va.us':
1047 return 1
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +00001048 p = CustomProxy('http', at_cnri, 'proxy.cnri.reston.va.us')
1049 ph = CustomProxyHandler(p)
1050
1051 install_opener(build_opener(dauth, bauth, cfh, GopherHandler, ph))
1052
1053 for url in urls:
1054 if type(url) == types.TupleType:
1055 url, req = url
1056 else:
1057 req = None
1058 print url
1059 try:
1060 f = urlopen(url, req)
1061 except IOError, err:
Fred Drake13a2c272000-02-10 17:17:14 +00001062 print "IOError:", err
1063 except socket.error, err:
1064 print "socket.error:", err
Jeremy Hylton6d7e47b2000-01-20 18:19:08 +00001065 else:
1066 buf = f.read()
1067 f.close()
1068 print "read %d bytes" % len(buf)
1069 print
1070 time.sleep(0.1)