~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/http/_urllib2_wrappers.py

  • Committer: Vincent Ladeuil
  • Date: 2012-02-14 17:22:37 UTC
  • mfrom: (6466 +trunk)
  • mto: This revision was merged to the branch mainline in revision 6468.
  • Revision ID: v.ladeuil+lp@free.fr-20120214172237-7dv7er3n4uy8d5m4
Merge trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2011 Canonical Ltd
 
1
# Copyright (C) 2006-2012 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
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
16
16
 
17
 
"""Implementaion of urllib2 tailored to bzr needs
 
17
"""Implementation of urllib2 tailored to bzr needs
18
18
 
19
19
This file complements the urllib2 class hierarchy with custom classes.
20
20
 
50
50
 
51
51
import errno
52
52
import httplib
 
53
import os
53
54
import socket
54
55
import urllib
55
56
import urllib2
63
64
    config,
64
65
    debug,
65
66
    errors,
 
67
    lazy_import,
66
68
    osutils,
67
69
    trace,
68
70
    transport,
69
71
    urlutils,
70
72
    )
71
 
 
 
73
lazy_import.lazy_import(globals(), """
 
74
import ssl
 
75
""")
 
76
 
 
77
 
 
78
# Note for packagers: if there is no package providing certs for your platform,
 
79
# the curl project produces http://curl.haxx.se/ca/cacert.pem weekly.
 
80
_ssl_ca_certs_known_locations = [
 
81
    u'/etc/ssl/certs/ca-certificates.crt', # Ubuntu/debian/gentoo
 
82
    u'/etc/pki/tls/certs/ca-bundle.crt', # Fedora/CentOS/RH
 
83
    u'/etc/ssl/ca-bundle.pem', # OpenSuse
 
84
    u'/etc/ssl/cert.pem', # OpenSuse
 
85
    u"/usr/local/share/certs/ca-root-nss.crt", # FreeBSD
 
86
    # XXX: Needs checking, can't trust the interweb ;) -- vila 2012-01-25
 
87
    u'/etc/openssl/certs/ca-certificates.crt', # Solaris
 
88
    ]
 
89
 
 
90
def default_ca_certs():
 
91
    if sys.platform == 'win32':
 
92
        return os.path.join(os.path.dirname(sys.executable), u"ca_bundle.crt")
 
93
    elif sys.platform == 'darwin':
 
94
        # FIXME: Needs some default value for osx, waiting for osx installers
 
95
        # guys feedback -- vila 2012-01-25
 
96
        pass
 
97
    else:
 
98
        # Try known locations for friendly OSes providing the root certificates
 
99
        # without making them hard to use for any https client.
 
100
        for path in _ssl_ca_certs_known_locations:
 
101
            if os.path.exists(path):
 
102
                # First found wins
 
103
                return path
 
104
    # A default path that makes sense and will be mentioned in the error
 
105
    # presented to the user, even if not correct for all platforms
 
106
    return _ssl_ca_certs_known_locations[0]
 
107
 
 
108
 
 
109
def ca_certs_from_store(path):
 
110
    if not os.path.exists(path):
 
111
        raise ValueError("ca certs path %s does not exist" % path)
 
112
    return path
 
113
 
 
114
 
 
115
def cert_reqs_from_store(unicode_str):
 
116
    import ssl
 
117
    try:
 
118
        return {
 
119
            "required": ssl.CERT_REQUIRED,
 
120
            "none": ssl.CERT_NONE
 
121
            }[unicode_str]
 
122
    except KeyError:
 
123
        raise ValueError("invalid value %s" % unicode_str)
 
124
 
 
125
 
 
126
opt_ssl_ca_certs = config.Option('ssl.ca_certs',
 
127
        from_unicode=ca_certs_from_store,
 
128
        default=default_ca_certs,
 
129
        invalid='warning',
 
130
        help="""\
 
131
Path to certification authority certificates to trust.
 
132
 
 
133
This should be a valid path to a bundle containing all root Certificate
 
134
Authorities used to verify an https server certificate.
 
135
 
 
136
Use ssl.cert_reqs=none to disable certificate verification.
 
137
""")
 
138
 
 
139
opt_ssl_cert_reqs = config.Option('ssl.cert_reqs',
 
140
        default=u"required",
 
141
        from_unicode=cert_reqs_from_store,
 
142
        invalid='error',
 
143
        help="""\
 
144
Whether to require a certificate from the remote side. (default:required)
 
145
 
 
146
Possible values:
 
147
 * none: Certificates ignored
 
148
 * required: Certificates required and validated
 
149
""")
72
150
 
73
151
checked_kerberos = False
74
152
kerberos = None
299
377
 
300
378
    # XXX: Needs refactoring at the caller level.
301
379
    def __init__(self, host, port=None, proxied_host=None,
302
 
                 report_activity=None):
 
380
                 report_activity=None, ca_certs=None):
303
381
        AbstractHTTPConnection.__init__(self, report_activity=report_activity)
304
382
        # Use strict=True since we don't support HTTP/0.9
305
383
        httplib.HTTPConnection.__init__(self, host, port, strict=True)
306
384
        self.proxied_host = proxied_host
 
385
        # ca_certs is ignored, it's only relevant for https
307
386
 
308
387
    def connect(self):
309
388
        if 'http' in debug.debug_flags:
312
391
        self._wrap_socket_for_reporting(self.sock)
313
392
 
314
393
 
315
 
# Build the appropriate socket wrapper for ssl
316
 
try:
317
 
    # python 2.6 introduced a better ssl package
318
 
    import ssl
319
 
    _ssl_wrap_socket = ssl.wrap_socket
320
 
except ImportError:
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)
 
394
# These two methods were imported from Python 3.2's ssl module
 
395
 
 
396
def _dnsname_to_pat(dn):
 
397
    pats = []
 
398
    for frag in dn.split(r'.'):
 
399
        if frag == '*':
 
400
            # When '*' is a fragment by itself, it matches a non-empty dotless
 
401
            # fragment.
 
402
            pats.append('[^.]+')
 
403
        else:
 
404
            # Otherwise, '*' matches any dotless fragment.
 
405
            frag = re.escape(frag)
 
406
            pats.append(frag.replace(r'\*', '[^.]*'))
 
407
    return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
 
408
 
 
409
 
 
410
def match_hostname(cert, hostname):
 
411
    """Verify that *cert* (in decoded format as returned by
 
412
    SSLSocket.getpeercert()) matches the *hostname*.  RFC 2818 rules
 
413
    are mostly followed, but IP addresses are not accepted for *hostname*.
 
414
 
 
415
    CertificateError is raised on failure. On success, the function
 
416
    returns nothing.
 
417
    """
 
418
    if not cert:
 
419
        raise ValueError("empty or no certificate")
 
420
    dnsnames = []
 
421
    san = cert.get('subjectAltName', ())
 
422
    for key, value in san:
 
423
        if key == 'DNS':
 
424
            if _dnsname_to_pat(value).match(hostname):
 
425
                return
 
426
            dnsnames.append(value)
 
427
    if not san:
 
428
        # The subject is only checked when subjectAltName is empty
 
429
        for sub in cert.get('subject', ()):
 
430
            for key, value in sub:
 
431
                # XXX according to RFC 2818, the most specific Common Name
 
432
                # must be used.
 
433
                if key == 'commonName':
 
434
                    if _dnsname_to_pat(value).match(hostname):
 
435
                        return
 
436
                    dnsnames.append(value)
 
437
    if len(dnsnames) > 1:
 
438
        raise errors.CertificateError(
 
439
            "hostname %r doesn't match either of %s"
 
440
            % (hostname, ', '.join(map(repr, dnsnames))))
 
441
    elif len(dnsnames) == 1:
 
442
        raise errors.CertificateError("hostname %r doesn't match %r" %
 
443
                                      (hostname, dnsnames[0]))
 
444
    else:
 
445
        raise errors.CertificateError("no appropriate commonName or "
 
446
            "subjectAltName fields were found")
326
447
 
327
448
 
328
449
class HTTPSConnection(AbstractHTTPConnection, httplib.HTTPSConnection):
329
450
 
330
451
    def __init__(self, host, port=None, key_file=None, cert_file=None,
331
452
                 proxied_host=None,
332
 
                 report_activity=None):
 
453
                 report_activity=None, ca_certs=None):
333
454
        AbstractHTTPConnection.__init__(self, report_activity=report_activity)
334
455
        # Use strict=True since we don't support HTTP/0.9
335
456
        httplib.HTTPSConnection.__init__(self, host, port,
336
457
                                         key_file, cert_file, strict=True)
337
458
        self.proxied_host = proxied_host
 
459
        self.ca_certs = ca_certs
338
460
 
339
461
    def connect(self):
340
462
        if 'http' in debug.debug_flags:
345
467
            self.connect_to_origin()
346
468
 
347
469
    def connect_to_origin(self):
348
 
        ssl_sock = _ssl_wrap_socket(self.sock, self.key_file, self.cert_file)
 
470
        # FIXME JRV 2011-12-18: Use location config here?
 
471
        config_stack = config.GlobalStack()
 
472
        cert_reqs = config_stack.get('ssl.cert_reqs')
 
473
        if cert_reqs == ssl.CERT_NONE:
 
474
            trace.warning("Not checking SSL certificate for %s: %d",
 
475
                self.host, self.port)
 
476
            ca_certs = None
 
477
        else:
 
478
            if self.ca_certs is None:
 
479
                ca_certs = config_stack.get('ssl.ca_certs')
 
480
            else:
 
481
                ca_certs = self.ca_certs
 
482
            if ca_certs is None:
 
483
                trace.warning(
 
484
                    "No valid trusted SSL CA certificates file set. See "
 
485
                    "'bzr help ssl.ca_certs' for more information on setting "
 
486
                    "trusted CAs.")
 
487
        try:
 
488
            ssl_sock = ssl.wrap_socket(self.sock, self.key_file, self.cert_file,
 
489
                cert_reqs=cert_reqs, ca_certs=ca_certs)
 
490
        except ssl.SSLError, e:
 
491
            trace.note(
 
492
                "\n"
 
493
                "See `bzr help ssl.ca_certs` for how to specify trusted CA"
 
494
                "certificates.\n"
 
495
                "Pass -Ossl.cert_reqs=none to disable certificate "
 
496
                "verification entirely.\n")
 
497
            raise
 
498
        if cert_reqs == ssl.CERT_REQUIRED:
 
499
            peer_cert = ssl_sock.getpeercert()
 
500
            match_hostname(peer_cert, self.host)
 
501
 
349
502
        # Wrap the ssl socket before anybody use it
350
503
        self._wrap_socket_for_reporting(ssl_sock)
351
504
 
453
606
 
454
607
    handler_order = 1000 # after all pre-processings
455
608
 
456
 
    def __init__(self, report_activity=None):
 
609
    def __init__(self, report_activity=None, ca_certs=None):
457
610
        self._report_activity = report_activity
 
611
        self.ca_certs = ca_certs
458
612
 
459
613
    def create_connection(self, request, http_connection_class):
460
614
        host = request.get_host()
468
622
        try:
469
623
            connection = http_connection_class(
470
624
                host, proxied_host=request.proxied_host,
471
 
                report_activity=self._report_activity)
 
625
                report_activity=self._report_activity,
 
626
                ca_certs=self.ca_certs)
472
627
        except httplib.InvalidURL, exception:
473
628
            # There is only one occurrence of InvalidURL in httplib
474
629
            raise errors.InvalidURL(request.get_full_url(),
660
815
                    % (request, request.connection.sock.getsockname())
661
816
            response = connection.getresponse()
662
817
            convert_to_addinfourl = True
 
818
        except (ssl.SSLError, errors.CertificateError):
 
819
            # Something is wrong with either the certificate or the hostname,
 
820
            # re-trying won't help
 
821
            raise
663
822
        except (socket.gaierror, httplib.BadStatusLine, httplib.UnknownProtocol,
664
823
                socket.error, httplib.HTTPException):
665
824
            response = self.retry_or_raise(http_class, request, first_try)
1657
1816
                 connection=ConnectionHandler,
1658
1817
                 redirect=HTTPRedirectHandler,
1659
1818
                 error=HTTPErrorProcessor,
1660
 
                 report_activity=None):
 
1819
                 report_activity=None,
 
1820
                 ca_certs=None):
1661
1821
        self._opener = urllib2.build_opener(
1662
 
            connection(report_activity=report_activity),
 
1822
            connection(report_activity=report_activity, ca_certs=ca_certs),
1663
1823
            redirect, error,
1664
1824
            ProxyHandler(),
1665
1825
            HTTPBasicAuthHandler(),