]> granicus.if.org Git - python/commitdiff
initial commit of a new HTTP library, supporting HTTP/1.1 and persistent
authorGreg Stein <gstein@lyra.org>
Mon, 26 Jun 2000 08:28:01 +0000 (08:28 +0000)
committerGreg Stein <gstein@lyra.org>
Mon, 26 Jun 2000 08:28:01 +0000 (08:28 +0000)
connections.

Lib/httplib.py

index feb97b3a04d2b2ee2500e9a70260d0d46121c939..5c0dacd0d39892a446f345e13695d2bfc7451b5e 100644 (file)
-"""HTTP client class
-
-See the following URL for a description of the HTTP/1.0 protocol:
-http://www.w3.org/hypertext/WWW/Protocols/
-(I actually implemented it from a much earlier draft.)
-
-Example:
-
->>> from httplib import HTTP
->>> h = HTTP('www.python.org')
->>> h.putrequest('GET', '/index.html')
->>> h.putheader('Host', 'www.python.org')
->>> h.putheader('Accept', 'text/html')
->>> h.putheader('Accept', 'text/plain')
->>> h.endheaders()
->>> errcode, errmsg, headers = h.getreply()
->>> if errcode == 200:
-...     f = h.getfile()
-...     print f.read() # Print the raw HTML
-...
-<HEAD>
-<TITLE>Python Language Home Page</TITLE>
-[...many more lines...]
->>>
-
-Note that an HTTP object is used for a single request -- to issue a
-second request to the same server, you create a new HTTP object.
-(This is in accordance with the protocol, which uses a new TCP
-connection for each request.)
+#
+# HTTP/1.1 client library
+#
+
+# ### this may as well go into a doc string...
+"""HTTP/1.1 client library
+
+<intro stuff goes here>
+<other stuff, too>
+
+HTTPConnection go through a number of "states", which defines when a client
+may legally make another request or fetch the response for a particular
+request. This diagram details these state transitions:
+
+    (null)
+      |
+      | HTTPConnection()
+      v
+    Idle
+      |
+      | putrequest()
+      v
+    Request-started
+      |
+      | ( putheader() )*  endheaders()
+      v
+    Request-sent
+      |
+      | response = getresponse()
+      v
+    Unread-response   [Response-headers-read]
+      |\____________________
+      |                     \
+      | response.read()      | putrequest()
+      v                      v
+    Idle                   Req-started-unread-response
+                     _______/|
+                    /        |
+   response.read() |         | ( putheader() )*  endheaders()
+                   v         v
+       Request-started     Req-sent-unread-response
+                             |
+                             | response.read()
+                             v
+                           Request-sent
+
+This diagram presents the following rules:
+  -- a second request may not be started until {response-headers-read}
+  -- a response [object] cannot be retrieved until {request-sent}
+  -- there is no differentiation between an unread response body and a
+     partially read response body
+
+Note: this enforcement is applied by the HTTPConnection class. The
+      HTTPResponse class does not enforce this state machine, which
+      implies sophisticated clients may accelerate the request/response
+      pipeline. Caution should be taken, though: accelerating the states
+      beyond the above pattern may imply knowledge of the server's
+      connection-close behavior for certain requests. For example, it
+      is impossible to tell whether the server will close the connection
+      UNTIL the response headers have been read; this means that further
+      requests cannot be placed into the pipeline until it is known that
+      the server will NOT be closing the connection.
+
+Logical State                  __state            __response
+-------------                  -------            ----------
+Idle                           _CS_IDLE           None
+Request-started                _CS_REQ_STARTED    None
+Request-sent                   _CS_REQ_SENT       None
+Unread-response                _CS_IDLE           <response_class>
+Req-started-unread-response    _CS_REQ_STARTED    <response_class>
+Req-sent-unread-response       _CS_REQ_SENT       <response_class>
 """
 
-import os
+
 import socket
 import string
 import mimetools
 
 try:
-    from cStringIO import StringIO
-except:
-    from StringIO import StringIO
+  from cStringIO import StringIO
+except ImportError:
+  from StringIO import StringIO
 
-HTTP_VERSION = 'HTTP/1.0'
 HTTP_PORT = 80
 HTTPS_PORT = 443
 
+_UNKNOWN = 'UNKNOWN'
+
+# connection states
+_CS_IDLE = 'Idle'
+_CS_REQ_STARTED = 'Request-started'
+_CS_REQ_SENT = 'Request-sent'
+
+
+class HTTPResponse:
+  def __init__(self, sock):
+    self.fp = sock.makefile('rb', 0)
+
+    self.msg = None
+
+    # from the Status-Line of the response
+    self.version = _UNKNOWN    # HTTP-Version
+    self.status = _UNKNOWN     # Status-Code
+    self.reason = _UNKNOWN     # Reason-Phrase
+
+    self.chunked = _UNKNOWN    # is "chunked" being used?
+    self.chunk_left = _UNKNOWN # bytes left to read in current chunk
+    self.length = _UNKNOWN     # number of bytes left in response
+    self.will_close = _UNKNOWN # connection will close at end of response
+
+  def begin(self):
+    if self.msg is not None:
+      # we've already started reading the response
+      return
+
+    line = self.fp.readline()
+    try:
+      [version, status, reason] = string.split(line, None, 2)
+    except ValueError:
+      try:
+        [version, status] = string.split(line, None, 1)
+        reason = ""
+      except ValueError:
+        self.close()
+        raise BadStatusLine(line)
+    if version[:5] != 'HTTP/':
+      self.close()
+      raise BadStatusLine(line)
+
+    self.status = status = int(status)
+    self.reason = string.strip(reason)
+
+    if version == 'HTTP/1.0':
+      self.version = 10
+    elif version[:7] == 'HTTP/1.':
+      self.version = 11        # use HTTP/1.1 code for HTTP/1.x where x>=1
+    else:
+      raise UnknownProtocol(version)
+
+    self.msg = mimetools.Message(self.fp, 0)
+
+    # don't let the msg keep an fp
+    self.msg.fp = None
+
+    # are we using the chunked-style of transfer encoding?
+    tr_enc = self.msg.getheader('transfer-encoding')
+    if tr_enc:
+      if string.lower(tr_enc) != 'chunked':
+        raise UnknownTransferEncoding()
+      self.chunked = 1
+      self.chunk_left = None
+    else:
+      self.chunked = 0
+
+    # will the connection close at the end of the response?
+    conn = self.msg.getheader('connection')
+    if conn:
+      conn = string.lower(conn)
+      # a "Connection: close" will always close the connection. if we
+      # don't see that and this is not HTTP/1.1, then the connection will
+      # close unless we see a Keep-Alive header.
+      self.will_close = string.find(conn, 'close') != -1 or \
+                        ( self.version != 11 and \
+                          not self.msg.getheader('keep-alive') )
+    else:
+      # for HTTP/1.1, the connection will always remain open
+      # otherwise, it will remain open IFF we see a Keep-Alive header
+      self.will_close = self.version != 11 and \
+                        not self.msg.getheader('keep-alive')
+
+    # do we have a Content-Length?
+    # NOTE: RFC 2616, S4.4, #3 says we ignore this if tr_enc is "chunked"
+    length = self.msg.getheader('content-length')
+    if length and not self.chunked:
+      self.length = int(length)
+    else:
+      self.length = None
+
+    # does the body have a fixed length? (of zero)
+    if (status == 204 or               # No Content
+        status == 304 or               # Not Modified
+        100 <= status < 200):          # 1xx codes
+      self.length = 0
+
+    # if the connection remains open, and we aren't using chunked, and
+    # a content-length was not provided, then assume that the connection
+    # WILL close.
+    if not self.will_close and \
+       not self.chunked and \
+       self.length is None:
+      self.will_close = 1
+
+    # if there is no body, then close NOW. read() may never be called, thus
+    # we will never mark self as closed.
+    if self.length == 0:
+      self.close()
+
+  def close(self):
+    if self.fp:
+      self.fp.close()
+      self.fp = None
+
+  def isclosed(self):
+    # NOTE: it is possible that we will not ever call self.close(). This
+    #       case occurs when will_close is TRUE, length is None, and we
+    #       read up to the last byte, but NOT past it.
+    #
+    # IMPLIES: if will_close is FALSE, then self.close() will ALWAYS be
+    #          called, meaning self.isclosed() is meaningful.
+    return self.fp is None
+
+  def read(self, amt=None):
+    if self.fp is None:
+      return ''
+
+    if self.chunked:
+      chunk_left = self.chunk_left
+      value = ''
+      while 1:
+        if chunk_left is None:
+          line = self.fp.readline()
+          i = string.find(line, ';')
+          if i >= 0:
+            line = line[:i]    # strip chunk-extensions
+          chunk_left = string.atoi(line, 16)
+          if chunk_left == 0:
+            break
+        if amt is None:
+          value = value + self._safe_read(chunk_left)
+        elif amt < chunk_left:
+          value = value + self._safe_read(amt)
+          self.chunk_left = chunk_left - amt
+          return value
+        elif amt == chunk_left:
+          value = value + self._safe_read(amt)
+          self._safe_read(2)   # toss the CRLF at the end of the chunk
+          self.chunk_left = None
+          return value
+        else:
+          value = value + self._safe_read(chunk_left)
+          amt = amt - chunk_left
+
+        # we read the whole chunk, get another
+        self._safe_read(2)     # toss the CRLF at the end of the chunk
+        chunk_left = None
+
+      # read and discard trailer up to the CRLF terminator
+      ### note: we shouldn't have any trailers!
+      while 1:
+        line = self.fp.readline()
+        if line == '\r\n':
+          break
+
+      # we read everything; close the "file"
+      self.close()
+
+      return value
+
+    elif amt is None:
+      # unbounded read
+      if self.will_close:
+        s = self.fp.read()
+      else:
+        s = self._safe_read(self.length)
+      self.close()     # we read everything
+      return s
+
+    if self.length is not None:
+      if amt > self.length:
+        # clip the read to the "end of response"
+        amt = self.length
+      self.length = self.length - amt
+
+    # we do not use _safe_read() here because this may be a .will_close
+    # connection, and the user is reading more bytes than will be provided
+    # (for example, reading in 1k chunks)
+    s = self.fp.read(amt)
+
+    # close our "file" if we know we should
+    ### I'm not sure about the len(s) < amt part; we should be safe because
+    ### we shouldn't be using non-blocking sockets
+    if self.length == 0 or len(s) < amt:
+      self.close()
+
+    return s
+
+  def _safe_read(self, amt):
+    """Read the number of bytes requested, compensating for partial reads.
+
+    Normally, we have a blocking socket, but a read() can be interrupted
+    by a signal (resulting in a partial read).
+
+    Note that we cannot distinguish between EOF and an interrupt when zero
+    bytes have been read. IncompleteRead() will be raised in this situation.
+
+    This function should be used when <amt> bytes "should" be present for
+    reading. If the bytes are truly not available (due to EOF), then the
+    IncompleteRead exception can be used to detect the problem.
+    """
+    s = ''
+    while amt > 0:
+      chunk = self.fp.read(amt)
+      if not chunk:
+        raise IncompleteRead(s)
+      s = s + chunk
+      amt = amt - len(chunk)
+    return s
+
+  def getheader(self, name, default=None):
+    if self.msg is None:
+      raise ResponseNotReady()
+    return self.msg.getheader(name, default)
+
+
+class HTTPConnection:
+
+  _http_vsn = 11
+  _http_vsn_str = 'HTTP/1.1'
+
+  response_class = HTTPResponse
+  default_port = HTTP_PORT
+  auto_open = 1
+
+  def __init__(self, host, port=None):
+    self.sock = None
+    self.__response = None
+    self.__state = _CS_IDLE
+
+    self._set_hostport(host, port)
+
+  def _set_hostport(self, host, port):
+    if port is None:
+      i = string.find(host, ':')
+      if i >= 0:
+        port = int(host[i+1:])
+        host = host[:i]
+      else:
+        port = self.default_port
+    self.host = host
+    self.port = port
+
+  def connect(self):
+    """Connect to the host and port specified in __init__."""
+    self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    self.sock.connect((self.host, self.port))
+
+  def close(self):
+    """Close the connection to the HTTP server."""
+    if self.sock:
+      self.sock.close()        # close it manually... there may be other refs
+      self.sock = None
+    if self.__response:
+      self.__response.close()
+      self.__response = None
+    self.__state = _CS_IDLE
+
+  def send(self, str):
+    """Send `str' to the server."""
+    if self.sock is None:
+      if self.auto_open:
+        self.connect()
+      else:
+        raise NotConnected()
+
+    # send the data to the server. if we get a broken pipe, then close
+    # the socket. we want to reconnect when somebody tries to send again.
+    #
+    # NOTE: we DO propagate the error, though, because we cannot simply
+    #       ignore the error... the caller will know if they can retry.
+    try:
+      self.sock.send(str)
+    except socket.error, v:
+      if v[0] == 32:   # Broken pipe
+        self.close()
+      raise
+
+  def putrequest(self, method, url):
+    """Send a request to the server.
+
+    `method' specifies an HTTP request method, e.g. 'GET'.
+    `url' specifies the object being requested, e.g. '/index.html'.
+    """
+
+    # check if a prior response has been completed
+    if self.__response and self.__response.isclosed():
+      self.__response = None
+
+    #
+    # in certain cases, we cannot issue another request on this connection.
+    # this occurs when:
+    #   1) we are in the process of sending a request.   (_CS_REQ_STARTED)
+    #   2) a response to a previous request has signalled that it is going
+    #      to close the connection upon completion.
+    #   3) the headers for the previous response have not been read, thus
+    #      we cannot determine whether point (2) is true.   (_CS_REQ_SENT)
+    #
+    # if there is no prior response, then we can request at will.
+    #
+    # if point (2) is true, then we will have passed the socket to the
+    # response (effectively meaning, "there is no prior response"), and will
+    # open a new one when a new request is made.
+    #
+    # Note: if a prior response exists, then we *can* start a new request.
+    #       We are not allowed to begin fetching the response to this new
+    #       request, however, until that prior response is complete.
+    #
+    if self.__state == _CS_IDLE:
+      self.__state = _CS_REQ_STARTED
+    else:
+      raise CannotSendRequest()
+
+    if not url:
+      url = '/'
+    str = '%s %s %s\r\n' % (method, url, self._http_vsn_str)
+
+    try:
+      self.send(str)
+    except socket.error, v:
+      # trap 'Broken pipe' if we're allowed to automatically reconnect
+      if v[0] != 32 or not self.auto_open:
+        raise
+      # try one more time (the socket was closed; this will reopen)
+      self.send(str)
+
+    if self._http_vsn == 11:
+      # Issue some standard headers for better HTTP/1.1 compliance
+
+      # this header is issued *only* for HTTP/1.1 connections. more
+      # specifically, this means it is only issued when the client uses
+      # the new HTTPConnection() class. backwards-compat clients will
+      # be using HTTP/1.0 and those clients may be issuing this header
+      # themselves. we should NOT issue it twice; some web servers (such
+      # as Apache) barf when they see two Host: headers
+      self.putheader('Host', self.host)
+
+      # note: we are assuming that clients will not attempt to set these
+      #       headers since *this* library must deal with the consequences.
+      #       this also means that when the supporting libraries are
+      #       updated to recognize other forms, then this code should be
+      #       changed (removed or updated).
+
+      # we only want a Content-Encoding of "identity" since we don't
+      # support encodings such as x-gzip or x-deflate.
+      self.putheader('Accept-Encoding', 'identity')
+
+      # we can accept "chunked" Transfer-Encodings, but no others
+      # NOTE: no TE header implies *only* "chunked"
+      #self.putheader('TE', 'chunked')
+
+      # if TE is supplied in the header, then it must appear in a
+      # Connection header.
+      #self.putheader('Connection', 'TE')
+
+    else:
+      # For HTTP/1.0, the server will assume "not chunked"
+      pass
+
+  def putheader(self, header, value):
+    """Send a request header line to the server.
+
+    For example: h.putheader('Accept', 'text/html')
+    """
+    if self.__state != _CS_REQ_STARTED:
+      raise CannotSendHeader()
+
+    str = '%s: %s\r\n' % (header, value)
+    self.send(str)
+
+  def endheaders(self):
+    """Indicate that the last header line has been sent to the server."""
+
+    if self.__state == _CS_REQ_STARTED:
+      self.__state = _CS_REQ_SENT
+    else:
+      raise CannotSendHeader()
+
+    self.send('\r\n')
+
+  def request(self, method, url, body=None, headers={}):
+    """Send a complete request to the server."""
+
+    try:
+      self._send_request(method, url, body, headers)
+    except socket.error, v:
+      # trap 'Broken pipe' if we're allowed to automatically reconnect
+      if v[0] != 32 or not self.auto_open:
+        raise
+      # try one more time
+      self._send_request(method, url, body, headers)
+
+  def _send_request(self, method, url, body, headers):
+    self.putrequest(method, url)
+
+    if body:
+      self.putheader('Content-Length', str(len(body)))
+    for hdr, value in headers.items():
+      self.putheader(hdr, value)
+    self.endheaders()
+
+    if body:
+      self.send(body)
+
+  def getresponse(self):
+    "Get the response from the server."
+
+    # check if a prior response has been completed
+    if self.__response and self.__response.isclosed():
+      self.__response = None
+
+    #
+    # if a prior response exists, then it must be completed (otherwise, we
+    # cannot read this response's header to determine the connection-close
+    # behavior)
+    #
+    # note: if a prior response existed, but was connection-close, then the
+    # socket and response were made independent of this HTTPConnection object
+    # since a new request requires that we open a whole new connection
+    #
+    # this means the prior response had one of two states:
+    #   1) will_close: this connection was reset and the prior socket and
+    #                  response operate independently
+    #   2) persistent: the response was retained and we await its isclosed()
+    #                  status to become true.
+    #
+    if self.__state != _CS_REQ_SENT or self.__response:
+      raise ResponseNotReady()
+
+    response = self.response_class(self.sock)
+
+    response.begin()
+    self.__state = _CS_IDLE
+
+    if response.will_close:
+      # this effectively passes the connection to the response
+      self.close()
+    else:
+      # remember this, so we can tell when it is complete
+      self.__response = response
+
+    return response
+
+
 class FakeSocket:
-    def __init__(self, sock, ssl):
-        self.__sock = sock
-        self.__ssl = ssl
-        return
-
-    def makefile(self, mode):           # hopefully, never have to write
-        msgbuf = ""
-        while 1:
-            try:
-                msgbuf = msgbuf + self.__ssl.read()
-            except socket.sslerror, msg:
-                break
-        return StringIO(msgbuf)
-
-    def send(self, stuff, flags = 0):
-        return self.__ssl.write(stuff)
-
-    def recv(self, len = 1024, flags = 0):
-        return self.__ssl.read(len)
-
-    def __getattr__(self, attr):
-        return getattr(self.__sock, attr)
-
-class HTTP:
-    """This class manages a connection to an HTTP server."""
-
-    def __init__(self, host = '', port = 0, **x509):
-        """Initialize a new instance.
-
-        If specified, `host' is the name of the remote host to which
-        to connect.  If specified, `port' specifies the port to which
-        to connect.  By default, httplib.HTTP_PORT is used.
-
-        """
-        self.key_file = x509.get('key_file')
-        self.cert_file = x509.get('cert_file')
-        self.debuglevel = 0
-        self.file = None
-        if host: self.connect(host, port)
-
-    def set_debuglevel(self, debuglevel):
-        """Set the debug output level.
-
-        A non-false value results in debug messages for connection and
-        for all messages sent to and received from the server.
-
-        """
-        self.debuglevel = debuglevel
-
-    def connect(self, host, port = 0):
-        """Connect to a host on a given port.
-
-        Note:  This method is automatically invoked by __init__,
-        if a host is specified during instantiation.
-
-        """
-        if not port:
-            i = string.find(host, ':')
-            if i >= 0:
-                host, port = host[:i], host[i+1:]
-                try: port = string.atoi(port)
-                except string.atoi_error:
-                    raise socket.error, "nonnumeric port"
-        if not port: port = HTTP_PORT
-        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        if self.debuglevel > 0: print 'connect:', (host, port)
-        self.sock.connect((host, port))
-
-    def send(self, str):
-        """Send `str' to the server."""
-        if self.debuglevel > 0: print 'send:', `str`
-        self.sock.send(str)
-
-    def putrequest(self, request, selector):
-        """Send a request to the server.
-
-        `request' specifies an HTTP request method, e.g. 'GET'.
-        `selector' specifies the object being requested, e.g.
-        '/index.html'.
-
-        """
-        if not selector: selector = '/'
-        str = '%s %s %s\r\n' % (request, selector, HTTP_VERSION)
-        self.send(str)
-
-    def putheader(self, header, *args):
-        """Send a request header line to the server.
-
-        For example: h.putheader('Accept', 'text/html')
-
-        """
-        str = '%s: %s\r\n' % (header, string.joinfields(args,'\r\n\t'))
-        self.send(str)
-
-    def endheaders(self):
-        """Indicate that the last header line has been sent to the server."""
-        self.send('\r\n')
-
-    def getreply(self):
-        """Get a reply from the server.
-
-        Returns a tuple consisting of:
-        - server response code (e.g. '200' if all goes well)
-        - server response string corresponding to response code
-        - any RFC822 headers in the response from the server
-
-        """
-        self.file = self.sock.makefile('rb')
-        line = self.file.readline()
-        if self.debuglevel > 0: print 'reply:', `line`
-        try:
-            [ver, code, msg] = string.split(line, None, 2)
-        except ValueError:
-          try:
-              [ver, code] = string.split(line, None, 1)
-              msg = ""
-          except ValueError:
-              self.headers = None
-              return -1, line, self.headers
-        if ver[:5] != 'HTTP/':
-            self.headers = None
-            return -1, line, self.headers
-        errcode = string.atoi(code)
-        errmsg = string.strip(msg)
-        self.headers = mimetools.Message(self.file, 0)
-        return errcode, errmsg, self.headers
-
-    def getfile(self):
-        """Get a file object from which to receive data from the HTTP server.
-
-        NOTE:  This method must not be invoked until getreplies
-        has been invoked.
-
-        """
-        return self.file
-
-    def close(self):
-        """Close the connection to the HTTP server."""
-        if self.file:
-            self.file.close()
-        self.file = None
-        if self.sock:
-            self.sock.close()
-        self.sock = None
-
-if hasattr(socket, "ssl"):
-    class HTTPS(HTTP):
-        """This class allows communication via SSL."""
-
-        def connect(self, host, port = 0):
-            """Connect to a host on a given port.
-
-            Note:  This method is automatically invoked by __init__,
-            if a host is specified during instantiation.
-
-            """
-            if not port:
-                i = string.find(host, ':')
-                if i >= 0:
-                    host, port = host[:i], host[i+1:]
-                    try: port = string.atoi(port)
-                    except string.atoi_error:
-                        raise socket.error, "nonnumeric port"
-            if not port: port = HTTPS_PORT
-            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-            if self.debuglevel > 0: print 'connect:', (host, port)
-            sock.connect((host, port))
-            ssl = socket.ssl(sock, self.key_file, self.cert_file)
-            self.sock = FakeSocket(sock, ssl)
+  def __init__(self, sock, ssl):
+    self.__sock = sock
+    self.__ssl = ssl
+
+  def makefile(self, mode):           # hopefully, never have to write
+    if mode != 'r' and mode != 'rb':
+      raise UnimplementedFileMode()
+
+    msgbuf = ""
+    while 1:
+      try:
+        msgbuf = msgbuf + self.__ssl.read()
+      except socket.sslerror, msg:
+        break
+    return StringIO(msgbuf)
+
+  def send(self, stuff, flags = 0):
+    return self.__ssl.write(stuff)
+
+  def recv(self, len = 1024, flags = 0):
+    return self.__ssl.read(len)
+
+  def __getattr__(self, attr):
+    return getattr(self.__sock, attr)
+
+
+class HTTPSConnection(HTTPConnection):
+  "This class allows communication via SSL."
+
+  default_port = HTTPS_PORT
+
+  def __init__(self, host, port=None, **x509):
+    keys = x509.keys()
+    try:
+      keys.remove('key_file')
+    except ValueError:
+      pass
+    try:
+      keys.remove('cert_file')
+    except ValueError:
+      pass
+    if keys:
+      raise IllegalKeywordArgument()
+    HTTPConnection.__init__(self, host, port)
+    self.key_file = x509.get('key_file')
+    self.cert_file = x509.get('cert_file')
+
+  def connect(self):
+    "Connect to a host on a given (SSL) port."
+
+    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    sock.connect((self.host, self.port))
+    ssl = socket.ssl(sock, self.key_file, self.cert_file)
+    self.sock = FakeSocket(sock, ssl)
+
+
+class HTTP(HTTPConnection):
+  "Compatibility class with httplib.py from 1.5."
+
+  _http_vsn = 10
+  _http_vsn_str = 'HTTP/1.0'
+
+  debuglevel = 0
+
+  def __init__(self, host='', port=None, **x509):
+    "Provide a default host, since the superclass requires one."
+
+    # some joker passed 0 explicitly, meaning default port
+    if port == 0:
+      port = None
+
+    # Note that we may pass an empty string as the host; this will throw
+    # an error when we attempt to connect. Presumably, the client code
+    # will call connect before then, with a proper host.
+    HTTPConnection.__init__(self, host, port)
+
+    # we never actually use these for anything, but we keep them here for
+    # compatibility with post-1.5.2 CVS.
+    self.key_file = x509.get('key_file')
+    self.cert_file = x509.get('cert_file')
+
+    self.file = None
+
+  def connect(self, host=None, port=None):
+    "Accept arguments to set the host/port, since the superclass doesn't."
+
+    if host is not None:
+      self._set_hostport(host, port)
+    HTTPConnection.connect(self)
 
+  def set_debuglevel(self, debuglevel):
+    "The class no longer supports the debuglevel."
+    pass
 
+  def getfile(self):
+    "Provide a getfile, since the superclass' does not use this concept."
+    return self.file
+
+  def putheader(self, header, *values):
+    "The superclass allows only one value argument."
+    HTTPConnection.putheader(self, header, string.joinfields(values, '\r\n\t'))
+
+  def getreply(self):
+    """Compat definition since superclass does not define it.
+
+    Returns a tuple consisting of:
+    - server status code (e.g. '200' if all goes well)
+    - server "reason" corresponding to status code
+    - any RFC822 headers in the response from the server
+    """
+    try:
+      response = self.getresponse()
+    except BadStatusLine, e:
+      ### hmm. if getresponse() ever closes the socket on a bad request,
+      ### then we are going to have problems with self.sock
+
+      ### should we keep this behavior? do people use it?
+      # keep the socket open (as a file), and return it
+      self.file = self.sock.makefile('rb', 0)
+
+      # close our socket -- we want to restart after any protocol error
+      self.close()
+
+      self.headers = None
+      return -1, e.line, None
+
+    self.headers = response.msg
+    self.file = response.fp
+    return response.status, response.reason, response.msg
+
+  def close(self):
+    HTTPConnection.close(self)
+
+    # note that self.file == response.fp, which gets closed by the
+    # superclass. just clear the object ref here.
+    ### hmm. messy. if status==-1, then self.file is owned by us.
+    ### well... we aren't explicitly closing, but losing this ref will do it
+    self.file = None
+
+
+class HTTPException(Exception):
+  pass
+
+class NotConnected(HTTPException):
+  pass
+
+class UnknownProtocol(HTTPException):
+  def __init__(self, version):
+    self.version = version
+
+class UnknownTransferEncoding(HTTPException):
+  pass
+
+class IllegalKeywordArgument(HTTPException):
+  pass
+
+class UnimplementedFileMode(HTTPException):
+  pass
+
+class IncompleteRead(HTTPException):
+  def __init__(self, partial):
+    self.partial = partial
+
+class ImproperConnectionState(HTTPException):
+  pass
+
+class CannotSendRequest(ImproperConnectionState):
+  pass
+
+class CannotSendHeader(ImproperConnectionState):
+  pass
+
+class ResponseNotReady(ImproperConnectionState):
+  pass
+
+class BadStatusLine(HTTPException):
+  def __init__(self, line):
+    self.line = line
+
+# for backwards compatibility
+error = HTTPException
+
+
+#
+# snarfed from httplib.py for now...
+#
 def test():
     """Test this module.
 
     The test consists of retrieving and displaying the Python
     home page, along with the error code and error string returned
     by the www.python.org server.
-
     """
+
     import sys
     import getopt
     opts, args = getopt.getopt(sys.argv[1:], 'd')
     dl = 0
     for o, a in opts:
         if o == '-d': dl = dl + 1
-    print "testing HTTP..."
     host = 'www.python.org'
     selector = '/'
     if args[0:]: host = args[0]
@@ -239,34 +750,29 @@ def test():
     h.connect(host)
     h.putrequest('GET', selector)
     h.endheaders()
-    errcode, errmsg, headers = h.getreply()
-    print 'errcode =', errcode
-    print 'errmsg  =', errmsg
+    status, reason, headers = h.getreply()
+    print 'status =', status
+    print 'reason =', reason
     print
     if headers:
         for header in headers.headers: print string.strip(header)
     print
     print h.getfile().read()
-    if hasattr(socket, "ssl"):
-        print "-"*40
-        print "testing HTTPS..."
-        host = 'synergy.as.cmu.edu'
-        selector = '/~geek/'
-        if args[0:]: host = args[0]
-        if args[1:]: selector = args[1]
-        h = HTTPS()
-        h.set_debuglevel(dl)
-        h.connect(host)
-        h.putrequest('GET', selector)
-        h.endheaders()
-        errcode, errmsg, headers = h.getreply()
-        print 'errcode =', errcode
-        print 'errmsg  =', errmsg
-        print
-        if headers:
-            for header in headers.headers: print string.strip(header)
-        print
-        print h.getfile().read()
+
+    if hasattr(socket, 'ssl'):
+      host = 'www.c2.net'
+      hs = HTTPS()
+      hs.connect(host)
+      hs.putrequest('GET', selector)
+      hs.endheaders()
+      status, reason, headers = hs.getreply()
+      print 'status =', status
+      print 'reason =', reason
+      print
+      if headers:
+          for header in headers.headers: print string.strip(header)
+      print
+      print hs.getfile().read()
 
 
 if __name__ == '__main__':