urllib2.py 37.8 KB
Newer Older
1
"""An extensible library for opening URLs using a variety of protocols
Jeremy Hylton's avatar
Jeremy Hylton committed
2 3

The simplest way to use this module is to call the urlopen function,
4
which accepts a string containing a URL or a Request object (described
Jeremy Hylton's avatar
Jeremy Hylton committed
5 6 7
below).  It opens the URL and returns the results as file-like
object; the returned object has some extra methods described below.

Jeremy Hylton's avatar
Jeremy Hylton committed
8
The OpenerDirector manages a collection of Handler objects that do
9
all the actual work.  Each Handler implements a particular protocol or
Jeremy Hylton's avatar
Jeremy Hylton committed
10 11 12 13
option.  The OpenerDirector is a composite object that invokes the
Handlers needed to open the requested URL.  For example, the
HTTPHandler performs HTTP GET and POST requests and deals with
non-error returns.  The HTTPRedirectHandler automatically deals with
14 15
HTTP 301, 302, 303 and 307 redirect errors, and the HTTPDigestAuthHandler
deals with digest authentication.
Jeremy Hylton's avatar
Jeremy Hylton committed
16 17 18

urlopen(url, data=None) -- basic usage is that same as original
urllib.  pass the url and optionally data to post to an HTTP URL, and
19
get a file-like object back.  One difference is that you can also pass
Jeremy Hylton's avatar
Jeremy Hylton committed
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
a Request instance instead of URL.  Raises a URLError (subclass of
IOError); for HTTP errors, raises an HTTPError, which can also be
treated as a valid response.

build_opener -- function that creates a new OpenerDirector instance.
will install the default handlers.  accepts one or more Handlers as
arguments, either instances or Handler classes that it will
instantiate.  if one of the argument is a subclass of the default
handler, the argument will be installed instead of the default.

install_opener -- installs a new opener as the default opener.

objects of interest:
OpenerDirector --

Request -- an object that encapsulates the state of a request.  the
state can be a simple as the URL.  it can also include extra HTTP
headers, e.g. a User-Agent.

BaseHandler --

exceptions:
URLError-- a subclass of IOError, individual protocols have their own
specific subclass

45
HTTPError-- also a valid HTTP response, so you can treat an HTTP error
Jeremy Hylton's avatar
Jeremy Hylton committed
46 47 48 49 50 51 52 53 54 55 56 57 58 59
as an exceptional event or valid response

internals:
BaseHandler and parent
_call_chain conventions

Example usage:

import urllib2

# set up authentication info
authinfo = urllib2.HTTPBasicAuthHandler()
authinfo.add_password('realm', 'host', 'username', 'password')

60 61
proxy_support = urllib2.ProxyHandler({"http" : "http://ahad-haam:3128"})

62
# build a new opener that adds authentication and caching FTP handlers
63
opener = urllib2.build_opener(proxy_support, authinfo, urllib2.CacheFTPHandler)
Jeremy Hylton's avatar
Jeremy Hylton committed
64 65 66 67 68 69 70 71 72 73 74

# install it
urllib2.install_opener(opener)

f = urllib2.urlopen('http://www.python.org/')


"""

# XXX issues:
# If an authentication error handler that tries to perform
75 76 77 78 79
# authentication for some reason but fails, how should the error be
# signalled?  The client needs to know the HTTP error code.  But if
# the handler knows that the problem was, e.g., that it didn't know
# that hash algo that requested in the challenge, it would be good to
# pass that information along to the client, too.
Jeremy Hylton's avatar
Jeremy Hylton committed
80 81 82 83 84 85 86 87 88 89 90 91

# XXX to do:
# name!
# documentation (getting there)
# complex proxies
# abstract factory for opener
# ftp errors aren't handled cleanly
# gopher can return a socket.error
# check digest against correct (i.e. non-apache) implementation

import socket
import httplib
92
import inspect
Jeremy Hylton's avatar
Jeremy Hylton committed
93 94 95 96 97 98
import re
import base64
import urlparse
import md5
import mimetypes
import mimetools
99
import rfc822
Jeremy Hylton's avatar
Jeremy Hylton committed
100 101 102
import ftplib
import sys
import time
103
import os
Jeremy Hylton's avatar
Jeremy Hylton committed
104
import gopherlib
105
import posixpath
Jeremy Hylton's avatar
Jeremy Hylton committed
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126

try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO

try:
    import sha
except ImportError:
    # need 1.5.2 final
    sha = None

# not sure how many of these need to be gotten rid of
from urllib import unwrap, unquote, splittype, splithost, \
     addinfourl, splitport, splitgophertype, splitquery, \
     splitattr, ftpwrapper, noheaders

# support for proxies via environment variables
from urllib import getproxies

# support for FileHandler
127
from urllib import localhost, url2pathname
Jeremy Hylton's avatar
Jeremy Hylton committed
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142

__version__ = "2.0a1"

_opener = None
def urlopen(url, data=None):
    global _opener
    if _opener is None:
        _opener = build_opener()
    return _opener.open(url, data)

def install_opener(opener):
    global _opener
    _opener = opener

# do these error classes make sense?
143
# make sure all of the IOError stuff is overridden.  we just want to be
144
# subtypes.
Jeremy Hylton's avatar
Jeremy Hylton committed
145 146 147 148 149

class URLError(IOError):
    # URLError is a sub-type of IOError, but it doesn't share any of
    # the implementation.  need to override __init__ and __str__
    def __init__(self, reason):
Fred Drake's avatar
Fred Drake committed
150
        self.reason = reason
Jeremy Hylton's avatar
Jeremy Hylton committed
151 152

    def __str__(self):
Fred Drake's avatar
Fred Drake committed
153
        return '<urlopen error %s>' % self.reason
Jeremy Hylton's avatar
Jeremy Hylton committed
154 155 156

class HTTPError(URLError, addinfourl):
    """Raised when HTTP error occurs, but also acts like non-error return"""
Jeremy Hylton's avatar
Jeremy Hylton committed
157
    __super_init = addinfourl.__init__
Jeremy Hylton's avatar
Jeremy Hylton committed
158 159

    def __init__(self, url, code, msg, hdrs, fp):
Fred Drake's avatar
Fred Drake committed
160 161 162 163 164
        self.code = code
        self.msg = msg
        self.hdrs = hdrs
        self.fp = fp
        self.filename = url
165 166 167
        # The addinfourl classes depend on fp being a valid file
        # object.  In some cases, the HTTPError may not have a valid
        # file object.  If this happens, the simplest workaround is to
Tim Peters's avatar
Tim Peters committed
168
        # not initialize the base classes.
169 170
        if fp is not None:
            self.__super_init(fp, hdrs, url)
171

Jeremy Hylton's avatar
Jeremy Hylton committed
172
    def __str__(self):
Fred Drake's avatar
Fred Drake committed
173
        return 'HTTP Error %s: %s' % (self.code, self.msg)
Jeremy Hylton's avatar
Jeremy Hylton committed
174 175

    def __del__(self):
Fred Drake's avatar
Fred Drake committed
176 177
        # XXX is this safe? what if user catches exception, then
        # extracts fp and discards exception?
Jeremy Hylton's avatar
Jeremy Hylton committed
178 179
        if self.fp:
            self.fp.close()
Jeremy Hylton's avatar
Jeremy Hylton committed
180 181 182 183

class GopherError(URLError):
    pass

184

Jeremy Hylton's avatar
Jeremy Hylton committed
185
class Request:
186

Jeremy Hylton's avatar
Jeremy Hylton committed
187
    def __init__(self, url, data=None, headers={}):
Fred Drake's avatar
Fred Drake committed
188 189 190 191 192 193
        # unwrap('<URL:type://host/path>') --> 'type://host/path'
        self.__original = unwrap(url)
        self.type = None
        # self.__r_type is what's left after doing the splittype
        self.host = None
        self.port = None
Jeremy Hylton's avatar
Jeremy Hylton committed
194
        self.data = data
Fred Drake's avatar
Fred Drake committed
195
        self.headers = {}
196
        for key, value in headers.items():
197
            self.add_header(key, value)
Jeremy Hylton's avatar
Jeremy Hylton committed
198 199

    def __getattr__(self, attr):
Fred Drake's avatar
Fred Drake committed
200
        # XXX this is a fallback mechanism to guard against these
201
        # methods getting called in a non-standard order.  this may be
Fred Drake's avatar
Fred Drake committed
202 203 204 205 206 207 208 209
        # too complicated and/or unnecessary.
        # XXX should the __r_XXX attributes be public?
        if attr[:12] == '_Request__r_':
            name = attr[12:]
            if hasattr(Request, 'get_' + name):
                getattr(self, 'get_' + name)()
                return getattr(self, attr)
        raise AttributeError, attr
Jeremy Hylton's avatar
Jeremy Hylton committed
210

211 212 213 214 215 216
    def get_method(self):
        if self.has_data():
            return "POST"
        else:
            return "GET"

Jeremy Hylton's avatar
Jeremy Hylton committed
217 218 219 220 221 222 223 224 225 226 227 228 229
    def add_data(self, data):
        self.data = data

    def has_data(self):
        return self.data is not None

    def get_data(self):
        return self.data

    def get_full_url(self):
        return self.__original

    def get_type(self):
Fred Drake's avatar
Fred Drake committed
230 231
        if self.type is None:
            self.type, self.__r_type = splittype(self.__original)
232 233
            if self.type is None:
                raise ValueError, "unknown url type: %s" % self.__original
Fred Drake's avatar
Fred Drake committed
234
        return self.type
Jeremy Hylton's avatar
Jeremy Hylton committed
235 236

    def get_host(self):
Fred Drake's avatar
Fred Drake committed
237 238 239 240 241
        if self.host is None:
            self.host, self.__r_host = splithost(self.__r_type)
            if self.host:
                self.host = unquote(self.host)
        return self.host
Jeremy Hylton's avatar
Jeremy Hylton committed
242 243

    def get_selector(self):
Fred Drake's avatar
Fred Drake committed
244
        return self.__r_host
Jeremy Hylton's avatar
Jeremy Hylton committed
245

246 247
    def set_proxy(self, host, type):
        self.host, self.type = host, type
Fred Drake's avatar
Fred Drake committed
248
        self.__r_host = self.__original
Jeremy Hylton's avatar
Jeremy Hylton committed
249 250

    def add_header(self, key, val):
Fred Drake's avatar
Fred Drake committed
251
        # useful for something like authentication
252
        self.headers[key.capitalize()] = val
Jeremy Hylton's avatar
Jeremy Hylton committed
253 254 255 256

class OpenerDirector:
    def __init__(self):
        server_version = "Python-urllib/%s" % __version__
257
        self.addheaders = [('User-Agent', server_version)]
Jeremy Hylton's avatar
Jeremy Hylton committed
258 259 260 261 262 263 264
        # manage the individual handlers
        self.handlers = []
        self.handle_open = {}
        self.handle_error = {}

    def add_handler(self, handler):
        added = 0
265
        for meth in dir(handler):
Jeremy Hylton's avatar
Jeremy Hylton committed
266 267
            if meth[-5:] == '_open':
                protocol = meth[:-5]
268
                if protocol in self.handle_open:
Jeremy Hylton's avatar
Jeremy Hylton committed
269
                    self.handle_open[protocol].append(handler)
270
                    self.handle_open[protocol].sort()
Jeremy Hylton's avatar
Jeremy Hylton committed
271 272 273 274
                else:
                    self.handle_open[protocol] = [handler]
                added = 1
                continue
Eric S. Raymond's avatar
Eric S. Raymond committed
275 276
            i = meth.find('_')
            j = meth[i+1:].find('_') + i + 1
Jeremy Hylton's avatar
Jeremy Hylton committed
277 278 279 280
            if j != -1 and meth[i+1:j] == 'error':
                proto = meth[:i]
                kind = meth[j+1:]
                try:
Eric S. Raymond's avatar
Eric S. Raymond committed
281
                    kind = int(kind)
Jeremy Hylton's avatar
Jeremy Hylton committed
282 283 284
                except ValueError:
                    pass
                dict = self.handle_error.get(proto, {})
285
                if kind in dict:
Jeremy Hylton's avatar
Jeremy Hylton committed
286
                    dict[kind].append(handler)
287
                    dict[kind].sort()
Jeremy Hylton's avatar
Jeremy Hylton committed
288 289 290 291 292 293 294
                else:
                    dict[kind] = [handler]
                self.handle_error[proto] = dict
                added = 1
                continue
        if added:
            self.handlers.append(handler)
295
            self.handlers.sort()
Jeremy Hylton's avatar
Jeremy Hylton committed
296
            handler.add_parent(self)
297

Jeremy Hylton's avatar
Jeremy Hylton committed
298 299 300 301 302 303 304 305 306 307 308 309 310 311
    def __del__(self):
        self.close()

    def close(self):
        for handler in self.handlers:
            handler.close()
        self.handlers = []

    def _call_chain(self, chain, kind, meth_name, *args):
        # XXX raise an exception if no one else should try to handle
        # this url.  return None if you can't but someone else could.
        handlers = chain.get(kind, ())
        for handler in handlers:
            func = getattr(handler, meth_name)
Jeremy Hylton's avatar
Jeremy Hylton committed
312 313

            result = func(*args)
Jeremy Hylton's avatar
Jeremy Hylton committed
314 315 316 317
            if result is not None:
                return result

    def open(self, fullurl, data=None):
Fred Drake's avatar
Fred Drake committed
318
        # accept a URL or a Request object
319
        if isinstance(fullurl, basestring):
Fred Drake's avatar
Fred Drake committed
320
            req = Request(fullurl, data)
Jeremy Hylton's avatar
Jeremy Hylton committed
321 322 323 324
        else:
            req = fullurl
            if data is not None:
                req.add_data(data)
325

Jeremy Hylton's avatar
Jeremy Hylton committed
326
        result = self._call_chain(self.handle_open, 'default',
327
                                  'default_open', req)
Jeremy Hylton's avatar
Jeremy Hylton committed
328 329 330
        if result:
            return result

Fred Drake's avatar
Fred Drake committed
331
        type_ = req.get_type()
Jeremy Hylton's avatar
Jeremy Hylton committed
332
        result = self._call_chain(self.handle_open, type_, type_ + \
Jeremy Hylton's avatar
Jeremy Hylton committed
333
                                  '_open', req)
Jeremy Hylton's avatar
Jeremy Hylton committed
334 335 336 337 338 339 340
        if result:
            return result

        return self._call_chain(self.handle_open, 'unknown',
                                'unknown_open', req)

    def error(self, proto, *args):
341
        if proto in ['http', 'https']:
342 343
            # XXX http[s] protocols are special-cased
            dict = self.handle_error['http'] # https is not different than http
Jeremy Hylton's avatar
Jeremy Hylton committed
344 345 346 347 348 349 350 351 352
            proto = args[2]  # YUCK!
            meth_name = 'http_error_%d' % proto
            http_err = 1
            orig_args = args
        else:
            dict = self.handle_error
            meth_name = proto + '_error'
            http_err = 0
        args = (dict, proto, meth_name) + args
Jeremy Hylton's avatar
Jeremy Hylton committed
353
        result = self._call_chain(*args)
Jeremy Hylton's avatar
Jeremy Hylton committed
354 355 356 357 358
        if result:
            return result

        if http_err:
            args = (dict, 'default', 'http_error_default') + orig_args
Jeremy Hylton's avatar
Jeremy Hylton committed
359
            return self._call_chain(*args)
Jeremy Hylton's avatar
Jeremy Hylton committed
360

361 362 363
# XXX probably also want an abstract factory that knows when it makes
# sense to skip a superclass in favor of a subclass and when it might
# make sense to include both
Jeremy Hylton's avatar
Jeremy Hylton committed
364 365 366 367 368

def build_opener(*handlers):
    """Create an opener object from a list of handlers.

    The opener will use several default handlers, including support
369
    for HTTP and FTP.
Jeremy Hylton's avatar
Jeremy Hylton committed
370 371 372 373

    If any of the handlers passed as arguments are subclasses of the
    default handlers, the default handlers will not be used.
    """
374

Jeremy Hylton's avatar
Jeremy Hylton committed
375 376 377 378
    opener = OpenerDirector()
    default_classes = [ProxyHandler, UnknownHandler, HTTPHandler,
                       HTTPDefaultErrorHandler, HTTPRedirectHandler,
                       FTPHandler, FileHandler]
379 380
    if hasattr(httplib, 'HTTPS'):
        default_classes.append(HTTPSHandler)
Jeremy Hylton's avatar
Jeremy Hylton committed
381 382 383
    skip = []
    for klass in default_classes:
        for check in handlers:
384
            if inspect.isclass(check):
Jeremy Hylton's avatar
Jeremy Hylton committed
385 386
                if issubclass(check, klass):
                    skip.append(klass)
387 388
            elif isinstance(check, klass):
                skip.append(klass)
Jeremy Hylton's avatar
Jeremy Hylton committed
389 390 391 392 393 394 395
    for klass in skip:
        default_classes.remove(klass)

    for klass in default_classes:
        opener.add_handler(klass())

    for h in handlers:
396
        if inspect.isclass(h):
Jeremy Hylton's avatar
Jeremy Hylton committed
397 398 399 400 401
            h = h()
        opener.add_handler(h)
    return opener

class BaseHandler:
402 403
    handler_order = 500

Jeremy Hylton's avatar
Jeremy Hylton committed
404 405 406 407
    def add_parent(self, parent):
        self.parent = parent
    def close(self):
        self.parent = None
408 409 410 411 412 413 414 415
    def __lt__(self, other):
        if not hasattr(other, "handler_order"):
            # Try to preserve the old behavior of having custom classes
            # inserted after default ones (works only for custom user
            # classes which are not aware of handler_order).
            return True
        return self.handler_order < other.handler_order
            
Jeremy Hylton's avatar
Jeremy Hylton committed
416 417 418

class HTTPDefaultErrorHandler(BaseHandler):
    def http_error_default(self, req, fp, code, msg, hdrs):
Fred Drake's avatar
Fred Drake committed
419
        raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
Jeremy Hylton's avatar
Jeremy Hylton committed
420 421

class HTTPRedirectHandler(BaseHandler):
422
    def redirect_request(self, req, fp, code, msg, headers, newurl):
423 424 425 426 427 428 429 430
        """Return a Request or None in response to a redirect.

        This is called by the http_error_30x methods when a redirection
        response is received.  If a redirection should take place, return a new
        Request to allow http_error_30x to perform the redirect.  Otherwise,
        raise HTTPError if no-one else should try to handle this url.  Return
        None if you can't but another Handler might.
        """
431 432 433 434 435 436 437 438
        m = req.get_method()
        if (code in (301, 302, 303, 307) and m in ("GET", "HEAD")
            or code in (302, 303) and m == "POST"):
            # Strictly (according to RFC 2616), 302 in response to a
            # POST MUST NOT cause a redirection without confirmation
            # from the user (of urllib2, in this case).  In practice,
            # essentially all clients do redirect in this case, so we
            # do the same.
439 440 441 442
            return Request(newurl, headers=req.headers)
        else:
            raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)

Jeremy Hylton's avatar
Jeremy Hylton committed
443 444 445 446 447
    # Implementation note: To avoid the server sending us into an
    # infinite loop, the request object needs to track what URLs we
    # have already seen.  Do this by adding a handler-specific
    # attribute to the Request object.
    def http_error_302(self, req, fp, code, msg, headers):
448
        if 'location' in headers:
Jeremy Hylton's avatar
Jeremy Hylton committed
449
            newurl = headers['location']
450
        elif 'uri' in headers:
Jeremy Hylton's avatar
Jeremy Hylton committed
451 452 453
            newurl = headers['uri']
        else:
            return
Jeremy Hylton's avatar
Jeremy Hylton committed
454 455
        newurl = urlparse.urljoin(req.get_full_url(), newurl)

Jeremy Hylton's avatar
Jeremy Hylton committed
456 457 458
        # XXX Probably want to forget about the state of the current
        # request, although that might interact poorly with other
        # handlers that also use handler-specific request attributes
459
        new = self.redirect_request(req, fp, code, msg, headers, newurl)
460 461 462 463
        if new is None:
            return

        # loop detection
Jeremy Hylton's avatar
Jeremy Hylton committed
464 465
        new.error_302_dict = {}
        if hasattr(req, 'error_302_dict'):
466
            if len(req.error_302_dict)>10 or \
467
               newurl in req.error_302_dict:
Jeremy Hylton's avatar
Jeremy Hylton committed
468
                raise HTTPError(req.get_full_url(), code,
469
                                self.inf_msg + msg, headers, fp)
Jeremy Hylton's avatar
Jeremy Hylton committed
470 471
            new.error_302_dict.update(req.error_302_dict)
        new.error_302_dict[newurl] = newurl
472 473

        # Don't close the fp until we are sure that we won't use it
Tim Peters's avatar
Tim Peters committed
474
        # with HTTPError.
475 476 477
        fp.read()
        fp.close()

Jeremy Hylton's avatar
Jeremy Hylton committed
478 479
        return self.parent.open(new)

480
    http_error_301 = http_error_303 = http_error_307 = http_error_302
Jeremy Hylton's avatar
Jeremy Hylton committed
481 482

    inf_msg = "The HTTP server returned a redirect error that would" \
483
              "lead to an infinite loop.\n" \
Jeremy Hylton's avatar
Jeremy Hylton committed
484 485 486
              "The last 302 error message was:\n"

class ProxyHandler(BaseHandler):
487 488 489
    # Proxies must be in front
    handler_order = 100

Jeremy Hylton's avatar
Jeremy Hylton committed
490
    def __init__(self, proxies=None):
Fred Drake's avatar
Fred Drake committed
491 492 493 494
        if proxies is None:
            proxies = getproxies()
        assert hasattr(proxies, 'has_key'), "proxies must be a mapping"
        self.proxies = proxies
495
        for type, url in proxies.items():
496
            setattr(self, '%s_open' % type,
Fred Drake's avatar
Fred Drake committed
497 498
                    lambda r, proxy=url, type=type, meth=self.proxy_open: \
                    meth(r, proxy, type))
Jeremy Hylton's avatar
Jeremy Hylton committed
499 500

    def proxy_open(self, req, proxy, type):
Fred Drake's avatar
Fred Drake committed
501
        orig_type = req.get_type()
502 503 504 505
        type, r_type = splittype(proxy)
        host, XXX = splithost(r_type)
        if '@' in host:
            user_pass, host = host.split('@', 1)
506 507
            if ':' in user_pass:
                user, password = user_pass.split(':', 1)
Tim Peters's avatar
Tim Peters committed
508
                user_pass = base64.encodestring('%s:%s' % (unquote(user),
509 510
                                                           unquote(password)))
                req.add_header('Proxy-Authorization', 'Basic ' + user_pass)
511 512
        host = unquote(host)
        req.set_proxy(host, type)
Fred Drake's avatar
Fred Drake committed
513 514 515 516 517 518 519 520 521
        if orig_type == type:
            # let other handlers take care of it
            # XXX this only makes sense if the proxy is before the
            # other handlers
            return None
        else:
            # need to start over, because the other handlers don't
            # grok the proxy's URL type
            return self.parent.open(req)
Jeremy Hylton's avatar
Jeremy Hylton committed
522 523 524 525 526 527

# feature suggested by Duncan Booth
# XXX custom is not a good name
class CustomProxy:
    # either pass a function to the constructor or override handle
    def __init__(self, proto, func=None, proxy_addr=None):
Fred Drake's avatar
Fred Drake committed
528 529 530
        self.proto = proto
        self.func = func
        self.addr = proxy_addr
Jeremy Hylton's avatar
Jeremy Hylton committed
531 532

    def handle(self, req):
Fred Drake's avatar
Fred Drake committed
533 534
        if self.func and self.func(req):
            return 1
Jeremy Hylton's avatar
Jeremy Hylton committed
535 536

    def get_proxy(self):
Fred Drake's avatar
Fred Drake committed
537
        return self.addr
Jeremy Hylton's avatar
Jeremy Hylton committed
538 539

class CustomProxyHandler(BaseHandler):
540 541 542
    # Proxies must be in front
    handler_order = 100

Jeremy Hylton's avatar
Jeremy Hylton committed
543
    def __init__(self, *proxies):
Fred Drake's avatar
Fred Drake committed
544
        self.proxies = {}
Jeremy Hylton's avatar
Jeremy Hylton committed
545 546

    def proxy_open(self, req):
Fred Drake's avatar
Fred Drake committed
547 548 549 550 551 552 553 554 555 556
        proto = req.get_type()
        try:
            proxies = self.proxies[proto]
        except KeyError:
            return None
        for p in proxies:
            if p.handle(req):
                req.set_proxy(p.get_proxy())
                return self.parent.open(req)
        return None
Jeremy Hylton's avatar
Jeremy Hylton committed
557 558

    def do_proxy(self, p, req):
Fred Drake's avatar
Fred Drake committed
559
        return self.parent.open(req)
Jeremy Hylton's avatar
Jeremy Hylton committed
560 561

    def add_proxy(self, cpo):
562
        if cpo.proto in self.proxies:
Fred Drake's avatar
Fred Drake committed
563 564 565
            self.proxies[cpo.proto].append(cpo)
        else:
            self.proxies[cpo.proto] = [cpo]
Jeremy Hylton's avatar
Jeremy Hylton committed
566 567 568

class HTTPPasswordMgr:
    def __init__(self):
Fred Drake's avatar
Fred Drake committed
569
        self.passwd = {}
Jeremy Hylton's avatar
Jeremy Hylton committed
570 571

    def add_password(self, realm, uri, user, passwd):
Fred Drake's avatar
Fred Drake committed
572
        # uri could be a single URI or a sequence
573
        if isinstance(uri, basestring):
Fred Drake's avatar
Fred Drake committed
574 575
            uri = [uri]
        uri = tuple(map(self.reduce_uri, uri))
576
        if not realm in self.passwd:
Fred Drake's avatar
Fred Drake committed
577 578
            self.passwd[realm] = {}
        self.passwd[realm][uri] = (user, passwd)
Jeremy Hylton's avatar
Jeremy Hylton committed
579 580

    def find_user_password(self, realm, authuri):
Fred Drake's avatar
Fred Drake committed
581 582
        domains = self.passwd.get(realm, {})
        authuri = self.reduce_uri(authuri)
583
        for uris, authinfo in domains.iteritems():
Fred Drake's avatar
Fred Drake committed
584 585 586 587
            for uri in uris:
                if self.is_suburi(uri, authuri):
                    return authinfo
        return None, None
Jeremy Hylton's avatar
Jeremy Hylton committed
588 589

    def reduce_uri(self, uri):
Fred Drake's avatar
Fred Drake committed
590 591 592 593 594 595
        """Accept netloc or URI and extract only the netloc and path"""
        parts = urlparse.urlparse(uri)
        if parts[1]:
            return parts[1], parts[2] or '/'
        else:
            return parts[2], '/'
Jeremy Hylton's avatar
Jeremy Hylton committed
596 597

    def is_suburi(self, base, test):
Fred Drake's avatar
Fred Drake committed
598 599 600 601 602
        """Check if test is below base in a URI tree

        Both args must be URIs in reduced form.
        """
        if base == test:
603
            return True
Fred Drake's avatar
Fred Drake committed
604
        if base[0] != test[0]:
605
            return False
606
        common = posixpath.commonprefix((base[1], test[1]))
Fred Drake's avatar
Fred Drake committed
607
        if len(common) == len(base[1]):
608 609
            return True
        return False
610

Jeremy Hylton's avatar
Jeremy Hylton committed
611

612 613 614 615 616 617 618 619 620 621 622
class HTTPPasswordMgrWithDefaultRealm(HTTPPasswordMgr):

    def find_user_password(self, realm, authuri):
        user, password = HTTPPasswordMgr.find_user_password(self,realm,authuri)
        if user is not None:
            return user, password
        return HTTPPasswordMgr.find_user_password(self, None, authuri)


class AbstractBasicAuthHandler:

623
    rx = re.compile('[ \t]*([^ \t]+)[ \t]+realm="([^"]*)"', re.I)
Jeremy Hylton's avatar
Jeremy Hylton committed
624 625 626 627 628

    # XXX there can actually be multiple auth-schemes in a
    # www-authenticate header.  should probably be a lot more careful
    # in parsing them to extract multiple alternatives

629 630 631 632
    def __init__(self, password_mgr=None):
        if password_mgr is None:
            password_mgr = HTTPPasswordMgr()
        self.passwd = password_mgr
Fred Drake's avatar
Fred Drake committed
633
        self.add_password = self.passwd.add_password
634

635 636 637
    def http_error_auth_reqed(self, authreq, host, req, headers):
        # XXX could be multiple headers
        authreq = headers.get(authreq, None)
Jeremy Hylton's avatar
Jeremy Hylton committed
638
        if authreq:
639
            mo = AbstractBasicAuthHandler.rx.match(authreq)
Jeremy Hylton's avatar
Jeremy Hylton committed
640 641
            if mo:
                scheme, realm = mo.groups()
Eric S. Raymond's avatar
Eric S. Raymond committed
642
                if scheme.lower() == 'basic':
643
                    return self.retry_http_basic_auth(host, req, realm)
Jeremy Hylton's avatar
Jeremy Hylton committed
644

645
    def retry_http_basic_auth(self, host, req, realm):
Jeremy Hylton's avatar
Jeremy Hylton committed
646 647
        user,pw = self.passwd.find_user_password(realm, host)
        if pw:
Fred Drake's avatar
Fred Drake committed
648
            raw = "%s:%s" % (user, pw)
649 650 651 652 653
            auth = 'Basic %s' % base64.encodestring(raw).strip()
            if req.headers.get(self.auth_header, None) == auth:
                return None
            req.add_header(self.auth_header, auth)
            return self.parent.open(req)
Jeremy Hylton's avatar
Jeremy Hylton committed
654 655 656
        else:
            return None

657
class HTTPBasicAuthHandler(AbstractBasicAuthHandler, BaseHandler):
Jeremy Hylton's avatar
Jeremy Hylton committed
658

659
    auth_header = 'Authorization'
Jeremy Hylton's avatar
Jeremy Hylton committed
660

661 662
    def http_error_401(self, req, fp, code, msg, headers):
        host = urlparse.urlparse(req.get_full_url())[1]
Tim Peters's avatar
Tim Peters committed
663
        return self.http_error_auth_reqed('www-authenticate',
664 665 666 667 668
                                          host, req, headers)


class ProxyBasicAuthHandler(AbstractBasicAuthHandler, BaseHandler):

669
    auth_header = 'Proxy-Authorization'
670 671 672

    def http_error_407(self, req, fp, code, msg, headers):
        host = req.get_host()
Tim Peters's avatar
Tim Peters committed
673
        return self.http_error_auth_reqed('proxy-authenticate',
674 675 676 677 678 679 680
                                          host, req, headers)


class AbstractDigestAuthHandler:

    def __init__(self, passwd=None):
        if passwd is None:
681
            passwd = HTTPPasswordMgr()
682
        self.passwd = passwd
Fred Drake's avatar
Fred Drake committed
683
        self.add_password = self.passwd.add_password
Jeremy Hylton's avatar
Jeremy Hylton committed
684

685
    def http_error_auth_reqed(self, authreq, host, req, headers):
686
        authreq = headers.get(self.auth_header, None)
Fred Drake's avatar
Fred Drake committed
687
        if authreq:
Eric S. Raymond's avatar
Eric S. Raymond committed
688
            kind = authreq.split()[0]
Fred Drake's avatar
Fred Drake committed
689 690
            if kind == 'Digest':
                return self.retry_http_digest_auth(req, authreq)
Jeremy Hylton's avatar
Jeremy Hylton committed
691 692

    def retry_http_digest_auth(self, req, auth):
Eric S. Raymond's avatar
Eric S. Raymond committed
693
        token, challenge = auth.split(' ', 1)
Fred Drake's avatar
Fred Drake committed
694 695 696
        chal = parse_keqv_list(parse_http_list(challenge))
        auth = self.get_authorization(req, chal)
        if auth:
697 698 699 700
            auth_val = 'Digest %s' % auth
            if req.headers.get(self.auth_header, None) == auth_val:
                return None
            req.add_header(self.auth_header, auth_val)
Fred Drake's avatar
Fred Drake committed
701 702
            resp = self.parent.open(req)
            return resp
Jeremy Hylton's avatar
Jeremy Hylton committed
703 704

    def get_authorization(self, req, chal):
Fred Drake's avatar
Fred Drake committed
705 706 707 708 709 710 711 712 713 714 715 716 717 718 719
        try:
            realm = chal['realm']
            nonce = chal['nonce']
            algorithm = chal.get('algorithm', 'MD5')
            # mod_digest doesn't send an opaque, even though it isn't
            # supposed to be optional
            opaque = chal.get('opaque', None)
        except KeyError:
            return None

        H, KD = self.get_algorithm_impls(algorithm)
        if H is None:
            return None

        user, pw = self.passwd.find_user_password(realm,
720
                                                  req.get_full_url())
Fred Drake's avatar
Fred Drake committed
721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746
        if user is None:
            return None

        # XXX not implemented yet
        if req.has_data():
            entdig = self.get_entity_digest(req.get_data(), chal)
        else:
            entdig = None

        A1 = "%s:%s:%s" % (user, realm, pw)
        A2 = "%s:%s" % (req.has_data() and 'POST' or 'GET',
                        # XXX selector: what about proxies and full urls
                        req.get_selector())
        respdig = KD(H(A1), "%s:%s" % (nonce, H(A2)))
        # XXX should the partial digests be encoded too?

        base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \
               'response="%s"' % (user, realm, nonce, req.get_selector(),
                                  respdig)
        if opaque:
            base = base + ', opaque="%s"' % opaque
        if entdig:
            base = base + ', digest="%s"' % entdig
        if algorithm != 'MD5':
            base = base + ', algorithm="%s"' % algorithm
        return base
Jeremy Hylton's avatar
Jeremy Hylton committed
747 748

    def get_algorithm_impls(self, algorithm):
Fred Drake's avatar
Fred Drake committed
749 750 751 752 753 754 755 756
        # lambdas assume digest modules are imported at the top level
        if algorithm == 'MD5':
            H = lambda x, e=encode_digest:e(md5.new(x).digest())
        elif algorithm == 'SHA':
            H = lambda x, e=encode_digest:e(sha.new(x).digest())
        # XXX MD5-sess
        KD = lambda s, d, H=H: H("%s:%s" % (s, d))
        return H, KD
Jeremy Hylton's avatar
Jeremy Hylton committed
757 758

    def get_entity_digest(self, data, chal):
Fred Drake's avatar
Fred Drake committed
759 760
        # XXX not implemented yet
        return None
Jeremy Hylton's avatar
Jeremy Hylton committed
761

762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785

class HTTPDigestAuthHandler(BaseHandler, AbstractDigestAuthHandler):
    """An authentication protocol defined by RFC 2069

    Digest authentication improves on basic authentication because it
    does not transmit passwords in the clear.
    """

    header = 'Authorization'

    def http_error_401(self, req, fp, code, msg, headers):
        host = urlparse.urlparse(req.get_full_url())[1]
        self.http_error_auth_reqed('www-authenticate', host, req, headers)


class ProxyDigestAuthHandler(BaseHandler, AbstractDigestAuthHandler):

    header = 'Proxy-Authorization'

    def http_error_407(self, req, fp, code, msg, headers):
        host = req.get_host()
        self.http_error_auth_reqed('proxy-authenticate', host, req, headers)


Jeremy Hylton's avatar
Jeremy Hylton committed
786 787 788
def encode_digest(digest):
    hexrep = []
    for c in digest:
Fred Drake's avatar
Fred Drake committed
789 790 791 792
        n = (ord(c) >> 4) & 0xf
        hexrep.append(hex(n)[-1])
        n = ord(c) & 0xf
        hexrep.append(hex(n)[-1])
Eric S. Raymond's avatar
Eric S. Raymond committed
793
    return ''.join(hexrep)
794 795


796 797
class AbstractHTTPHandler(BaseHandler):

798 799 800
    # XXX Should rewrite do_open() to use the new httplib interface,
    # would would be a little simpler.

801
    def do_open(self, http_class, req):
802
        host = req.get_host()
Jeremy Hylton's avatar
Jeremy Hylton committed
803 804 805
        if not host:
            raise URLError('no host given')

806 807 808 809 810 811 812 813 814 815 816
        h = http_class(host) # will parse host:port
        if req.has_data():
            data = req.get_data()
            h.putrequest('POST', req.get_selector())
            if not 'Content-type' in req.headers:
                h.putheader('Content-type',
                            'application/x-www-form-urlencoded')
            if not 'Content-length' in req.headers:
                h.putheader('Content-length', '%d' % len(data))
        else:
            h.putrequest('GET', req.get_selector())
817

818 819 820
        scheme, sel = splittype(req.get_selector())
        sel_host, sel_path = splithost(sel)
        h.putheader('Host', sel_host or host)
Jeremy Hylton's avatar
Jeremy Hylton committed
821
        for args in self.parent.addheaders:
822
            name, value = args
823 824
            if name not in req.headers:
                h.putheader(*args)
825
        for k, v in req.headers.items():
Fred Drake's avatar
Fred Drake committed
826
            h.putheader(k, v)
827 828
        # httplib will attempt to connect() here.  be prepared
        # to convert a socket error to a URLError.
829 830 831 832
        try:
            h.endheaders()
        except socket.error, err:
            raise URLError(err)
Jeremy Hylton's avatar
Jeremy Hylton committed
833
        if req.has_data():
834
            h.send(data)
Jeremy Hylton's avatar
Jeremy Hylton committed
835 836 837 838 839 840 841 842

        code, msg, hdrs = h.getreply()
        fp = h.getfile()
        if code == 200:
            return addinfourl(fp, hdrs, req.get_full_url())
        else:
            return self.parent.error('http', req, fp, code, msg, hdrs)

843 844 845 846 847 848 849 850 851 852 853 854 855 856

class HTTPHandler(AbstractHTTPHandler):

    def http_open(self, req):
        return self.do_open(httplib.HTTP, req)


if hasattr(httplib, 'HTTPS'):
    class HTTPSHandler(AbstractHTTPHandler):

        def https_open(self, req):
            return self.do_open(httplib.HTTPS, req)


Jeremy Hylton's avatar
Jeremy Hylton committed
857 858
class UnknownHandler(BaseHandler):
    def unknown_open(self, req):
Fred Drake's avatar
Fred Drake committed
859
        type = req.get_type()
Jeremy Hylton's avatar
Jeremy Hylton committed
860 861 862 863 864 865
        raise URLError('unknown url type: %s' % type)

def parse_keqv_list(l):
    """Parse list of key=value strings where keys are not duplicated."""
    parsed = {}
    for elt in l:
Eric S. Raymond's avatar
Eric S. Raymond committed
866
        k, v = elt.split('=', 1)
Fred Drake's avatar
Fred Drake committed
867 868 869
        if v[0] == '"' and v[-1] == '"':
            v = v[1:-1]
        parsed[k] = v
Jeremy Hylton's avatar
Jeremy Hylton committed
870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886
    return parsed

def parse_http_list(s):
    """Parse lists as described by RFC 2068 Section 2.

    In particular, parse comman-separated lists where the elements of
    the list may include quoted-strings.  A quoted-string could
    contain a comma.
    """
    # XXX this function could probably use more testing

    list = []
    end = len(s)
    i = 0
    inquote = 0
    start = 0
    while i < end:
Fred Drake's avatar
Fred Drake committed
887
        cur = s[i:]
Eric S. Raymond's avatar
Eric S. Raymond committed
888 889
        c = cur.find(',')
        q = cur.find('"')
Fred Drake's avatar
Fred Drake committed
890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906
        if c == -1:
            list.append(s[start:])
            break
        if q == -1:
            if inquote:
                raise ValueError, "unbalanced quotes"
            else:
                list.append(s[start:i+c])
                i = i + c + 1
                continue
        if inquote:
            if q < c:
                list.append(s[start:i+c])
                i = i + c + 1
                start = i
                inquote = 0
            else:
907
                i = i + q
Fred Drake's avatar
Fred Drake committed
908 909 910 911 912 913 914 915
        else:
            if c < q:
                list.append(s[start:i+c])
                i = i + c + 1
                start = i
            else:
                inquote = 1
                i = i + q + 1
Eric S. Raymond's avatar
Eric S. Raymond committed
916
    return map(lambda x: x.strip(), list)
Jeremy Hylton's avatar
Jeremy Hylton committed
917 918 919 920

class FileHandler(BaseHandler):
    # Use local file or FTP depending on form of URL
    def file_open(self, req):
Fred Drake's avatar
Fred Drake committed
921 922 923 924 925 926
        url = req.get_selector()
        if url[:2] == '//' and url[2:3] != '/':
            req.type = 'ftp'
            return self.parent.open(req)
        else:
            return self.open_local_file(req)
Jeremy Hylton's avatar
Jeremy Hylton committed
927 928 929 930

    # names for the localhost
    names = None
    def get_names(self):
Fred Drake's avatar
Fred Drake committed
931
        if FileHandler.names is None:
932
            FileHandler.names = (socket.gethostbyname('localhost'),
Fred Drake's avatar
Fred Drake committed
933 934
                                 socket.gethostbyname(socket.gethostname()))
        return FileHandler.names
Jeremy Hylton's avatar
Jeremy Hylton committed
935 936 937

    # not entirely sure what the rules are here
    def open_local_file(self, req):
Fred Drake's avatar
Fred Drake committed
938 939
        host = req.get_host()
        file = req.get_selector()
940 941
        localfile = url2pathname(file)
        stats = os.stat(localfile)
942 943
        size = stats.st_size
        modified = rfc822.formatdate(stats.st_mtime)
944 945 946 947
        mtype = mimetypes.guess_type(file)[0]
        headers = mimetools.Message(StringIO(
            'Content-Type: %s\nContent-Length: %d\nLast-modified: %s\n' %
            (mtype or 'text/plain', size, modified)))
Fred Drake's avatar
Fred Drake committed
948 949 950 951
        if host:
            host, port = splitport(host)
        if not host or \
           (not port and socket.gethostbyname(host) in self.get_names()):
952
            return addinfourl(open(localfile, 'rb'),
Fred Drake's avatar
Fred Drake committed
953 954
                              headers, 'file:'+file)
        raise URLError('file not on local host')
Jeremy Hylton's avatar
Jeremy Hylton committed
955 956 957

class FTPHandler(BaseHandler):
    def ftp_open(self, req):
Fred Drake's avatar
Fred Drake committed
958 959 960 961
        host = req.get_host()
        if not host:
            raise IOError, ('ftp error', 'no host given')
        # XXX handle custom username & password
Jeremy Hylton's avatar
Jeremy Hylton committed
962 963 964 965
        try:
            host = socket.gethostbyname(host)
        except socket.error, msg:
            raise URLError(msg)
Fred Drake's avatar
Fred Drake committed
966 967 968 969 970
        host, port = splitport(host)
        if port is None:
            port = ftplib.FTP_PORT
        path, attrs = splitattr(req.get_selector())
        path = unquote(path)
Eric S. Raymond's avatar
Eric S. Raymond committed
971
        dirs = path.split('/')
Fred Drake's avatar
Fred Drake committed
972 973 974 975 976 977 978 979 980
        dirs, file = dirs[:-1], dirs[-1]
        if dirs and not dirs[0]:
            dirs = dirs[1:]
        user = passwd = '' # XXX
        try:
            fw = self.connect_ftp(user, passwd, host, port, dirs)
            type = file and 'I' or 'D'
            for attr in attrs:
                attr, value = splitattr(attr)
Eric S. Raymond's avatar
Eric S. Raymond committed
981
                if attr.lower() == 'type' and \
Fred Drake's avatar
Fred Drake committed
982
                   value in ('a', 'A', 'i', 'I', 'd', 'D'):
Eric S. Raymond's avatar
Eric S. Raymond committed
983
                    type = value.upper()
Fred Drake's avatar
Fred Drake committed
984
            fp, retrlen = fw.retrfile(file, type)
985 986 987 988
            headers = ""
            mtype = mimetypes.guess_type(req.get_full_url())[0]
            if mtype:
                headers += "Content-Type: %s\n" % mtype
Fred Drake's avatar
Fred Drake committed
989
            if retrlen is not None and retrlen >= 0:
990 991 992
                headers += "Content-Length: %d\n" % retrlen
            sf = StringIO(headers)
            headers = mimetools.Message(sf)
Fred Drake's avatar
Fred Drake committed
993 994 995
            return addinfourl(fp, headers, req.get_full_url())
        except ftplib.all_errors, msg:
            raise IOError, ('ftp error', msg), sys.exc_info()[2]
Jeremy Hylton's avatar
Jeremy Hylton committed
996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009

    def connect_ftp(self, user, passwd, host, port, dirs):
        fw = ftpwrapper(user, passwd, host, port, dirs)
##        fw.ftp.set_debuglevel(1)
        return fw

class CacheFTPHandler(FTPHandler):
    # XXX would be nice to have pluggable cache strategies
    # XXX this stuff is definitely not thread safe
    def __init__(self):
        self.cache = {}
        self.timeout = {}
        self.soonest = 0
        self.delay = 60
Fred Drake's avatar
Fred Drake committed
1010
        self.max_conns = 16
Jeremy Hylton's avatar
Jeremy Hylton committed
1011 1012 1013 1014 1015

    def setTimeout(self, t):
        self.delay = t

    def setMaxConns(self, m):
Fred Drake's avatar
Fred Drake committed
1016
        self.max_conns = m
Jeremy Hylton's avatar
Jeremy Hylton committed
1017 1018 1019

    def connect_ftp(self, user, passwd, host, port, dirs):
        key = user, passwd, host, port
1020
        if key in self.cache:
Jeremy Hylton's avatar
Jeremy Hylton committed
1021 1022 1023 1024
            self.timeout[key] = time.time() + self.delay
        else:
            self.cache[key] = ftpwrapper(user, passwd, host, port, dirs)
            self.timeout[key] = time.time() + self.delay
Fred Drake's avatar
Fred Drake committed
1025
        self.check_cache()
Jeremy Hylton's avatar
Jeremy Hylton committed
1026 1027 1028
        return self.cache[key]

    def check_cache(self):
Fred Drake's avatar
Fred Drake committed
1029
        # first check for old ones
Jeremy Hylton's avatar
Jeremy Hylton committed
1030 1031
        t = time.time()
        if self.soonest <= t:
1032
            for k, v in self.timeout.items():
Jeremy Hylton's avatar
Jeremy Hylton committed
1033 1034 1035 1036 1037 1038 1039
                if v < t:
                    self.cache[k].close()
                    del self.cache[k]
                    del self.timeout[k]
        self.soonest = min(self.timeout.values())

        # then check the size
Fred Drake's avatar
Fred Drake committed
1040
        if len(self.cache) == self.max_conns:
1041
            for k, v in self.timeout.items():
Fred Drake's avatar
Fred Drake committed
1042 1043 1044 1045 1046
                if v == self.soonest:
                    del self.cache[k]
                    del self.timeout[k]
                    break
            self.soonest = min(self.timeout.values())
Jeremy Hylton's avatar
Jeremy Hylton committed
1047 1048 1049

class GopherHandler(BaseHandler):
    def gopher_open(self, req):
Fred Drake's avatar
Fred Drake committed
1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063
        host = req.get_host()
        if not host:
            raise GopherError('no host given')
        host = unquote(host)
        selector = req.get_selector()
        type, selector = splitgophertype(selector)
        selector, query = splitquery(selector)
        selector = unquote(selector)
        if query:
            query = unquote(query)
            fp = gopherlib.send_query(selector, query, host)
        else:
            fp = gopherlib.send_selector(selector, host)
        return addinfourl(fp, noheaders(), req.get_full_url())
Jeremy Hylton's avatar
Jeremy Hylton committed
1064 1065 1066 1067 1068

#bleck! don't use this yet
class OpenerFactory:

    default_handlers = [UnknownHandler, HTTPHandler,
1069
                        HTTPDefaultErrorHandler, HTTPRedirectHandler,
Fred Drake's avatar
Fred Drake committed
1070
                        FTPHandler, FileHandler]
Jeremy Hylton's avatar
Jeremy Hylton committed
1071 1072 1073 1074
    handlers = []
    replacement_handlers = []

    def add_handler(self, h):
Fred Drake's avatar
Fred Drake committed
1075
        self.handlers = self.handlers + [h]
Jeremy Hylton's avatar
Jeremy Hylton committed
1076 1077

    def replace_handler(self, h):
Fred Drake's avatar
Fred Drake committed
1078
        pass
Jeremy Hylton's avatar
Jeremy Hylton committed
1079 1080

    def build_opener(self):
1081
        opener = OpenerDirector()
1082
        for ph in self.default_handlers:
1083
            if inspect.isclass(ph):
Fred Drake's avatar
Fred Drake committed
1084 1085
                ph = ph()
            opener.add_handler(ph)
Jeremy Hylton's avatar
Jeremy Hylton committed
1086 1087

if __name__ == "__main__":
1088
    # XXX some of the test code depends on machine configurations that
Jeremy Hylton's avatar
Jeremy Hylton committed
1089 1090 1091 1092
    # are internal to CNRI.   Need to set up a public server with the
    # right authentication configuration for test purposes.
    if socket.gethostname() == 'bitdiddle':
        localhost = 'bitdiddle.cnri.reston.va.us'
Jeremy Hylton's avatar
Jeremy Hylton committed
1093
    elif socket.gethostname() == 'bitdiddle.concentric.net':
Jeremy Hylton's avatar
Jeremy Hylton committed
1094 1095 1096 1097
        localhost = 'localhost'
    else:
        localhost = None
    urls = [
Fred Drake's avatar
Fred Drake committed
1098 1099 1100
        # Thanks to Fred for finding these!
        'gopher://gopher.lib.ncsu.edu/11/library/stacks/Alex',
        'gopher://gopher.vt.edu:10010/10/33',
Jeremy Hylton's avatar
Jeremy Hylton committed
1101

Fred Drake's avatar
Fred Drake committed
1102 1103
        'file:/etc/passwd',
        'file://nonsensename/etc/passwd',
1104
        'ftp://www.python.org/pub/python/misc/sousa.au',
Jeremy Hylton's avatar
Jeremy Hylton committed
1105
        'ftp://www.python.org/pub/tmp/blat',
Fred Drake's avatar
Fred Drake committed
1106 1107
        'http://www.espn.com/', # redirect
        'http://www.python.org/Spanish/Inquistion/',
1108
        ('http://www.python.org/cgi-bin/faqw.py',
Fred Drake's avatar
Fred Drake committed
1109 1110
         'query=pythonistas&querytype=simple&casefold=yes&req=search'),
        'http://www.python.org/',
1111
        'ftp://gatekeeper.research.compaq.com/pub/DEC/SRC/research-reports/00README-Legal-Rules-Regs',
Jeremy Hylton's avatar
Jeremy Hylton committed
1112 1113
            ]

1114 1115 1116 1117 1118 1119 1120
##    if localhost is not None:
##        urls = urls + [
##            'file://%s/etc/passwd' % localhost,
##            'http://%s/simple/' % localhost,
##            'http://%s/digest/' % localhost,
##            'http://%s/not/found.h' % localhost,
##            ]
Jeremy Hylton's avatar
Jeremy Hylton committed
1121

1122 1123 1124 1125 1126 1127
##        bauth = HTTPBasicAuthHandler()
##        bauth.add_password('basic_test_realm', localhost, 'jhylton',
##                           'password')
##        dauth = HTTPDigestAuthHandler()
##        dauth.add_password('digest_test_realm', localhost, 'jhylton',
##                           'password')
1128

Jeremy Hylton's avatar
Jeremy Hylton committed
1129 1130 1131 1132

    cfh = CacheFTPHandler()
    cfh.setTimeout(1)

1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143
##    # XXX try out some custom proxy objects too!
##    def at_cnri(req):
##        host = req.get_host()
##        print host
##        if host[-18:] == '.cnri.reston.va.us':
##            return 1
##    p = CustomProxy('http', at_cnri, 'proxy.cnri.reston.va.us')
##    ph = CustomProxyHandler(p)

##    install_opener(build_opener(dauth, bauth, cfh, GopherHandler, ph))
    install_opener(build_opener(cfh, GopherHandler))
Jeremy Hylton's avatar
Jeremy Hylton committed
1144 1145

    for url in urls:
1146
        if isinstance(url, tuple):
Jeremy Hylton's avatar
Jeremy Hylton committed
1147 1148 1149 1150 1151 1152 1153
            url, req = url
        else:
            req = None
        print url
        try:
            f = urlopen(url, req)
        except IOError, err:
Fred Drake's avatar
Fred Drake committed
1154 1155 1156
            print "IOError:", err
        except socket.error, err:
            print "socket.error:", err
Jeremy Hylton's avatar
Jeremy Hylton committed
1157 1158 1159 1160 1161 1162
        else:
            buf = f.read()
            f.close()
            print "read %d bytes" % len(buf)
        print
        time.sleep(0.1)