urllib2.py 43.3 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
import base64
import ftplib
Jeremy Hylton's avatar
Jeremy Hylton committed
92
import httplib
93
import inspect
Jeremy Hylton's avatar
Jeremy Hylton committed
94 95 96
import md5
import mimetypes
import mimetools
97 98 99 100 101 102
import os
import posixpath
import random
import re
import sha
import socket
Jeremy Hylton's avatar
Jeremy Hylton committed
103 104
import sys
import time
105
import urlparse
106
import bisect
107
import cookielib
Jeremy Hylton's avatar
Jeremy Hylton committed
108 109 110 111 112 113 114

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

# not sure how many of these need to be gotten rid of
Andrew M. Kuchling's avatar
Andrew M. Kuchling committed
115 116 117
from urllib import (unwrap, unquote, splittype, splithost,
     addinfourl, splitport, splitgophertype, splitquery,
     splitattr, ftpwrapper, noheaders, splituser, splitpasswd, splitvalue)
Jeremy Hylton's avatar
Jeremy Hylton committed
118

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

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

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

class URLError(IOError):
    # URLError is a sub-type of IOError, but it doesn't share any of
141 142 143 144
    # 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
145
    def __init__(self, reason):
146
        self.args = reason,
Fred Drake's avatar
Fred Drake committed
147
        self.reason = reason
Jeremy Hylton's avatar
Jeremy Hylton committed
148 149

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

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

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

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

class GopherError(URLError):
    pass

175

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

178 179
    def __init__(self, url, data=None, headers={},
                 origin_req_host=None, unverifiable=False):
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 = {}
191 192 193 194
        if origin_req_host is None:
            origin_req_host = cookielib.request_host(self)
        self.origin_req_host = origin_req_host
        self.unverifiable = unverifiable
Jeremy Hylton's avatar
Jeremy Hylton committed
195 196

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

208 209 210 211 212 213
    def get_method(self):
        if self.has_data():
            return "POST"
        else:
            return "GET"

214 215
    # XXX these helper methods are lame

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

    def get_host(self):
Fred Drake's avatar
Fred Drake committed
236 237 238 239 240
        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
241 242

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

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

249 250 251 252 253 254
    def get_origin_req_host(self):
        return self.origin_req_host

    def is_unverifiable(self):
        return self.unverifiable

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

259 260 261 262 263
    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):
264 265
        return (header_name in self.headers or
                header_name in self.unredirected_hdrs)
266

267 268 269 270 271 272 273 274 275
    def get_header(self, header_name, default=None):
        return self.headers.get(
            header_name,
            self.unredirected_hdrs.get(header_name, default))

    def header_items(self):
        hdrs = self.unredirected_hdrs.copy()
        hdrs.update(self.headers)
        return hdrs.items()
276

Jeremy Hylton's avatar
Jeremy Hylton committed
277 278
class OpenerDirector:
    def __init__(self):
279 280
        client_version = "Python-urllib/%s" % __version__
        self.addheaders = [('User-agent', client_version)]
Jeremy Hylton's avatar
Jeremy Hylton committed
281 282 283 284
        # manage the individual handlers
        self.handlers = []
        self.handle_open = {}
        self.handle_error = {}
285 286
        self.process_response = {}
        self.process_request = {}
Jeremy Hylton's avatar
Jeremy Hylton committed
287 288

    def add_handler(self, handler):
289
        added = False
290
        for meth in dir(handler):
291 292 293 294 295
            i = meth.find("_")
            protocol = meth[:i]
            condition = meth[i+1:]

            if condition.startswith("error"):
296
                j = condition.find("_") + i + 1
Jeremy Hylton's avatar
Jeremy Hylton committed
297 298
                kind = meth[j+1:]
                try:
Eric S. Raymond's avatar
Eric S. Raymond committed
299
                    kind = int(kind)
Jeremy Hylton's avatar
Jeremy Hylton committed
300 301
                except ValueError:
                    pass
302 303 304 305
                lookup = self.handle_error.get(protocol, {})
                self.handle_error[protocol] = lookup
            elif condition == "open":
                kind = protocol
Raymond Hettinger's avatar
Raymond Hettinger committed
306 307
                lookup = self.handle_open
            elif condition == "response":
308
                kind = protocol
Raymond Hettinger's avatar
Raymond Hettinger committed
309 310 311 312
                lookup = self.process_response
            elif condition == "request":
                kind = protocol
                lookup = self.process_request
313
            else:
Jeremy Hylton's avatar
Jeremy Hylton committed
314
                continue
315 316 317 318 319 320 321 322

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

Jeremy Hylton's avatar
Jeremy Hylton committed
323
        if added:
324 325
            # XXX why does self.handlers need to be sorted?
            bisect.insort(self.handlers, handler)
Jeremy Hylton's avatar
Jeremy Hylton committed
326
            handler.add_parent(self)
327

Jeremy Hylton's avatar
Jeremy Hylton committed
328
    def close(self):
329 330
        # Only exists for backwards compatibility.
        pass
Jeremy Hylton's avatar
Jeremy Hylton committed
331 332 333 334 335 336 337

    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
338 339

            result = func(*args)
Jeremy Hylton's avatar
Jeremy Hylton committed
340 341 342 343
            if result is not None:
                return result

    def open(self, fullurl, data=None):
Fred Drake's avatar
Fred Drake committed
344
        # accept a URL or a Request object
345
        if isinstance(fullurl, basestring):
Fred Drake's avatar
Fred Drake committed
346
            req = Request(fullurl, data)
Jeremy Hylton's avatar
Jeremy Hylton committed
347 348 349 350
        else:
            req = fullurl
            if data is not None:
                req.add_data(data)
351

352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
        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
371
        result = self._call_chain(self.handle_open, 'default',
372
                                  'default_open', req)
Jeremy Hylton's avatar
Jeremy Hylton committed
373 374 375
        if result:
            return result

376 377
        protocol = req.get_type()
        result = self._call_chain(self.handle_open, protocol, protocol +
Jeremy Hylton's avatar
Jeremy Hylton committed
378
                                  '_open', req)
Jeremy Hylton's avatar
Jeremy Hylton committed
379 380 381 382 383 384 385
        if result:
            return result

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

    def error(self, proto, *args):
386
        if proto in ('http', 'https'):
387 388
            # 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
389
            proto = args[2]  # YUCK!
390
            meth_name = 'http_error_%s' % proto
Jeremy Hylton's avatar
Jeremy Hylton committed
391 392 393 394 395 396 397
            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
398
        result = self._call_chain(*args)
Jeremy Hylton's avatar
Jeremy Hylton committed
399 400 401 402 403
        if result:
            return result

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

406 407 408
# 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
409 410 411 412 413

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

    The opener will use several default handlers, including support
414
    for HTTP and FTP.
Jeremy Hylton's avatar
Jeremy Hylton committed
415 416 417 418

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

Jeremy Hylton's avatar
Jeremy Hylton committed
420 421 422
    opener = OpenerDirector()
    default_classes = [ProxyHandler, UnknownHandler, HTTPHandler,
                       HTTPDefaultErrorHandler, HTTPRedirectHandler,
423
                       FTPHandler, FileHandler, HTTPErrorProcessor]
424 425
    if hasattr(httplib, 'HTTPS'):
        default_classes.append(HTTPSHandler)
Jeremy Hylton's avatar
Jeremy Hylton committed
426 427 428
    skip = []
    for klass in default_classes:
        for check in handlers:
429
            if inspect.isclass(check):
Jeremy Hylton's avatar
Jeremy Hylton committed
430 431
                if issubclass(check, klass):
                    skip.append(klass)
432 433
            elif isinstance(check, klass):
                skip.append(klass)
Jeremy Hylton's avatar
Jeremy Hylton committed
434 435 436 437 438 439 440
    for klass in skip:
        default_classes.remove(klass)

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

    for h in handlers:
441
        if inspect.isclass(h):
Jeremy Hylton's avatar
Jeremy Hylton committed
442 443 444 445 446
            h = h()
        opener.add_handler(h)
    return opener

class BaseHandler:
447 448
    handler_order = 500

Jeremy Hylton's avatar
Jeremy Hylton committed
449 450
    def add_parent(self, parent):
        self.parent = parent
Tim Peters's avatar
Tim Peters committed
451

Jeremy Hylton's avatar
Jeremy Hylton committed
452
    def close(self):
453 454
        # Only exists for backwards compatibility
        pass
Tim Peters's avatar
Tim Peters committed
455

456 457 458 459 460 461 462
    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
463

Jeremy Hylton's avatar
Jeremy Hylton committed
464

465 466 467 468 469 470 471
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()

472
        if code not in (200, 206):
473 474 475 476 477 478 479
            response = self.parent.error(
                'http', request, response, code, msg, hdrs)

        return response

    https_response = http_response

Jeremy Hylton's avatar
Jeremy Hylton committed
480 481
class HTTPDefaultErrorHandler(BaseHandler):
    def http_error_default(self, req, fp, code, msg, hdrs):
Fred Drake's avatar
Fred Drake committed
482
        raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
Jeremy Hylton's avatar
Jeremy Hylton committed
483 484

class HTTPRedirectHandler(BaseHandler):
485 486 487 488 489
    # maximum number of redirections to any single URL
    # this is needed because of the state that cookies introduce
    max_repeats = 4
    # maximum total number of redirections (regardless of URL) before
    # assuming we're in a loop
490 491
    max_redirections = 10

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

495 496 497 498 499 500
        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.
501
        """
502 503
        m = req.get_method()
        if (code in (301, 302, 303, 307) and m in ("GET", "HEAD")
504 505 506
            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
507 508 509
            # from the user (of urllib2, in this case).  In practice,
            # essentially all clients do redirect in this case, so we
            # do the same.
510 511 512 513
            return Request(newurl,
                           headers=req.headers,
                           origin_req_host=req.get_origin_req_host(),
                           unverifiable=True)
514
        else:
515
            raise HTTPError(req.get_full_url(), code, msg, headers, fp)
516

Jeremy Hylton's avatar
Jeremy Hylton committed
517 518 519 520 521
    # 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):
522 523
        # Some servers (incorrectly) return multiple Location headers
        # (so probably same goes for URI).  Use first header.
524
        if 'location' in headers:
525
            newurl = headers.getheaders('location')[0]
526
        elif 'uri' in headers:
527
            newurl = headers.getheaders('uri')[0]
Jeremy Hylton's avatar
Jeremy Hylton committed
528 529
        else:
            return
Jeremy Hylton's avatar
Jeremy Hylton committed
530 531
        newurl = urlparse.urljoin(req.get_full_url(), newurl)

Jeremy Hylton's avatar
Jeremy Hylton committed
532 533 534
        # 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
535
        new = self.redirect_request(req, fp, code, msg, headers, newurl)
536 537 538 539
        if new is None:
            return

        # loop detection
540
        # .redirect_dict has a key url if url was previously visited.
541 542
        if hasattr(req, 'redirect_dict'):
            visited = new.redirect_dict = req.redirect_dict
543 544
            if (visited.get(newurl, 0) >= self.max_repeats or
                len(visited) >= self.max_redirections):
Jeremy Hylton's avatar
Jeremy Hylton committed
545
                raise HTTPError(req.get_full_url(), code,
546
                                self.inf_msg + msg, headers, fp)
547 548
        else:
            visited = new.redirect_dict = req.redirect_dict = {}
549
        visited[newurl] = visited.get(newurl, 0) + 1
550 551

        # Don't close the fp until we are sure that we won't use it
Tim Peters's avatar
Tim Peters committed
552
        # with HTTPError.
553 554 555
        fp.read()
        fp.close()

Jeremy Hylton's avatar
Jeremy Hylton committed
556 557
        return self.parent.open(new)

558
    http_error_301 = http_error_303 = http_error_307 = http_error_302
Jeremy Hylton's avatar
Jeremy Hylton committed
559

560
    inf_msg = "The HTTP server returned a redirect error that would " \
561
              "lead to an infinite loop.\n" \
562
              "The last 30x error message was:\n"
Jeremy Hylton's avatar
Jeremy Hylton committed
563 564

class ProxyHandler(BaseHandler):
565 566 567
    # Proxies must be in front
    handler_order = 100

Jeremy Hylton's avatar
Jeremy Hylton committed
568
    def __init__(self, proxies=None):
Fred Drake's avatar
Fred Drake committed
569 570 571 572
        if proxies is None:
            proxies = getproxies()
        assert hasattr(proxies, 'has_key'), "proxies must be a mapping"
        self.proxies = proxies
573
        for type, url in proxies.items():
574
            setattr(self, '%s_open' % type,
Fred Drake's avatar
Fred Drake committed
575 576
                    lambda r, proxy=url, type=type, meth=self.proxy_open: \
                    meth(r, proxy, type))
Jeremy Hylton's avatar
Jeremy Hylton committed
577 578

    def proxy_open(self, req, proxy, type):
Fred Drake's avatar
Fred Drake committed
579
        orig_type = req.get_type()
580
        type, r_type = splittype(proxy)
581 582 583 584 585 586 587 588 589 590 591 592 593
        if not type or r_type.isdigit():
            # proxy is specified without protocol
            type = orig_type
            host = proxy
        else:
            host, r_host = splithost(r_type)
        user_pass, host = splituser(host)
        user, password = splitpasswd(user_pass)
        if user and password:
            user, password = user_pass.split(':', 1)
            user_pass = base64.encodestring('%s:%s' % (unquote(user),
                                            unquote(password))).strip()
            req.add_header('Proxy-authorization', 'Basic ' + user_pass)
594 595
        host = unquote(host)
        req.set_proxy(host, type)
Fred Drake's avatar
Fred Drake committed
596 597 598 599 600 601 602 603 604
        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
605 606 607 608 609 610

# 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
611 612 613
        self.proto = proto
        self.func = func
        self.addr = proxy_addr
Jeremy Hylton's avatar
Jeremy Hylton committed
614 615

    def handle(self, req):
Fred Drake's avatar
Fred Drake committed
616 617
        if self.func and self.func(req):
            return 1
Jeremy Hylton's avatar
Jeremy Hylton committed
618 619

    def get_proxy(self):
Fred Drake's avatar
Fred Drake committed
620
        return self.addr
Jeremy Hylton's avatar
Jeremy Hylton committed
621 622

class CustomProxyHandler(BaseHandler):
623 624 625
    # Proxies must be in front
    handler_order = 100

Jeremy Hylton's avatar
Jeremy Hylton committed
626
    def __init__(self, *proxies):
Fred Drake's avatar
Fred Drake committed
627
        self.proxies = {}
Jeremy Hylton's avatar
Jeremy Hylton committed
628 629

    def proxy_open(self, req):
Fred Drake's avatar
Fred Drake committed
630 631 632 633 634 635 636 637 638 639
        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
640 641

    def do_proxy(self, p, req):
Fred Drake's avatar
Fred Drake committed
642
        return self.parent.open(req)
Jeremy Hylton's avatar
Jeremy Hylton committed
643 644

    def add_proxy(self, cpo):
645
        if cpo.proto in self.proxies:
Fred Drake's avatar
Fred Drake committed
646 647 648
            self.proxies[cpo.proto].append(cpo)
        else:
            self.proxies[cpo.proto] = [cpo]
Jeremy Hylton's avatar
Jeremy Hylton committed
649 650 651

class HTTPPasswordMgr:
    def __init__(self):
Fred Drake's avatar
Fred Drake committed
652
        self.passwd = {}
Jeremy Hylton's avatar
Jeremy Hylton committed
653 654

    def add_password(self, realm, uri, user, passwd):
Fred Drake's avatar
Fred Drake committed
655
        # uri could be a single URI or a sequence
656
        if isinstance(uri, basestring):
Fred Drake's avatar
Fred Drake committed
657 658
            uri = [uri]
        uri = tuple(map(self.reduce_uri, uri))
659
        if not realm in self.passwd:
Fred Drake's avatar
Fred Drake committed
660 661
            self.passwd[realm] = {}
        self.passwd[realm][uri] = (user, passwd)
Jeremy Hylton's avatar
Jeremy Hylton committed
662 663

    def find_user_password(self, realm, authuri):
Fred Drake's avatar
Fred Drake committed
664 665
        domains = self.passwd.get(realm, {})
        authuri = self.reduce_uri(authuri)
666
        for uris, authinfo in domains.iteritems():
Fred Drake's avatar
Fred Drake committed
667 668 669 670
            for uri in uris:
                if self.is_suburi(uri, authuri):
                    return authinfo
        return None, None
Jeremy Hylton's avatar
Jeremy Hylton committed
671 672

    def reduce_uri(self, uri):
Fred Drake's avatar
Fred Drake committed
673 674 675 676 677 678
        """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
679 680

    def is_suburi(self, base, test):
Fred Drake's avatar
Fred Drake committed
681 682 683 684 685
        """Check if test is below base in a URI tree

        Both args must be URIs in reduced form.
        """
        if base == test:
686
            return True
Fred Drake's avatar
Fred Drake committed
687
        if base[0] != test[0]:
688
            return False
689
        common = posixpath.commonprefix((base[1], test[1]))
Fred Drake's avatar
Fred Drake committed
690
        if len(common) == len(base[1]):
691 692
            return True
        return False
693

Jeremy Hylton's avatar
Jeremy Hylton committed
694

695 696 697
class HTTPPasswordMgrWithDefaultRealm(HTTPPasswordMgr):

    def find_user_password(self, realm, authuri):
698 699
        user, password = HTTPPasswordMgr.find_user_password(self, realm,
                                                            authuri)
700 701 702 703 704 705 706
        if user is not None:
            return user, password
        return HTTPPasswordMgr.find_user_password(self, None, authuri)


class AbstractBasicAuthHandler:

707
    rx = re.compile('[ \t]*([^ \t]+)[ \t]+realm="([^"]*)"', re.I)
Jeremy Hylton's avatar
Jeremy Hylton committed
708 709 710 711 712

    # 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

713 714 715 716
    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
717
        self.add_password = self.passwd.add_password
718

719 720 721
    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
722
        if authreq:
723
            mo = AbstractBasicAuthHandler.rx.search(authreq)
Jeremy Hylton's avatar
Jeremy Hylton committed
724 725
            if mo:
                scheme, realm = mo.groups()
Eric S. Raymond's avatar
Eric S. Raymond committed
726
                if scheme.lower() == 'basic':
727
                    return self.retry_http_basic_auth(host, req, realm)
Jeremy Hylton's avatar
Jeremy Hylton committed
728

729
    def retry_http_basic_auth(self, host, req, realm):
730 731 732 733
        # TODO(jhylton): Remove the host argument? It depends on whether
        # retry_http_basic_auth() is consider part of the public API.
        # It probably is.
        user, pw = self.passwd.find_user_password(realm, req.get_full_url())
734
        if pw is not None:
Fred Drake's avatar
Fred Drake committed
735
            raw = "%s:%s" % (user, pw)
736 737 738 739 740
            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
741 742 743
        else:
            return None

744
class HTTPBasicAuthHandler(AbstractBasicAuthHandler, BaseHandler):
Jeremy Hylton's avatar
Jeremy Hylton committed
745

746
    auth_header = 'Authorization'
Jeremy Hylton's avatar
Jeremy Hylton committed
747

748 749
    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
750
        return self.http_error_auth_reqed('www-authenticate',
751 752 753 754 755
                                          host, req, headers)


class ProxyBasicAuthHandler(AbstractBasicAuthHandler, BaseHandler):

756
    auth_header = 'Proxy-authorization'
757 758 759

    def http_error_407(self, req, fp, code, msg, headers):
        host = req.get_host()
Tim Peters's avatar
Tim Peters committed
760
        return self.http_error_auth_reqed('proxy-authenticate',
761 762 763
                                          host, req, headers)


764 765 766 767 768 769 770 771 772 773 774 775 776 777
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)

778
class AbstractDigestAuthHandler:
779 780 781 782 783 784 785 786 787
    # 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
788 789 790

    def __init__(self, passwd=None):
        if passwd is None:
791
            passwd = HTTPPasswordMgr()
792
        self.passwd = passwd
Fred Drake's avatar
Fred Drake committed
793
        self.add_password = self.passwd.add_password
794 795 796 797 798 799 800 801 802 803 804 805 806 807
        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
808
            raise HTTPError(req.get_full_url(), 401, "digest auth failed",
809 810 811
                            headers, None)
        else:
            self.retried += 1
Fred Drake's avatar
Fred Drake committed
812
        if authreq:
813 814
            scheme = authreq.split()[0]
            if scheme.lower() == 'digest':
Fred Drake's avatar
Fred Drake committed
815
                return self.retry_http_digest_auth(req, authreq)
816 817 818
            else:
                raise ValueError("AbstractDigestAuthHandler doesn't know "
                                 "about %s"%(scheme))
Jeremy Hylton's avatar
Jeremy Hylton committed
819 820

    def retry_http_digest_auth(self, req, auth):
Eric S. Raymond's avatar
Eric S. Raymond committed
821
        token, challenge = auth.split(' ', 1)
Fred Drake's avatar
Fred Drake committed
822 823 824
        chal = parse_keqv_list(parse_http_list(challenge))
        auth = self.get_authorization(req, chal)
        if auth:
825 826 827 828
            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
829 830
            resp = self.parent.open(req)
            return resp
Jeremy Hylton's avatar
Jeremy Hylton committed
831

832 833 834 835 836 837 838 839 840 841
    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
842
    def get_authorization(self, req, chal):
Fred Drake's avatar
Fred Drake committed
843 844 845
        try:
            realm = chal['realm']
            nonce = chal['nonce']
846
            qop = chal.get('qop')
Fred Drake's avatar
Fred Drake committed
847 848 849 850 851 852 853 854 855 856 857
            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

858
        user, pw = self.passwd.find_user_password(realm, req.get_full_url())
Fred Drake's avatar
Fred Drake committed
859 860 861 862 863 864 865 866 867 868
        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)
869
        A2 = "%s:%s" % (req.get_method(),
Fred Drake's avatar
Fred Drake committed
870 871
                        # XXX selector: what about proxies and full urls
                        req.get_selector())
872 873 874 875 876 877 878 879 880 881 882
        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
883

Fred Drake's avatar
Fred Drake committed
884 885 886 887 888 889
        # 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:
890
            base += ', opaque="%s"' % opaque
Fred Drake's avatar
Fred Drake committed
891
        if entdig:
892 893
            base += ', digest="%s"' % entdig
        base += ', algorithm="%s"' % algorithm
894
        if qop:
895
            base += ', qop=auth, nc=%s, cnonce="%s"' % (ncvalue, cnonce)
Fred Drake's avatar
Fred Drake committed
896
        return base
Jeremy Hylton's avatar
Jeremy Hylton committed
897 898

    def get_algorithm_impls(self, algorithm):
Fred Drake's avatar
Fred Drake committed
899 900
        # lambdas assume digest modules are imported at the top level
        if algorithm == 'MD5':
901
            H = lambda x: md5.new(x).hexdigest()
Fred Drake's avatar
Fred Drake committed
902
        elif algorithm == 'SHA':
903
            H = lambda x: sha.new(x).hexdigest()
Fred Drake's avatar
Fred Drake committed
904
        # XXX MD5-sess
905
        KD = lambda s, d: H("%s:%s" % (s, d))
Fred Drake's avatar
Fred Drake committed
906
        return H, KD
Jeremy Hylton's avatar
Jeremy Hylton committed
907 908

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

912 913 914 915 916 917 918 919

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

920
    auth_header = 'Authorization'
921 922 923

    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
924
        retry = self.http_error_auth_reqed('www-authenticate',
925 926 927
                                           host, req, headers)
        self.reset_retry_count()
        return retry
928 929 930 931


class ProxyDigestAuthHandler(BaseHandler, AbstractDigestAuthHandler):

932
    auth_header = 'Proxy-Authorization'
933 934 935

    def http_error_407(self, req, fp, code, msg, headers):
        host = req.get_host()
Tim Peters's avatar
Tim Peters committed
936
        retry = self.http_error_auth_reqed('proxy-authenticate',
937 938 939
                                           host, req, headers)
        self.reset_retry_count()
        return retry
940

941 942
class AbstractHTTPHandler(BaseHandler):

943 944 945 946 947 948
    def __init__(self, debuglevel=0):
        self._debuglevel = debuglevel

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

949
    def do_request_(self, request):
950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974
        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

975
    def do_open(self, http_class, req):
976 977 978 979 980 981 982 983 984
        """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
        """
985
        host = req.get_host()
Jeremy Hylton's avatar
Jeremy Hylton committed
986 987 988
        if not host:
            raise URLError('no host given')

989
        h = http_class(host) # will parse host:port
990
        h.set_debuglevel(self._debuglevel)
991

992 993
        headers = dict(req.headers)
        headers.update(req.unredirected_hdrs)
994 995 996 997 998 999 1000
        # We want to make an HTTP/1.1 request, but the addinfourl
        # class isn't prepared to deal with a persistent connection.
        # It will try to read all remaining data from the socket,
        # which will block while the server waits for the next request.
        # So make sure the connection gets closed after the (only)
        # request.
        headers["Connection"] = "close"
1001
        try:
1002 1003 1004
            h.request(req.get_method(), req.get_selector(), req.data, headers)
            r = h.getresponse()
        except socket.error, err: # XXX what error?
1005
            raise URLError(err)
1006

1007
        # Pick apart the HTTPResponse object to get the addinfourl
1008 1009 1010 1011 1012 1013
        # object initialized properly.

        # Wrap the HTTPResponse object in socket's file object adapter
        # for Windows.  That adapter calls recv(), so delegate recv()
        # to read().  This weird wrapping allows the returned object to
        # have readline() and readlines() methods.
Tim Peters's avatar
Tim Peters committed
1014

1015 1016
        # XXX It might be better to extract the read buffering code
        # out of socket._fileobject() and into a base class.
Tim Peters's avatar
Tim Peters committed
1017

1018 1019
        r.recv = r.read
        fp = socket._fileobject(r)
Tim Peters's avatar
Tim Peters committed
1020

1021
        resp = addinfourl(fp, r.msg, req.get_full_url())
1022 1023 1024
        resp.code = r.status
        resp.msg = r.reason
        return resp
Jeremy Hylton's avatar
Jeremy Hylton committed
1025

1026 1027 1028 1029

class HTTPHandler(AbstractHTTPHandler):

    def http_open(self, req):
1030
        return self.do_open(httplib.HTTPConnection, req)
1031

1032
    http_request = AbstractHTTPHandler.do_request_
1033 1034 1035 1036 1037

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

        def https_open(self, req):
1038
            return self.do_open(httplib.HTTPSConnection, req)
1039

1040 1041 1042 1043 1044
        https_request = AbstractHTTPHandler.do_request_

class HTTPCookieProcessor(BaseHandler):
    def __init__(self, cookiejar=None):
        if cookiejar is None:
1045
            cookiejar = cookielib.CookieJar()
1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057
        self.cookiejar = cookiejar

    def http_request(self, request):
        self.cookiejar.add_cookie_header(request)
        return request

    def http_response(self, request, response):
        self.cookiejar.extract_cookies(response, request)
        return response

    https_request = http_request
    https_response = http_response
1058

Jeremy Hylton's avatar
Jeremy Hylton committed
1059 1060
class UnknownHandler(BaseHandler):
    def unknown_open(self, req):
Fred Drake's avatar
Fred Drake committed
1061
        type = req.get_type()
Jeremy Hylton's avatar
Jeremy Hylton committed
1062 1063 1064 1065 1066 1067
        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
1068
        k, v = elt.split('=', 1)
Fred Drake's avatar
Fred Drake committed
1069 1070 1071
        if v[0] == '"' and v[-1] == '"':
            v = v[1:-1]
        parsed[k] = v
Jeremy Hylton's avatar
Jeremy Hylton committed
1072 1073 1074 1075
    return parsed

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

Andrew M. Kuchling's avatar
Andrew M. Kuchling committed
1077
    In particular, parse comma-separated lists where the elements of
Jeremy Hylton's avatar
Jeremy Hylton committed
1078
    the list may include quoted-strings.  A quoted-string could
1079 1080 1081
    contain a comma.  A non-quoted string could have quotes in the
    middle.  Neither commas nor quotes count if they are escaped.
    Only double-quotes count, not single-quotes.
Jeremy Hylton's avatar
Jeremy Hylton committed
1082
    """
1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094
    res = []
    part = ''

    escape = quote = False
    for cur in s:
        if escape:
            part += cur
            escape = False
            continue
        if quote:
            if cur == '\\':
                escape = True
Fred Drake's avatar
Fred Drake committed
1095
                continue
1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107
            elif cur == '"':
                quote = False
            part += cur
            continue

        if cur == ',':
            res.append(part)
            part = ''
            continue

        if cur == '"':
            quote = True
1108

1109 1110 1111 1112 1113 1114 1115
        part += cur

    # append last part
    if part:
        res.append(part)

    return [part.strip() for part in res]
Jeremy Hylton's avatar
Jeremy Hylton committed
1116 1117 1118 1119

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
1120 1121 1122 1123 1124 1125
        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
1126 1127 1128 1129

    # names for the localhost
    names = None
    def get_names(self):
Fred Drake's avatar
Fred Drake committed
1130
        if FileHandler.names is None:
1131
            FileHandler.names = (socket.gethostbyname('localhost'),
Fred Drake's avatar
Fred Drake committed
1132 1133
                                 socket.gethostbyname(socket.gethostname()))
        return FileHandler.names
Jeremy Hylton's avatar
Jeremy Hylton committed
1134 1135 1136

    # not entirely sure what the rules are here
    def open_local_file(self, req):
1137
        import email.Utils
Fred Drake's avatar
Fred Drake committed
1138 1139
        host = req.get_host()
        file = req.get_selector()
1140 1141
        localfile = url2pathname(file)
        stats = os.stat(localfile)
1142
        size = stats.st_size
1143
        modified = email.Utils.formatdate(stats.st_mtime, usegmt=True)
1144 1145
        mtype = mimetypes.guess_type(file)[0]
        headers = mimetools.Message(StringIO(
1146
            'Content-type: %s\nContent-length: %d\nLast-modified: %s\n' %
1147
            (mtype or 'text/plain', size, modified)))
Fred Drake's avatar
Fred Drake committed
1148 1149 1150 1151
        if host:
            host, port = splitport(host)
        if not host or \
           (not port and socket.gethostbyname(host) in self.get_names()):
1152
            return addinfourl(open(localfile, 'rb'),
Fred Drake's avatar
Fred Drake committed
1153 1154
                              headers, 'file:'+file)
        raise URLError('file not on local host')
Jeremy Hylton's avatar
Jeremy Hylton committed
1155 1156 1157

class FTPHandler(BaseHandler):
    def ftp_open(self, req):
Fred Drake's avatar
Fred Drake committed
1158 1159 1160
        host = req.get_host()
        if not host:
            raise IOError, ('ftp error', 'no host given')
1161 1162 1163
        host, port = splitport(host)
        if port is None:
            port = ftplib.FTP_PORT
1164 1165
        else:
            port = int(port)
1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176

        # 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
1177 1178 1179 1180
        try:
            host = socket.gethostbyname(host)
        except socket.error, msg:
            raise URLError(msg)
Fred Drake's avatar
Fred Drake committed
1181
        path, attrs = splitattr(req.get_selector())
Eric S. Raymond's avatar
Eric S. Raymond committed
1182
        dirs = path.split('/')
1183
        dirs = map(unquote, dirs)
Fred Drake's avatar
Fred Drake committed
1184 1185 1186 1187 1188 1189 1190
        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:
1191
                attr, value = splitvalue(attr)
Eric S. Raymond's avatar
Eric S. Raymond committed
1192
                if attr.lower() == 'type' and \
Fred Drake's avatar
Fred Drake committed
1193
                   value in ('a', 'A', 'i', 'I', 'd', 'D'):
Eric S. Raymond's avatar
Eric S. Raymond committed
1194
                    type = value.upper()
Fred Drake's avatar
Fred Drake committed
1195
            fp, retrlen = fw.retrfile(file, type)
1196 1197 1198
            headers = ""
            mtype = mimetypes.guess_type(req.get_full_url())[0]
            if mtype:
1199
                headers += "Content-type: %s\n" % mtype
Fred Drake's avatar
Fred Drake committed
1200
            if retrlen is not None and retrlen >= 0:
1201
                headers += "Content-length: %d\n" % retrlen
1202 1203
            sf = StringIO(headers)
            headers = mimetools.Message(sf)
Fred Drake's avatar
Fred Drake committed
1204 1205 1206
            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
1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220

    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
1221
        self.max_conns = 16
Jeremy Hylton's avatar
Jeremy Hylton committed
1222 1223 1224 1225 1226

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

    def setMaxConns(self, m):
Fred Drake's avatar
Fred Drake committed
1227
        self.max_conns = m
Jeremy Hylton's avatar
Jeremy Hylton committed
1228 1229

    def connect_ftp(self, user, passwd, host, port, dirs):
1230
        key = user, host, port, '/'.join(dirs)
1231
        if key in self.cache:
Jeremy Hylton's avatar
Jeremy Hylton committed
1232 1233 1234 1235
            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
1236
        self.check_cache()
Jeremy Hylton's avatar
Jeremy Hylton committed
1237 1238 1239
        return self.cache[key]

    def check_cache(self):
Fred Drake's avatar
Fred Drake committed
1240
        # first check for old ones
Jeremy Hylton's avatar
Jeremy Hylton committed
1241 1242
        t = time.time()
        if self.soonest <= t:
1243
            for k, v in self.timeout.items():
Jeremy Hylton's avatar
Jeremy Hylton committed
1244 1245 1246 1247 1248 1249 1250
                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
1251
        if len(self.cache) == self.max_conns:
1252
            for k, v in self.timeout.items():
Fred Drake's avatar
Fred Drake committed
1253 1254 1255 1256 1257
                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
1258 1259 1260

class GopherHandler(BaseHandler):
    def gopher_open(self, req):
1261
        import gopherlib  # this raises DeprecationWarning in 2.5
Fred Drake's avatar
Fred Drake committed
1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275
        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
1276 1277 1278 1279 1280

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

    default_handlers = [UnknownHandler, HTTPHandler,
1281
                        HTTPDefaultErrorHandler, HTTPRedirectHandler,
Fred Drake's avatar
Fred Drake committed
1282
                        FTPHandler, FileHandler]
Jeremy Hylton's avatar
Jeremy Hylton committed
1283 1284 1285 1286
    handlers = []
    replacement_handlers = []

    def add_handler(self, h):
Fred Drake's avatar
Fred Drake committed
1287
        self.handlers = self.handlers + [h]
Jeremy Hylton's avatar
Jeremy Hylton committed
1288 1289

    def replace_handler(self, h):
Fred Drake's avatar
Fred Drake committed
1290
        pass
Jeremy Hylton's avatar
Jeremy Hylton committed
1291 1292

    def build_opener(self):
1293
        opener = OpenerDirector()
1294
        for ph in self.default_handlers:
1295
            if inspect.isclass(ph):
Fred Drake's avatar
Fred Drake committed
1296 1297
                ph = ph()
            opener.add_handler(ph)