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