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
"""Implementation of urllib2 tailored to bzr needs
17
"""Implementaion of urllib2 tailored to bzr needs
19
19
This file complements the urllib2 class hierarchy with custom classes.
73
lazy_import.lazy_import(globals(), """
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
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
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):
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]
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)
115
def cert_reqs_from_store(unicode_str):
119
"required": ssl.CERT_REQUIRED,
120
"none": ssl.CERT_NONE
123
raise ValueError("invalid value %s" % unicode_str)
126
opt_ssl_ca_certs = config.Option('ssl.ca_certs',
127
from_unicode=ca_certs_from_store,
128
default=default_ca_certs,
131
Path to certification authority certificates to trust.
133
This should be a valid path to a bundle containing all root Certificate
134
Authorities used to verify an https server certificate.
136
Use ssl.cert_reqs=none to disable certificate verification.
139
opt_ssl_cert_reqs = config.Option('ssl.cert_reqs',
141
from_unicode=cert_reqs_from_store,
144
Whether to require a certificate from the remote side. (default:required)
147
* none: Certificates ignored
148
* required: Certificates required and validated
151
checked_kerberos = False
155
78
class addinfourl(urllib2.addinfourl):
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
387
309
def connect(self):
388
310
if 'http' in debug.debug_flags:
391
313
self._wrap_socket_for_reporting(self.sock)
394
# These two methods were imported from Python 3.2's ssl module
396
def _dnsname_to_pat(dn):
398
for frag in dn.split(r'.'):
400
# When '*' is a fragment by itself, it matches a non-empty dotless
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)
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*.
415
CertificateError is raised on failure. On success, the function
419
raise ValueError("empty or no certificate")
421
san = cert.get('subjectAltName', ())
422
for key, value in san:
424
if _dnsname_to_pat(value).match(hostname):
426
dnsnames.append(value)
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
433
if key == 'commonName':
434
if _dnsname_to_pat(value).match(hostname):
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]))
445
raise errors.CertificateError("no appropriate commonName or "
446
"subjectAltName fields were found")
316
# Build the appropriate socket wrapper for ssl
318
# python 2.6 introduced a better ssl package
320
_ssl_wrap_socket = ssl.wrap_socket
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)
449
329
class HTTPSConnection(AbstractHTTPConnection, httplib.HTTPSConnection):
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
461
340
def connect(self):
462
341
if 'http' in debug.debug_flags:
467
346
self.connect_to_origin()
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)
478
if self.ca_certs is None:
479
ca_certs = config_stack.get('ssl.ca_certs')
481
ca_certs = self.ca_certs
484
"No valid trusted SSL CA certificates file set. See "
485
"'bzr help ssl.ca_certs' for more information on setting "
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:
493
"See `bzr help ssl.ca_certs` for how to specify trusted CA"
495
"Pass -Ossl.cert_reqs=none to disable certificate "
496
"verification entirely.\n")
498
if cert_reqs == ssl.CERT_REQUIRED:
499
peer_cert = ssl_sock.getpeercert()
500
match_hostname(peer_cert, self.host)
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)
607
455
handler_order = 1000 # after all pre-processings
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
613
460
def create_connection(self, request, http_connection_class):
614
461
host = request.get_host()
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
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)
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,
1171
1012
# We ignore path since we connect to a proxy
1173
if parsed_url.port is None:
1174
phost = parsed_url.host
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)
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.
1816
1651
connection=ConnectionHandler,
1817
1652
redirect=HTTPRedirectHandler,
1818
1653
error=HTTPErrorProcessor,
1819
report_activity=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(),