Mercurial > hg
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)