14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
"""Implementaion of urllib2 tailored to bzr needs
17
"""Implementation of urllib2 tailored to bzr needs
19
19
This file complements the urllib2 class hierarchy with custom classes.
73
lazy_import.lazy_import(globals(), """
77
DEFAULT_CA_PATH = u"/etc/ssl/certs/ca-certificates.crt"
80
def default_ca_certs():
81
if not os.path.exists(DEFAULT_CA_PATH):
82
raise ValueError("default ca certs path %s does not exist" %
84
return DEFAULT_CA_PATH
87
def ca_certs_from_store(path):
88
if not os.path.exists(path):
89
raise ValueError("ca certs path %s does not exist" % path)
93
def default_cert_reqs():
97
def cert_reqs_from_store(unicode_str):
101
"required": ssl.CERT_REQUIRED,
102
"optional": ssl.CERT_OPTIONAL,
103
"none": ssl.CERT_NONE
106
raise ValueError("invalid value %s" % unicode_str)
109
opt_ssl_ca_certs = config.Option('ssl.ca_certs',
110
from_unicode=ca_certs_from_store,
111
default=default_ca_certs,
114
Path to certification authority certificates to trust.
117
opt_ssl_cert_reqs = config.Option('ssl.cert_reqs',
118
default=default_cert_reqs,
119
from_unicode=cert_reqs_from_store,
122
Whether to require a certificate from the remote side. (default:required)
125
* none: Certificates ignored
126
* optional: Certificates not required, but validated if provided
127
* required: Certificates required, and validated
73
130
checked_kerberos = False
300
357
# XXX: Needs refactoring at the caller level.
301
358
def __init__(self, host, port=None, proxied_host=None,
302
report_activity=None):
359
report_activity=None, ca_certs=None):
303
360
AbstractHTTPConnection.__init__(self, report_activity=report_activity)
304
361
# Use strict=True since we don't support HTTP/0.9
305
362
httplib.HTTPConnection.__init__(self, host, port, strict=True)
306
363
self.proxied_host = proxied_host
364
# ca_certs is ignored, it's only relevant for https
308
366
def connect(self):
309
367
if 'http' in debug.debug_flags:
312
370
self._wrap_socket_for_reporting(self.sock)
315
# Build the appropriate socket wrapper for ssl
317
# python 2.6 introduced a better ssl package
319
_ssl_wrap_socket = ssl.wrap_socket
321
# python versions prior to 2.6 don't have ssl and ssl.wrap_socket instead
322
# they use httplib.FakeSocket
323
def _ssl_wrap_socket(sock, key_file, cert_file):
324
ssl_sock = socket.ssl(sock, key_file, cert_file)
325
return httplib.FakeSocket(sock, ssl_sock)
373
# These two methods were imported from Python 3.2's ssl module
375
def _dnsname_to_pat(dn):
377
for frag in dn.split(r'.'):
379
# When '*' is a fragment by itself, it matches a non-empty dotless
383
# Otherwise, '*' matches any dotless fragment.
384
frag = re.escape(frag)
385
pats.append(frag.replace(r'\*', '[^.]*'))
386
return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
389
def match_hostname(cert, hostname):
390
"""Verify that *cert* (in decoded format as returned by
391
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules
392
are mostly followed, but IP addresses are not accepted for *hostname*.
394
CertificateError is raised on failure. On success, the function
398
raise ValueError("empty or no certificate")
400
san = cert.get('subjectAltName', ())
401
for key, value in san:
403
if _dnsname_to_pat(value).match(hostname):
405
dnsnames.append(value)
407
# The subject is only checked when subjectAltName is empty
408
for sub in cert.get('subject', ()):
409
for key, value in sub:
410
# XXX according to RFC 2818, the most specific Common Name
412
if key == 'commonName':
413
if _dnsname_to_pat(value).match(hostname):
415
dnsnames.append(value)
416
if len(dnsnames) > 1:
417
raise errors.CertificateError(
418
"hostname %r doesn't match either of %s"
419
% (hostname, ', '.join(map(repr, dnsnames))))
420
elif len(dnsnames) == 1:
421
raise errors.CertificateError("hostname %r doesn't match %r" %
422
(hostname, dnsnames[0]))
424
raise errors.CertificateError("no appropriate commonName or "
425
"subjectAltName fields were found")
328
428
class HTTPSConnection(AbstractHTTPConnection, httplib.HTTPSConnection):
330
430
def __init__(self, host, port=None, key_file=None, cert_file=None,
331
431
proxied_host=None,
332
report_activity=None):
432
report_activity=None, ca_certs=None):
333
433
AbstractHTTPConnection.__init__(self, report_activity=report_activity)
334
434
# Use strict=True since we don't support HTTP/0.9
335
435
httplib.HTTPSConnection.__init__(self, host, port,
336
436
key_file, cert_file, strict=True)
337
437
self.proxied_host = proxied_host
438
self.ca_certs = ca_certs
339
440
def connect(self):
340
441
if 'http' in debug.debug_flags:
345
446
self.connect_to_origin()
347
448
def connect_to_origin(self):
348
ssl_sock = _ssl_wrap_socket(self.sock, self.key_file, self.cert_file)
449
# FIXME JRV 2011-12-18: Use location config here?
450
config_stack = config.GlobalStack()
451
if self.ca_certs is None:
452
ca_certs = config_stack.get('ssl.ca_certs')
454
ca_certs = self.ca_certs
455
cert_reqs = config_stack.get('ssl.cert_reqs')
456
if cert_reqs == ssl.CERT_NONE:
457
trace.warning("not checking SSL certificates for %s: %d",
458
self.host, self.port)
462
"no valid trusted SSL CA certificates file set. See "
463
"'bzr help ssl.ca_certs' for more information on setting "
466
ssl_sock = ssl.wrap_socket(self.sock, self.key_file, self.cert_file,
467
cert_reqs=cert_reqs, ca_certs=ca_certs)
468
except ssl.SSLError, e:
469
if e.errno != ssl.SSL_ERROR_SSL:
472
"To disable SSL certificate verification, use "
473
"-Ossl.cert_reqs=none. See ``bzr help ssl.ca_certs`` for "
474
"more information on specifying trusted CA certificates.")
476
peer_cert = ssl_sock.getpeercert()
477
if (cert_reqs == ssl.CERT_REQUIRED or
478
(cert_reqs == ssl.CERT_OPTIONAL and peer_cert)):
479
match_hostname(peer_cert, self.host)
349
481
# Wrap the ssl socket before anybody use it
350
482
self._wrap_socket_for_reporting(ssl_sock)
454
586
handler_order = 1000 # after all pre-processings
456
def __init__(self, report_activity=None):
588
def __init__(self, report_activity=None, ca_certs=None):
457
589
self._report_activity = report_activity
590
self.ca_certs = ca_certs
459
592
def create_connection(self, request, http_connection_class):
460
593
host = request.get_host()
469
602
connection = http_connection_class(
470
603
host, proxied_host=request.proxied_host,
471
report_activity=self._report_activity)
604
report_activity=self._report_activity,
605
ca_certs=self.ca_certs)
472
606
except httplib.InvalidURL, exception:
473
607
# There is only one occurrence of InvalidURL in httplib
474
608
raise errors.InvalidURL(request.get_full_url(),
660
794
% (request, request.connection.sock.getsockname())
661
795
response = connection.getresponse()
662
796
convert_to_addinfourl = True
797
except (ssl.SSLError, errors.CertificateError):
798
# Something is wrong with either the certificate or the hostname,
799
# re-trying won't help
663
801
except (socket.gaierror, httplib.BadStatusLine, httplib.UnknownProtocol,
664
802
socket.error, httplib.HTTPException):
665
803
response = self.retry_or_raise(http_class, request, first_try)
1657
1795
connection=ConnectionHandler,
1658
1796
redirect=HTTPRedirectHandler,
1659
1797
error=HTTPErrorProcessor,
1660
report_activity=None):
1798
report_activity=None,
1661
1800
self._opener = urllib2.build_opener(
1662
connection(report_activity=report_activity),
1801
connection(report_activity=report_activity, ca_certs=ca_certs),
1663
1802
redirect, error,
1664
1803
ProxyHandler(),
1665
1804
HTTPBasicAuthHandler(),