##// END OF EJS Templates
keyring_check and keyring_clear can be run outside repository...
Marcin Kasperski -
r217:1efb4fd4 default
parent child Browse files
Show More
@@ -1,782 +1,794 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 #
2 #
3 # mercurial_keyring: save passwords in password database
3 # mercurial_keyring: save passwords in password database
4 #
4 #
5 # Copyright (c) 2009 Marcin Kasperski <Marcin.Kasperski@mekk.waw.pl>
5 # Copyright (c) 2009 Marcin Kasperski <Marcin.Kasperski@mekk.waw.pl>
6 # All rights reserved.
6 # All rights reserved.
7 #
7 #
8 # Redistribution and use in source and binary forms, with or without
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions
9 # modification, are permitted provided that the following conditions
10 # are met:
10 # are met:
11 # 1. Redistributions of source code must retain the above copyright
11 # 1. Redistributions of source code must retain the above copyright
12 # notice, this list of conditions and the following disclaimer.
12 # notice, this list of conditions and the following disclaimer.
13 # 2. Redistributions in binary form must reproduce the above copyright
13 # 2. Redistributions in binary form must reproduce the above copyright
14 # notice, this list of conditions and the following disclaimer in the
14 # notice, this list of conditions and the following disclaimer in the
15 # documentation and/or other materials provided with the distribution.
15 # documentation and/or other materials provided with the distribution.
16 # 3. The name of the author may not be used to endorse or promote products
16 # 3. The name of the author may not be used to endorse or promote products
17 # derived from this software without specific prior written permission.
17 # derived from this software without specific prior written permission.
18 #
18 #
19 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
19 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
20 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
20 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
21 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
21 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22 # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
22 # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
23 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
23 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
24 # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 #
29 #
30 # See README.txt for more details.
30 # See README.txt for more details.
31
31
32 '''securely save HTTP and SMTP passwords to encrypted storage
32 '''securely save HTTP and SMTP passwords to encrypted storage
33
33
34 mercurial_keyring securely saves HTTP and SMTP passwords in password
34 mercurial_keyring securely saves HTTP and SMTP passwords in password
35 databases (Gnome Keyring, KDE KWallet, OSXKeyChain, Win32 crypto
35 databases (Gnome Keyring, KDE KWallet, OSXKeyChain, Win32 crypto
36 services).
36 services).
37
37
38 The process is automatic. Whenever bare Mercurial just prompts for
38 The process is automatic. Whenever bare Mercurial just prompts for
39 the password, Mercurial with mercurial_keyring enabled checks whether
39 the password, Mercurial with mercurial_keyring enabled checks whether
40 saved password is available first. If so, it is used. If not, you
40 saved password is available first. If so, it is used. If not, you
41 will be prompted for the password, but entered password will be
41 will be prompted for the password, but entered password will be
42 saved for the future use.
42 saved for the future use.
43
43
44 In case saved password turns out to be invalid (HTTP or SMTP login
44 In case saved password turns out to be invalid (HTTP or SMTP login
45 fails) it is dropped, and you are asked for current password.
45 fails) it is dropped, and you are asked for current password.
46
46
47 Actual password storage is implemented by Python keyring library, this
47 Actual password storage is implemented by Python keyring library, this
48 extension glues those services to Mercurial. Consult keyring
48 extension glues those services to Mercurial. Consult keyring
49 documentation for information how to configure actual password
49 documentation for information how to configure actual password
50 backend (by default keyring guesses, usually correctly, for example
50 backend (by default keyring guesses, usually correctly, for example
51 you get KDE Wallet under KDE, and Gnome Keyring under Gnome or Unity).
51 you get KDE Wallet under KDE, and Gnome Keyring under Gnome or Unity).
52 '''
52 '''
53
53
54 from mercurial import util, sslutil
54 from mercurial import util, sslutil
55 from mercurial.i18n import _
55 from mercurial.i18n import _
56 from mercurial.url import passwordmgr
56 from mercurial.url import passwordmgr
57 from mercurial import mail
57 from mercurial import mail
58 from mercurial.mail import SMTPS, STARTTLS
58 from mercurial.mail import SMTPS, STARTTLS
59 from mercurial import encoding
59 from mercurial import encoding
60
60
61 import urllib2
61 import urllib2
62 import smtplib
62 import smtplib
63 import socket
63 import socket
64 import os
64 import os
65 import sys
65 import sys
66 import re
66 import re
67
67
68 # pylint: disable=invalid-name, line-too-long, protected-access, too-many-arguments
68 # pylint: disable=invalid-name, line-too-long, protected-access, too-many-arguments
69
69
70 ###########################################################################
70 ###########################################################################
71 # Specific import trickery
71 # Specific import trickery
72 ###########################################################################
72 ###########################################################################
73
73
74
74
75 def import_meu():
75 def import_meu():
76 """
76 """
77 Convoluted import of mercurial_extension_utils, which helps
77 Convoluted import of mercurial_extension_utils, which helps
78 TortoiseHg/Win setups. This routine and it's use below
78 TortoiseHg/Win setups. This routine and it's use below
79 performs equivalent of
79 performs equivalent of
80 from mercurial_extension_utils import monkeypatch_method
80 from mercurial_extension_utils import monkeypatch_method
81 but looks for some non-path directories.
81 but looks for some non-path directories.
82 """
82 """
83 try:
83 try:
84 import mercurial_extension_utils
84 import mercurial_extension_utils
85 except ImportError:
85 except ImportError:
86 my_dir = os.path.dirname(__file__)
86 my_dir = os.path.dirname(__file__)
87 sys.path.extend([
87 sys.path.extend([
88 # In the same dir (manual or site-packages after pip)
88 # In the same dir (manual or site-packages after pip)
89 my_dir,
89 my_dir,
90 # Developer clone
90 # Developer clone
91 os.path.join(os.path.dirname(my_dir), "extension_utils"),
91 os.path.join(os.path.dirname(my_dir), "extension_utils"),
92 # Side clone
92 # Side clone
93 os.path.join(os.path.dirname(my_dir), "mercurial-extension_utils"),
93 os.path.join(os.path.dirname(my_dir), "mercurial-extension_utils"),
94 ])
94 ])
95 try:
95 try:
96 import mercurial_extension_utils
96 import mercurial_extension_utils
97 except ImportError:
97 except ImportError:
98 raise util.Abort(_("""Can not import mercurial_extension_utils.
98 raise util.Abort(_("""Can not import mercurial_extension_utils.
99 Please install this module in Python path.
99 Please install this module in Python path.
100 See Installation chapter in https://bitbucket.org/Mekk/mercurial_keyring/ for details
100 See Installation chapter in https://bitbucket.org/Mekk/mercurial_keyring/ for details
101 (and for info about TortoiseHG on Windows, or other bundled Python)."""))
101 (and for info about TortoiseHG on Windows, or other bundled Python)."""))
102 return mercurial_extension_utils
102 return mercurial_extension_utils
103
103
104 meu = import_meu()
104 meu = import_meu()
105 monkeypatch_method = meu.monkeypatch_method
105 monkeypatch_method = meu.monkeypatch_method
106
106
107
107
108 def import_keyring():
108 def import_keyring():
109 """
109 """
110 Importing keyring happens to be costly if wallet is slow, so we delay it
110 Importing keyring happens to be costly if wallet is slow, so we delay it
111 until it is really needed. The routine below also works around various
111 until it is really needed. The routine below also works around various
112 demandimport-related problems.
112 demandimport-related problems.
113 """
113 """
114 # mercurial.demandimport incompatibility workaround.
114 # mercurial.demandimport incompatibility workaround.
115 # various keyring backends fail as they can't properly import helper
115 # various keyring backends fail as they can't properly import helper
116 # modules (as demandimport modifies python import behaviour).
116 # modules (as demandimport modifies python import behaviour).
117 # If you get import errors with demandimport in backtrace, try
117 # If you get import errors with demandimport in backtrace, try
118 # guessing what to block and extending the list below.
118 # guessing what to block and extending the list below.
119 mod, was_imported_now = meu.direct_import_ext(
119 mod, was_imported_now = meu.direct_import_ext(
120 "keyring", [
120 "keyring", [
121 "gobject._gobject",
121 "gobject._gobject",
122 "configparser",
122 "configparser",
123 "json",
123 "json",
124 "abc",
124 "abc",
125 "io",
125 "io",
126 "keyring",
126 "keyring",
127 "gdata.docs.service",
127 "gdata.docs.service",
128 "gdata.service",
128 "gdata.service",
129 "types",
129 "types",
130 "atom.http",
130 "atom.http",
131 "atom.http_interface",
131 "atom.http_interface",
132 "atom.service",
132 "atom.service",
133 "atom.token_store",
133 "atom.token_store",
134 "ctypes",
134 "ctypes",
135 "secretstorage.exceptions",
135 "secretstorage.exceptions",
136 "fs.opener",
136 "fs.opener",
137 ])
137 ])
138 if was_imported_now:
138 if was_imported_now:
139 # Shut up warning about uninitialized logging by keyring
139 # Shut up warning about uninitialized logging by keyring
140 meu.disable_logging("keyring")
140 meu.disable_logging("keyring")
141 return mod
141 return mod
142
142
143
143
144 #################################################################
144 #################################################################
145 # Actual implementation
145 # Actual implementation
146 #################################################################
146 #################################################################
147
147
148 KEYRING_SERVICE = "Mercurial"
148 KEYRING_SERVICE = "Mercurial"
149
149
150
150
151 class PasswordStore(object):
151 class PasswordStore(object):
152 """
152 """
153 Helper object handling keyring usage (password save&restore,
153 Helper object handling keyring usage (password save&restore,
154 the way passwords are keyed in the keyring).
154 the way passwords are keyed in the keyring).
155 """
155 """
156 def __init__(self):
156 def __init__(self):
157 self.cache = dict()
157 self.cache = dict()
158
158
159 def get_http_password(self, url, username):
159 def get_http_password(self, url, username):
160 """
160 """
161 Checks whether password of username for url is available,
161 Checks whether password of username for url is available,
162 returns it or None
162 returns it or None
163 """
163 """
164 return self._read_password_from_keyring(
164 return self._read_password_from_keyring(
165 self._format_http_key(url, username))
165 self._format_http_key(url, username))
166
166
167 def set_http_password(self, url, username, password):
167 def set_http_password(self, url, username, password):
168 """Saves password to keyring"""
168 """Saves password to keyring"""
169 self._save_password_to_keyring(
169 self._save_password_to_keyring(
170 self._format_http_key(url, username),
170 self._format_http_key(url, username),
171 password)
171 password)
172
172
173 def clear_http_password(self, url, username):
173 def clear_http_password(self, url, username):
174 """Drops saved password"""
174 """Drops saved password"""
175 self.set_http_password(url, username, "")
175 self.set_http_password(url, username, "")
176
176
177 @staticmethod
177 @staticmethod
178 def _format_http_key(url, username):
178 def _format_http_key(url, username):
179 """Construct actual key for password identification"""
179 """Construct actual key for password identification"""
180 return "%s@@%s" % (username, url)
180 return "%s@@%s" % (username, url)
181
181
182 def get_smtp_password(self, machine, port, username):
182 def get_smtp_password(self, machine, port, username):
183 """Checks for SMTP password in keyring, returns
183 """Checks for SMTP password in keyring, returns
184 password or None"""
184 password or None"""
185 return self._read_password_from_keyring(
185 return self._read_password_from_keyring(
186 self._format_smtp_key(machine, port, username))
186 self._format_smtp_key(machine, port, username))
187
187
188 def set_smtp_password(self, machine, port, username, password):
188 def set_smtp_password(self, machine, port, username, password):
189 """Saves SMTP password to keyring"""
189 """Saves SMTP password to keyring"""
190 self._save_password_to_keyring(
190 self._save_password_to_keyring(
191 self._format_smtp_key(machine, port, username),
191 self._format_smtp_key(machine, port, username),
192 password)
192 password)
193
193
194 def clear_smtp_password(self, machine, port, username):
194 def clear_smtp_password(self, machine, port, username):
195 """Drops saved SMTP password"""
195 """Drops saved SMTP password"""
196 self.set_smtp_password(machine, port, username, "")
196 self.set_smtp_password(machine, port, username, "")
197
197
198 @staticmethod
198 @staticmethod
199 def _format_smtp_key(machine, port, username):
199 def _format_smtp_key(machine, port, username):
200 """Construct key for SMTP password identification"""
200 """Construct key for SMTP password identification"""
201 return "%s@@%s:%s" % (username, machine, str(port))
201 return "%s@@%s:%s" % (username, machine, str(port))
202
202
203 @staticmethod
203 @staticmethod
204 def _read_password_from_keyring(pwdkey):
204 def _read_password_from_keyring(pwdkey):
205 """Physically read from keyring"""
205 """Physically read from keyring"""
206 keyring = import_keyring()
206 keyring = import_keyring()
207 password = keyring.get_password(KEYRING_SERVICE, pwdkey)
207 password = keyring.get_password(KEYRING_SERVICE, pwdkey)
208 # Reverse recoding from next routine
208 # Reverse recoding from next routine
209 if isinstance(password, unicode):
209 if isinstance(password, unicode):
210 return encoding.tolocal(password.encode('utf-8'))
210 return encoding.tolocal(password.encode('utf-8'))
211 return password
211 return password
212
212
213 @staticmethod
213 @staticmethod
214 def _save_password_to_keyring(pwdkey, password):
214 def _save_password_to_keyring(pwdkey, password):
215 """Physically write to keyring"""
215 """Physically write to keyring"""
216 keyring = import_keyring()
216 keyring = import_keyring()
217 # keyring in general expects unicode.
217 # keyring in general expects unicode.
218 # Mercurial provides "local" encoding. See #33
218 # Mercurial provides "local" encoding. See #33
219 password = encoding.fromlocal(password).decode('utf-8')
219 password = encoding.fromlocal(password).decode('utf-8')
220 keyring.set_password(
220 keyring.set_password(
221 KEYRING_SERVICE, pwdkey, password)
221 KEYRING_SERVICE, pwdkey, password)
222
222
223 password_store = PasswordStore()
223 password_store = PasswordStore()
224
224
225
225
226 ############################################################
226 ############################################################
227 # Various utils
227 # Various utils
228 ############################################################
228 ############################################################
229
229
230 def _debug(ui, msg):
230 def _debug(ui, msg):
231 """Generic debug message"""
231 """Generic debug message"""
232 ui.debug("keyring: " + msg + "\n")
232 ui.debug("keyring: " + msg + "\n")
233
233
234
234
235 class PwdCache(object):
235 class PwdCache(object):
236 """Short term cache, used to preserve passwords
236 """Short term cache, used to preserve passwords
237 if they are used twice during a command"""
237 if they are used twice during a command"""
238 def __init__(self):
238 def __init__(self):
239 self._cache = {}
239 self._cache = {}
240
240
241 def store(self, realm, url, user, pwd):
241 def store(self, realm, url, user, pwd):
242 """Saves password"""
242 """Saves password"""
243 cache_key = (realm, url, user)
243 cache_key = (realm, url, user)
244 self._cache[cache_key] = pwd
244 self._cache[cache_key] = pwd
245
245
246 def check(self, realm, url, user):
246 def check(self, realm, url, user):
247 """Checks for cached password"""
247 """Checks for cached password"""
248 cache_key = (realm, url, user)
248 cache_key = (realm, url, user)
249 return self._cache.get(cache_key)
249 return self._cache.get(cache_key)
250
250
251
251
252 _re_http_url = re.compile(r'^https?://')
252 _re_http_url = re.compile(r'^https?://')
253
253
254 def is_http_path(url):
254 def is_http_path(url):
255 return bool(_re_http_url.search(url))
255 return bool(_re_http_url.search(url))
256
256
257 ############################################################
257 ############################################################
258 # HTTP password management
258 # HTTP password management
259 ############################################################
259 ############################################################
260
260
261
261
262 class HTTPPasswordHandler(object):
262 class HTTPPasswordHandler(object):
263 """
263 """
264 Actual implementation of password handling (user prompting,
264 Actual implementation of password handling (user prompting,
265 configuration file searching, keyring save&restore).
265 configuration file searching, keyring save&restore).
266
266
267 Object of this class is bound as passwordmgr attribute.
267 Object of this class is bound as passwordmgr attribute.
268 """
268 """
269 def __init__(self):
269 def __init__(self):
270 self.pwd_cache = PwdCache()
270 self.pwd_cache = PwdCache()
271 self.last_reply = None
271 self.last_reply = None
272
272
273 # Markers and also names used in debug notes. Password source
273 # Markers and also names used in debug notes. Password source
274 SRC_URL = "repository URL"
274 SRC_URL = "repository URL"
275 SRC_CFGAUTH = "hgrc"
275 SRC_CFGAUTH = "hgrc"
276 SRC_MEMCACHE = "temporary cache"
276 SRC_MEMCACHE = "temporary cache"
277 SRC_URLCACHE = "urllib temporary cache"
277 SRC_URLCACHE = "urllib temporary cache"
278 SRC_KEYRING = "keyring"
278 SRC_KEYRING = "keyring"
279
279
280 def get_credentials(self, pwmgr, realm, authuri, skip_caches=False):
280 def get_credentials(self, pwmgr, realm, authuri, skip_caches=False):
281 """
281 """
282 Looks up for user credentials in various places, returns them
282 Looks up for user credentials in various places, returns them
283 and information about their source.
283 and information about their source.
284
284
285 Used internally inside find_auth and inside informative
285 Used internally inside find_auth and inside informative
286 commands (thiis method doesn't cache, doesn't detect bad
286 commands (thiis method doesn't cache, doesn't detect bad
287 passwords etc, doesn't prompt interactively, doesn't store
287 passwords etc, doesn't prompt interactively, doesn't store
288 password in keyring).
288 password in keyring).
289
289
290 Returns: user, password, SRC_*, actual_url
290 Returns: user, password, SRC_*, actual_url
291
291
292 If not found, password and SRC is None, user can be given or
292 If not found, password and SRC is None, user can be given or
293 not, url is always set
293 not, url is always set
294 """
294 """
295 ui = pwmgr.ui
295 ui = pwmgr.ui
296
296
297 parsed_url, url_user, url_passwd = self.unpack_url(authuri)
297 parsed_url, url_user, url_passwd = self.unpack_url(authuri)
298 base_url = str(parsed_url)
298 base_url = str(parsed_url)
299 ui.debug(_('keyring: base url: %s, url user: %s, url pwd: %s\n') %
299 ui.debug(_('keyring: base url: %s, url user: %s, url pwd: %s\n') %
300 (base_url, url_user or '', url_passwd and '******' or ''))
300 (base_url, url_user or '', url_passwd and '******' or ''))
301
301
302 # Extract username (or password) stored directly in url
302 # Extract username (or password) stored directly in url
303 if url_user and url_passwd:
303 if url_user and url_passwd:
304 return url_user, url_passwd, self.SRC_URL, base_url
304 return url_user, url_passwd, self.SRC_URL, base_url
305
305
306 # Extract data from urllib (in case it was already stored)
306 # Extract data from urllib (in case it was already stored)
307 urllib_user, urllib_pwd \
307 urllib_user, urllib_pwd \
308 = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
308 = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
309 pwmgr, realm, authuri)
309 pwmgr, realm, authuri)
310 if urllib_user and urllib_pwd:
310 if urllib_user and urllib_pwd:
311 return urllib_user, urllib_pwd, self.SRC_URLCACHE, base_url
311 return urllib_user, urllib_pwd, self.SRC_URLCACHE, base_url
312
312
313 actual_user = url_user or urllib_user
313 actual_user = url_user or urllib_user
314
314
315 # Consult configuration to normalize url to prefix, and find username
315 # Consult configuration to normalize url to prefix, and find username
316 # (and maybe password)
316 # (and maybe password)
317 auth_user, auth_pwd, keyring_url = self.get_url_config(
317 auth_user, auth_pwd, keyring_url = self.get_url_config(
318 ui, parsed_url, actual_user)
318 ui, parsed_url, actual_user)
319 if auth_user and actual_user and (actual_user != auth_user):
319 if auth_user and actual_user and (actual_user != auth_user):
320 raise util.Abort(_('keyring: username for %s specified both in repository path (%s) and in .hg/hgrc/[auth] (%s). Please, leave only one of those' % (base_url, actual_user, auth_user)))
320 raise util.Abort(_('keyring: username for %s specified both in repository path (%s) and in .hg/hgrc/[auth] (%s). Please, leave only one of those' % (base_url, actual_user, auth_user)))
321 if auth_user and auth_pwd:
321 if auth_user and auth_pwd:
322 return auth_user, auth_pwd, self.SRC_CFGAUTH, keyring_url
322 return auth_user, auth_pwd, self.SRC_CFGAUTH, keyring_url
323
323
324 actual_user = actual_user or auth_user
324 actual_user = actual_user or auth_user
325
325
326 if skip_caches:
326 if skip_caches:
327 return actual_user, None, None, keyring_url
327 return actual_user, None, None, keyring_url
328
328
329 # Check memory cache (reuse )
329 # Check memory cache (reuse )
330 # Checking the memory cache (there may be many http calls per command)
330 # Checking the memory cache (there may be many http calls per command)
331 cached_pwd = self.pwd_cache.check(realm, keyring_url, actual_user)
331 cached_pwd = self.pwd_cache.check(realm, keyring_url, actual_user)
332 if cached_pwd:
332 if cached_pwd:
333 return actual_user, cached_pwd, self.SRC_MEMCACHE, keyring_url
333 return actual_user, cached_pwd, self.SRC_MEMCACHE, keyring_url
334
334
335 # Load from keyring.
335 # Load from keyring.
336 if actual_user:
336 if actual_user:
337 ui.debug(_("keyring: looking for password (user %s, url %s)\n") % (actual_user, keyring_url))
337 ui.debug(_("keyring: looking for password (user %s, url %s)\n") % (actual_user, keyring_url))
338 keyring_pwd = password_store.get_http_password(keyring_url, actual_user)
338 keyring_pwd = password_store.get_http_password(keyring_url, actual_user)
339 if keyring_pwd:
339 if keyring_pwd:
340 return actual_user, keyring_pwd, self.SRC_KEYRING, keyring_url
340 return actual_user, keyring_pwd, self.SRC_KEYRING, keyring_url
341
341
342 return actual_user, None, None, keyring_url
342 return actual_user, None, None, keyring_url
343
343
344 @staticmethod
344 @staticmethod
345 def prompt_interactively(ui, user, realm, url):
345 def prompt_interactively(ui, user, realm, url):
346 """Actual interactive prompt"""
346 """Actual interactive prompt"""
347 if not ui.interactive():
347 if not ui.interactive():
348 raise util.Abort(_('keyring: http authorization required but program used in non-interactive mode'))
348 raise util.Abort(_('keyring: http authorization required but program used in non-interactive mode'))
349
349
350 if not user:
350 if not user:
351 ui.status(_("keyring: username not specified in hgrc (or in url). Password will not be saved.\n"))
351 ui.status(_("keyring: username not specified in hgrc (or in url). Password will not be saved.\n"))
352
352
353 ui.write(_("http authorization required\n"))
353 ui.write(_("http authorization required\n"))
354 ui.status(_("realm: %s\n") % realm)
354 ui.status(_("realm: %s\n") % realm)
355 ui.status(_("url: %s\n") % url)
355 ui.status(_("url: %s\n") % url)
356 if user:
356 if user:
357 ui.write(_("user: %s (fixed in hgrc or url)\n" % user))
357 ui.write(_("user: %s (fixed in hgrc or url)\n" % user))
358 else:
358 else:
359 user = ui.prompt(_("user:"), default=None)
359 user = ui.prompt(_("user:"), default=None)
360 pwd = ui.getpass(_("password: "))
360 pwd = ui.getpass(_("password: "))
361 return user, pwd
361 return user, pwd
362
362
363 def find_auth(self, pwmgr, realm, authuri, req):
363 def find_auth(self, pwmgr, realm, authuri, req):
364 """
364 """
365 Actual implementation of find_user_password - different
365 Actual implementation of find_user_password - different
366 ways of obtaining the username and password.
366 ways of obtaining the username and password.
367
367
368 Returns pair username, password
368 Returns pair username, password
369 """
369 """
370 ui = pwmgr.ui
370 ui = pwmgr.ui
371 after_bad_auth = self._after_bad_auth(ui, realm, authuri, req)
371 after_bad_auth = self._after_bad_auth(ui, realm, authuri, req)
372
372
373 # Look in url, cache, etc
373 # Look in url, cache, etc
374 user, pwd, src, final_url = self.get_credentials(
374 user, pwd, src, final_url = self.get_credentials(
375 pwmgr, realm, authuri, skip_caches=after_bad_auth)
375 pwmgr, realm, authuri, skip_caches=after_bad_auth)
376 if pwd:
376 if pwd:
377 if src != self.SRC_MEMCACHE:
377 if src != self.SRC_MEMCACHE:
378 self.pwd_cache.store(realm, final_url, user, pwd)
378 self.pwd_cache.store(realm, final_url, user, pwd)
379 self._note_last_reply(realm, authuri, user, req)
379 self._note_last_reply(realm, authuri, user, req)
380 _debug(ui, _("Password found in " + src))
380 _debug(ui, _("Password found in " + src))
381 return user, pwd
381 return user, pwd
382
382
383 # Last resort: interactive prompt
383 # Last resort: interactive prompt
384 user, pwd = self.prompt_interactively(ui, user, realm, final_url)
384 user, pwd = self.prompt_interactively(ui, user, realm, final_url)
385
385
386 if user:
386 if user:
387 # Saving password to the keyring.
387 # Saving password to the keyring.
388 # It is done only if username is permanently set.
388 # It is done only if username is permanently set.
389 # Otherwise we won't be able to find the password so it
389 # Otherwise we won't be able to find the password so it
390 # does not make much sense to preserve it
390 # does not make much sense to preserve it
391 _debug(ui, _("Saving password for %s to keyring") % user)
391 _debug(ui, _("Saving password for %s to keyring") % user)
392 password_store.set_http_password(final_url, user, pwd)
392 password_store.set_http_password(final_url, user, pwd)
393
393
394 # Saving password to the memory cache
394 # Saving password to the memory cache
395 self.pwd_cache.store(realm, final_url, user, pwd)
395 self.pwd_cache.store(realm, final_url, user, pwd)
396 self._note_last_reply(realm, authuri, user, req)
396 self._note_last_reply(realm, authuri, user, req)
397 _debug(ui, _("Manually entered password"))
397 _debug(ui, _("Manually entered password"))
398 return user, pwd
398 return user, pwd
399
399
400 def get_url_config(self, ui, parsed_url, user):
400 def get_url_config(self, ui, parsed_url, user):
401 """
401 """
402 Checks configuration to decide whether/which username, prefix,
402 Checks configuration to decide whether/which username, prefix,
403 and password are configured for given url. Consults [auth] section.
403 and password are configured for given url. Consults [auth] section.
404
404
405 Returns tuple (username, password, prefix) containing elements
405 Returns tuple (username, password, prefix) containing elements
406 found. username and password can be None (if unset), if prefix
406 found. username and password can be None (if unset), if prefix
407 is not found, url itself is returned.
407 is not found, url itself is returned.
408 """
408 """
409 base_url = str(parsed_url)
409 base_url = str(parsed_url)
410
410
411 from mercurial.httpconnection import readauthforuri
411 from mercurial.httpconnection import readauthforuri
412 _debug(ui, _("Checking for hgrc info about url %s, user %s") % (base_url, user))
412 _debug(ui, _("Checking for hgrc info about url %s, user %s") % (base_url, user))
413 res = readauthforuri(ui, base_url, user)
413 res = readauthforuri(ui, base_url, user)
414 # If it user-less version not work, let's try with added username to handle
414 # If it user-less version not work, let's try with added username to handle
415 # both config conventions
415 # both config conventions
416 if (not res) and user:
416 if (not res) and user:
417 parsed_url.user = user
417 parsed_url.user = user
418 res = readauthforuri(ui, str(parsed_url), user)
418 res = readauthforuri(ui, str(parsed_url), user)
419 parsed_url.user = None
419 parsed_url.user = None
420 if res:
420 if res:
421 group, auth_token = res
421 group, auth_token = res
422 else:
422 else:
423 auth_token = None
423 auth_token = None
424
424
425 if auth_token:
425 if auth_token:
426 username = auth_token.get('username')
426 username = auth_token.get('username')
427 password = auth_token.get('password')
427 password = auth_token.get('password')
428 prefix = auth_token.get('prefix')
428 prefix = auth_token.get('prefix')
429 else:
429 else:
430 username = user
430 username = user
431 password = None
431 password = None
432 prefix = None
432 prefix = None
433
433
434 password_url = self.password_url(base_url, prefix)
434 password_url = self.password_url(base_url, prefix)
435
435
436 _debug(ui, _("Password url: %s, user: %s, password: %s (prefix: %s)") % (
436 _debug(ui, _("Password url: %s, user: %s, password: %s (prefix: %s)") % (
437 password_url, username, '********' if password else '', prefix))
437 password_url, username, '********' if password else '', prefix))
438
438
439 return username, password, password_url
439 return username, password, password_url
440
440
441 def _note_last_reply(self, realm, authuri, user, req):
441 def _note_last_reply(self, realm, authuri, user, req):
442 """
442 """
443 Internal helper. Saves info about auth-data obtained,
443 Internal helper. Saves info about auth-data obtained,
444 preserves them in last_reply, and returns pair user, pwd
444 preserves them in last_reply, and returns pair user, pwd
445 """
445 """
446 self.last_reply = dict(realm=realm, authuri=authuri,
446 self.last_reply = dict(realm=realm, authuri=authuri,
447 user=user, req=req)
447 user=user, req=req)
448
448
449 def _after_bad_auth(self, ui, realm, authuri, req):
449 def _after_bad_auth(self, ui, realm, authuri, req):
450 """
450 """
451 If we are called again just after identical previous
451 If we are called again just after identical previous
452 request, then the previously returned auth must have been
452 request, then the previously returned auth must have been
453 wrong. So we note this to force password prompt (and avoid
453 wrong. So we note this to force password prompt (and avoid
454 reusing bad password indefinitely).
454 reusing bad password indefinitely).
455
455
456 This routine checks for this condition.
456 This routine checks for this condition.
457 """
457 """
458 if self.last_reply:
458 if self.last_reply:
459 if (self.last_reply['realm'] == realm) \
459 if (self.last_reply['realm'] == realm) \
460 and (self.last_reply['authuri'] == authuri) \
460 and (self.last_reply['authuri'] == authuri) \
461 and (self.last_reply['req'] == req):
461 and (self.last_reply['req'] == req):
462 _debug(ui, _("Working after bad authentication, cached passwords not used %s") % str(self.last_reply))
462 _debug(ui, _("Working after bad authentication, cached passwords not used %s") % str(self.last_reply))
463 return True
463 return True
464 return False
464 return False
465
465
466 @staticmethod
466 @staticmethod
467 def password_url(base_url, prefix):
467 def password_url(base_url, prefix):
468 """Calculates actual url identifying the password. Takes
468 """Calculates actual url identifying the password. Takes
469 configured prefix under consideration (so can be shorter
469 configured prefix under consideration (so can be shorter
470 than repo url)"""
470 than repo url)"""
471 if not prefix or prefix == '*':
471 if not prefix or prefix == '*':
472 return base_url
472 return base_url
473 scheme, hostpath = base_url.split('://', 1)
473 scheme, hostpath = base_url.split('://', 1)
474 p = prefix.split('://', 1)
474 p = prefix.split('://', 1)
475 if len(p) > 1:
475 if len(p) > 1:
476 prefix_host_path = p[1]
476 prefix_host_path = p[1]
477 else:
477 else:
478 prefix_host_path = prefix
478 prefix_host_path = prefix
479 password_url = scheme + '://' + prefix_host_path
479 password_url = scheme + '://' + prefix_host_path
480 return password_url
480 return password_url
481
481
482 @staticmethod
482 @staticmethod
483 def unpack_url(authuri):
483 def unpack_url(authuri):
484 """
484 """
485 Takes original url for which authentication is attempted and:
485 Takes original url for which authentication is attempted and:
486
486
487 - Strips query params from url. Used to convert urls like
487 - Strips query params from url. Used to convert urls like
488 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
488 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
489 to
489 to
490 https://repo.machine.com/repos/apps/module
490 https://repo.machine.com/repos/apps/module
491
491
492 - Extracts username and password, if present, and removes them from url
492 - Extracts username and password, if present, and removes them from url
493 (so prefix matching works properly)
493 (so prefix matching works properly)
494
494
495 Returns url, user, password
495 Returns url, user, password
496 where url is mercurial.util.url object already stripped of all those
496 where url is mercurial.util.url object already stripped of all those
497 params.
497 params.
498 """
498 """
499 # mercurial.util.url, rather handy url parser
499 # mercurial.util.url, rather handy url parser
500 parsed_url = util.url(authuri)
500 parsed_url = util.url(authuri)
501 parsed_url.query = ''
501 parsed_url.query = ''
502 parsed_url.fragment = None
502 parsed_url.fragment = None
503 # Strip arguments to get actual remote repository url.
503 # Strip arguments to get actual remote repository url.
504 # base_url = "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
504 # base_url = "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
505 # parsed_url.path)
505 # parsed_url.path)
506 user = parsed_url.user
506 user = parsed_url.user
507 passwd = parsed_url.passwd
507 passwd = parsed_url.passwd
508 parsed_url.user = None
508 parsed_url.user = None
509 parsed_url.passwd = None
509 parsed_url.passwd = None
510
510
511 return parsed_url, user, passwd
511 return parsed_url, user, passwd
512
512
513
513
514 ############################################################
514 ############################################################
515 # Mercurial monkey-patching
515 # Mercurial monkey-patching
516 ############################################################
516 ############################################################
517
517
518
518
519 @monkeypatch_method(passwordmgr)
519 @monkeypatch_method(passwordmgr)
520 def find_user_password(self, realm, authuri):
520 def find_user_password(self, realm, authuri):
521 """
521 """
522 keyring-based implementation of username/password query
522 keyring-based implementation of username/password query
523 for HTTP(S) connections
523 for HTTP(S) connections
524
524
525 Passwords are saved in gnome keyring, OSX/Chain or other platform
525 Passwords are saved in gnome keyring, OSX/Chain or other platform
526 specific storage and keyed by the repository url
526 specific storage and keyed by the repository url
527 """
527 """
528 # Extend object attributes
528 # Extend object attributes
529 if not hasattr(self, '_pwd_handler'):
529 if not hasattr(self, '_pwd_handler'):
530 self._pwd_handler = HTTPPasswordHandler()
530 self._pwd_handler = HTTPPasswordHandler()
531
531
532 if hasattr(self, '_http_req'):
532 if hasattr(self, '_http_req'):
533 req = self._http_req
533 req = self._http_req
534 else:
534 else:
535 req = None
535 req = None
536
536
537 return self._pwd_handler.find_auth(self, realm, authuri, req)
537 return self._pwd_handler.find_auth(self, realm, authuri, req)
538
538
539
539
540 @monkeypatch_method(urllib2.AbstractBasicAuthHandler, "http_error_auth_reqed")
540 @monkeypatch_method(urllib2.AbstractBasicAuthHandler, "http_error_auth_reqed")
541 def basic_http_error_auth_reqed(self, authreq, host, req, headers):
541 def basic_http_error_auth_reqed(self, authreq, host, req, headers):
542 """Preserves current HTTP request so it can be consulted
542 """Preserves current HTTP request so it can be consulted
543 in find_user_password above"""
543 in find_user_password above"""
544 self.passwd._http_req = req
544 self.passwd._http_req = req
545 try:
545 try:
546 return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
546 return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
547 finally:
547 finally:
548 self.passwd._http_req = None
548 self.passwd._http_req = None
549
549
550
550
551 @monkeypatch_method(urllib2.AbstractDigestAuthHandler, "http_error_auth_reqed")
551 @monkeypatch_method(urllib2.AbstractDigestAuthHandler, "http_error_auth_reqed")
552 def digest_http_error_auth_reqed(self, authreq, host, req, headers):
552 def digest_http_error_auth_reqed(self, authreq, host, req, headers):
553 """Preserves current HTTP request so it can be consulted
553 """Preserves current HTTP request so it can be consulted
554 in find_user_password above"""
554 in find_user_password above"""
555 self.passwd._http_req = req
555 self.passwd._http_req = req
556 try:
556 try:
557 return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
557 return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
558 finally:
558 finally:
559 self.passwd._http_req = None
559 self.passwd._http_req = None
560
560
561 ############################################################
561 ############################################################
562 # SMTP support
562 # SMTP support
563 ############################################################
563 ############################################################
564
564
565
565
566 def try_smtp_login(ui, smtp_obj, username, password):
566 def try_smtp_login(ui, smtp_obj, username, password):
567 """
567 """
568 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
568 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
569 password.
569 password.
570
570
571 Returns:
571 Returns:
572 - True if login succeeded
572 - True if login succeeded
573 - False if login failed due to the wrong credentials
573 - False if login failed due to the wrong credentials
574
574
575 Throws Abort exception if login failed for any other reason.
575 Throws Abort exception if login failed for any other reason.
576
576
577 Immediately returns False if password is empty
577 Immediately returns False if password is empty
578 """
578 """
579 if not password:
579 if not password:
580 return False
580 return False
581 try:
581 try:
582 ui.note(_('(authenticating to mail server as %s)\n') %
582 ui.note(_('(authenticating to mail server as %s)\n') %
583 (username))
583 (username))
584 smtp_obj.login(username, password)
584 smtp_obj.login(username, password)
585 return True
585 return True
586 except smtplib.SMTPException, inst:
586 except smtplib.SMTPException, inst:
587 if inst.smtp_code == 535:
587 if inst.smtp_code == 535:
588 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
588 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
589 return False
589 return False
590 else:
590 else:
591 raise util.Abort(inst)
591 raise util.Abort(inst)
592
592
593
593
594 def keyring_supported_smtp(ui, username):
594 def keyring_supported_smtp(ui, username):
595 """
595 """
596 keyring-integrated replacement for mercurial.mail._smtp
596 keyring-integrated replacement for mercurial.mail._smtp
597 Used only when configuration file contains username, but
597 Used only when configuration file contains username, but
598 does not contain the password.
598 does not contain the password.
599
599
600 Most of the routine below is copied as-is from
600 Most of the routine below is copied as-is from
601 mercurial.mail._smtp. The only changed part is
601 mercurial.mail._smtp. The only changed part is
602 marked with # >>>>> and # <<<<< markers
602 marked with # >>>>> and # <<<<< markers
603 """
603 """
604 local_hostname = ui.config('smtp', 'local_hostname')
604 local_hostname = ui.config('smtp', 'local_hostname')
605 tls = ui.config('smtp', 'tls', 'none')
605 tls = ui.config('smtp', 'tls', 'none')
606 # backward compatible: when tls = true, we use starttls.
606 # backward compatible: when tls = true, we use starttls.
607 starttls = tls == 'starttls' or util.parsebool(tls)
607 starttls = tls == 'starttls' or util.parsebool(tls)
608 smtps = tls == 'smtps'
608 smtps = tls == 'smtps'
609 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
609 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
610 raise util.Abort(_("can't use TLS: Python SSL support not installed"))
610 raise util.Abort(_("can't use TLS: Python SSL support not installed"))
611 mailhost = ui.config('smtp', 'host')
611 mailhost = ui.config('smtp', 'host')
612 if not mailhost:
612 if not mailhost:
613 raise util.Abort(_('smtp.host not configured - cannot send mail'))
613 raise util.Abort(_('smtp.host not configured - cannot send mail'))
614 verifycert = ui.config('smtp', 'verifycert', 'strict')
614 verifycert = ui.config('smtp', 'verifycert', 'strict')
615 if verifycert not in ['strict', 'loose']:
615 if verifycert not in ['strict', 'loose']:
616 if util.parsebool(verifycert) is not False:
616 if util.parsebool(verifycert) is not False:
617 raise util.Abort(_('invalid smtp.verifycert configuration: %s')
617 raise util.Abort(_('invalid smtp.verifycert configuration: %s')
618 % (verifycert))
618 % (verifycert))
619 verifycert = False
619 verifycert = False
620 if (starttls or smtps) and verifycert:
620 if (starttls or smtps) and verifycert:
621 sslkwargs = sslutil.sslkwargs(ui, mailhost)
621 sslkwargs = sslutil.sslkwargs(ui, mailhost)
622 else:
622 else:
623 sslkwargs = {}
623 sslkwargs = {}
624 if smtps:
624 if smtps:
625 ui.note(_('(using smtps)\n'))
625 ui.note(_('(using smtps)\n'))
626 s = SMTPS(sslkwargs, local_hostname=local_hostname)
626 s = SMTPS(sslkwargs, local_hostname=local_hostname)
627 elif starttls:
627 elif starttls:
628 s = STARTTLS(sslkwargs, local_hostname=local_hostname)
628 s = STARTTLS(sslkwargs, local_hostname=local_hostname)
629 else:
629 else:
630 s = smtplib.SMTP(local_hostname=local_hostname)
630 s = smtplib.SMTP(local_hostname=local_hostname)
631 if smtps:
631 if smtps:
632 defaultport = 465
632 defaultport = 465
633 else:
633 else:
634 defaultport = 25
634 defaultport = 25
635 mailport = util.getport(ui.config('smtp', 'port', defaultport))
635 mailport = util.getport(ui.config('smtp', 'port', defaultport))
636 ui.note(_('sending mail: smtp host %s, port %s\n') %
636 ui.note(_('sending mail: smtp host %s, port %s\n') %
637 (mailhost, mailport))
637 (mailhost, mailport))
638 s.connect(host=mailhost, port=mailport)
638 s.connect(host=mailhost, port=mailport)
639 if starttls:
639 if starttls:
640 ui.note(_('(using starttls)\n'))
640 ui.note(_('(using starttls)\n'))
641 s.ehlo()
641 s.ehlo()
642 s.starttls()
642 s.starttls()
643 s.ehlo()
643 s.ehlo()
644 if (starttls or smtps) and verifycert:
644 if (starttls or smtps) and verifycert:
645 ui.note(_('(verifying remote certificate)\n'))
645 ui.note(_('(verifying remote certificate)\n'))
646 sslutil.validator(ui, mailhost)(s.sock, verifycert == 'strict')
646 sslutil.validator(ui, mailhost)(s.sock, verifycert == 'strict')
647
647
648 # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
648 # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
649 stored = password = password_store.get_smtp_password(
649 stored = password = password_store.get_smtp_password(
650 mailhost, mailport, username)
650 mailhost, mailport, username)
651 # No need to check whether password was found as try_smtp_login
651 # No need to check whether password was found as try_smtp_login
652 # just returns False if it is absent.
652 # just returns False if it is absent.
653 while not try_smtp_login(ui, s, username, password):
653 while not try_smtp_login(ui, s, username, password):
654 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
654 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
655
655
656 if stored != password:
656 if stored != password:
657 password_store.set_smtp_password(
657 password_store.set_smtp_password(
658 mailhost, mailport, username, password)
658 mailhost, mailport, username, password)
659 # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
659 # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
660
660
661 def send(sender, recipients, msg):
661 def send(sender, recipients, msg):
662 try:
662 try:
663 return s.sendmail(sender, recipients, msg)
663 return s.sendmail(sender, recipients, msg)
664 except smtplib.SMTPRecipientsRefused, inst:
664 except smtplib.SMTPRecipientsRefused, inst:
665 recipients = [r[1] for r in inst.recipients.values()]
665 recipients = [r[1] for r in inst.recipients.values()]
666 raise util.Abort('\n' + '\n'.join(recipients))
666 raise util.Abort('\n' + '\n'.join(recipients))
667 except smtplib.SMTPException, inst:
667 except smtplib.SMTPException, inst:
668 raise util.Abort(inst)
668 raise util.Abort(inst)
669
669
670 return send
670 return send
671
671
672 ############################################################
672 ############################################################
673 # SMTP monkeypatching
673 # SMTP monkeypatching
674 ############################################################
674 ############################################################
675
675
676
676
677 @monkeypatch_method(mail)
677 @monkeypatch_method(mail)
678 def _smtp(ui):
678 def _smtp(ui):
679 """
679 """
680 build an smtp connection and return a function to send email
680 build an smtp connection and return a function to send email
681
681
682 This is the monkeypatched version of _smtp(ui) function from
682 This is the monkeypatched version of _smtp(ui) function from
683 mercurial/mail.py. It calls the original unless username
683 mercurial/mail.py. It calls the original unless username
684 without password is given in the configuration.
684 without password is given in the configuration.
685 """
685 """
686 username = ui.config('smtp', 'username')
686 username = ui.config('smtp', 'username')
687 password = ui.config('smtp', 'password')
687 password = ui.config('smtp', 'password')
688
688
689 if username and not password:
689 if username and not password:
690 return keyring_supported_smtp(ui, username)
690 return keyring_supported_smtp(ui, username)
691 else:
691 else:
692 return _smtp.orig(ui)
692 return _smtp.orig(ui)
693
693
694
694
695 ############################################################
695 ############################################################
696 # Custom commands
696 # Custom commands
697 ############################################################
697 ############################################################
698
698
699 cmdtable = {}
699 cmdtable = {}
700 command = meu.command(cmdtable)
700 command = meu.command(cmdtable)
701
701
702
702
703 @command('keyring_check',
703 @command('keyring_check',
704 [],
704 [],
705 _("keyring_check [PATH]"))
705 _("keyring_check [PATH]"),
706 optionalrepo=True)
706 def cmd_keyring_check(ui, repo, *path_args, **opts): # pylint: disable=unused-argument
707 def cmd_keyring_check(ui, repo, *path_args, **opts): # pylint: disable=unused-argument
707 """
708 """
708 Prints basic info (whether password is currently saved, and how is
709 Prints basic info (whether password is currently saved, and how is
709 it identified) for given path or for all defined repo paths which are HTTP.
710 it identified) for given path.
711
712 Can be run without parameters to show status for all (current repository) paths which
713 are HTTP-like.
710 """
714 """
711 defined_paths = [(name, url)
715 defined_paths = [(name, url)
712 for name, url in ui.configitems('paths')]
716 for name, url in ui.configitems('paths')]
713 if path_args:
717 if path_args:
714 # Maybe parameter is an alias
718 # Maybe parameter is an alias
715 defined_paths_dic = dict(defined_paths)
719 defined_paths_dic = dict(defined_paths)
716 paths = [(path_arg, defined_paths_dic.get(path_arg, path_arg))
720 paths = [(path_arg, defined_paths_dic.get(path_arg, path_arg))
717 for path_arg in path_args]
721 for path_arg in path_args]
718 else:
722 else:
723 if not repo:
724 ui.status(_("Url to check not specified. Either run ``hg keyring_check https://...``, or run the command inside some repository (to test all defined paths).\n"))
725 return
719 paths = [(name, url) for name, url in defined_paths]
726 paths = [(name, url) for name, url in defined_paths]
720
727
721 if not paths:
728 if not paths:
722 ui.status(_("keyring_check: no paths defined\n"))
729 ui.status(_("keyring_check: no paths defined\n"))
730 return
723
731
724 handler = HTTPPasswordHandler()
732 handler = HTTPPasswordHandler()
725
733
726 ui.status(_("keyring password save status:\n"))
734 ui.status(_("keyring password save status:\n"))
727 for name, url in paths:
735 for name, url in paths:
728 if not is_http_path(url):
736 if not is_http_path(url):
729 if path_args:
737 if path_args:
730 ui.status(_(" %s: non-http path (%s)\n") % (name, url))
738 ui.status(_(" %s: non-http path (%s)\n") % (name, url))
731 continue
739 continue
732 user, pwd, source, final_url = handler.get_credentials(
740 user, pwd, source, final_url = handler.get_credentials(
733 passwordmgr(ui), name, url)
741 passwordmgr(ui), name, url)
734 if pwd:
742 if pwd:
735 ui.status(_(" %s: password available, source: %s, bound to user %s, url %s\n") % (
743 ui.status(_(" %s: password available, source: %s, bound to user %s, url %s\n") % (
736 name, source, user, final_url))
744 name, source, user, final_url))
737 elif user:
745 elif user:
738 ui.status(_(" %s: password not available, once entered, will be bound to user %s, url %s\n") % (
746 ui.status(_(" %s: password not available, once entered, will be bound to user %s, url %s\n") % (
739 name, user, final_url))
747 name, user, final_url))
740 else:
748 else:
741 ui.status(_(" %s: password not available, user unknown, url %s\n") % (
749 ui.status(_(" %s: password not available, user unknown, url %s\n") % (
742 name, final_url))
750 name, final_url))
743
751
744
752
745 @command('keyring_clear',
753 @command('keyring_clear',
746 [],
754 [],
747 _('hg keyring_clear PATH'))
755 _('hg keyring_clear PATH-OR-ALIAS'),
756 optionalrepo=True)
748 def cmd_keyring_clear(ui, repo, path, **opts): # pylint: disable=unused-argument
757 def cmd_keyring_clear(ui, repo, path, **opts): # pylint: disable=unused-argument
749 """
758 """
750 Drops password bound to given path (if any is saved).
759 Drops password bound to given path (if any is saved).
760
761 Parameter can be given as full url (``https://John@bitbucket.org``) or as the name
762 of path alias (``bitbucket``).
751 """
763 """
752 path_url = path
764 path_url = path
753 for name, url in ui.configitems('paths'):
765 for name, url in ui.configitems('paths'):
754 if name == path:
766 if name == path:
755 path_url = url
767 path_url = url
756 break
768 break
757 if not is_http_path(path_url):
769 if not is_http_path(path_url):
758 ui.warn(_("%s is not a http path (%s)") % (path, path_url))
770 ui.status(_("%s is not a http path (and %s can't be resolved as path alias)\n") % (path, path_url))
759 return
771 return
760
772
761 handler = HTTPPasswordHandler()
773 handler = HTTPPasswordHandler()
762
774
763 user, pwd, source, final_url = handler.get_credentials(
775 user, pwd, source, final_url = handler.get_credentials(
764 passwordmgr(ui), path, path_url)
776 passwordmgr(ui), path, path_url)
765 if not user:
777 if not user:
766 ui.status(_("Username not configured for url %s\n") % final_url)
778 ui.status(_("Username not configured for url %s\n") % final_url)
767 return
779 return
768 if not pwd:
780 if not pwd:
769 ui.status(_("No password is saved for user %s, url %s\n") % (
781 ui.status(_("No password is saved for user %s, url %s\n") % (
770 user, final_url))
782 user, final_url))
771 return
783 return
772
784
773 if source != handler.SRC_KEYRING:
785 if source != handler.SRC_KEYRING:
774 ui.status(_("Password for user %s, url %s is saved in %s, not in keyring\n") % (
786 ui.status(_("Password for user %s, url %s is saved in %s, not in keyring\n") % (
775 user, final_url, source))
787 user, final_url, source))
776
788
777 password_store.clear_http_password(final_url, user)
789 password_store.clear_http_password(final_url, user)
778 ui.status(_("Password removed for user %s, url %s\n") % (
790 ui.status(_("Password removed for user %s, url %s\n") % (
779 user, final_url))
791 user, final_url))
780
792
781
793
782 buglink = 'https://bitbucket.org/Mekk/mercurial_keyring/issues'
794 buglink = 'https://bitbucket.org/Mekk/mercurial_keyring/issues'
General Comments 0
You need to be logged in to leave comments. Login now