~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Samuel Bronson
  • Date: 2012-08-30 20:36:18 UTC
  • mto: (6015.57.3 2.4)
  • mto: This revision was merged to the branch mainline in revision 6558.
  • Revision ID: naesten@gmail.com-20120830203618-y2dzw91nqpvpgxvx
Update INSTALL for switch to Python 2.6 and up.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2012 Canonical Ltd
 
1
# Copyright (C) 2006-2011 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
 
"""Implementation of urllib2 tailored to bzr needs
 
17
"""Implementaion of urllib2 tailored to bzr needs
18
18
 
19
19
This file complements the urllib2 class hierarchy with custom classes.
20
20
 
36
36
request (see AbstractHTTPHandler.do_open).
37
37
"""
38
38
 
39
 
from __future__ import absolute_import
40
 
 
41
39
DEBUG = 0
42
40
 
43
41
# FIXME: Oversimplifying, two kind of exceptions should be
50
48
 
51
49
import errno
52
50
import httplib
53
 
import os
 
51
try:
 
52
    import kerberos
 
53
except ImportError:
 
54
    have_kerberos = False
 
55
else:
 
56
    have_kerberos = True
54
57
import socket
55
58
import urllib
56
59
import urllib2
64
67
    config,
65
68
    debug,
66
69
    errors,
67
 
    lazy_import,
68
70
    osutils,
69
71
    trace,
70
72
    transport,
 
73
    ui,
71
74
    urlutils,
72
75
    )
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
 
""")
150
 
 
151
 
checked_kerberos = False
152
 
kerberos = None
153
76
 
154
77
 
155
78
class addinfourl(urllib2.addinfourl):
377
300
 
378
301
    # XXX: Needs refactoring at the caller level.
379
302
    def __init__(self, host, port=None, proxied_host=None,
380
 
                 report_activity=None, ca_certs=None):
 
303
                 report_activity=None):
381
304
        AbstractHTTPConnection.__init__(self, report_activity=report_activity)
382
305
        # Use strict=True since we don't support HTTP/0.9
383
306
        httplib.HTTPConnection.__init__(self, host, port, strict=True)
384
307
        self.proxied_host = proxied_host
385
 
        # ca_certs is ignored, it's only relevant for https
386
308
 
387
309
    def connect(self):
388
310
        if 'http' in debug.debug_flags:
391
313
        self._wrap_socket_for_reporting(self.sock)
392
314
 
393
315
 
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")
 
316
# Build the appropriate socket wrapper for ssl
 
317
try:
 
318
    # python 2.6 introduced a better ssl package
 
319
    import ssl
 
320
    _ssl_wrap_socket = ssl.wrap_socket
 
321
except ImportError:
 
322
    # python versions prior to 2.6 don't have ssl and ssl.wrap_socket instead
 
323
    # they use httplib.FakeSocket
 
324
    def _ssl_wrap_socket(sock, key_file, cert_file):
 
325
        ssl_sock = socket.ssl(sock, key_file, cert_file)
 
326
        return httplib.FakeSocket(sock, ssl_sock)
447
327
 
448
328
 
449
329
class HTTPSConnection(AbstractHTTPConnection, httplib.HTTPSConnection):
450
330
 
451
331
    def __init__(self, host, port=None, key_file=None, cert_file=None,
452
332
                 proxied_host=None,
453
 
                 report_activity=None, ca_certs=None):
 
333
                 report_activity=None):
454
334
        AbstractHTTPConnection.__init__(self, report_activity=report_activity)
455
335
        # Use strict=True since we don't support HTTP/0.9
456
336
        httplib.HTTPSConnection.__init__(self, host, port,
457
337
                                         key_file, cert_file, strict=True)
458
338
        self.proxied_host = proxied_host
459
 
        self.ca_certs = ca_certs
460
339
 
461
340
    def connect(self):
462
341
        if 'http' in debug.debug_flags:
467
346
            self.connect_to_origin()
468
347
 
469
348
    def connect_to_origin(self):
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
        ssl_sock = _ssl_wrap_socket(self.sock, self.key_file, self.cert_file)
502
350
        # Wrap the ssl socket before anybody use it
503
351
        self._wrap_socket_for_reporting(ssl_sock)
504
352
 
606
454
 
607
455
    handler_order = 1000 # after all pre-processings
608
456
 
609
 
    def __init__(self, report_activity=None, ca_certs=None):
 
457
    def __init__(self, report_activity=None):
610
458
        self._report_activity = report_activity
611
 
        self.ca_certs = ca_certs
612
459
 
613
460
    def create_connection(self, request, http_connection_class):
614
461
        host = request.get_host()
622
469
        try:
623
470
            connection = http_connection_class(
624
471
                host, proxied_host=request.proxied_host,
625
 
                report_activity=self._report_activity,
626
 
                ca_certs=self.ca_certs)
 
472
                report_activity=self._report_activity)
627
473
        except httplib.InvalidURL, exception:
628
474
            # There is only one occurrence of InvalidURL in httplib
629
475
            raise errors.InvalidURL(request.get_full_url(),
815
661
                    % (request, request.connection.sock.getsockname())
816
662
            response = connection.getresponse()
817
663
            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
822
664
        except (socket.gaierror, httplib.BadStatusLine, httplib.UnknownProtocol,
823
665
                socket.error, httplib.HTTPException):
824
666
            response = self.retry_or_raise(http_class, request, first_try)
1154
996
        # grok user:password@host:port as well as
1155
997
        # http://user:password@host:port
1156
998
 
1157
 
        parsed_url = transport.ConnectedTransport._split_url(proxy)
1158
 
        if not parsed_url.host:
 
999
        (scheme, user, password,
 
1000
         host, port, path) = transport.ConnectedTransport._split_url(proxy)
 
1001
        if not host:
1159
1002
            raise errors.InvalidURL(proxy, 'No host component')
1160
1003
 
1161
1004
        if request.proxy_auth == {}:
1163
1006
            # proxied request, intialize.  scheme (the authentication scheme)
1164
1007
            # and realm will be set by the AuthHandler
1165
1008
            request.proxy_auth = {
1166
 
                                  'host': parsed_url.host,
1167
 
                                  'port': parsed_url.port,
1168
 
                                  'user': parsed_url.user,
1169
 
                                  'password': parsed_url.password,
1170
 
                                  'protocol': parsed_url.scheme,
 
1009
                                  'host': host, 'port': port,
 
1010
                                  'user': user, 'password': password,
 
1011
                                  'protocol': scheme,
1171
1012
                                   # We ignore path since we connect to a proxy
1172
1013
                                  'path': None}
1173
 
        if parsed_url.port is None:
1174
 
            phost = parsed_url.host
 
1014
        if port is None:
 
1015
            phost = host
1175
1016
        else:
1176
 
            phost = parsed_url.host + ':%d' % parsed_url.port
 
1017
            phost = host + ':%d' % port
1177
1018
        request.set_proxy(phost, type)
1178
1019
        if self._debuglevel >= 3:
1179
1020
            print 'set_proxy: proxy set to %s://%s' % (type, phost)
1288
1129
        auth['modified'] = False
1289
1130
        # Put some common info in auth if the caller didn't
1290
1131
        if auth.get('path', None) is None:
1291
 
            parsed_url = urlutils.URL.from_string(request.get_full_url())
1292
 
            self.update_auth(auth, 'protocol', parsed_url.scheme)
1293
 
            self.update_auth(auth, 'host', parsed_url.host)
1294
 
            self.update_auth(auth, 'port', parsed_url.port)
1295
 
            self.update_auth(auth, 'path', parsed_url.path)
 
1132
            (protocol, _, _,
 
1133
             host, port, path) = urlutils.parse_url(request.get_full_url())
 
1134
            self.update_auth(auth, 'protocol', protocol)
 
1135
            self.update_auth(auth, 'host', host)
 
1136
            self.update_auth(auth, 'port', port)
 
1137
            self.update_auth(auth, 'path', path)
1296
1138
        # FIXME: the auth handler should be selected at a single place instead
1297
1139
        # of letting all handlers try to match all headers, but the current
1298
1140
        # design doesn't allow a simple implementation.
1484
1326
 
1485
1327
    def _auth_match_kerberos(self, auth):
1486
1328
        """Try to create a GSSAPI response for authenticating against a host."""
1487
 
        global kerberos, checked_kerberos
1488
 
        if kerberos is None and not checked_kerberos:
1489
 
            try:
1490
 
                import kerberos
1491
 
            except ImportError:
1492
 
                kerberos = None
1493
 
            checked_kerberos = True
1494
 
        if kerberos is None:
 
1329
        if not have_kerberos:
1495
1330
            return None
1496
1331
        ret, vc = kerberos.authGSSClientInit("HTTP@%(host)s" % auth)
1497
1332
        if ret < 1:
1816
1651
                 connection=ConnectionHandler,
1817
1652
                 redirect=HTTPRedirectHandler,
1818
1653
                 error=HTTPErrorProcessor,
1819
 
                 report_activity=None,
1820
 
                 ca_certs=None):
 
1654
                 report_activity=None):
1821
1655
        self._opener = urllib2.build_opener(
1822
 
            connection(report_activity=report_activity, ca_certs=ca_certs),
 
1656
            connection(report_activity=report_activity),
1823
1657
            redirect, error,
1824
1658
            ProxyHandler(),
1825
1659
            HTTPBasicAuthHandler(),