changeset 9852:917cf6bb6d0c

url: generalise HTTPS proxy handling to accomodate Python changes Python 2.6.3 introduced HTTPS proxy tunnelling in a way that interferes with the way HTTPS proxying is handled in Mercurial. This fix generalises it to work on Python 2.4 to 2.6.
author Henrik Stuart <hg@hstuart.dk>
date Fri, 13 Nov 2009 06:29:49 +0100
parents 004bf1d6e6af
children a033929bd34e
files mercurial/url.py
diffstat 1 files changed, 144 insertions(+), 117 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/url.py	Thu Nov 12 14:34:07 2009 -0600
+++ b/mercurial/url.py	Fri Nov 13 06:29:49 2009 +0100
@@ -262,108 +262,11 @@
     # must be able to send big bundle as stream.
     send = _gen_sendfile(keepalive.HTTPConnection)
 
-    def _proxytunnel(self):
-        proxyheaders = dict(
-                [(x, self.headers[x]) for x in self.headers
-                 if x.lower().startswith('proxy-')])
-        self._set_hostport(self.host, self.port)
-        self.send('CONNECT %s:%d HTTP/1.0\r\n' % (self.realhost, self.realport))
-        for header in proxyheaders.iteritems():
-            self.send('%s: %s\r\n' % header)
-        self.send('\r\n')
-
-        # majority of the following code is duplicated from
-        # httplib.HTTPConnection as there are no adequate places to
-        # override functions to provide the needed functionality
-        res = self.response_class(self.sock,
-                                  strict=self.strict,
-                                  method=self._method)
-
-        while True:
-            version, status, reason = res._read_status()
-            if status != httplib.CONTINUE:
-                break
-            while True:
-                skip = res.fp.readline().strip()
-                if not skip:
-                    break
-        res.status = status
-        res.reason = reason.strip()
-
-        if res.status == 200:
-            while True:
-                line = res.fp.readline()
-                if line == '\r\n':
-                    break
-            return True
-
-        if version == 'HTTP/1.0':
-            res.version = 10
-        elif version.startswith('HTTP/1.'):
-            res.version = 11
-        elif version == 'HTTP/0.9':
-            res.version = 9
-        else:
-            raise httplib.UnknownProtocol(version)
-
-        if res.version == 9:
-            res.length = None
-            res.chunked = 0
-            res.will_close = 1
-            res.msg = httplib.HTTPMessage(cStringIO.StringIO())
-            return False
-
-        res.msg = httplib.HTTPMessage(res.fp)
-        res.msg.fp = None
-
-        # are we using the chunked-style of transfer encoding?
-        trenc = res.msg.getheader('transfer-encoding')
-        if trenc and trenc.lower() == "chunked":
-            res.chunked = 1
-            res.chunk_left = None
-        else:
-            res.chunked = 0
-
-        # will the connection close at the end of the response?
-        res.will_close = res._check_close()
-
-        # do we have a Content-Length?
-        # NOTE: RFC 2616, S4.4, #3 says we ignore this if tr_enc is "chunked"
-        length = res.msg.getheader('content-length')
-        if length and not res.chunked:
-            try:
-                res.length = int(length)
-            except ValueError:
-                res.length = None
-            else:
-                if res.length < 0:  # ignore nonsensical negative lengths
-                    res.length = None
-        else:
-            res.length = None
-
-        # does the body have a fixed length? (of zero)
-        if (status == httplib.NO_CONTENT or status == httplib.NOT_MODIFIED or
-            100 <= status < 200 or # 1xx codes
-            res._method == 'HEAD'):
-            res.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 res.will_close and
-           not res.chunked and
-           res.length is None):
-            res.will_close = 1
-
-        self.proxyres = res
-
-        return False
-
     def connect(self):
         if has_https and self.realhost: # use CONNECT proxy
             self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
             self.sock.connect((self.host, self.port))
-            if self._proxytunnel():
+            if _generic_proxytunnel(self):
                 # we do not support client x509 certificates
                 self.sock = _ssl_wrap_socket(self.sock, None, None)
         else:
@@ -378,30 +281,141 @@
             return proxyres
         return keepalive.HTTPConnection.getresponse(self)
 
+# general transaction handler to support different ways to handle
+# HTTPS proxying before and after Python 2.6.3.
+def _generic_start_transaction(handler, h, req):
+    if hasattr(req, '_tunnel_host') and req._tunnel_host:
+        tunnel_host = req._tunnel_host
+        if tunnel_host[:7] not in ['http://', 'https:/']:
+            tunnel_host = 'https://' + tunnel_host
+        new_tunnel = True
+    else:
+        tunnel_host = req.get_selector()
+        new_tunnel = False
+
+    if new_tunnel or tunnel_host == req.get_full_url(): # has proxy
+        urlparts = urlparse.urlparse(tunnel_host)
+        if new_tunnel or urlparts[0] == 'https': # only use CONNECT for HTTPS
+            if ':' in urlparts[1]:
+                realhost, realport = urlparts[1].split(':')
+                realport = int(realport)
+            else:
+                realhost = urlparts[1]
+                realport = 443
+
+            h.realhost = realhost
+            h.realport = realport
+            h.headers = req.headers.copy()
+            h.headers.update(handler.parent.addheaders)
+            return
+
+    h.realhost = None
+    h.realport = None
+    h.headers = None
+
+def _generic_proxytunnel(self):
+    proxyheaders = dict(
+            [(x, self.headers[x]) for x in self.headers
+             if x.lower().startswith('proxy-')])
+    self._set_hostport(self.host, self.port)
+    self.send('CONNECT %s:%d HTTP/1.0\r\n' % (self.realhost, self.realport))
+    for header in proxyheaders.iteritems():
+        self.send('%s: %s\r\n' % header)
+    self.send('\r\n')
+
+    # majority of the following code is duplicated from
+    # httplib.HTTPConnection as there are no adequate places to
+    # override functions to provide the needed functionality
+    res = self.response_class(self.sock,
+                              strict=self.strict,
+                              method=self._method)
+
+    while True:
+        version, status, reason = res._read_status()
+        if status != httplib.CONTINUE:
+            break
+        while True:
+            skip = res.fp.readline().strip()
+            if not skip:
+                break
+    res.status = status
+    res.reason = reason.strip()
+
+    if res.status == 200:
+        while True:
+            line = res.fp.readline()
+            if line == '\r\n':
+                break
+        return True
+
+    if version == 'HTTP/1.0':
+        res.version = 10
+    elif version.startswith('HTTP/1.'):
+        res.version = 11
+    elif version == 'HTTP/0.9':
+        res.version = 9
+    else:
+        raise httplib.UnknownProtocol(version)
+
+    if res.version == 9:
+        res.length = None
+        res.chunked = 0
+        res.will_close = 1
+        res.msg = httplib.HTTPMessage(cStringIO.StringIO())
+        return False
+
+    res.msg = httplib.HTTPMessage(res.fp)
+    res.msg.fp = None
+
+    # are we using the chunked-style of transfer encoding?
+    trenc = res.msg.getheader('transfer-encoding')
+    if trenc and trenc.lower() == "chunked":
+        res.chunked = 1
+        res.chunk_left = None
+    else:
+        res.chunked = 0
+
+    # will the connection close at the end of the response?
+    res.will_close = res._check_close()
+
+    # do we have a Content-Length?
+    # NOTE: RFC 2616, S4.4, #3 says we ignore this if tr_enc is "chunked"
+    length = res.msg.getheader('content-length')
+    if length and not res.chunked:
+        try:
+            res.length = int(length)
+        except ValueError:
+            res.length = None
+        else:
+            if res.length < 0:  # ignore nonsensical negative lengths
+                res.length = None
+    else:
+        res.length = None
+
+    # does the body have a fixed length? (of zero)
+    if (status == httplib.NO_CONTENT or status == httplib.NOT_MODIFIED or
+        100 <= status < 200 or # 1xx codes
+        res._method == 'HEAD'):
+        res.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 res.will_close and
+       not res.chunked and
+       res.length is None):
+        res.will_close = 1
+
+    self.proxyres = res
+
+    return False
+
 class httphandler(keepalive.HTTPHandler):
     def http_open(self, req):
         return self.do_open(httpconnection, req)
 
     def _start_transaction(self, h, req):
-        if req.get_selector() == req.get_full_url(): # has proxy
-            urlparts = urlparse.urlparse(req.get_selector())
-            if urlparts[0] == 'https': # only use CONNECT for HTTPS
-                if ':' in urlparts[1]:
-                    realhost, realport = urlparts[1].split(':')
-                    realport = int(realport)
-                else:
-                    realhost = urlparts[1]
-                    realport = 443
-
-                h.realhost = realhost
-                h.realport = realport
-                h.headers = req.headers.copy()
-                h.headers.update(self.parent.addheaders)
-                return keepalive.HTTPHandler._start_transaction(self, h, req)
-
-        h.realhost = None
-        h.realport = None
-        h.headers = None
+        _generic_start_transaction(self, h, req)
         return keepalive.HTTPHandler._start_transaction(self, h, req)
 
     def __del__(self):
@@ -417,6 +431,15 @@
         send = _gen_sendfile(BetterHTTPS)
         getresponse = keepalive.wrapgetresponse(httplib.HTTPSConnection)
 
+        def connect(self):
+            if self.realhost: # use CONNECT proxy
+                self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+                self.sock.connect((self.host, self.port))
+                if _generic_proxytunnel(self):
+                    self.sock = _ssl_wrap_socket(self.sock, self.cert_file, self.key_file)
+            else:
+                BetterHTTPS.connect(self)
+
     class httpshandler(keepalive.KeepAliveHandler, urllib2.HTTPSHandler):
         def __init__(self, ui):
             keepalive.KeepAliveHandler.__init__(self)
@@ -424,6 +447,10 @@
             self.ui = ui
             self.pwmgr = passwordmgr(self.ui)
 
+        def _start_transaction(self, h, req):
+            _generic_start_transaction(self, h, req)
+            return keepalive.KeepAliveHandler._start_transaction(self, h, req)
+
         def https_open(self, req):
             self.auth = self.pwmgr.readauthtoken(req.get_full_url())
             return self.do_open(self._makeconnection, req)