From 6d7e47b8ea1b8cf82927dacc364689b8eeb8708b Mon Sep 17 00:00:00 2001 From: Jeremy Hylton Date: Thu, 20 Jan 2000 18:19:08 +0000 Subject: [PATCH] EXPERIMENTAL An extensible library for opening URLs using a variety protocols. Intended as a replacement for urllib. --- Lib/urllib2.py | 1063 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1063 insertions(+) create mode 100644 Lib/urllib2.py diff --git a/Lib/urllib2.py b/Lib/urllib2.py new file mode 100644 index 00000000000..40a6715a7fb --- /dev/null +++ b/Lib/urllib2.py @@ -0,0 +1,1063 @@ +"""An extensible library for opening URLs using a variety protocols + +The simplest way to use this module is to call the urlopen function, +which accepts a string containing a URL or a Request object (described +below). It opens the URL and returns the results as file-like +object; the returned object has some extra methods described below. + +The OpenerDirectory manages a collection of Handler objects that do +all the actual work. Each Handler implements a particular protocol or +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 +HTTP 301 & 302 redirect errors, and the HTTPDigestAuthHandler deals +with digest authentication. + +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 +get a file-like object back. One difference is that you can also pass +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 + +HTTPError-- also a valid HTTP response, so you can treat an HTTP error +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') + +# build a new opener that adds authentication and caching FTP handlers +opener = urllib2.build_opener(authinfo, urllib2.CacheFTPHandler) + +# install it +urllib2.install_opener(opener) + +f = urllib2.urlopen('http://www.python.org/') + + +""" + +# XXX issues: +# If an authentication error handler that tries to perform + # 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. + +# XXX to do: +# name! +# documentation (getting there) +# complex proxies +# abstract factory for opener +# ftp errors aren't handled cleanly +# gopher can return a socket.error +# check digest against correct (i.e. non-apache) implementation + +import string +import socket +import UserDict +import httplib +import re +import base64 +import types +import urlparse +import os +import md5 +import mimetypes +import mimetools +import ftplib +import sys +import time +import gopherlib + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +try: + import sha +except ImportError: + # need 1.5.2 final + sha = None + +# not sure how many of these need to be gotten rid of +from urllib import unwrap, unquote, splittype, splithost, \ + addinfourl, splitport, splitgophertype, splitquery, \ + splitattr, ftpwrapper, noheaders + +# support for proxies via environment variables +from urllib import getproxies + +# support for FileHandler +from urllib import localhost, thishost, url2pathname, pathname2url + +# support for GopherHandler +from urllib import splitgophertype, splitquery + +__version__ = "2.0a1" + +_opener = None +def urlopen(url, data=None): + global _opener + if _opener is None: + _opener = build_opener() + return _opener.open(url, data) + +def install_opener(opener): + global _opener + _opener = opener + +# do these error classes make sense? +# make sure all of the IOError stuff is overriden. we just want to be + # subtypes. + +class URLError(IOError): + # URLError is a sub-type of IOError, but it doesn't share any of + # the implementation. need to override __init__ and __str__ + def __init__(self, reason): + self.reason = reason + + def __str__(self): + return '' % self.reason + +class HTTPError(URLError, addinfourl): + """Raised when HTTP error occurs, but also acts like non-error return""" + + def __init__(self, url, code, msg, hdrs, fp): + addinfourl.__init__(self, fp, hdrs, url) + self.code = code + self.msg = msg + self.hdrs = hdrs + self.fp = fp + # XXX + self.filename = url + + def __str__(self): + return 'HTTP Error %s: %s' % (self.code, self.msg) + + def __del__(self): + # XXX is this safe? what if user catches exception, then + # extracts fp and discards exception? + self.fp.close() + +class GopherError(URLError): + pass + +class Request: + def __init__(self, url, data=None, headers={}): + # unwrap('') --> '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 + self.data = data + self.headers = {} + self.headers.update(headers) + + def __getattr__(self, attr): + # XXX this is a fallback mechanism to guard against these + # methods getting called in a non-standard order. this may be + # 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 + + 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): + if self.type is None: + self.type, self.__r_type = splittype(self.__original) + return self.type + + def get_host(self): + 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 + + def get_selector(self): + return self.__r_host + + def set_proxy(self, proxy): + self.__proxy = proxy + # XXX this code is based on urllib, but it doesn't seem + # correct. specifically, if the proxy has a port number then + # splittype will return the hostname as the type and the port + # will be include with everything else + self.type, self.__r_type = splittype(self.__proxy) + self.host, XXX = splithost(self.__r_type) + self.host = unquote(self.host) + self.__r_host = self.__original + + def add_header(self, key, val): + # useful for something like authentication + self.headers[key] = val + +class OpenerDirector: + def __init__(self): + server_version = "Python-urllib/%s" % __version__ + self.addheaders = [('User-agent', server_version)] + # manage the individual handlers + self.handlers = [] + self.handle_open = {} + self.handle_error = {} + + def add_handler(self, handler): + added = 0 + for meth in get_methods(handler): + if meth[-5:] == '_open': + protocol = meth[:-5] + if self.handle_open.has_key(protocol): + self.handle_open[protocol].append(handler) + else: + self.handle_open[protocol] = [handler] + added = 1 + continue + i = string.find(meth, '_') + j = string.find(meth[i+1:], '_') + i + 1 + if j != -1 and meth[i+1:j] == 'error': + proto = meth[:i] + kind = meth[j+1:] + try: + kind = string.atoi(kind) + except ValueError: + pass + dict = self.handle_error.get(proto, {}) + if dict.has_key(kind): + dict[kind].append(handler) + else: + dict[kind] = [handler] + self.handle_error[proto] = dict + added = 1 + continue + if added: + self.handlers.append(handler) + handler.add_parent(self) + + def __del__(self): + self.close() + + def close(self): + for handler in self.handlers: + handler.close() + self.handlers = [] + + def _call_chain(self, chain, kind, meth_name, *args): + # XXX raise an exception if no one else should try to handle + # this url. return None if you can't but someone else could. + handlers = chain.get(kind, ()) + for handler in handlers: + func = getattr(handler, meth_name) + result = apply(func, args) + if result is not None: + return result + + def open(self, fullurl, data=None): + # accept a URL or a Request object + if type(fullurl) == types.StringType: + req = Request(fullurl, data) + else: + req = fullurl + if data is not None: + req.add_data(data) + assert isinstance(req, Request) # really only care about interface + + result = self._call_chain(self.handle_open, 'default', + 'default_open', req) + if result: + return result + + type_ = req.get_type() + result = self._call_chain(self.handle_open, type_, type_ + \ + '_open', req) + if result: + return result + + return self._call_chain(self.handle_open, 'unknown', + 'unknown_open', req) + + def error(self, proto, *args): + if proto == 'http': + # XXX http protocol is special cased + dict = self.handle_error[proto] + proto = args[2] # YUCK! + meth_name = 'http_error_%d' % proto + http_err = 1 + orig_args = args + else: + dict = self.handle_error + meth_name = proto + '_error' + http_err = 0 + args = (dict, proto, meth_name) + args + result = apply(self._call_chain, args) + if result: + return result + + if http_err: + args = (dict, 'default', 'http_error_default') + orig_args + return apply(self._call_chain, args) + +def is_callable(obj): + # not quite like builtin callable (which I didn't know existed), + # not entirely sure it needs to be different + if type(obj) in (types.BuiltinFunctionType, + types.BuiltinMethodType, types.LambdaType, + types.MethodType): + return 1 + if type(obj) == types.InstanceType: + return hasattr(obj, '__call__') + return 0 + +def get_methods(inst): + methods = {} + classes = [] + classes.append(inst.__class__) + while classes: + klass = classes[0] + del classes[0] + classes = classes + list(klass.__bases__) + for name in dir(klass): + attr = getattr(klass, name) + if type(attr) == types.UnboundMethodType: + methods[name] = 1 + for name in dir(inst): + if is_callable(getattr(inst, name)): + methods[name] = 1 + return methods.keys() + +# XXX probably also want an abstract factory that knows things like + # the fact that a ProxyHandler needs to get inserted first. +# would also know when it makes sense to skip a superclass in favor of + # a subclass and when it might make sense to include both + +def build_opener(*handlers): + """Create an opener object from a list of handlers. + + The opener will use several default handlers, including support + for HTTP and FTP. If there is a ProxyHandler, it must be at the + front of the list of handlers. (Yuck.) + + If any of the handlers passed as arguments are subclasses of the + default handlers, the default handlers will not be used. + """ + + opener = OpenerDirector() + default_classes = [ProxyHandler, UnknownHandler, HTTPHandler, + HTTPDefaultErrorHandler, HTTPRedirectHandler, + FTPHandler, FileHandler] + skip = [] + for klass in default_classes: + for check in handlers: + if type(check) == types.ClassType: + if issubclass(check, klass): + skip.append(klass) + elif type(check) == types.InstanceType: + if isinstance(check, klass): + skip.append(klass) + for klass in skip: + default_classes.remove(klass) + + for klass in default_classes: + opener.add_handler(klass()) + + for h in handlers: + if type(h) == types.ClassType: + h = h() + opener.add_handler(h) + return opener + +class BaseHandler: + def add_parent(self, parent): + self.parent = parent + def close(self): + self.parent = None + +class HTTPDefaultErrorHandler(BaseHandler): + def http_error_default(self, req, fp, code, msg, hdrs): + raise HTTPError(req.get_full_url(), code, msg, hdrs, fp) + +class HTTPRedirectHandler(BaseHandler): + # 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): + if headers.has_key('location'): + newurl = headers['location'] + elif headers.has_key('uri'): + newurl = headers['uri'] + else: + return + nil = fp.read() + fp.close() + + # 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 + new = Request(newurl, req.get_data()) + new.error_302_dict = {} + if hasattr(req, 'error_302_dict'): + if req.error_302_dict.has_key(newurl): + raise HTTPError(req.get_full_url(), code, + self.inf_msg + msg, headers) + new.error_302_dict.update(req.error_302_dict) + new.error_302_dict[newurl] = newurl + return self.parent.open(new) + + http_error_301 = http_error_302 + + inf_msg = "The HTTP server returned a redirect error that would" \ + "lead to an inifinte loop.\n" \ + "The last 302 error message was:\n" + +class ProxyHandler(BaseHandler): + def __init__(self, proxies=None): + if proxies is None: + proxies = getproxies() + assert hasattr(proxies, 'has_key'), "proxies must be a mapping" + self.proxies = proxies + for type, url in proxies.items(): + setattr(self, '%s_open' % type, + lambda r, proxy=url, type=type, meth=self.proxy_open: \ + meth(r, proxy, type)) + + def proxy_open(self, req, proxy, type): + orig_type = req.get_type() + req.set_proxy(proxy) + 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) + +# 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): + self.proto = proto + self.func = func + self.addr = proxy_addr + + def handle(self, req): + if self.func and self.func(req): + return 1 + + def get_proxy(self): + return self.addr + +class CustomProxyHandler(BaseHandler): + def __init__(self, *proxies): + self.proxies = {} + + def proxy_open(self, req): + 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 + + def do_proxy(self, p, req): + p + return self.parent.open(req) + + def add_proxy(self, cpo): + if self.proxies.has_key(cpo.proto): + self.proxies[cpo.proto].append(cpo) + else: + self.proxies[cpo.proto] = [cpo] + +class HTTPPasswordMgr: + def __init__(self): + self.passwd = {} + + def add_password(self, realm, uri, user, passwd): + # uri could be a single URI or a sequence + if type(uri) == types.StringType: + uri = [uri] + uri = tuple(map(self.reduce_uri, uri)) + if not self.passwd.has_key(realm): + self.passwd[realm] = {} + self.passwd[realm][uri] = (user, passwd) + + def find_user_password(self, realm, authuri): + domains = self.passwd.get(realm, {}) + authuri = self.reduce_uri(authuri) + for uris, authinfo in domains.items(): + for uri in uris: + if self.is_suburi(uri, authuri): + return authinfo + return None, None + + def reduce_uri(self, uri): + """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], '/' + + def is_suburi(self, base, test): + """Check if test is below base in a URI tree + + Both args must be URIs in reduced form. + """ + if base == test: + return 1 + if base[0] != test[0]: + return 0 + common = os.path.commonprefix((base[1], test[1])) + if len(common) == len(base[1]): + return 1 + return 0 + + +class HTTPBasicAuthHandler(BaseHandler): + rx = re.compile('[ \t]*([^ \t]+)[ \t]+realm="([^"]*)"') + + # 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 + + def __init__(self): + self.passwd = HTTPPasswordMgr() + self.add_password = self.passwd.add_password + self.__current_realm = None + # if __current_realm is not None, then the server must have + # refused our name/password and is asking for authorization + # again. must be careful to set it to None on successful + # return. + + def http_error_401(self, req, fp, code, msg, headers): + # XXX could be mult. headers + authreq = headers.get('www-authenticate', None) + if authreq: + mo = HTTPBasicAuthHandler.rx.match(authreq) + if mo: + scheme, realm = mo.groups() + if string.lower(scheme) == 'basic': + return self.retry_http_basic_auth(req, realm) + + def retry_http_basic_auth(self, req, realm): + if self.__current_realm is None: + self.__current_realm = realm + else: + self.__current_realm = realm + return None + # XXX host isn't really the correct URI? + host = req.get_host() + user,pw = self.passwd.find_user_password(realm, host) + if pw: + raw = "%s:%s" % (user, pw) + auth = string.strip(base64.encodestring(raw)) + req.add_header('Authorization', 'Basic %s' % auth) + resp = self.parent.open(req) + self.__current_realm = None + return resp + else: + self.__current_realm = None + return None + +class HTTPDigestAuthHandler(BaseHandler): + """An authentication protocol defined by RFC 2069 + + Digest authentication improves on basic authentication because it + does not transmit passwords in the clear. + """ + + def __init__(self): + self.passwd = HTTPPasswordMgr() + self.add_password = self.passwd.add_password + self.__current_realm = None + + def http_error_401(self, req, fp, code, msg, headers): + # XXX could be mult. headers + authreq = headers.get('www-authenticate', None) + if authreq: + kind = string.split(authreq)[0] + if kind == 'Digest': + return self.retry_http_digest_auth(req, authreq) + + def retry_http_digest_auth(self, req, auth): + token, challenge = string.split(auth, ' ', 1) + chal = parse_keqv_list(parse_http_list(challenge)) + auth = self.get_authorization(req, chal) + if auth: + req.add_header('Authorization', 'Digest %s' % auth) + resp = self.parent.open(req) + self.__current_realm = None + return resp + + def get_authorization(self, req, chal): + try: + realm = chal['realm'] + nonce = chal['nonce'] + algorithm = chal.get('algorithm', 'MD5') + # mod_digest doesn't send an opaque, even though it isn't + # supposed to be optional + opaque = chal.get('opaque', None) + except KeyError: + return None + + if self.__current_realm is None: + self.__current_realm = realm + else: + self.__current_realm = realm + return None + + H, KD = self.get_algorithm_impls(algorithm) + if H is None: + return None + + user, pw = self.passwd.find_user_password(realm, + req.get_full_url()) + if user is None: + return None + + # XXX not implemented yet + if req.has_data(): + entdig = self.get_entity_digest(req.get_data(), chal) + else: + entdig = None + + A1 = "%s:%s:%s" % (user, realm, pw) + A2 = "%s:%s" % (req.has_data() and 'POST' or 'GET', + # XXX selector: what about proxies and full urls + req.get_selector()) + respdig = KD(H(A1), "%s:%s" % (nonce, H(A2))) + # XXX should the partial digests be encoded too? + + base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \ + 'response="%s"' % (user, realm, nonce, req.get_selector(), + respdig) + if opaque: + base = base + ', opaque="%s"' % opaque + if entdig: + base = base + ', digest="%s"' % entdig + if algorithm != 'MD5': + base = base + ', algorithm="%s"' % algorithm + return base + + def get_algorithm_impls(self, algorithm): + # lambdas assume digest modules are imported at the top level + if algorithm == 'MD5': + H = lambda x, e=encode_digest:e(md5.new(x).digest()) + elif algorithm == 'SHA': + H = lambda x, e=encode_digest:e(sha.new(x).digest()) + # XXX MD5-sess + KD = lambda s, d, H=H: H("%s:%s" % (s, d)) + return H, KD + + def get_entity_digest(self, data, chal): + # XXX not implemented yet + return None + +def encode_digest(digest): + hexrep = [] + for c in digest: + n = (ord(c) >> 4) & 0xf + hexrep.append(hex(n)[-1]) + n = ord(c) & 0xf + hexrep.append(hex(n)[-1]) + return string.join(hexrep, '') + + +class HTTPHandler(BaseHandler): + def http_open(self, req): + # XXX devise a new mechanism to specify user/password + host = req.get_host() + if not host: + raise URLError('no host given') + + h = httplib.HTTP(host) # will parse host:port +## h.set_debuglevel(1) + if req.has_data(): + data = req.get_data() + h.putrequest('POST', req.get_selector()) + h.putheader('Content-type', 'application/x-www-form-urlencoded') + h.putheader('Content-length', '%d' % len(data)) + else: + h.putrequest('GET', req.get_selector()) + # XXX proxies would have different host here + h.putheader('Host', host) + for args in self.parent.addheaders: + apply(h.putheader, args) + for k, v in req.headers.items(): + h.putheader(k, v) + h.endheaders() + if req.has_data(): + h.send(data + '\r\n') + + code, msg, hdrs = h.getreply() + fp = h.getfile() + if code == 200: + return addinfourl(fp, hdrs, req.get_full_url()) + else: + # want to make sure the socket is closed, even if error + # handling doesn't return immediately. the socket won't + # actually be closed until fp is also closed. + if h.sock: + h.sock.close() + h.sock = None + return self.parent.error('http', req, fp, code, msg, hdrs) + +class UnknownHandler(BaseHandler): + def unknown_open(self, req): + type = req.get_type() + 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: + k, v = string.split(elt, '=', 1) + if v[0] == '"' and v[-1] == '"': + v = v[1:-1] + parsed[k] = v + 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: + cur = s[i:] + c = string.find(cur, ',') + q = string.find(cur, '"') + 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: + i = i + q + else: + if c < q: + list.append(s[start:i+c]) + i = i + c + 1 + start = i + else: + inquote = 1 + i = i + q + 1 + return map(string.strip, list) + +class FileHandler(BaseHandler): + # Use local file or FTP depending on form of URL + def file_open(self, req): + 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) + + # names for the localhost + names = None + def get_names(self): + if FileHandler.names is None: + FileHandler.names = (socket.gethostbyname('localhost'), + socket.gethostbyname(socket.gethostname())) + return FileHandler.names + + # not entirely sure what the rules are here + def open_local_file(self, req): + mtype = mimetypes.guess_type(req.get_selector())[0] + headers = mimetools.Message(StringIO('Content-Type: %s\n' \ + % (mtype or 'text/plain'))) + host = req.get_host() + file = req.get_selector() + if host: + host, port = splitport(host) + if not host or \ + (not port and socket.gethostbyname(host) in self.get_names()): + return addinfourl(open(url2pathname(file), 'rb'), + headers, 'file:'+file) + raise URLError('file not on local host') + +class FTPHandler(BaseHandler): + def ftp_open(self, req): + host = req.get_host() + if not host: + raise IOError, ('ftp error', 'no host given') + # XXX handle custom username & password + host = socket.gethostbyname(host) + host, port = splitport(host) + if port is None: + port = ftplib.FTP_PORT + path, attrs = splitattr(req.get_selector()) + path = unquote(path) + dirs = string.splitfields(path, '/') + dirs, file = dirs[:-1], dirs[-1] + if dirs and not dirs[0]: + dirs = dirs[1:] + user = passwd = '' # XXX + try: + fw = self.connect_ftp(user, passwd, host, port, dirs) + type = file and 'I' or 'D' + for attr in attrs: + attr, value = splitattr(attr) + if string.lower(attr) == 'type' and \ + value in ('a', 'A', 'i', 'I', 'd', 'D'): + type = string.upper(value) + fp, retrlen = fw.retrfile(file, type) + if retrlen is not None and retrlen >= 0: + sf = StringIO('Content-Length: %d\n' % retrlen) + headers = mimetools.Message(sf) + else: + headers = noheaders() + return addinfourl(fp, headers, req.get_full_url()) + except ftplib.all_errors, msg: + raise IOError, ('ftp error', msg), sys.exc_info()[2] + + 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 + self.max_conns = 16 + + def setTimeout(self, t): + self.delay = t + + def setMaxConns(self, m): + self.max_conns = m + + def connect_ftp(self, user, passwd, host, port, dirs): + key = user, passwd, host, port + if self.cache.has_key(key): + self.timeout[key] = time.time() + self.delay + else: + self.cache[key] = ftpwrapper(user, passwd, host, port, dirs) + self.timeout[key] = time.time() + self.delay + self.check_cache() + return self.cache[key] + + def check_cache(self): + # first check for old ones + t = time.time() + if self.soonest <= t: + for k, v in self.timeout.items(): + 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 + if len(self.cache) == self.max_conns: + for k, v in self.timeout.items(): + if v == self.soonest: + del self.cache[k] + del self.timeout[k] + break + self.soonest = min(self.timeout.values()) + +class GopherHandler(BaseHandler): + def gopher_open(self, req): + 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()) + +#bleck! don't use this yet +class OpenerFactory: + + default_handlers = [UnknownHandler, HTTPHandler, + HTTPDefaultErrorHandler, HTTPRedirectHandler, + FTPHandler, FileHandler] + proxy_handlers = [ProxyHandler] + handlers = [] + replacement_handlers = [] + + def add_proxy_handler(self, ph): + self.proxy_handlers = self.proxy_handlers + [ph] + + def add_handler(self, h): + self.handlers = self.handlers + [h] + + def replace_handler(self, h): + pass + + def build_opener(self): + opener = OpenerDirectory() + for ph in self.proxy_handlers: + if type(ph) == types.ClassType: + ph = ph() + opener.add_handler(ph) + +if __name__ == "__main__": + # XXX some of the test code depends on machine configurations that + # 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' + elif socket.gethostname() == 'walden': + localhost = 'localhost' + else: + localhost = None + urls = [ + # Thanks to Fred for finding these! + 'gopher://gopher.lib.ncsu.edu/11/library/stacks/Alex', + 'gopher://gopher.vt.edu:10010/10/33', + + 'file:/etc/passwd', + 'file://nonsensename/etc/passwd', + 'ftp://www.python.org/pub/tmp/httplib.py', + 'ftp://www.python.org/pub/tmp/imageop.c', + 'ftp://www.python.org/pub/tmp/blat', + 'http://www.espn.com/', # redirect + 'http://www.python.org/Spanish/Inquistion/', + ('http://grail.cnri.reston.va.us/cgi-bin/faqw.py', + 'query=pythonistas&querytype=simple&casefold=yes&req=search'), + 'http://www.python.org/', + 'ftp://prep.ai.mit.edu/welcome.msg', + 'ftp://www.python.org/pub/tmp/figure.prn', + 'ftp://www.python.org/pub/tmp/interp.pl', + 'http://checkproxy.cnri.reston.va.us/test/test.html', + ] + + 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, + ] + + bauth = HTTPBasicAuthHandler() + bauth.add_password('basic_test_realm', localhost, 'jhylton', + 'password') + dauth = HTTPDigestAuthHandler() + dauth.add_password('digest_test_realm', localhost, 'jhylton', + 'password') + + + cfh = CacheFTPHandler() + cfh.setTimeout(1) + + # 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)) + + for url in urls: + if type(url) == types.TupleType: + url, req = url + else: + req = None + print url + try: + f = urlopen(url, req) + except IOError, err: + print "IOError:", err + except socket.error, err: + print "socket.error:", err + else: + buf = f.read() + f.close() + print "read %d bytes" % len(buf) + print + time.sleep(0.1)