##// END OF EJS Templates
sslutil: pass ui to _defaultcacerts...
Gregory Szorc -
r29483:918dce4b default
parent child Browse files
Show More
@@ -1,521 +1,521
1 1 # sslutil.py - SSL handling for mercurial
2 2 #
3 3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 from __future__ import absolute_import
11 11
12 12 import hashlib
13 13 import os
14 14 import re
15 15 import ssl
16 16 import sys
17 17
18 18 from .i18n import _
19 19 from . import (
20 20 error,
21 21 util,
22 22 )
23 23
24 24 # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
25 25 # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
26 26 # all exposed via the "ssl" module.
27 27 #
28 28 # Depending on the version of Python being used, SSL/TLS support is either
29 29 # modern/secure or legacy/insecure. Many operations in this module have
30 30 # separate code paths depending on support in Python.
31 31
32 32 hassni = getattr(ssl, 'HAS_SNI', False)
33 33
34 34 try:
35 35 OP_NO_SSLv2 = ssl.OP_NO_SSLv2
36 36 OP_NO_SSLv3 = ssl.OP_NO_SSLv3
37 37 except AttributeError:
38 38 OP_NO_SSLv2 = 0x1000000
39 39 OP_NO_SSLv3 = 0x2000000
40 40
41 41 try:
42 42 # ssl.SSLContext was added in 2.7.9 and presence indicates modern
43 43 # SSL/TLS features are available.
44 44 SSLContext = ssl.SSLContext
45 45 modernssl = True
46 46 _canloaddefaultcerts = util.safehasattr(SSLContext, 'load_default_certs')
47 47 except AttributeError:
48 48 modernssl = False
49 49 _canloaddefaultcerts = False
50 50
51 51 # We implement SSLContext using the interface from the standard library.
52 52 class SSLContext(object):
53 53 # ssl.wrap_socket gained the "ciphers" named argument in 2.7.
54 54 _supportsciphers = sys.version_info >= (2, 7)
55 55
56 56 def __init__(self, protocol):
57 57 # From the public interface of SSLContext
58 58 self.protocol = protocol
59 59 self.check_hostname = False
60 60 self.options = 0
61 61 self.verify_mode = ssl.CERT_NONE
62 62
63 63 # Used by our implementation.
64 64 self._certfile = None
65 65 self._keyfile = None
66 66 self._certpassword = None
67 67 self._cacerts = None
68 68 self._ciphers = None
69 69
70 70 def load_cert_chain(self, certfile, keyfile=None, password=None):
71 71 self._certfile = certfile
72 72 self._keyfile = keyfile
73 73 self._certpassword = password
74 74
75 75 def load_default_certs(self, purpose=None):
76 76 pass
77 77
78 78 def load_verify_locations(self, cafile=None, capath=None, cadata=None):
79 79 if capath:
80 80 raise error.Abort(_('capath not supported'))
81 81 if cadata:
82 82 raise error.Abort(_('cadata not supported'))
83 83
84 84 self._cacerts = cafile
85 85
86 86 def set_ciphers(self, ciphers):
87 87 if not self._supportsciphers:
88 88 raise error.Abort(_('setting ciphers not supported'))
89 89
90 90 self._ciphers = ciphers
91 91
92 92 def wrap_socket(self, socket, server_hostname=None, server_side=False):
93 93 # server_hostname is unique to SSLContext.wrap_socket and is used
94 94 # for SNI in that context. So there's nothing for us to do with it
95 95 # in this legacy code since we don't support SNI.
96 96
97 97 args = {
98 98 'keyfile': self._keyfile,
99 99 'certfile': self._certfile,
100 100 'server_side': server_side,
101 101 'cert_reqs': self.verify_mode,
102 102 'ssl_version': self.protocol,
103 103 'ca_certs': self._cacerts,
104 104 }
105 105
106 106 if self._supportsciphers:
107 107 args['ciphers'] = self._ciphers
108 108
109 109 return ssl.wrap_socket(socket, **args)
110 110
111 111 def _hostsettings(ui, hostname):
112 112 """Obtain security settings for a hostname.
113 113
114 114 Returns a dict of settings relevant to that hostname.
115 115 """
116 116 s = {
117 117 # Whether we should attempt to load default/available CA certs
118 118 # if an explicit ``cafile`` is not defined.
119 119 'allowloaddefaultcerts': True,
120 120 # List of 2-tuple of (hash algorithm, hash).
121 121 'certfingerprints': [],
122 122 # Path to file containing concatenated CA certs. Used by
123 123 # SSLContext.load_verify_locations().
124 124 'cafile': None,
125 125 # Whether certificate verification should be disabled.
126 126 'disablecertverification': False,
127 127 # Whether the legacy [hostfingerprints] section has data for this host.
128 128 'legacyfingerprint': False,
129 129 # ssl.CERT_* constant used by SSLContext.verify_mode.
130 130 'verifymode': None,
131 131 }
132 132
133 133 # Look for fingerprints in [hostsecurity] section. Value is a list
134 134 # of <alg>:<fingerprint> strings.
135 135 fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % hostname,
136 136 [])
137 137 for fingerprint in fingerprints:
138 138 if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))):
139 139 raise error.Abort(_('invalid fingerprint for %s: %s') % (
140 140 hostname, fingerprint),
141 141 hint=_('must begin with "sha1:", "sha256:", '
142 142 'or "sha512:"'))
143 143
144 144 alg, fingerprint = fingerprint.split(':', 1)
145 145 fingerprint = fingerprint.replace(':', '').lower()
146 146 s['certfingerprints'].append((alg, fingerprint))
147 147
148 148 # Fingerprints from [hostfingerprints] are always SHA-1.
149 149 for fingerprint in ui.configlist('hostfingerprints', hostname, []):
150 150 fingerprint = fingerprint.replace(':', '').lower()
151 151 s['certfingerprints'].append(('sha1', fingerprint))
152 152 s['legacyfingerprint'] = True
153 153
154 154 # If a host cert fingerprint is defined, it is the only thing that
155 155 # matters. No need to validate CA certs.
156 156 if s['certfingerprints']:
157 157 s['verifymode'] = ssl.CERT_NONE
158 158 s['allowloaddefaultcerts'] = False
159 159
160 160 # If --insecure is used, don't take CAs into consideration.
161 161 elif ui.insecureconnections:
162 162 s['disablecertverification'] = True
163 163 s['verifymode'] = ssl.CERT_NONE
164 164 s['allowloaddefaultcerts'] = False
165 165
166 166 if ui.configbool('devel', 'disableloaddefaultcerts'):
167 167 s['allowloaddefaultcerts'] = False
168 168
169 169 # If both fingerprints and a per-host ca file are specified, issue a warning
170 170 # because users should not be surprised about what security is or isn't
171 171 # being performed.
172 172 cafile = ui.config('hostsecurity', '%s:verifycertsfile' % hostname)
173 173 if s['certfingerprints'] and cafile:
174 174 ui.warn(_('(hostsecurity.%s:verifycertsfile ignored when host '
175 175 'fingerprints defined; using host fingerprints for '
176 176 'verification)\n') % hostname)
177 177
178 178 # Try to hook up CA certificate validation unless something above
179 179 # makes it not necessary.
180 180 if s['verifymode'] is None:
181 181 # Look at per-host ca file first.
182 182 if cafile:
183 183 cafile = util.expandpath(cafile)
184 184 if not os.path.exists(cafile):
185 185 raise error.Abort(_('path specified by %s does not exist: %s') %
186 186 ('hostsecurity.%s:verifycertsfile' % hostname,
187 187 cafile))
188 188 s['cafile'] = cafile
189 189 else:
190 190 # Find global certificates file in config.
191 191 cafile = ui.config('web', 'cacerts')
192 192
193 193 if cafile:
194 194 cafile = util.expandpath(cafile)
195 195 if not os.path.exists(cafile):
196 196 raise error.Abort(_('could not find web.cacerts: %s') %
197 197 cafile)
198 198 else:
199 199 # CAs not defined in config. Try to find system bundles.
200 cafile = _defaultcacerts()
200 cafile = _defaultcacerts(ui)
201 201 if cafile:
202 202 ui.debug('using %s for CA file\n' % cafile)
203 203
204 204 s['cafile'] = cafile
205 205
206 206 # Require certificate validation if CA certs are being loaded and
207 207 # verification hasn't been disabled above.
208 208 if cafile or (_canloaddefaultcerts and s['allowloaddefaultcerts']):
209 209 s['verifymode'] = ssl.CERT_REQUIRED
210 210 else:
211 211 # At this point we don't have a fingerprint, aren't being
212 212 # explicitly insecure, and can't load CA certs. Connecting
213 213 # is insecure. We allow the connection and abort during
214 214 # validation (once we have the fingerprint to print to the
215 215 # user).
216 216 s['verifymode'] = ssl.CERT_NONE
217 217
218 218 assert s['verifymode'] is not None
219 219
220 220 return s
221 221
222 222 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None):
223 223 """Add SSL/TLS to a socket.
224 224
225 225 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
226 226 choices based on what security options are available.
227 227
228 228 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
229 229 the following additional arguments:
230 230
231 231 * serverhostname - The expected hostname of the remote server. If the
232 232 server (and client) support SNI, this tells the server which certificate
233 233 to use.
234 234 """
235 235 if not serverhostname:
236 236 raise error.Abort(_('serverhostname argument is required'))
237 237
238 238 settings = _hostsettings(ui, serverhostname)
239 239
240 240 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
241 241 # that both ends support, including TLS protocols. On legacy stacks,
242 242 # the highest it likely goes in TLS 1.0. On modern stacks, it can
243 243 # support TLS 1.2.
244 244 #
245 245 # The PROTOCOL_TLSv* constants select a specific TLS version
246 246 # only (as opposed to multiple versions). So the method for
247 247 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
248 248 # disable protocols via SSLContext.options and OP_NO_* constants.
249 249 # However, SSLContext.options doesn't work unless we have the
250 250 # full/real SSLContext available to us.
251 251 #
252 252 # SSLv2 and SSLv3 are broken. We ban them outright.
253 253 if modernssl:
254 254 protocol = ssl.PROTOCOL_SSLv23
255 255 else:
256 256 protocol = ssl.PROTOCOL_TLSv1
257 257
258 258 # TODO use ssl.create_default_context() on modernssl.
259 259 sslcontext = SSLContext(protocol)
260 260
261 261 # This is a no-op on old Python.
262 262 sslcontext.options |= OP_NO_SSLv2 | OP_NO_SSLv3
263 263
264 264 # This still works on our fake SSLContext.
265 265 sslcontext.verify_mode = settings['verifymode']
266 266
267 267 if certfile is not None:
268 268 def password():
269 269 f = keyfile or certfile
270 270 return ui.getpass(_('passphrase for %s: ') % f, '')
271 271 sslcontext.load_cert_chain(certfile, keyfile, password)
272 272
273 273 if settings['cafile'] is not None:
274 274 try:
275 275 sslcontext.load_verify_locations(cafile=settings['cafile'])
276 276 except ssl.SSLError as e:
277 277 raise error.Abort(_('error loading CA file %s: %s') % (
278 278 settings['cafile'], e.args[1]),
279 279 hint=_('file is empty or malformed?'))
280 280 caloaded = True
281 281 elif settings['allowloaddefaultcerts']:
282 282 # This is a no-op on old Python.
283 283 sslcontext.load_default_certs()
284 284 caloaded = True
285 285 else:
286 286 caloaded = False
287 287
288 288 try:
289 289 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
290 290 except ssl.SSLError:
291 291 # If we're doing certificate verification and no CA certs are loaded,
292 292 # that is almost certainly the reason why verification failed. Provide
293 293 # a hint to the user.
294 294 # Only modern ssl module exposes SSLContext.get_ca_certs() so we can
295 295 # only show this warning if modern ssl is available.
296 296 if (caloaded and settings['verifymode'] == ssl.CERT_REQUIRED and
297 297 modernssl and not sslcontext.get_ca_certs()):
298 298 ui.warn(_('(an attempt was made to load CA certificates but none '
299 299 'were loaded; see '
300 300 'https://mercurial-scm.org/wiki/SecureConnections for '
301 301 'how to configure Mercurial to avoid this error)\n'))
302 302 raise
303 303
304 304 # check if wrap_socket failed silently because socket had been
305 305 # closed
306 306 # - see http://bugs.python.org/issue13721
307 307 if not sslsocket.cipher():
308 308 raise error.Abort(_('ssl connection failed'))
309 309
310 310 sslsocket._hgstate = {
311 311 'caloaded': caloaded,
312 312 'hostname': serverhostname,
313 313 'settings': settings,
314 314 'ui': ui,
315 315 }
316 316
317 317 return sslsocket
318 318
319 319 class wildcarderror(Exception):
320 320 """Represents an error parsing wildcards in DNS name."""
321 321
322 322 def _dnsnamematch(dn, hostname, maxwildcards=1):
323 323 """Match DNS names according RFC 6125 section 6.4.3.
324 324
325 325 This code is effectively copied from CPython's ssl._dnsname_match.
326 326
327 327 Returns a bool indicating whether the expected hostname matches
328 328 the value in ``dn``.
329 329 """
330 330 pats = []
331 331 if not dn:
332 332 return False
333 333
334 334 pieces = dn.split(r'.')
335 335 leftmost = pieces[0]
336 336 remainder = pieces[1:]
337 337 wildcards = leftmost.count('*')
338 338 if wildcards > maxwildcards:
339 339 raise wildcarderror(
340 340 _('too many wildcards in certificate DNS name: %s') % dn)
341 341
342 342 # speed up common case w/o wildcards
343 343 if not wildcards:
344 344 return dn.lower() == hostname.lower()
345 345
346 346 # RFC 6125, section 6.4.3, subitem 1.
347 347 # The client SHOULD NOT attempt to match a presented identifier in which
348 348 # the wildcard character comprises a label other than the left-most label.
349 349 if leftmost == '*':
350 350 # When '*' is a fragment by itself, it matches a non-empty dotless
351 351 # fragment.
352 352 pats.append('[^.]+')
353 353 elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
354 354 # RFC 6125, section 6.4.3, subitem 3.
355 355 # The client SHOULD NOT attempt to match a presented identifier
356 356 # where the wildcard character is embedded within an A-label or
357 357 # U-label of an internationalized domain name.
358 358 pats.append(re.escape(leftmost))
359 359 else:
360 360 # Otherwise, '*' matches any dotless string, e.g. www*
361 361 pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
362 362
363 363 # add the remaining fragments, ignore any wildcards
364 364 for frag in remainder:
365 365 pats.append(re.escape(frag))
366 366
367 367 pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
368 368 return pat.match(hostname) is not None
369 369
370 370 def _verifycert(cert, hostname):
371 371 '''Verify that cert (in socket.getpeercert() format) matches hostname.
372 372 CRLs is not handled.
373 373
374 374 Returns error message if any problems are found and None on success.
375 375 '''
376 376 if not cert:
377 377 return _('no certificate received')
378 378
379 379 dnsnames = []
380 380 san = cert.get('subjectAltName', [])
381 381 for key, value in san:
382 382 if key == 'DNS':
383 383 try:
384 384 if _dnsnamematch(value, hostname):
385 385 return
386 386 except wildcarderror as e:
387 387 return e.message
388 388
389 389 dnsnames.append(value)
390 390
391 391 if not dnsnames:
392 392 # The subject is only checked when there is no DNS in subjectAltName.
393 393 for sub in cert.get('subject', []):
394 394 for key, value in sub:
395 395 # According to RFC 2818 the most specific Common Name must
396 396 # be used.
397 397 if key == 'commonName':
398 398 # 'subject' entries are unicide.
399 399 try:
400 400 value = value.encode('ascii')
401 401 except UnicodeEncodeError:
402 402 return _('IDN in certificate not supported')
403 403
404 404 try:
405 405 if _dnsnamematch(value, hostname):
406 406 return
407 407 except wildcarderror as e:
408 408 return e.message
409 409
410 410 dnsnames.append(value)
411 411
412 412 if len(dnsnames) > 1:
413 413 return _('certificate is for %s') % ', '.join(dnsnames)
414 414 elif len(dnsnames) == 1:
415 415 return _('certificate is for %s') % dnsnames[0]
416 416 else:
417 417 return _('no commonName or subjectAltName found in certificate')
418 418
419 419 def _plainapplepython():
420 420 """return true if this seems to be a pure Apple Python that
421 421 * is unfrozen and presumably has the whole mercurial module in the file
422 422 system
423 423 * presumably is an Apple Python that uses Apple OpenSSL which has patches
424 424 for using system certificate store CAs in addition to the provided
425 425 cacerts file
426 426 """
427 427 if sys.platform != 'darwin' or util.mainfrozen() or not sys.executable:
428 428 return False
429 429 exe = os.path.realpath(sys.executable).lower()
430 430 return (exe.startswith('/usr/bin/python') or
431 431 exe.startswith('/system/library/frameworks/python.framework/'))
432 432
433 def _defaultcacerts():
433 def _defaultcacerts(ui):
434 434 """return path to default CA certificates or None."""
435 435 if _plainapplepython():
436 436 dummycert = os.path.join(os.path.dirname(__file__), 'dummycert.pem')
437 437 if os.path.exists(dummycert):
438 438 return dummycert
439 439
440 440 return None
441 441
442 442 def validatesocket(sock):
443 443 """Validate a socket meets security requiremnets.
444 444
445 445 The passed socket must have been created with ``wrapsocket()``.
446 446 """
447 447 host = sock._hgstate['hostname']
448 448 ui = sock._hgstate['ui']
449 449 settings = sock._hgstate['settings']
450 450
451 451 try:
452 452 peercert = sock.getpeercert(True)
453 453 peercert2 = sock.getpeercert()
454 454 except AttributeError:
455 455 raise error.Abort(_('%s ssl connection error') % host)
456 456
457 457 if not peercert:
458 458 raise error.Abort(_('%s certificate error: '
459 459 'no certificate received') % host)
460 460
461 461 if settings['disablecertverification']:
462 462 # We don't print the certificate fingerprint because it shouldn't
463 463 # be necessary: if the user requested certificate verification be
464 464 # disabled, they presumably already saw a message about the inability
465 465 # to verify the certificate and this message would have printed the
466 466 # fingerprint. So printing the fingerprint here adds little to no
467 467 # value.
468 468 ui.warn(_('warning: connection security to %s is disabled per current '
469 469 'settings; communication is susceptible to eavesdropping '
470 470 'and tampering\n') % host)
471 471 return
472 472
473 473 # If a certificate fingerprint is pinned, use it and only it to
474 474 # validate the remote cert.
475 475 peerfingerprints = {
476 476 'sha1': hashlib.sha1(peercert).hexdigest(),
477 477 'sha256': hashlib.sha256(peercert).hexdigest(),
478 478 'sha512': hashlib.sha512(peercert).hexdigest(),
479 479 }
480 480
481 481 def fmtfingerprint(s):
482 482 return ':'.join([s[x:x + 2] for x in range(0, len(s), 2)])
483 483
484 484 nicefingerprint = 'sha256:%s' % fmtfingerprint(peerfingerprints['sha256'])
485 485
486 486 if settings['certfingerprints']:
487 487 for hash, fingerprint in settings['certfingerprints']:
488 488 if peerfingerprints[hash].lower() == fingerprint:
489 489 ui.debug('%s certificate matched fingerprint %s:%s\n' %
490 490 (host, hash, fmtfingerprint(fingerprint)))
491 491 return
492 492
493 493 # Pinned fingerprint didn't match. This is a fatal error.
494 494 if settings['legacyfingerprint']:
495 495 section = 'hostfingerprint'
496 496 nice = fmtfingerprint(peerfingerprints['sha1'])
497 497 else:
498 498 section = 'hostsecurity'
499 499 nice = '%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
500 500 raise error.Abort(_('certificate for %s has unexpected '
501 501 'fingerprint %s') % (host, nice),
502 502 hint=_('check %s configuration') % section)
503 503
504 504 # Security is enabled but no CAs are loaded. We can't establish trust
505 505 # for the cert so abort.
506 506 if not sock._hgstate['caloaded']:
507 507 raise error.Abort(
508 508 _('unable to verify security of %s (no loaded CA certificates); '
509 509 'refusing to connect') % host,
510 510 hint=_('see https://mercurial-scm.org/wiki/SecureConnections for '
511 511 'how to configure Mercurial to avoid this error or set '
512 512 'hostsecurity.%s:fingerprints=%s to trust this server') %
513 513 (host, nicefingerprint))
514 514
515 515 msg = _verifycert(peercert2, host)
516 516 if msg:
517 517 raise error.Abort(_('%s certificate error: %s') % (host, msg),
518 518 hint=_('set hostsecurity.%s:certfingerprints=%s '
519 519 'config setting or use --insecure to connect '
520 520 'insecurely') %
521 521 (host, nicefingerprint))
@@ -1,550 +1,552
1 1 from __future__ import absolute_import
2 2
3 3 import errno
4 4 import os
5 5 import re
6 6 import socket
7 7 import stat
8 8 import subprocess
9 9 import sys
10 10 import tempfile
11 11
12 12 tempprefix = 'hg-hghave-'
13 13
14 14 checks = {
15 15 "true": (lambda: True, "yak shaving"),
16 16 "false": (lambda: False, "nail clipper"),
17 17 }
18 18
19 19 def check(name, desc):
20 20 """Registers a check function for a feature."""
21 21 def decorator(func):
22 22 checks[name] = (func, desc)
23 23 return func
24 24 return decorator
25 25
26 26 def checkvers(name, desc, vers):
27 27 """Registers a check function for each of a series of versions.
28 28
29 29 vers can be a list or an iterator"""
30 30 def decorator(func):
31 31 def funcv(v):
32 32 def f():
33 33 return func(v)
34 34 return f
35 35 for v in vers:
36 36 v = str(v)
37 37 f = funcv(v)
38 38 checks['%s%s' % (name, v.replace('.', ''))] = (f, desc % v)
39 39 return func
40 40 return decorator
41 41
42 42 def checkfeatures(features):
43 43 result = {
44 44 'error': [],
45 45 'missing': [],
46 46 'skipped': [],
47 47 }
48 48
49 49 for feature in features:
50 50 negate = feature.startswith('no-')
51 51 if negate:
52 52 feature = feature[3:]
53 53
54 54 if feature not in checks:
55 55 result['missing'].append(feature)
56 56 continue
57 57
58 58 check, desc = checks[feature]
59 59 try:
60 60 available = check()
61 61 except Exception:
62 62 result['error'].append('hghave check failed: %s' % feature)
63 63 continue
64 64
65 65 if not negate and not available:
66 66 result['skipped'].append('missing feature: %s' % desc)
67 67 elif negate and available:
68 68 result['skipped'].append('system supports %s' % desc)
69 69
70 70 return result
71 71
72 72 def require(features):
73 73 """Require that features are available, exiting if not."""
74 74 result = checkfeatures(features)
75 75
76 76 for missing in result['missing']:
77 77 sys.stderr.write('skipped: unknown feature: %s\n' % missing)
78 78 for msg in result['skipped']:
79 79 sys.stderr.write('skipped: %s\n' % msg)
80 80 for msg in result['error']:
81 81 sys.stderr.write('%s\n' % msg)
82 82
83 83 if result['missing']:
84 84 sys.exit(2)
85 85
86 86 if result['skipped'] or result['error']:
87 87 sys.exit(1)
88 88
89 89 def matchoutput(cmd, regexp, ignorestatus=False):
90 90 """Return the match object if cmd executes successfully and its output
91 91 is matched by the supplied regular expression.
92 92 """
93 93 r = re.compile(regexp)
94 94 try:
95 95 p = subprocess.Popen(
96 96 cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
97 97 except OSError as e:
98 98 if e.errno != errno.ENOENT:
99 99 raise
100 100 ret = -1
101 101 ret = p.wait()
102 102 s = p.stdout.read()
103 103 return (ignorestatus or not ret) and r.search(s)
104 104
105 105 @check("baz", "GNU Arch baz client")
106 106 def has_baz():
107 107 return matchoutput('baz --version 2>&1', br'baz Bazaar version')
108 108
109 109 @check("bzr", "Canonical's Bazaar client")
110 110 def has_bzr():
111 111 try:
112 112 import bzrlib
113 113 return bzrlib.__doc__ is not None
114 114 except ImportError:
115 115 return False
116 116
117 117 @checkvers("bzr", "Canonical's Bazaar client >= %s", (1.14,))
118 118 def has_bzr_range(v):
119 119 major, minor = v.split('.')[0:2]
120 120 try:
121 121 import bzrlib
122 122 return (bzrlib.__doc__ is not None
123 123 and bzrlib.version_info[:2] >= (int(major), int(minor)))
124 124 except ImportError:
125 125 return False
126 126
127 127 @check("chg", "running with chg")
128 128 def has_chg():
129 129 return 'CHGHG' in os.environ
130 130
131 131 @check("cvs", "cvs client/server")
132 132 def has_cvs():
133 133 re = br'Concurrent Versions System.*?server'
134 134 return matchoutput('cvs --version 2>&1', re) and not has_msys()
135 135
136 136 @check("cvs112", "cvs client/server 1.12.* (not cvsnt)")
137 137 def has_cvs112():
138 138 re = br'Concurrent Versions System \(CVS\) 1.12.*?server'
139 139 return matchoutput('cvs --version 2>&1', re) and not has_msys()
140 140
141 141 @check("cvsnt", "cvsnt client/server")
142 142 def has_cvsnt():
143 143 re = br'Concurrent Versions System \(CVSNT\) (\d+).(\d+).*\(client/server\)'
144 144 return matchoutput('cvsnt --version 2>&1', re)
145 145
146 146 @check("darcs", "darcs client")
147 147 def has_darcs():
148 148 return matchoutput('darcs --version', br'2\.[2-9]', True)
149 149
150 150 @check("mtn", "monotone client (>= 1.0)")
151 151 def has_mtn():
152 152 return matchoutput('mtn --version', br'monotone', True) and not matchoutput(
153 153 'mtn --version', br'monotone 0\.', True)
154 154
155 155 @check("eol-in-paths", "end-of-lines in paths")
156 156 def has_eol_in_paths():
157 157 try:
158 158 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix, suffix='\n\r')
159 159 os.close(fd)
160 160 os.remove(path)
161 161 return True
162 162 except (IOError, OSError):
163 163 return False
164 164
165 165 @check("execbit", "executable bit")
166 166 def has_executablebit():
167 167 try:
168 168 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
169 169 fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
170 170 try:
171 171 os.close(fh)
172 172 m = os.stat(fn).st_mode & 0o777
173 173 new_file_has_exec = m & EXECFLAGS
174 174 os.chmod(fn, m ^ EXECFLAGS)
175 175 exec_flags_cannot_flip = ((os.stat(fn).st_mode & 0o777) == m)
176 176 finally:
177 177 os.unlink(fn)
178 178 except (IOError, OSError):
179 179 # we don't care, the user probably won't be able to commit anyway
180 180 return False
181 181 return not (new_file_has_exec or exec_flags_cannot_flip)
182 182
183 183 @check("icasefs", "case insensitive file system")
184 184 def has_icasefs():
185 185 # Stolen from mercurial.util
186 186 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
187 187 os.close(fd)
188 188 try:
189 189 s1 = os.stat(path)
190 190 d, b = os.path.split(path)
191 191 p2 = os.path.join(d, b.upper())
192 192 if path == p2:
193 193 p2 = os.path.join(d, b.lower())
194 194 try:
195 195 s2 = os.stat(p2)
196 196 return s2 == s1
197 197 except OSError:
198 198 return False
199 199 finally:
200 200 os.remove(path)
201 201
202 202 @check("fifo", "named pipes")
203 203 def has_fifo():
204 204 if getattr(os, "mkfifo", None) is None:
205 205 return False
206 206 name = tempfile.mktemp(dir='.', prefix=tempprefix)
207 207 try:
208 208 os.mkfifo(name)
209 209 os.unlink(name)
210 210 return True
211 211 except OSError:
212 212 return False
213 213
214 214 @check("killdaemons", 'killdaemons.py support')
215 215 def has_killdaemons():
216 216 return True
217 217
218 218 @check("cacheable", "cacheable filesystem")
219 219 def has_cacheable_fs():
220 220 from mercurial import util
221 221
222 222 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
223 223 os.close(fd)
224 224 try:
225 225 return util.cachestat(path).cacheable()
226 226 finally:
227 227 os.remove(path)
228 228
229 229 @check("lsprof", "python lsprof module")
230 230 def has_lsprof():
231 231 try:
232 232 import _lsprof
233 233 _lsprof.Profiler # silence unused import warning
234 234 return True
235 235 except ImportError:
236 236 return False
237 237
238 238 def gethgversion():
239 239 m = matchoutput('hg --version --quiet 2>&1', br'(\d+)\.(\d+)')
240 240 if not m:
241 241 return (0, 0)
242 242 return (int(m.group(1)), int(m.group(2)))
243 243
244 244 @checkvers("hg", "Mercurial >= %s",
245 245 list([(1.0 * x) / 10 for x in range(9, 40)]))
246 246 def has_hg_range(v):
247 247 major, minor = v.split('.')[0:2]
248 248 return gethgversion() >= (int(major), int(minor))
249 249
250 250 @check("hg08", "Mercurial >= 0.8")
251 251 def has_hg08():
252 252 if checks["hg09"][0]():
253 253 return True
254 254 return matchoutput('hg help annotate 2>&1', '--date')
255 255
256 256 @check("hg07", "Mercurial >= 0.7")
257 257 def has_hg07():
258 258 if checks["hg08"][0]():
259 259 return True
260 260 return matchoutput('hg --version --quiet 2>&1', 'Mercurial Distributed SCM')
261 261
262 262 @check("hg06", "Mercurial >= 0.6")
263 263 def has_hg06():
264 264 if checks["hg07"][0]():
265 265 return True
266 266 return matchoutput('hg --version --quiet 2>&1', 'Mercurial version')
267 267
268 268 @check("gettext", "GNU Gettext (msgfmt)")
269 269 def has_gettext():
270 270 return matchoutput('msgfmt --version', br'GNU gettext-tools')
271 271
272 272 @check("git", "git command line client")
273 273 def has_git():
274 274 return matchoutput('git --version 2>&1', br'^git version')
275 275
276 276 @check("docutils", "Docutils text processing library")
277 277 def has_docutils():
278 278 try:
279 279 import docutils.core
280 280 docutils.core.publish_cmdline # silence unused import
281 281 return True
282 282 except ImportError:
283 283 return False
284 284
285 285 def getsvnversion():
286 286 m = matchoutput('svn --version --quiet 2>&1', br'^(\d+)\.(\d+)')
287 287 if not m:
288 288 return (0, 0)
289 289 return (int(m.group(1)), int(m.group(2)))
290 290
291 291 @checkvers("svn", "subversion client and admin tools >= %s", (1.3, 1.5))
292 292 def has_svn_range(v):
293 293 major, minor = v.split('.')[0:2]
294 294 return getsvnversion() >= (int(major), int(minor))
295 295
296 296 @check("svn", "subversion client and admin tools")
297 297 def has_svn():
298 298 return matchoutput('svn --version 2>&1', br'^svn, version') and \
299 299 matchoutput('svnadmin --version 2>&1', br'^svnadmin, version')
300 300
301 301 @check("svn-bindings", "subversion python bindings")
302 302 def has_svn_bindings():
303 303 try:
304 304 import svn.core
305 305 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
306 306 if version < (1, 4):
307 307 return False
308 308 return True
309 309 except ImportError:
310 310 return False
311 311
312 312 @check("p4", "Perforce server and client")
313 313 def has_p4():
314 314 return (matchoutput('p4 -V', br'Rev\. P4/') and
315 315 matchoutput('p4d -V', br'Rev\. P4D/'))
316 316
317 317 @check("symlink", "symbolic links")
318 318 def has_symlink():
319 319 if getattr(os, "symlink", None) is None:
320 320 return False
321 321 name = tempfile.mktemp(dir='.', prefix=tempprefix)
322 322 try:
323 323 os.symlink(".", name)
324 324 os.unlink(name)
325 325 return True
326 326 except (OSError, AttributeError):
327 327 return False
328 328
329 329 @check("hardlink", "hardlinks")
330 330 def has_hardlink():
331 331 from mercurial import util
332 332 fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
333 333 os.close(fh)
334 334 name = tempfile.mktemp(dir='.', prefix=tempprefix)
335 335 try:
336 336 util.oslink(fn, name)
337 337 os.unlink(name)
338 338 return True
339 339 except OSError:
340 340 return False
341 341 finally:
342 342 os.unlink(fn)
343 343
344 344 @check("tla", "GNU Arch tla client")
345 345 def has_tla():
346 346 return matchoutput('tla --version 2>&1', br'The GNU Arch Revision')
347 347
348 348 @check("gpg", "gpg client")
349 349 def has_gpg():
350 350 return matchoutput('gpg --version 2>&1', br'GnuPG')
351 351
352 352 @check("unix-permissions", "unix-style permissions")
353 353 def has_unix_permissions():
354 354 d = tempfile.mkdtemp(dir='.', prefix=tempprefix)
355 355 try:
356 356 fname = os.path.join(d, 'foo')
357 357 for umask in (0o77, 0o07, 0o22):
358 358 os.umask(umask)
359 359 f = open(fname, 'w')
360 360 f.close()
361 361 mode = os.stat(fname).st_mode
362 362 os.unlink(fname)
363 363 if mode & 0o777 != ~umask & 0o666:
364 364 return False
365 365 return True
366 366 finally:
367 367 os.rmdir(d)
368 368
369 369 @check("unix-socket", "AF_UNIX socket family")
370 370 def has_unix_socket():
371 371 return getattr(socket, 'AF_UNIX', None) is not None
372 372
373 373 @check("root", "root permissions")
374 374 def has_root():
375 375 return getattr(os, 'geteuid', None) and os.geteuid() == 0
376 376
377 377 @check("pyflakes", "Pyflakes python linter")
378 378 def has_pyflakes():
379 379 return matchoutput("sh -c \"echo 'import re' 2>&1 | pyflakes\"",
380 380 br"<stdin>:1: 're' imported but unused",
381 381 True)
382 382
383 383 @check("pygments", "Pygments source highlighting library")
384 384 def has_pygments():
385 385 try:
386 386 import pygments
387 387 pygments.highlight # silence unused import warning
388 388 return True
389 389 except ImportError:
390 390 return False
391 391
392 392 @check("outer-repo", "outer repo")
393 393 def has_outer_repo():
394 394 # failing for other reasons than 'no repo' imply that there is a repo
395 395 return not matchoutput('hg root 2>&1',
396 396 br'abort: no repository found', True)
397 397
398 398 @check("ssl", "ssl module available")
399 399 def has_ssl():
400 400 try:
401 401 import ssl
402 402 ssl.CERT_NONE
403 403 return True
404 404 except ImportError:
405 405 return False
406 406
407 407 @check("sslcontext", "python >= 2.7.9 ssl")
408 408 def has_sslcontext():
409 409 try:
410 410 import ssl
411 411 ssl.SSLContext
412 412 return True
413 413 except (ImportError, AttributeError):
414 414 return False
415 415
416 416 @check("defaultcacerts", "can verify SSL certs by system's CA certs store")
417 417 def has_defaultcacerts():
418 from mercurial import sslutil
419 return sslutil._defaultcacerts() or sslutil._canloaddefaultcerts
418 from mercurial import sslutil, ui as uimod
419 ui = uimod.ui()
420 return sslutil._defaultcacerts(ui) or sslutil._canloaddefaultcerts
420 421
421 422 @check("defaultcacertsloaded", "detected presence of loaded system CA certs")
422 423 def has_defaultcacertsloaded():
423 424 import ssl
424 from mercurial import sslutil
425 from mercurial import sslutil, ui as uimod
425 426
426 427 if not has_defaultcacerts():
427 428 return False
428 429 if not has_sslcontext():
429 430 return False
430 431
431 cafile = sslutil._defaultcacerts()
432 ui = uimod.ui()
433 cafile = sslutil._defaultcacerts(ui)
432 434 ctx = ssl.create_default_context()
433 435 if cafile:
434 436 ctx.load_verify_locations(cafile=cafile)
435 437 else:
436 438 ctx.load_default_certs()
437 439
438 440 return len(ctx.get_ca_certs()) > 0
439 441
440 442 @check("windows", "Windows")
441 443 def has_windows():
442 444 return os.name == 'nt'
443 445
444 446 @check("system-sh", "system() uses sh")
445 447 def has_system_sh():
446 448 return os.name != 'nt'
447 449
448 450 @check("serve", "platform and python can manage 'hg serve -d'")
449 451 def has_serve():
450 452 return os.name != 'nt' # gross approximation
451 453
452 454 @check("test-repo", "running tests from repository")
453 455 def has_test_repo():
454 456 t = os.environ["TESTDIR"]
455 457 return os.path.isdir(os.path.join(t, "..", ".hg"))
456 458
457 459 @check("tic", "terminfo compiler and curses module")
458 460 def has_tic():
459 461 try:
460 462 import curses
461 463 curses.COLOR_BLUE
462 464 return matchoutput('test -x "`which tic`"', br'')
463 465 except ImportError:
464 466 return False
465 467
466 468 @check("msys", "Windows with MSYS")
467 469 def has_msys():
468 470 return os.getenv('MSYSTEM')
469 471
470 472 @check("aix", "AIX")
471 473 def has_aix():
472 474 return sys.platform.startswith("aix")
473 475
474 476 @check("osx", "OS X")
475 477 def has_osx():
476 478 return sys.platform == 'darwin'
477 479
478 480 @check("osxpackaging", "OS X packaging tools")
479 481 def has_osxpackaging():
480 482 try:
481 483 return (matchoutput('pkgbuild', br'Usage: pkgbuild ', ignorestatus=1)
482 484 and matchoutput(
483 485 'productbuild', br'Usage: productbuild ',
484 486 ignorestatus=1)
485 487 and matchoutput('lsbom', br'Usage: lsbom', ignorestatus=1)
486 488 and matchoutput(
487 489 'xar --help', br'Usage: xar', ignorestatus=1))
488 490 except ImportError:
489 491 return False
490 492
491 493 @check("docker", "docker support")
492 494 def has_docker():
493 495 pat = br'A self-sufficient runtime for'
494 496 if matchoutput('docker --help', pat):
495 497 if 'linux' not in sys.platform:
496 498 # TODO: in theory we should be able to test docker-based
497 499 # package creation on non-linux using boot2docker, but in
498 500 # practice that requires extra coordination to make sure
499 501 # $TESTTEMP is going to be visible at the same path to the
500 502 # boot2docker VM. If we figure out how to verify that, we
501 503 # can use the following instead of just saying False:
502 504 # return 'DOCKER_HOST' in os.environ
503 505 return False
504 506
505 507 return True
506 508 return False
507 509
508 510 @check("debhelper", "debian packaging tools")
509 511 def has_debhelper():
510 512 dpkg = matchoutput('dpkg --version',
511 513 br"Debian `dpkg' package management program")
512 514 dh = matchoutput('dh --help',
513 515 br'dh is a part of debhelper.', ignorestatus=True)
514 516 dh_py2 = matchoutput('dh_python2 --help',
515 517 br'other supported Python versions')
516 518 return dpkg and dh and dh_py2
517 519
518 520 @check("absimport", "absolute_import in __future__")
519 521 def has_absimport():
520 522 import __future__
521 523 from mercurial import util
522 524 return util.safehasattr(__future__, "absolute_import")
523 525
524 526 @check("py3k", "running with Python 3.x")
525 527 def has_py3k():
526 528 return 3 == sys.version_info[0]
527 529
528 530 @check("py3exe", "a Python 3.x interpreter is available")
529 531 def has_python3exe():
530 532 return 'PYTHON3' in os.environ
531 533
532 534 @check("pure", "running with pure Python code")
533 535 def has_pure():
534 536 return any([
535 537 os.environ.get("HGMODULEPOLICY") == "py",
536 538 os.environ.get("HGTEST_RUN_TESTS_PURE") == "--pure",
537 539 ])
538 540
539 541 @check("slow", "allow slow tests")
540 542 def has_slow():
541 543 return os.environ.get('HGTEST_SLOW') == 'slow'
542 544
543 545 @check("hypothesis", "Hypothesis automated test generation")
544 546 def has_hypothesis():
545 547 try:
546 548 import hypothesis
547 549 hypothesis.given
548 550 return True
549 551 except ImportError:
550 552 return False
General Comments 0
You need to be logged in to leave comments. Login now