urllib2.py 43.4 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

# 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

90 91 92
import base64
import ftplib
import gopherlib
Jeremy Hylton's avatar
Jeremy Hylton committed
93
import httplib
94
import inspect
Jeremy Hylton's avatar
Jeremy Hylton committed
95 96 97
import md5
import mimetypes
import mimetools
98 99 100 101
import os
import posixpath
import random
import re
102
import rfc822
103 104
import sha
import socket
Jeremy Hylton's avatar
Jeremy Hylton committed
105 106
import sys
import time
107
import urlparse
108
import bisect
Jeremy Hylton's avatar
Jeremy Hylton committed
109 110 111 112 113 114 115 116 117

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

# not sure how many of these need to be gotten rid of
from urllib import unwrap, unquote, splittype, splithost, \
     addinfourl, splitport, splitgophertype, splitquery, \
118
     splitattr, ftpwrapper, noheaders, splituser, splitpasswd
Jeremy Hylton's avatar
Jeremy Hylton committed
119

120 121
# support for FileHandler, proxies via environment variables
from urllib import localhost, url2pathname, getproxies
Jeremy Hylton's avatar
Jeremy Hylton committed
122

123
__version__ = "2.4"
Jeremy Hylton's avatar
Jeremy Hylton committed
124 125 126 127 128 129 130 131 132 133 134 135 136

_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?
137
# make sure all of the IOError stuff is overridden.  we just want to be
138
# subtypes.
Jeremy Hylton's avatar
Jeremy Hylton committed
139 140 141

class URLError(IOError):
    # URLError is a sub-type of IOError, but it doesn't share any of
142 143 144 145
    # the implementation.  need to override __init__ and __str__.
    # It sets self.args for compatibility with other EnvironmentError
    # subclasses, but args doesn't have the typical format with errno in
    # slot 0 and strerror in slot 1.  This may be better than nothing.
Jeremy Hylton's avatar
Jeremy Hylton committed
146
    def __init__(self, reason):
147
        self.args = reason,
Fred Drake's avatar
Fred Drake committed
148
        self.reason = reason
Jeremy Hylton's avatar
Jeremy Hylton committed
149 150

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

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

    def __init__(self, url, code, msg, hdrs, fp):
Fred Drake's avatar
Fred Drake committed
158 159 160 161 162
        self.code = code
        self.msg = msg
        self.hdrs = hdrs
        self.fp = fp
        self.filename = url
163 164 165
        # 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
166
        # not initialize the base classes.
167 168
        if fp is not None:
            self.__super_init(fp, hdrs, url)
169

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

class GopherError(URLError):
    pass

176

Jeremy Hylton's avatar
Jeremy Hylton committed
177
class Request:
178

Jeremy Hylton's avatar
Jeremy Hylton committed
179
    def __init__(self, url, data=None, headers={}):
Fred Drake's avatar
Fred Drake committed
180 181 182 183 184 185
        # 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
186
        self.data = data
Fred Drake's avatar
Fred Drake committed
187
        self.headers = {}
188
        for key, value in headers.items():
189
            self.add_header(key, value)
190
        self.unredirected_hdrs = {}
Jeremy Hylton's avatar
Jeremy Hylton committed
191 192

    def __getattr__(self, attr):
Fred Drake's avatar
Fred Drake committed
193
        # XXX this is a fallback mechanism to guard against these
194
        # methods getting called in a non-standard order.  this may be
Fred Drake's avatar
Fred Drake committed
195 196 197 198 199 200 201 202
        # 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
203

204 205 206 207 208 209
    def get_method(self):
        if self.has_data():
            return "POST"
        else:
            return "GET"

210 211
    # XXX these helper methods are lame

Jeremy Hylton's avatar
Jeremy Hylton committed
212 213 214 215 216 217 218 219 220 221 222 223 224
    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
225 226
        if self.type is None:
            self.type, self.__r_type = splittype(self.__original)
227 228
            if self.type is None:
                raise ValueError, "unknown url type: %s" % self.__original
Fred Drake's avatar
Fred Drake committed
229
        return self.type
Jeremy Hylton's avatar
Jeremy Hylton committed
230 231

    def get_host(self):
Fred Drake's avatar
Fred Drake committed
232 233 234 235 236
        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
237 238

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

241 242
    def set_proxy(self, host, type):
        self.host, self.type = host, type
Fred Drake's avatar
Fred Drake committed
243
        self.__r_host = self.__original
Jeremy Hylton's avatar
Jeremy Hylton committed
244 245

    def add_header(self, key, val):
Fred Drake's avatar
Fred Drake committed
246
        # useful for something like authentication
247
        self.headers[key.capitalize()] = val
Jeremy Hylton's avatar
Jeremy Hylton committed
248

249 250 251 252 253 254 255 256 257
    def add_unredirected_header(self, key, val):
        # will not be added to a redirected request
        self.unredirected_hdrs[key.capitalize()] = val

    def has_header(self, header_name):
        return bool(header_name in self.headers or
                    header_name in self.unredirected_hdrs)


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

    def add_handler(self, handler):
270
        added = False
271
        for meth in dir(handler):
272 273 274 275 276 277
            i = meth.find("_")
            protocol = meth[:i]
            condition = meth[i+1:]

            if condition.startswith("error"):
                j = meth[i+1:].find("_") + i + 1
Jeremy Hylton's avatar
Jeremy Hylton committed
278 279
                kind = meth[j+1:]
                try:
Eric S. Raymond's avatar
Eric S. Raymond committed
280
                    kind = int(kind)
Jeremy Hylton's avatar
Jeremy Hylton committed
281 282
                except ValueError:
                    pass
283 284 285 286 287 288 289 290 291
                lookup = self.handle_error.get(protocol, {})
                self.handle_error[protocol] = lookup
            elif condition == "open":
                kind = protocol
                lookup = getattr(self, "handle_"+condition)
            elif condition in ["response", "request"]:
                kind = protocol
                lookup = getattr(self, "process_"+condition)
            else:
Jeremy Hylton's avatar
Jeremy Hylton committed
292
                continue
293 294 295 296 297 298 299 300

            handlers = lookup.setdefault(kind, [])
            if handlers:
                bisect.insort(handlers, handler)
            else:
                handlers.append(handler)
            added = True

Jeremy Hylton's avatar
Jeremy Hylton committed
301
        if added:
302 303
            # XXX why does self.handlers need to be sorted?
            bisect.insort(self.handlers, handler)
Jeremy Hylton's avatar
Jeremy Hylton committed
304
            handler.add_parent(self)
305

Jeremy Hylton's avatar
Jeremy Hylton committed
306
    def close(self):
307 308
        # Only exists for backwards compatibility.
        pass
Jeremy Hylton's avatar
Jeremy Hylton committed
309 310 311 312 313 314 315

    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
316 317

            result = func(*args)
Jeremy Hylton's avatar
Jeremy Hylton committed
318 319 320 321
            if result is not None:
                return result

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

330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
        protocol = req.get_type()

        # pre-process request
        meth_name = protocol+"_request"
        for processor in self.process_request.get(protocol, []):
            meth = getattr(processor, meth_name)
            req = meth(req)

        response = self._open(req, data)

        # post-process response
        meth_name = protocol+"_response"
        for processor in self.process_response.get(protocol, []):
            meth = getattr(processor, meth_name)
            response = meth(req, response)

        return response

    def _open(self, req, data=None):
Jeremy Hylton's avatar
Jeremy Hylton committed
349
        result = self._call_chain(self.handle_open, 'default',
350
                                  'default_open', req)
Jeremy Hylton's avatar
Jeremy Hylton committed
351 352 353
        if result:
            return result

354 355
        protocol = req.get_type()
        result = self._call_chain(self.handle_open, protocol, protocol +
Jeremy Hylton's avatar
Jeremy Hylton committed
356
                                  '_open', req)
Jeremy Hylton's avatar
Jeremy Hylton committed
357 358 359 360 361 362 363
        if result:
            return result

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

    def error(self, proto, *args):
364
        if proto in ['http', 'https']:
365 366
            # 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
367
            proto = args[2]  # YUCK!
368
            meth_name = 'http_error_%s' % proto
Jeremy Hylton's avatar
Jeremy Hylton committed
369 370 371 372 373 374 375
            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
376
        result = self._call_chain(*args)
Jeremy Hylton's avatar
Jeremy Hylton committed
377 378 379 380 381
        if result:
            return result

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

384 385 386
# 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
387 388 389 390 391

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

    The opener will use several default handlers, including support
392
    for HTTP and FTP.
Jeremy Hylton's avatar
Jeremy Hylton committed
393 394 395 396

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

Jeremy Hylton's avatar
Jeremy Hylton committed
398 399 400
    opener = OpenerDirector()
    default_classes = [ProxyHandler, UnknownHandler, HTTPHandler,
                       HTTPDefaultErrorHandler, HTTPRedirectHandler,
401
                       FTPHandler, FileHandler, HTTPErrorProcessor]
402 403
    if hasattr(httplib, 'HTTPS'):
        default_classes.append(HTTPSHandler)
Jeremy Hylton's avatar
Jeremy Hylton committed
404 405 406
    skip = []
    for klass in default_classes:
        for check in handlers:
407
            if inspect.isclass(check):
Jeremy Hylton's avatar
Jeremy Hylton committed
408 409
                if issubclass(check, klass):
                    skip.append(klass)
410 411
            elif isinstance(check, klass):
                skip.append(klass)
Jeremy Hylton's avatar
Jeremy Hylton committed
412 413 414 415 416 417 418
    for klass in skip:
        default_classes.remove(klass)

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

    for h in handlers:
419
        if inspect.isclass(h):
Jeremy Hylton's avatar
Jeremy Hylton committed
420 421 422 423 424
            h = h()
        opener.add_handler(h)
    return opener

class BaseHandler:
425 426
    handler_order = 500

Jeremy Hylton's avatar
Jeremy Hylton committed
427 428
    def add_parent(self, parent):
        self.parent = parent
Tim Peters's avatar
Tim Peters committed
429

Jeremy Hylton's avatar
Jeremy Hylton committed
430
    def close(self):
431 432
        # Only exists for backwards compatibility
        pass
Tim Peters's avatar
Tim Peters committed
433

434 435 436 437 438 439 440
    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
Tim Peters's avatar
Tim Peters committed
441

Jeremy Hylton's avatar
Jeremy Hylton committed
442

443 444 445 446 447 448 449 450 451 452 453 454 455 456 457
class HTTPErrorProcessor(BaseHandler):
    """Process HTTP error responses."""
    handler_order = 1000  # after all other processing

    def http_response(self, request, response):
        code, msg, hdrs = response.code, response.msg, response.info()

        if code != 200:
            response = self.parent.error(
                'http', request, response, code, msg, hdrs)

        return response

    https_response = http_response

Jeremy Hylton's avatar
Jeremy Hylton committed
458 459
class HTTPDefaultErrorHandler(BaseHandler):
    def http_error_default(self, req, fp, code, msg, hdrs):
Fred Drake's avatar
Fred Drake committed
460
        raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
Jeremy Hylton's avatar
Jeremy Hylton committed
461 462

class HTTPRedirectHandler(BaseHandler):
463 464 465
    # maximum number of redirections before assuming we're in a loop
    max_redirections = 10

466
    def redirect_request(self, req, fp, code, msg, headers, newurl):
467 468
        """Return a Request or None in response to a redirect.

469 470 471 472 473 474
        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.
475
        """
476 477
        m = req.get_method()
        if (code in (301, 302, 303, 307) and m in ("GET", "HEAD")
478 479 480
            or code in (301, 302, 303) and m == "POST"):
            # Strictly (according to RFC 2616), 301 or 302 in response
            # to a POST MUST NOT cause a redirection without confirmation
481 482 483
            # from the user (of urllib2, in this case).  In practice,
            # essentially all clients do redirect in this case, so we
            # do the same.
484 485
            return Request(newurl, headers=req.headers)
        else:
486
            raise HTTPError(req.get_full_url(), code, msg, headers, fp)
487

Jeremy Hylton's avatar
Jeremy Hylton committed
488 489 490 491 492
    # 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):
493
        if 'location' in headers:
Jeremy Hylton's avatar
Jeremy Hylton committed
494
            newurl = headers['location']
495
        elif 'uri' in headers:
Jeremy Hylton's avatar
Jeremy Hylton committed
496 497 498
            newurl = headers['uri']
        else:
            return
Jeremy Hylton's avatar
Jeremy Hylton committed
499 500
        newurl = urlparse.urljoin(req.get_full_url(), newurl)

Jeremy Hylton's avatar
Jeremy Hylton committed
501 502 503
        # 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
504
        new = self.redirect_request(req, fp, code, msg, headers, newurl)
505 506 507 508
        if new is None:
            return

        # loop detection
509 510 511 512 513 514 515 516 517
        # .redirect_dict has a key (url, code) if url was previously
        # visited as a result of a redirection with that code.  The
        # code is needed in addition to the URL because visiting a URL
        # twice isn't necessarily a loop: there is more than one way
        # to redirect (301, 302, 303, 307, refresh).
        key = (newurl, code)
        if hasattr(req, 'redirect_dict'):
            visited = new.redirect_dict = req.redirect_dict
            if key in visited or len(visited) >= self.max_redirections:
Jeremy Hylton's avatar
Jeremy Hylton committed
518
                raise HTTPError(req.get_full_url(), code,
519
                                self.inf_msg + msg, headers, fp)
520 521 522
        else:
            visited = new.redirect_dict = req.redirect_dict = {}
        visited[key] = None
523 524

        # Don't close the fp until we are sure that we won't use it
Tim Peters's avatar
Tim Peters committed
525
        # with HTTPError.
526 527 528
        fp.read()
        fp.close()

Jeremy Hylton's avatar
Jeremy Hylton committed
529 530
        return self.parent.open(new)

531
    http_error_301 = http_error_303 = http_error_307 = http_error_302
Jeremy Hylton's avatar
Jeremy Hylton committed
532

533
    inf_msg = "The HTTP server returned a redirect error that would " \
534
              "lead to an infinite loop.\n" \
535
              "The last 30x error message was:\n"
Jeremy Hylton's avatar
Jeremy Hylton committed
536 537

class ProxyHandler(BaseHandler):
538 539 540
    # Proxies must be in front
    handler_order = 100

Jeremy Hylton's avatar
Jeremy Hylton committed
541
    def __init__(self, proxies=None):
Fred Drake's avatar
Fred Drake committed
542 543 544 545
        if proxies is None:
            proxies = getproxies()
        assert hasattr(proxies, 'has_key'), "proxies must be a mapping"
        self.proxies = proxies
546
        for type, url in proxies.items():
547
            setattr(self, '%s_open' % type,
Fred Drake's avatar
Fred Drake committed
548 549
                    lambda r, proxy=url, type=type, meth=self.proxy_open: \
                    meth(r, proxy, type))
Jeremy Hylton's avatar
Jeremy Hylton committed
550 551

    def proxy_open(self, req, proxy, type):
Fred Drake's avatar
Fred Drake committed
552
        orig_type = req.get_type()
553 554 555 556
        type, r_type = splittype(proxy)
        host, XXX = splithost(r_type)
        if '@' in host:
            user_pass, host = host.split('@', 1)
557 558
            if ':' in user_pass:
                user, password = user_pass.split(':', 1)
Tim Peters's avatar
Tim Peters committed
559
                user_pass = base64.encodestring('%s:%s' % (unquote(user),
560
                                                           unquote(password)))
561
                req.add_header('Proxy-authorization', 'Basic ' + user_pass)
562 563
        host = unquote(host)
        req.set_proxy(host, type)
Fred Drake's avatar
Fred Drake committed
564 565 566 567 568 569 570 571 572
        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
573 574 575 576 577 578

# 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
579 580 581
        self.proto = proto
        self.func = func
        self.addr = proxy_addr
Jeremy Hylton's avatar
Jeremy Hylton committed
582 583

    def handle(self, req):
Fred Drake's avatar
Fred Drake committed
584 585
        if self.func and self.func(req):
            return 1
Jeremy Hylton's avatar
Jeremy Hylton committed
586 587

    def get_proxy(self):
Fred Drake's avatar
Fred Drake committed
588
        return self.addr
Jeremy Hylton's avatar
Jeremy Hylton committed
589 590

class CustomProxyHandler(BaseHandler):
591 592 593
    # Proxies must be in front
    handler_order = 100

Jeremy Hylton's avatar
Jeremy Hylton committed
594
    def __init__(self, *proxies):
Fred Drake's avatar
Fred Drake committed
595
        self.proxies = {}
Jeremy Hylton's avatar
Jeremy Hylton committed
596 597

    def proxy_open(self, req):
Fred Drake's avatar
Fred Drake committed
598 599 600 601 602 603 604 605 606 607
        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
608 609

    def do_proxy(self, p, req):
Fred Drake's avatar
Fred Drake committed
610
        return self.parent.open(req)
Jeremy Hylton's avatar
Jeremy Hylton committed
611 612

    def add_proxy(self, cpo):
613
        if cpo.proto in self.proxies:
Fred Drake's avatar
Fred Drake committed
614 615 616
            self.proxies[cpo.proto].append(cpo)
        else:
            self.proxies[cpo.proto] = [cpo]
Jeremy Hylton's avatar
Jeremy Hylton committed
617 618 619

class HTTPPasswordMgr:
    def __init__(self):
Fred Drake's avatar
Fred Drake committed
620
        self.passwd = {}
Jeremy Hylton's avatar
Jeremy Hylton committed
621 622

    def add_password(self, realm, uri, user, passwd):
Fred Drake's avatar
Fred Drake committed
623
        # uri could be a single URI or a sequence
624
        if isinstance(uri, basestring):
Fred Drake's avatar
Fred Drake committed
625 626
            uri = [uri]
        uri = tuple(map(self.reduce_uri, uri))
627
        if not realm in self.passwd:
Fred Drake's avatar
Fred Drake committed
628 629
            self.passwd[realm] = {}
        self.passwd[realm][uri] = (user, passwd)
Jeremy Hylton's avatar
Jeremy Hylton committed
630 631

    def find_user_password(self, realm, authuri):
Fred Drake's avatar
Fred Drake committed
632 633
        domains = self.passwd.get(realm, {})
        authuri = self.reduce_uri(authuri)
634
        for uris, authinfo in domains.iteritems():
Fred Drake's avatar
Fred Drake committed
635 636 637 638
            for uri in uris:
                if self.is_suburi(uri, authuri):
                    return authinfo
        return None, None
Jeremy Hylton's avatar
Jeremy Hylton committed
639 640

    def reduce_uri(self, uri):
Fred Drake's avatar
Fred Drake committed
641 642 643 644 645 646
        """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
647 648

    def is_suburi(self, base, test):
Fred Drake's avatar
Fred Drake committed
649 650 651 652 653
        """Check if test is below base in a URI tree

        Both args must be URIs in reduced form.
        """
        if base == test:
654
            return True
Fred Drake's avatar
Fred Drake committed
655
        if base[0] != test[0]:
656
            return False
657
        common = posixpath.commonprefix((base[1], test[1]))
Fred Drake's avatar
Fred Drake committed
658
        if len(common) == len(base[1]):
659 660
            return True
        return False
661

Jeremy Hylton's avatar
Jeremy Hylton committed
662

663 664 665
class HTTPPasswordMgrWithDefaultRealm(HTTPPasswordMgr):

    def find_user_password(self, realm, authuri):
666 667
        user, password = HTTPPasswordMgr.find_user_password(self, realm,
                                                            authuri)
668 669 670 671 672 673 674
        if user is not None:
            return user, password
        return HTTPPasswordMgr.find_user_password(self, None, authuri)


class AbstractBasicAuthHandler:

675
    rx = re.compile('[ \t]*([^ \t]+)[ \t]+realm="([^"]*)"', re.I)
Jeremy Hylton's avatar
Jeremy Hylton committed
676 677 678 679 680

    # 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

681 682 683 684
    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
685
        self.add_password = self.passwd.add_password
686

687 688 689
    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
690
        if authreq:
691
            mo = AbstractBasicAuthHandler.rx.match(authreq)
Jeremy Hylton's avatar
Jeremy Hylton committed
692 693
            if mo:
                scheme, realm = mo.groups()
Eric S. Raymond's avatar
Eric S. Raymond committed
694
                if scheme.lower() == 'basic':
695
                    return self.retry_http_basic_auth(host, req, realm)
Jeremy Hylton's avatar
Jeremy Hylton committed
696

697
    def retry_http_basic_auth(self, host, req, realm):
Jeremy Hylton's avatar
Jeremy Hylton committed
698 699
        user,pw = self.passwd.find_user_password(realm, host)
        if pw:
Fred Drake's avatar
Fred Drake committed
700
            raw = "%s:%s" % (user, pw)
701 702 703 704 705
            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
706 707 708
        else:
            return None

709
class HTTPBasicAuthHandler(AbstractBasicAuthHandler, BaseHandler):
Jeremy Hylton's avatar
Jeremy Hylton committed
710

711
    auth_header = 'Authorization'
Jeremy Hylton's avatar
Jeremy Hylton committed
712

713 714
    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
715
        return self.http_error_auth_reqed('www-authenticate',
716 717 718 719 720
                                          host, req, headers)


class ProxyBasicAuthHandler(AbstractBasicAuthHandler, BaseHandler):

721
    auth_header = 'Proxy-authorization'
722 723 724

    def http_error_407(self, req, fp, code, msg, headers):
        host = req.get_host()
Tim Peters's avatar
Tim Peters committed
725
        return self.http_error_auth_reqed('proxy-authenticate',
726 727 728
                                          host, req, headers)


729 730 731 732 733 734 735 736 737 738 739 740 741 742
def randombytes(n):
    """Return n random bytes."""
    # Use /dev/urandom if it is available.  Fall back to random module
    # if not.  It might be worthwhile to extend this function to use
    # other platform-specific mechanisms for getting random bytes.
    if os.path.exists("/dev/urandom"):
        f = open("/dev/urandom")
        s = f.read(n)
        f.close()
        return s
    else:
        L = [chr(random.randrange(0, 256)) for i in range(n)]
        return "".join(L)

743
class AbstractDigestAuthHandler:
744 745 746 747 748 749 750 751 752
    # Digest authentication is specified in RFC 2617.

    # XXX The client does not inspect the Authentication-Info header
    # in a successful response.

    # XXX It should be possible to test this implementation against
    # a mock server that just generates a static set of challenges.

    # XXX qop="auth-int" supports is shaky
753 754 755

    def __init__(self, passwd=None):
        if passwd is None:
756
            passwd = HTTPPasswordMgr()
757
        self.passwd = passwd
Fred Drake's avatar
Fred Drake committed
758
        self.add_password = self.passwd.add_password
759 760 761 762 763 764 765 766 767 768 769 770 771 772
        self.retried = 0
        self.nonce_count = 0

    def reset_retry_count(self):
        self.retried = 0

    def http_error_auth_reqed(self, auth_header, host, req, headers):
        authreq = headers.get(auth_header, None)
        if self.retried > 5:
            # Don't fail endlessly - if we failed once, we'll probably
            # fail a second time. Hm. Unless the Password Manager is
            # prompting for the information. Crap. This isn't great
            # but it's better than the current 'repeat until recursion
            # depth exceeded' approach <wink>
Tim Peters's avatar
Tim Peters committed
773
            raise HTTPError(req.get_full_url(), 401, "digest auth failed",
774 775 776
                            headers, None)
        else:
            self.retried += 1
Fred Drake's avatar
Fred Drake committed
777
        if authreq:
778 779
            scheme = authreq.split()[0]
            if scheme.lower() == 'digest':
Fred Drake's avatar
Fred Drake committed
780
                return self.retry_http_digest_auth(req, authreq)
781 782 783
            else:
                raise ValueError("AbstractDigestAuthHandler doesn't know "
                                 "about %s"%(scheme))
Jeremy Hylton's avatar
Jeremy Hylton committed
784 785

    def retry_http_digest_auth(self, req, auth):
Eric S. Raymond's avatar
Eric S. Raymond committed
786
        token, challenge = auth.split(' ', 1)
Fred Drake's avatar
Fred Drake committed
787 788 789
        chal = parse_keqv_list(parse_http_list(challenge))
        auth = self.get_authorization(req, chal)
        if auth:
790 791 792 793
            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
794 795
            resp = self.parent.open(req)
            return resp
Jeremy Hylton's avatar
Jeremy Hylton committed
796

797 798 799 800 801 802 803 804 805 806
    def get_cnonce(self, nonce):
        # The cnonce-value is an opaque
        # quoted string value provided by the client and used by both client
        # and server to avoid chosen plaintext attacks, to provide mutual
        # authentication, and to provide some message integrity protection.
        # This isn't a fabulous effort, but it's probably Good Enough.
        dig = sha.new("%s:%s:%s:%s" % (self.nonce_count, nonce, time.ctime(),
                                       randombytes(8))).hexdigest()
        return dig[:16]

Jeremy Hylton's avatar
Jeremy Hylton committed
807
    def get_authorization(self, req, chal):
Fred Drake's avatar
Fred Drake committed
808 809 810
        try:
            realm = chal['realm']
            nonce = chal['nonce']
811
            qop = chal.get('qop')
Fred Drake's avatar
Fred Drake committed
812 813 814 815 816 817 818 819 820 821 822
            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

823
        user, pw = self.passwd.find_user_password(realm, req.get_full_url())
Fred Drake's avatar
Fred Drake committed
824 825 826 827 828 829 830 831 832 833 834 835 836
        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())
837 838 839 840 841 842 843 844 845 846 847
        if qop == 'auth':
            self.nonce_count += 1
            ncvalue = '%08x' % self.nonce_count
            cnonce = self.get_cnonce(nonce)
            noncebit = "%s:%s:%s:%s:%s" % (nonce, ncvalue, cnonce, qop, H(A2))
            respdig = KD(H(A1), noncebit)
        elif qop is None:
            respdig = KD(H(A1), "%s:%s" % (nonce, H(A2)))
        else:
            # XXX handle auth-int.
            pass
Tim Peters's avatar
Tim Peters committed
848

Fred Drake's avatar
Fred Drake committed
849 850 851 852 853 854 855 856 857 858 859
        # 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
860 861
        if qop:
            base = base + ', qop=auth, nc=%s, cnonce="%s"' % (ncvalue, cnonce)
Fred Drake's avatar
Fred Drake committed
862
        return base
Jeremy Hylton's avatar
Jeremy Hylton committed
863 864

    def get_algorithm_impls(self, algorithm):
Fred Drake's avatar
Fred Drake committed
865 866
        # lambdas assume digest modules are imported at the top level
        if algorithm == 'MD5':
867
            H = lambda x: md5.new(x).hexdigest()
Fred Drake's avatar
Fred Drake committed
868
        elif algorithm == 'SHA':
869
            H = lambda x: sha.new(x).hexdigest()
Fred Drake's avatar
Fred Drake committed
870
        # XXX MD5-sess
871
        KD = lambda s, d: H("%s:%s" % (s, d))
Fred Drake's avatar
Fred Drake committed
872
        return H, KD
Jeremy Hylton's avatar
Jeremy Hylton committed
873 874

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

878 879 880 881 882 883 884 885

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.
    """

886
    auth_header = 'Authorization'
887 888 889

    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
890
        retry = self.http_error_auth_reqed('www-authenticate',
891 892 893
                                           host, req, headers)
        self.reset_retry_count()
        return retry
894 895 896 897


class ProxyDigestAuthHandler(BaseHandler, AbstractDigestAuthHandler):

898
    auth_header = 'Proxy-Authorization'
899 900 901

    def http_error_407(self, req, fp, code, msg, headers):
        host = req.get_host()
Tim Peters's avatar
Tim Peters committed
902
        retry = self.http_error_auth_reqed('proxy-authenticate',
903 904 905
                                           host, req, headers)
        self.reset_retry_count()
        return retry
906

907 908
class AbstractHTTPHandler(BaseHandler):

909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940
    def __init__(self, debuglevel=0):
        self._debuglevel = debuglevel

    def set_http_debuglevel(self, level):
        self._debuglevel = level

    def do_request(self, request):
        host = request.get_host()
        if not host:
            raise URLError('no host given')

        if request.has_data():  # POST
            data = request.get_data()
            if not request.has_header('Content-type'):
                request.add_unredirected_header(
                    'Content-type',
                    'application/x-www-form-urlencoded')
            if not request.has_header('Content-length'):
                request.add_unredirected_header(
                    'Content-length', '%d' % len(data))

        scheme, sel = splittype(request.get_selector())
        sel_host, sel_path = splithost(sel)
        if not request.has_header('Host'):
            request.add_unredirected_header('Host', sel_host or host)
        for name, value in self.parent.addheaders:
            name = name.capitalize()
            if not request.has_header(name):
                request.add_unredirected_header(name, value)

        return request

941
    def do_open(self, http_class, req):
942 943 944 945 946 947 948 949 950
        """Return an addinfourl object for the request, using http_class.

        http_class must implement the HTTPConnection API from httplib.
        The addinfourl return value is a file-like object.  It also
        has methods and attributes including:
            - info(): return a mimetools.Message object for the headers
            - geturl(): return the original request URL
            - code: HTTP status code
        """
951
        host = req.get_host()
Jeremy Hylton's avatar
Jeremy Hylton committed
952 953 954
        if not host:
            raise URLError('no host given')

955
        h = http_class(host) # will parse host:port
956
        h.set_debuglevel(self._debuglevel)
957

958 959
        headers = dict(req.headers)
        headers.update(req.unredirected_hdrs)
960
        try:
961 962 963
            h.request(req.get_method(), req.get_selector(), req.data, headers)
            r = h.getresponse()
        except socket.error, err: # XXX what error?
964
            raise URLError(err)
965 966

        # Pick apart the HTTPResponse object to get the various pieces
Tim Peters's avatar
Tim Peters committed
967
        # of the
968 969 970 971
        resp = addinfourl(r.fp, r.msg, req.get_full_url())
        resp.code = r.status
        resp.msg = r.reason
        return resp
Jeremy Hylton's avatar
Jeremy Hylton committed
972

973 974 975 976

class HTTPHandler(AbstractHTTPHandler):

    def http_open(self, req):
977
        return self.do_open(httplib.HTTPConnection, req)
978

979
    http_request = AbstractHTTPHandler.do_request
980 981 982 983 984

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

        def https_open(self, req):
985
            return self.do_open(httplib.HTTPSConnection, req)
986

987
        https_request = AbstractHTTPHandler.do_request
988

Jeremy Hylton's avatar
Jeremy Hylton committed
989 990
class UnknownHandler(BaseHandler):
    def unknown_open(self, req):
Fred Drake's avatar
Fred Drake committed
991
        type = req.get_type()
Jeremy Hylton's avatar
Jeremy Hylton committed
992 993 994 995 996 997
        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
998
        k, v = elt.split('=', 1)
Fred Drake's avatar
Fred Drake committed
999 1000 1001
        if v[0] == '"' and v[-1] == '"':
            v = v[1:-1]
        parsed[k] = v
Jeremy Hylton's avatar
Jeremy Hylton committed
1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018
    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
1019
        cur = s[i:]
Eric S. Raymond's avatar
Eric S. Raymond committed
1020 1021
        c = cur.find(',')
        q = cur.find('"')
Fred Drake's avatar
Fred Drake committed
1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038
        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:
1039
                i = i + q
Fred Drake's avatar
Fred Drake committed
1040 1041 1042 1043 1044 1045 1046 1047
        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
1048
    return map(lambda x: x.strip(), list)
Jeremy Hylton's avatar
Jeremy Hylton committed
1049 1050 1051 1052

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
1053 1054 1055 1056 1057 1058
        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
1059 1060 1061 1062

    # names for the localhost
    names = None
    def get_names(self):
Fred Drake's avatar
Fred Drake committed
1063
        if FileHandler.names is None:
1064
            FileHandler.names = (socket.gethostbyname('localhost'),
Fred Drake's avatar
Fred Drake committed
1065 1066
                                 socket.gethostbyname(socket.gethostname()))
        return FileHandler.names
Jeremy Hylton's avatar
Jeremy Hylton committed
1067 1068 1069

    # not entirely sure what the rules are here
    def open_local_file(self, req):
Fred Drake's avatar
Fred Drake committed
1070 1071
        host = req.get_host()
        file = req.get_selector()
1072 1073
        localfile = url2pathname(file)
        stats = os.stat(localfile)
1074 1075
        size = stats.st_size
        modified = rfc822.formatdate(stats.st_mtime)
1076 1077
        mtype = mimetypes.guess_type(file)[0]
        headers = mimetools.Message(StringIO(
1078
            'Content-type: %s\nContent-length: %d\nLast-modified: %s\n' %
1079
            (mtype or 'text/plain', size, modified)))
Fred Drake's avatar
Fred Drake committed
1080 1081 1082 1083
        if host:
            host, port = splitport(host)
        if not host or \
           (not port and socket.gethostbyname(host) in self.get_names()):
1084
            return addinfourl(open(localfile, 'rb'),
Fred Drake's avatar
Fred Drake committed
1085 1086
                              headers, 'file:'+file)
        raise URLError('file not on local host')
Jeremy Hylton's avatar
Jeremy Hylton committed
1087 1088 1089

class FTPHandler(BaseHandler):
    def ftp_open(self, req):
Fred Drake's avatar
Fred Drake committed
1090 1091 1092
        host = req.get_host()
        if not host:
            raise IOError, ('ftp error', 'no host given')
1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106
        host, port = splitport(host)
        if port is None:
            port = ftplib.FTP_PORT

        # username/password handling
        user, host = splituser(host)
        if user:
            user, passwd = splitpasswd(user)
        else:
            passwd = None
        host = unquote(host)
        user = unquote(user or '')
        passwd = unquote(passwd or '')

Jeremy Hylton's avatar
Jeremy Hylton committed
1107 1108 1109 1110
        try:
            host = socket.gethostbyname(host)
        except socket.error, msg:
            raise URLError(msg)
Fred Drake's avatar
Fred Drake committed
1111
        path, attrs = splitattr(req.get_selector())
Eric S. Raymond's avatar
Eric S. Raymond committed
1112
        dirs = path.split('/')
1113
        dirs = map(unquote, dirs)
Fred Drake's avatar
Fred Drake committed
1114 1115 1116 1117 1118 1119 1120 1121
        dirs, file = dirs[:-1], dirs[-1]
        if dirs and not dirs[0]:
            dirs = dirs[1:]
        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
1122
                if attr.lower() == 'type' and \
Fred Drake's avatar
Fred Drake committed
1123
                   value in ('a', 'A', 'i', 'I', 'd', 'D'):
Eric S. Raymond's avatar
Eric S. Raymond committed
1124
                    type = value.upper()
Fred Drake's avatar
Fred Drake committed
1125
            fp, retrlen = fw.retrfile(file, type)
1126 1127 1128
            headers = ""
            mtype = mimetypes.guess_type(req.get_full_url())[0]
            if mtype:
1129
                headers += "Content-type: %s\n" % mtype
Fred Drake's avatar
Fred Drake committed
1130
            if retrlen is not None and retrlen >= 0:
1131
                headers += "Content-length: %d\n" % retrlen
1132 1133
            sf = StringIO(headers)
            headers = mimetools.Message(sf)
Fred Drake's avatar
Fred Drake committed
1134 1135 1136
            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
1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150

    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
1151
        self.max_conns = 16
Jeremy Hylton's avatar
Jeremy Hylton committed
1152 1153 1154 1155 1156

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

    def setMaxConns(self, m):
Fred Drake's avatar
Fred Drake committed
1157
        self.max_conns = m
Jeremy Hylton's avatar
Jeremy Hylton committed
1158 1159 1160

    def connect_ftp(self, user, passwd, host, port, dirs):
        key = user, passwd, host, port
1161
        if key in self.cache:
Jeremy Hylton's avatar
Jeremy Hylton committed
1162 1163 1164 1165
            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
1166
        self.check_cache()
Jeremy Hylton's avatar
Jeremy Hylton committed
1167 1168 1169
        return self.cache[key]

    def check_cache(self):
Fred Drake's avatar
Fred Drake committed
1170
        # first check for old ones
Jeremy Hylton's avatar
Jeremy Hylton committed
1171 1172
        t = time.time()
        if self.soonest <= t:
1173
            for k, v in self.timeout.items():
Jeremy Hylton's avatar
Jeremy Hylton committed
1174 1175 1176 1177 1178 1179 1180
                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
1181
        if len(self.cache) == self.max_conns:
1182
            for k, v in self.timeout.items():
Fred Drake's avatar
Fred Drake committed
1183 1184 1185 1186 1187
                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
1188 1189 1190

class GopherHandler(BaseHandler):
    def gopher_open(self, req):
Fred Drake's avatar
Fred Drake committed
1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204
        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
1205 1206 1207 1208 1209

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

    default_handlers = [UnknownHandler, HTTPHandler,
1210
                        HTTPDefaultErrorHandler, HTTPRedirectHandler,
Fred Drake's avatar
Fred Drake committed
1211
                        FTPHandler, FileHandler]
Jeremy Hylton's avatar
Jeremy Hylton committed
1212 1213 1214 1215
    handlers = []
    replacement_handlers = []

    def add_handler(self, h):
Fred Drake's avatar
Fred Drake committed
1216
        self.handlers = self.handlers + [h]
Jeremy Hylton's avatar
Jeremy Hylton committed
1217 1218

    def replace_handler(self, h):
Fred Drake's avatar
Fred Drake committed
1219
        pass
Jeremy Hylton's avatar
Jeremy Hylton committed
1220 1221

    def build_opener(self):
1222
        opener = OpenerDirector()
1223
        for ph in self.default_handlers:
1224
            if inspect.isclass(ph):
Fred Drake's avatar
Fred Drake committed
1225 1226
                ph = ph()
            opener.add_handler(ph)
Jeremy Hylton's avatar
Jeremy Hylton committed
1227 1228

if __name__ == "__main__":
1229
    # XXX some of the test code depends on machine configurations that
Jeremy Hylton's avatar
Jeremy Hylton committed
1230 1231 1232 1233
    # 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
1234
    elif socket.gethostname() == 'bitdiddle.concentric.net':
Jeremy Hylton's avatar
Jeremy Hylton committed
1235 1236 1237 1238
        localhost = 'localhost'
    else:
        localhost = None
    urls = [
Fred Drake's avatar
Fred Drake committed
1239 1240 1241
        # 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
1242

Fred Drake's avatar
Fred Drake committed
1243 1244
        'file:/etc/passwd',
        'file://nonsensename/etc/passwd',
1245
        'ftp://www.python.org/pub/python/misc/sousa.au',
Jeremy Hylton's avatar
Jeremy Hylton committed
1246
        'ftp://www.python.org/pub/tmp/blat',
Fred Drake's avatar
Fred Drake committed
1247 1248
        'http://www.espn.com/', # redirect
        'http://www.python.org/Spanish/Inquistion/',
1249
        ('http://www.python.org/cgi-bin/faqw.py',
Fred Drake's avatar
Fred Drake committed
1250 1251
         'query=pythonistas&querytype=simple&casefold=yes&req=search'),
        'http://www.python.org/',
1252
        'ftp://gatekeeper.research.compaq.com/pub/DEC/SRC/research-reports/00README-Legal-Rules-Regs',
Jeremy Hylton's avatar
Jeremy Hylton committed
1253 1254
            ]

1255 1256 1257 1258 1259 1260 1261
##    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
1262

1263 1264 1265 1266 1267 1268
##        bauth = HTTPBasicAuthHandler()
##        bauth.add_password('basic_test_realm', localhost, 'jhylton',
##                           'password')
##        dauth = HTTPDigestAuthHandler()
##        dauth.add_password('digest_test_realm', localhost, 'jhylton',
##                           'password')
1269

Jeremy Hylton's avatar
Jeremy Hylton committed
1270 1271 1272 1273

    cfh = CacheFTPHandler()
    cfh.setTimeout(1)

1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284
##    # 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
1285 1286

    for url in urls:
1287
        if isinstance(url, tuple):
Jeremy Hylton's avatar
Jeremy Hylton committed
1288 1289 1290 1291 1292 1293 1294
            url, req = url
        else:
            req = None
        print url
        try:
            f = urlopen(url, req)
        except IOError, err:
Fred Drake's avatar
Fred Drake committed
1295 1296 1297
            print "IOError:", err
        except socket.error, err:
            print "socket.error:", err
Jeremy Hylton's avatar
Jeremy Hylton committed
1298 1299 1300 1301 1302 1303
        else:
            buf = f.read()
            f.close()
            print "read %d bytes" % len(buf)
        print
        time.sleep(0.1)