~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Jelmer Vernooij
  • Date: 2012-01-23 15:02:51 UTC
  • mfrom: (6437.3.19 2.5)
  • mto: This revision was merged to the branch mainline in revision 6444.
  • Revision ID: jelmer@samba.org-20120123150251-skmauxb1q7h6w31t
Merge 2.5 branch.

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
DEFAULT_CA_PATH = u"/etc/ssl/certs/ca-certificates.crt"
 
78
 
 
79
 
 
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" %
 
83
            DEFAULT_CA_PATH)
 
84
    return DEFAULT_CA_PATH
 
85
 
 
86
 
 
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)
 
90
    return path
 
91
 
 
92
 
 
93
def default_cert_reqs():
 
94
    return u"required"
 
95
 
 
96
 
 
97
def cert_reqs_from_store(unicode_str):
 
98
    import ssl
 
99
    try:
 
100
        return {
 
101
            "required": ssl.CERT_REQUIRED,
 
102
            "optional": ssl.CERT_OPTIONAL,
 
103
            "none": ssl.CERT_NONE
 
104
            }[unicode_str]
 
105
    except KeyError:
 
106
        raise ValueError("invalid value %s" % unicode_str)
 
107
 
 
108
 
 
109
opt_ssl_ca_certs = config.Option('ssl.ca_certs',
 
110
        from_unicode=ca_certs_from_store,
 
111
        default=default_ca_certs,
 
112
        invalid='warning',
 
113
        help="""\
 
114
Path to certification authority certificates to trust.
 
115
""")
 
116
 
 
117
opt_ssl_cert_reqs = config.Option('ssl.cert_reqs',
 
118
        default=default_cert_reqs,
 
119
        from_unicode=cert_reqs_from_store,
 
120
        invalid='error',
 
121
        help="""\
 
122
Whether to require a certificate from the remote side. (default:required)
 
123
 
 
124
Possible values:
 
125
 * none: Certificates ignored
 
126
 * optional: Certificates not required, but validated if provided
 
127
 * required: Certificates required, and validated
 
128
""")
72
129
 
73
130
checked_kerberos = False
74
131
kerberos = None
299
356
 
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
307
365
 
308
366
    def connect(self):
309
367
        if 'http' in debug.debug_flags:
312
370
        self._wrap_socket_for_reporting(self.sock)
313
371
 
314
372
 
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)
 
373
# These two methods were imported from Python 3.2's ssl module
 
374
 
 
375
def _dnsname_to_pat(dn):
 
376
    pats = []
 
377
    for frag in dn.split(r'.'):
 
378
        if frag == '*':
 
379
            # When '*' is a fragment by itself, it matches a non-empty dotless
 
380
            # fragment.
 
381
            pats.append('[^.]+')
 
382
        else:
 
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)
 
387
 
 
388
 
 
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*.
 
393
 
 
394
    CertificateError is raised on failure. On success, the function
 
395
    returns nothing.
 
396
    """
 
397
    if not cert:
 
398
        raise ValueError("empty or no certificate")
 
399
    dnsnames = []
 
400
    san = cert.get('subjectAltName', ())
 
401
    for key, value in san:
 
402
        if key == 'DNS':
 
403
            if _dnsname_to_pat(value).match(hostname):
 
404
                return
 
405
            dnsnames.append(value)
 
406
    if not san:
 
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
 
411
                # must be used.
 
412
                if key == 'commonName':
 
413
                    if _dnsname_to_pat(value).match(hostname):
 
414
                        return
 
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]))
 
423
    else:
 
424
        raise errors.CertificateError("no appropriate commonName or "
 
425
            "subjectAltName fields were found")
326
426
 
327
427
 
328
428
class HTTPSConnection(AbstractHTTPConnection, httplib.HTTPSConnection):
329
429
 
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
338
439
 
339
440
    def connect(self):
340
441
        if 'http' in debug.debug_flags:
345
446
            self.connect_to_origin()
346
447
 
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')
 
453
        else:
 
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)
 
459
        else:
 
460
            if ca_certs is None:
 
461
                trace.warning(
 
462
                    "no valid trusted SSL CA certificates file set. See "
 
463
                    "'bzr help ssl.ca_certs' for more information on setting "
 
464
                    "trusted CA's.")
 
465
        try:
 
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:
 
470
                raise
 
471
            trace.note(
 
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.")
 
475
            raise
 
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)
 
480
 
349
481
        # Wrap the ssl socket before anybody use it
350
482
        self._wrap_socket_for_reporting(ssl_sock)
351
483
 
453
585
 
454
586
    handler_order = 1000 # after all pre-processings
455
587
 
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
458
591
 
459
592
    def create_connection(self, request, http_connection_class):
460
593
        host = request.get_host()
468
601
        try:
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
 
800
            raise
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,
 
1799
                 ca_certs=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(),