Mercurial > hg
comparison mercurial/url.py @ 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 | 430e59ff3437 |
children | 25e572394f5c |
comparison
equal
deleted
inserted
replaced
9850:004bf1d6e6af | 9852:917cf6bb6d0c |
---|---|
260 | 260 |
261 class httpconnection(keepalive.HTTPConnection): | 261 class httpconnection(keepalive.HTTPConnection): |
262 # must be able to send big bundle as stream. | 262 # must be able to send big bundle as stream. |
263 send = _gen_sendfile(keepalive.HTTPConnection) | 263 send = _gen_sendfile(keepalive.HTTPConnection) |
264 | 264 |
265 def _proxytunnel(self): | |
266 proxyheaders = dict( | |
267 [(x, self.headers[x]) for x in self.headers | |
268 if x.lower().startswith('proxy-')]) | |
269 self._set_hostport(self.host, self.port) | |
270 self.send('CONNECT %s:%d HTTP/1.0\r\n' % (self.realhost, self.realport)) | |
271 for header in proxyheaders.iteritems(): | |
272 self.send('%s: %s\r\n' % header) | |
273 self.send('\r\n') | |
274 | |
275 # majority of the following code is duplicated from | |
276 # httplib.HTTPConnection as there are no adequate places to | |
277 # override functions to provide the needed functionality | |
278 res = self.response_class(self.sock, | |
279 strict=self.strict, | |
280 method=self._method) | |
281 | |
282 while True: | |
283 version, status, reason = res._read_status() | |
284 if status != httplib.CONTINUE: | |
285 break | |
286 while True: | |
287 skip = res.fp.readline().strip() | |
288 if not skip: | |
289 break | |
290 res.status = status | |
291 res.reason = reason.strip() | |
292 | |
293 if res.status == 200: | |
294 while True: | |
295 line = res.fp.readline() | |
296 if line == '\r\n': | |
297 break | |
298 return True | |
299 | |
300 if version == 'HTTP/1.0': | |
301 res.version = 10 | |
302 elif version.startswith('HTTP/1.'): | |
303 res.version = 11 | |
304 elif version == 'HTTP/0.9': | |
305 res.version = 9 | |
306 else: | |
307 raise httplib.UnknownProtocol(version) | |
308 | |
309 if res.version == 9: | |
310 res.length = None | |
311 res.chunked = 0 | |
312 res.will_close = 1 | |
313 res.msg = httplib.HTTPMessage(cStringIO.StringIO()) | |
314 return False | |
315 | |
316 res.msg = httplib.HTTPMessage(res.fp) | |
317 res.msg.fp = None | |
318 | |
319 # are we using the chunked-style of transfer encoding? | |
320 trenc = res.msg.getheader('transfer-encoding') | |
321 if trenc and trenc.lower() == "chunked": | |
322 res.chunked = 1 | |
323 res.chunk_left = None | |
324 else: | |
325 res.chunked = 0 | |
326 | |
327 # will the connection close at the end of the response? | |
328 res.will_close = res._check_close() | |
329 | |
330 # do we have a Content-Length? | |
331 # NOTE: RFC 2616, S4.4, #3 says we ignore this if tr_enc is "chunked" | |
332 length = res.msg.getheader('content-length') | |
333 if length and not res.chunked: | |
334 try: | |
335 res.length = int(length) | |
336 except ValueError: | |
337 res.length = None | |
338 else: | |
339 if res.length < 0: # ignore nonsensical negative lengths | |
340 res.length = None | |
341 else: | |
342 res.length = None | |
343 | |
344 # does the body have a fixed length? (of zero) | |
345 if (status == httplib.NO_CONTENT or status == httplib.NOT_MODIFIED or | |
346 100 <= status < 200 or # 1xx codes | |
347 res._method == 'HEAD'): | |
348 res.length = 0 | |
349 | |
350 # if the connection remains open, and we aren't using chunked, and | |
351 # a content-length was not provided, then assume that the connection | |
352 # WILL close. | |
353 if (not res.will_close and | |
354 not res.chunked and | |
355 res.length is None): | |
356 res.will_close = 1 | |
357 | |
358 self.proxyres = res | |
359 | |
360 return False | |
361 | |
362 def connect(self): | 265 def connect(self): |
363 if has_https and self.realhost: # use CONNECT proxy | 266 if has_https and self.realhost: # use CONNECT proxy |
364 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | 267 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
365 self.sock.connect((self.host, self.port)) | 268 self.sock.connect((self.host, self.port)) |
366 if self._proxytunnel(): | 269 if _generic_proxytunnel(self): |
367 # we do not support client x509 certificates | 270 # we do not support client x509 certificates |
368 self.sock = _ssl_wrap_socket(self.sock, None, None) | 271 self.sock = _ssl_wrap_socket(self.sock, None, None) |
369 else: | 272 else: |
370 keepalive.HTTPConnection.connect(self) | 273 keepalive.HTTPConnection.connect(self) |
371 | 274 |
376 self.close() | 279 self.close() |
377 self.proxyres = None | 280 self.proxyres = None |
378 return proxyres | 281 return proxyres |
379 return keepalive.HTTPConnection.getresponse(self) | 282 return keepalive.HTTPConnection.getresponse(self) |
380 | 283 |
284 # general transaction handler to support different ways to handle | |
285 # HTTPS proxying before and after Python 2.6.3. | |
286 def _generic_start_transaction(handler, h, req): | |
287 if hasattr(req, '_tunnel_host') and req._tunnel_host: | |
288 tunnel_host = req._tunnel_host | |
289 if tunnel_host[:7] not in ['http://', 'https:/']: | |
290 tunnel_host = 'https://' + tunnel_host | |
291 new_tunnel = True | |
292 else: | |
293 tunnel_host = req.get_selector() | |
294 new_tunnel = False | |
295 | |
296 if new_tunnel or tunnel_host == req.get_full_url(): # has proxy | |
297 urlparts = urlparse.urlparse(tunnel_host) | |
298 if new_tunnel or urlparts[0] == 'https': # only use CONNECT for HTTPS | |
299 if ':' in urlparts[1]: | |
300 realhost, realport = urlparts[1].split(':') | |
301 realport = int(realport) | |
302 else: | |
303 realhost = urlparts[1] | |
304 realport = 443 | |
305 | |
306 h.realhost = realhost | |
307 h.realport = realport | |
308 h.headers = req.headers.copy() | |
309 h.headers.update(handler.parent.addheaders) | |
310 return | |
311 | |
312 h.realhost = None | |
313 h.realport = None | |
314 h.headers = None | |
315 | |
316 def _generic_proxytunnel(self): | |
317 proxyheaders = dict( | |
318 [(x, self.headers[x]) for x in self.headers | |
319 if x.lower().startswith('proxy-')]) | |
320 self._set_hostport(self.host, self.port) | |
321 self.send('CONNECT %s:%d HTTP/1.0\r\n' % (self.realhost, self.realport)) | |
322 for header in proxyheaders.iteritems(): | |
323 self.send('%s: %s\r\n' % header) | |
324 self.send('\r\n') | |
325 | |
326 # majority of the following code is duplicated from | |
327 # httplib.HTTPConnection as there are no adequate places to | |
328 # override functions to provide the needed functionality | |
329 res = self.response_class(self.sock, | |
330 strict=self.strict, | |
331 method=self._method) | |
332 | |
333 while True: | |
334 version, status, reason = res._read_status() | |
335 if status != httplib.CONTINUE: | |
336 break | |
337 while True: | |
338 skip = res.fp.readline().strip() | |
339 if not skip: | |
340 break | |
341 res.status = status | |
342 res.reason = reason.strip() | |
343 | |
344 if res.status == 200: | |
345 while True: | |
346 line = res.fp.readline() | |
347 if line == '\r\n': | |
348 break | |
349 return True | |
350 | |
351 if version == 'HTTP/1.0': | |
352 res.version = 10 | |
353 elif version.startswith('HTTP/1.'): | |
354 res.version = 11 | |
355 elif version == 'HTTP/0.9': | |
356 res.version = 9 | |
357 else: | |
358 raise httplib.UnknownProtocol(version) | |
359 | |
360 if res.version == 9: | |
361 res.length = None | |
362 res.chunked = 0 | |
363 res.will_close = 1 | |
364 res.msg = httplib.HTTPMessage(cStringIO.StringIO()) | |
365 return False | |
366 | |
367 res.msg = httplib.HTTPMessage(res.fp) | |
368 res.msg.fp = None | |
369 | |
370 # are we using the chunked-style of transfer encoding? | |
371 trenc = res.msg.getheader('transfer-encoding') | |
372 if trenc and trenc.lower() == "chunked": | |
373 res.chunked = 1 | |
374 res.chunk_left = None | |
375 else: | |
376 res.chunked = 0 | |
377 | |
378 # will the connection close at the end of the response? | |
379 res.will_close = res._check_close() | |
380 | |
381 # do we have a Content-Length? | |
382 # NOTE: RFC 2616, S4.4, #3 says we ignore this if tr_enc is "chunked" | |
383 length = res.msg.getheader('content-length') | |
384 if length and not res.chunked: | |
385 try: | |
386 res.length = int(length) | |
387 except ValueError: | |
388 res.length = None | |
389 else: | |
390 if res.length < 0: # ignore nonsensical negative lengths | |
391 res.length = None | |
392 else: | |
393 res.length = None | |
394 | |
395 # does the body have a fixed length? (of zero) | |
396 if (status == httplib.NO_CONTENT or status == httplib.NOT_MODIFIED or | |
397 100 <= status < 200 or # 1xx codes | |
398 res._method == 'HEAD'): | |
399 res.length = 0 | |
400 | |
401 # if the connection remains open, and we aren't using chunked, and | |
402 # a content-length was not provided, then assume that the connection | |
403 # WILL close. | |
404 if (not res.will_close and | |
405 not res.chunked and | |
406 res.length is None): | |
407 res.will_close = 1 | |
408 | |
409 self.proxyres = res | |
410 | |
411 return False | |
412 | |
381 class httphandler(keepalive.HTTPHandler): | 413 class httphandler(keepalive.HTTPHandler): |
382 def http_open(self, req): | 414 def http_open(self, req): |
383 return self.do_open(httpconnection, req) | 415 return self.do_open(httpconnection, req) |
384 | 416 |
385 def _start_transaction(self, h, req): | 417 def _start_transaction(self, h, req): |
386 if req.get_selector() == req.get_full_url(): # has proxy | 418 _generic_start_transaction(self, h, req) |
387 urlparts = urlparse.urlparse(req.get_selector()) | |
388 if urlparts[0] == 'https': # only use CONNECT for HTTPS | |
389 if ':' in urlparts[1]: | |
390 realhost, realport = urlparts[1].split(':') | |
391 realport = int(realport) | |
392 else: | |
393 realhost = urlparts[1] | |
394 realport = 443 | |
395 | |
396 h.realhost = realhost | |
397 h.realport = realport | |
398 h.headers = req.headers.copy() | |
399 h.headers.update(self.parent.addheaders) | |
400 return keepalive.HTTPHandler._start_transaction(self, h, req) | |
401 | |
402 h.realhost = None | |
403 h.realport = None | |
404 h.headers = None | |
405 return keepalive.HTTPHandler._start_transaction(self, h, req) | 419 return keepalive.HTTPHandler._start_transaction(self, h, req) |
406 | 420 |
407 def __del__(self): | 421 def __del__(self): |
408 self.close_all() | 422 self.close_all() |
409 | 423 |
414 class httpsconnection(BetterHTTPS): | 428 class httpsconnection(BetterHTTPS): |
415 response_class = keepalive.HTTPResponse | 429 response_class = keepalive.HTTPResponse |
416 # must be able to send big bundle as stream. | 430 # must be able to send big bundle as stream. |
417 send = _gen_sendfile(BetterHTTPS) | 431 send = _gen_sendfile(BetterHTTPS) |
418 getresponse = keepalive.wrapgetresponse(httplib.HTTPSConnection) | 432 getresponse = keepalive.wrapgetresponse(httplib.HTTPSConnection) |
433 | |
434 def connect(self): | |
435 if self.realhost: # use CONNECT proxy | |
436 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
437 self.sock.connect((self.host, self.port)) | |
438 if _generic_proxytunnel(self): | |
439 self.sock = _ssl_wrap_socket(self.sock, self.cert_file, self.key_file) | |
440 else: | |
441 BetterHTTPS.connect(self) | |
419 | 442 |
420 class httpshandler(keepalive.KeepAliveHandler, urllib2.HTTPSHandler): | 443 class httpshandler(keepalive.KeepAliveHandler, urllib2.HTTPSHandler): |
421 def __init__(self, ui): | 444 def __init__(self, ui): |
422 keepalive.KeepAliveHandler.__init__(self) | 445 keepalive.KeepAliveHandler.__init__(self) |
423 urllib2.HTTPSHandler.__init__(self) | 446 urllib2.HTTPSHandler.__init__(self) |
424 self.ui = ui | 447 self.ui = ui |
425 self.pwmgr = passwordmgr(self.ui) | 448 self.pwmgr = passwordmgr(self.ui) |
449 | |
450 def _start_transaction(self, h, req): | |
451 _generic_start_transaction(self, h, req) | |
452 return keepalive.KeepAliveHandler._start_transaction(self, h, req) | |
426 | 453 |
427 def https_open(self, req): | 454 def https_open(self, req): |
428 self.auth = self.pwmgr.readauthtoken(req.get_full_url()) | 455 self.auth = self.pwmgr.readauthtoken(req.get_full_url()) |
429 return self.do_open(self._makeconnection, req) | 456 return self.do_open(self._makeconnection, req) |
430 | 457 |