~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

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

  • Committer: Tarmac
  • Author(s): Vincent Ladeuil
  • Date: 2017-01-30 14:42:05 UTC
  • mfrom: (6620.1.1 trunk)
  • Revision ID: tarmac-20170130144205-r8fh2xpmiuxyozpv
Merge  2.7 into trunk including fix for bug #1657238 [r=vila]

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2011 Canonical Ltd
 
1
# Copyright (C) 2006-2013, 2016, 2017 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
 
36
36
request (see AbstractHTTPHandler.do_open).
37
37
"""
38
38
 
 
39
from __future__ import absolute_import
 
40
 
39
41
DEBUG = 0
40
42
 
41
43
# FIXME: Oversimplifying, two kind of exceptions should be
46
48
# actual code more or less do that, tests should be written to
47
49
# ensure that.
48
50
 
 
51
import base64
49
52
import errno
50
53
import httplib
51
 
try:
52
 
    import kerberos
53
 
except ImportError:
54
 
    have_kerberos = False
55
 
else:
56
 
    have_kerberos = True
 
54
import os
57
55
import socket
58
56
import urllib
59
57
import urllib2
60
58
import urlparse
61
59
import re
 
60
import ssl
62
61
import sys
63
62
import time
64
63
 
67
66
    config,
68
67
    debug,
69
68
    errors,
 
69
    lazy_import,
70
70
    osutils,
71
71
    trace,
72
72
    transport,
 
73
    ui,
73
74
    urlutils,
74
 
    )
 
75
)
 
76
 
 
77
try:
 
78
    _ = (ssl.match_hostname, ssl.CertificateError)
 
79
except AttributeError:
 
80
    # Provide fallbacks for python < 2.7.9
 
81
    def match_hostname(cert, host):
 
82
        trace.warning(
 
83
            '%s cannot be verified, https certificates verification is only'
 
84
            ' available for python versions >= 2.7.9' % (host,))
 
85
    ssl.match_hostname = match_hostname
 
86
    ssl.CertificateError = ValueError
 
87
 
 
88
 
 
89
# Note for packagers: if there is no package providing certs for your platform,
 
90
# the curl project produces http://curl.haxx.se/ca/cacert.pem weekly.
 
91
_ssl_ca_certs_known_locations = [
 
92
    u'/etc/ssl/certs/ca-certificates.crt',  # Ubuntu/debian/gentoo
 
93
    u'/etc/pki/tls/certs/ca-bundle.crt',  # Fedora/CentOS/RH
 
94
    u'/etc/ssl/ca-bundle.pem',  # OpenSuse
 
95
    u'/etc/ssl/cert.pem',  # OpenSuse
 
96
    u"/usr/local/share/certs/ca-root-nss.crt",  # FreeBSD
 
97
    # XXX: Needs checking, can't trust the interweb ;) -- vila 2012-01-25
 
98
    u'/etc/openssl/certs/ca-certificates.crt',  # Solaris
 
99
]
 
100
 
 
101
 
 
102
def default_ca_certs():
 
103
    if sys.platform == 'win32':
 
104
        return os.path.join(os.path.dirname(sys.executable), u"cacert.pem")
 
105
    elif sys.platform == 'darwin':
 
106
        # FIXME: Needs some default value for osx, waiting for osx installers
 
107
        # guys feedback -- vila 2012-01-25
 
108
        pass
 
109
    else:
 
110
        # Try known locations for friendly OSes providing the root certificates
 
111
        # without making them hard to use for any https client.
 
112
        for path in _ssl_ca_certs_known_locations:
 
113
            if os.path.exists(path):
 
114
                # First found wins
 
115
                return path
 
116
    # A default path that makes sense and will be mentioned in the error
 
117
    # presented to the user, even if not correct for all platforms
 
118
    return _ssl_ca_certs_known_locations[0]
 
119
 
 
120
 
 
121
def ca_certs_from_store(path):
 
122
    if not os.path.exists(path):
 
123
        raise ValueError("ca certs path %s does not exist" % path)
 
124
    return path
 
125
 
 
126
 
 
127
def cert_reqs_from_store(unicode_str):
 
128
    import ssl
 
129
    try:
 
130
        return {"required": ssl.CERT_REQUIRED,
 
131
                "none": ssl.CERT_NONE}[unicode_str]
 
132
    except KeyError:
 
133
        raise ValueError("invalid value %s" % unicode_str)
 
134
 
 
135
 
 
136
def default_ca_reqs():
 
137
    if sys.platform in ('win32', 'darwin'):
 
138
        # FIXME: Once we get a native access to root certificates there, this
 
139
        # won't needed anymore. See http://pad.lv/920455 -- vila 2012-02-15
 
140
        return u'none'
 
141
    else:
 
142
        return u'required'
 
143
 
 
144
opt_ssl_ca_certs = config.Option('ssl.ca_certs',
 
145
                                 from_unicode=ca_certs_from_store,
 
146
                                 default=default_ca_certs,
 
147
                                 invalid='warning',
 
148
                                 help="""\
 
149
Path to certification authority certificates to trust.
 
150
 
 
151
This should be a valid path to a bundle containing all root Certificate
 
152
Authorities used to verify an https server certificate.
 
153
 
 
154
Use ssl.cert_reqs=none to disable certificate verification.
 
155
""")
 
156
 
 
157
opt_ssl_cert_reqs = config.Option('ssl.cert_reqs',
 
158
                                  default=default_ca_reqs,
 
159
                                  from_unicode=cert_reqs_from_store,
 
160
                                  invalid='error',
 
161
                                  help="""\
 
162
Whether to require a certificate from the remote side. (default:required)
 
163
 
 
164
Possible values:
 
165
 * none: Certificates ignored
 
166
 * required: Certificates required and validated
 
167
""")
 
168
 
 
169
checked_kerberos = False
 
170
kerberos = None
75
171
 
76
172
 
77
173
class addinfourl(urllib2.addinfourl):
299
395
 
300
396
    # XXX: Needs refactoring at the caller level.
301
397
    def __init__(self, host, port=None, proxied_host=None,
302
 
                 report_activity=None):
 
398
                 report_activity=None, ca_certs=None):
303
399
        AbstractHTTPConnection.__init__(self, report_activity=report_activity)
304
400
        # Use strict=True since we don't support HTTP/0.9
305
401
        httplib.HTTPConnection.__init__(self, host, port, strict=True)
306
402
        self.proxied_host = proxied_host
 
403
        # ca_certs is ignored, it's only relevant for https
307
404
 
308
405
    def connect(self):
309
406
        if 'http' in debug.debug_flags:
312
409
        self._wrap_socket_for_reporting(self.sock)
313
410
 
314
411
 
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)
326
 
 
327
 
 
328
412
class HTTPSConnection(AbstractHTTPConnection, httplib.HTTPSConnection):
329
413
 
330
414
    def __init__(self, host, port=None, key_file=None, cert_file=None,
331
415
                 proxied_host=None,
332
 
                 report_activity=None):
 
416
                 report_activity=None, ca_certs=None):
333
417
        AbstractHTTPConnection.__init__(self, report_activity=report_activity)
334
418
        # Use strict=True since we don't support HTTP/0.9
335
419
        httplib.HTTPSConnection.__init__(self, host, port,
336
420
                                         key_file, cert_file, strict=True)
337
421
        self.proxied_host = proxied_host
 
422
        self.ca_certs = ca_certs
338
423
 
339
424
    def connect(self):
340
425
        if 'http' in debug.debug_flags:
345
430
            self.connect_to_origin()
346
431
 
347
432
    def connect_to_origin(self):
348
 
        ssl_sock = _ssl_wrap_socket(self.sock, self.key_file, self.cert_file)
 
433
        # FIXME JRV 2011-12-18: Use location config here?
 
434
        config_stack = config.GlobalStack()
 
435
        cert_reqs = config_stack.get('ssl.cert_reqs')
 
436
        if self.proxied_host is not None:
 
437
            host = self.proxied_host.split(":", 1)[0]
 
438
        else:
 
439
            host = self.host
 
440
        if cert_reqs == ssl.CERT_NONE:
 
441
            ui.ui_factory.show_user_warning('not_checking_ssl_cert', host=host)
 
442
            ui.ui_factory.suppressed_warnings.add('not_checking_ssl_cert')
 
443
            ca_certs = None
 
444
        else:
 
445
            if self.ca_certs is None:
 
446
                ca_certs = config_stack.get('ssl.ca_certs')
 
447
            else:
 
448
                ca_certs = self.ca_certs
 
449
            if ca_certs is None:
 
450
                trace.warning(
 
451
                    "No valid trusted SSL CA certificates file set. See "
 
452
                    "'bzr help ssl.ca_certs' for more information on setting "
 
453
                    "trusted CAs.")
 
454
        try:
 
455
            ssl_sock = ssl.wrap_socket(
 
456
                self.sock, self.key_file, self.cert_file,
 
457
                cert_reqs=cert_reqs, ca_certs=ca_certs)
 
458
        except ssl.SSLError:
 
459
            trace.note(
 
460
                "\n"
 
461
                "See `bzr help ssl.ca_certs` for how to specify trusted CA"
 
462
                "certificates.\n"
 
463
                "Pass -Ossl.cert_reqs=none to disable certificate "
 
464
                "verification entirely.\n")
 
465
            raise
 
466
        if cert_reqs == ssl.CERT_REQUIRED:
 
467
            peer_cert = ssl_sock.getpeercert()
 
468
            ssl.match_hostname(peer_cert, host)
 
469
 
349
470
        # Wrap the ssl socket before anybody use it
350
471
        self._wrap_socket_for_reporting(ssl_sock)
351
472
 
453
574
 
454
575
    handler_order = 1000 # after all pre-processings
455
576
 
456
 
    def __init__(self, report_activity=None):
 
577
    def __init__(self, report_activity=None, ca_certs=None):
457
578
        self._report_activity = report_activity
 
579
        self.ca_certs = ca_certs
458
580
 
459
581
    def create_connection(self, request, http_connection_class):
460
582
        host = request.get_host()
468
590
        try:
469
591
            connection = http_connection_class(
470
592
                host, proxied_host=request.proxied_host,
471
 
                report_activity=self._report_activity)
 
593
                report_activity=self._report_activity,
 
594
                ca_certs=self.ca_certs)
472
595
        except httplib.InvalidURL, exception:
473
596
            # There is only one occurrence of InvalidURL in httplib
474
597
            raise errors.InvalidURL(request.get_full_url(),
660
783
                    % (request, request.connection.sock.getsockname())
661
784
            response = connection.getresponse()
662
785
            convert_to_addinfourl = True
 
786
        except (ssl.SSLError, ssl.CertificateError):
 
787
            # Something is wrong with either the certificate or the hostname,
 
788
            # re-trying won't help
 
789
            raise
663
790
        except (socket.gaierror, httplib.BadStatusLine, httplib.UnknownProtocol,
664
791
                socket.error, httplib.HTTPException):
665
792
            response = self.retry_or_raise(http_class, request, first_try)
1325
1452
 
1326
1453
    def _auth_match_kerberos(self, auth):
1327
1454
        """Try to create a GSSAPI response for authenticating against a host."""
1328
 
        if not have_kerberos:
 
1455
        global kerberos, checked_kerberos
 
1456
        if kerberos is None and not checked_kerberos:
 
1457
            try:
 
1458
                import kerberos
 
1459
            except ImportError:
 
1460
                kerberos = None
 
1461
            checked_kerberos = True
 
1462
        if kerberos is None:
1329
1463
            return None
1330
1464
        ret, vc = kerberos.authGSSClientInit("HTTP@%(host)s" % auth)
1331
1465
        if ret < 1:
1358
1492
 
1359
1493
    def build_auth_header(self, auth, request):
1360
1494
        raw = '%s:%s' % (auth['user'], auth['password'])
1361
 
        auth_header = 'Basic ' + raw.encode('base64').strip()
 
1495
        auth_header = 'Basic ' + base64.b64encode(raw)
1362
1496
        return auth_header
1363
1497
 
1364
1498
    def extract_realm(self, header_value):
1650
1784
                 connection=ConnectionHandler,
1651
1785
                 redirect=HTTPRedirectHandler,
1652
1786
                 error=HTTPErrorProcessor,
1653
 
                 report_activity=None):
 
1787
                 report_activity=None,
 
1788
                 ca_certs=None):
1654
1789
        self._opener = urllib2.build_opener(
1655
 
            connection(report_activity=report_activity),
 
1790
            connection(report_activity=report_activity, ca_certs=ca_certs),
1656
1791
            redirect, error,
1657
1792
            ProxyHandler(),
1658
1793
            HTTPBasicAuthHandler(),