##// END OF EJS Templates
#45 Initial work on keyring_clear
Marcin Kasperski -
r189:b34ad2a5 default
parent child Browse files
Show More
@@ -1,728 +1,740 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 from urlparse import urlparse
61 from urlparse import urlparse
62 import urllib2
62 import urllib2
63 import smtplib
63 import smtplib
64 import socket
64 import socket
65 import os
65 import os
66 import sys
66 import sys
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-dynamic_username/ for details
100 See Installation chapter in https://bitbucket.org/Mekk/mercurial-dynamic_username/ 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 if 'keyring' in sys.modules:
114 if 'keyring' in sys.modules:
115 return sys.modules['keyring']
115 return sys.modules['keyring']
116 # mercurial.demandimport incompatibility workaround.
116 # mercurial.demandimport incompatibility workaround.
117 # various keyring backends fail as they can't properly import helper
117 # various keyring backends fail as they can't properly import helper
118 # modules (as demandimport modifies python import behaviour).
118 # modules (as demandimport modifies python import behaviour).
119 # If you get import errors with demandimport in backtrace, try
119 # If you get import errors with demandimport in backtrace, try
120 # guessing what to block and extending the list below.
120 # guessing what to block and extending the list below.
121 from mercurial import demandimport
121 from mercurial import demandimport
122 for blocked_module in [
122 for blocked_module in [
123 "gobject._gobject",
123 "gobject._gobject",
124 "configparser",
124 "configparser",
125 "json",
125 "json",
126 "abc",
126 "abc",
127 "io",
127 "io",
128 "keyring",
128 "keyring",
129 "gdata.docs.service",
129 "gdata.docs.service",
130 "gdata.service",
130 "gdata.service",
131 "types",
131 "types",
132 "atom.http",
132 "atom.http",
133 "atom.http_interface",
133 "atom.http_interface",
134 "atom.service",
134 "atom.service",
135 "atom.token_store",
135 "atom.token_store",
136 "ctypes",
136 "ctypes",
137 "secretstorage.exceptions",
137 "secretstorage.exceptions",
138 "fs.opener",
138 "fs.opener",
139 ]:
139 ]:
140 if blocked_module not in demandimport.ignore:
140 if blocked_module not in demandimport.ignore:
141 demandimport.ignore.append(blocked_module)
141 demandimport.ignore.append(blocked_module)
142
142
143 # Various attempts to define is_demandimport_enabled
143 # Various attempts to define is_demandimport_enabled
144 try:
144 try:
145 # Since Mercurial 2.9.1
145 # Since Mercurial 2.9.1
146 is_demandimport_enabled = demandimport.isenabled
146 is_demandimport_enabled = demandimport.isenabled
147 except AttributeError:
147 except AttributeError:
148 def is_demandimport_enabled():
148 def is_demandimport_enabled():
149 """Checks whether demandimport is enabled at the moment"""
149 """Checks whether demandimport is enabled at the moment"""
150 return __import__ == demandimport._demandimport
150 return __import__ == demandimport._demandimport
151
151
152 # Shut up warning about uninitialized logging for new keyring versions.
152 # Shut up warning about uninitialized logging for new keyring versions.
153 # But beware 2.6…
153 # But beware 2.6…
154 try:
154 try:
155 import logging
155 import logging
156 logging.getLogger("keyring").addHandler(logging.NullHandler())
156 logging.getLogger("keyring").addHandler(logging.NullHandler())
157 except: # pylint: disable=bare-except
157 except: # pylint: disable=bare-except
158 pass
158 pass
159
159
160 # Temporarily disable demandimport to make the need of extending
160 # Temporarily disable demandimport to make the need of extending
161 # the list above less likely.
161 # the list above less likely.
162 if is_demandimport_enabled():
162 if is_demandimport_enabled():
163 demandimport.disable()
163 demandimport.disable()
164 try:
164 try:
165 import keyring
165 import keyring
166 finally:
166 finally:
167 demandimport.enable()
167 demandimport.enable()
168 else:
168 else:
169 import keyring
169 import keyring
170 return keyring
170 return keyring
171
171
172 #################################################################
172 #################################################################
173 # Actual implementation
173 # Actual implementation
174 #################################################################
174 #################################################################
175
175
176 KEYRING_SERVICE = "Mercurial"
176 KEYRING_SERVICE = "Mercurial"
177
177
178
178
179 class PasswordStore(object):
179 class PasswordStore(object):
180 """
180 """
181 Helper object handling keyring usage (password save&restore,
181 Helper object handling keyring usage (password save&restore,
182 the way passwords are keyed in the keyring).
182 the way passwords are keyed in the keyring).
183 """
183 """
184 def __init__(self):
184 def __init__(self):
185 self.cache = dict()
185 self.cache = dict()
186
186
187 def get_http_password(self, url, username):
187 def get_http_password(self, url, username):
188 """
188 """
189 Checks whether password of username for url is available,
189 Checks whether password of username for url is available,
190 returns it or None
190 returns it or None
191 """
191 """
192 return self._read_password_from_keyring(
192 return self._read_password_from_keyring(
193 self._format_http_key(url, username))
193 self._format_http_key(url, username))
194
194
195 def set_http_password(self, url, username, password):
195 def set_http_password(self, url, username, password):
196 """Saves password to keyring"""
196 """Saves password to keyring"""
197 self._save_password_to_keyring(
197 self._save_password_to_keyring(
198 self._format_http_key(url, username),
198 self._format_http_key(url, username),
199 password)
199 password)
200
200
201 def clear_http_password(self, url, username):
201 def clear_http_password(self, url, username):
202 """Drops saved password"""
202 """Drops saved password"""
203 self.set_http_password(url, username, "")
203 self.set_http_password(url, username, "")
204
204
205 @staticmethod
205 @staticmethod
206 def _format_http_key(url, username):
206 def _format_http_key(url, username):
207 """Construct actual key for password identification"""
207 """Construct actual key for password identification"""
208 return "%s@@%s" % (username, url)
208 return "%s@@%s" % (username, url)
209
209
210 def get_smtp_password(self, machine, port, username):
210 def get_smtp_password(self, machine, port, username):
211 """Checks for SMTP password in keyring, returns
211 """Checks for SMTP password in keyring, returns
212 password or None"""
212 password or None"""
213 return self._read_password_from_keyring(
213 return self._read_password_from_keyring(
214 self._format_smtp_key(machine, port, username))
214 self._format_smtp_key(machine, port, username))
215
215
216 def set_smtp_password(self, machine, port, username, password):
216 def set_smtp_password(self, machine, port, username, password):
217 """Saves SMTP password to keyring"""
217 """Saves SMTP password to keyring"""
218 self._save_password_to_keyring(
218 self._save_password_to_keyring(
219 self._format_smtp_key(machine, port, username),
219 self._format_smtp_key(machine, port, username),
220 password)
220 password)
221
221
222 def clear_smtp_password(self, machine, port, username):
222 def clear_smtp_password(self, machine, port, username):
223 """Drops saved SMTP password"""
223 """Drops saved SMTP password"""
224 self.set_smtp_password(machine, port, username, "")
224 self.set_smtp_password(machine, port, username, "")
225
225
226 @staticmethod
226 @staticmethod
227 def _format_smtp_key(machine, port, username):
227 def _format_smtp_key(machine, port, username):
228 """Construct key for SMTP password identification"""
228 """Construct key for SMTP password identification"""
229 return "%s@@%s:%s" % (username, machine, str(port))
229 return "%s@@%s:%s" % (username, machine, str(port))
230
230
231 @staticmethod
231 @staticmethod
232 def _read_password_from_keyring(pwdkey):
232 def _read_password_from_keyring(pwdkey):
233 """Physically read from keyring"""
233 """Physically read from keyring"""
234 keyring = import_keyring()
234 keyring = import_keyring()
235 password = keyring.get_password(KEYRING_SERVICE, pwdkey)
235 password = keyring.get_password(KEYRING_SERVICE, pwdkey)
236 # Reverse recoding from next routine
236 # Reverse recoding from next routine
237 if isinstance(password, unicode):
237 if isinstance(password, unicode):
238 return encoding.tolocal(password.encode('utf-8'))
238 return encoding.tolocal(password.encode('utf-8'))
239 return password
239 return password
240
240
241 @staticmethod
241 @staticmethod
242 def _save_password_to_keyring(pwdkey, password):
242 def _save_password_to_keyring(pwdkey, password):
243 """Physically write to keyring"""
243 """Physically write to keyring"""
244 keyring = import_keyring()
244 keyring = import_keyring()
245 # keyring in general expects unicode.
245 # keyring in general expects unicode.
246 # Mercurial provides "local" encoding. See #33
246 # Mercurial provides "local" encoding. See #33
247 password = encoding.fromlocal(password).decode('utf-8')
247 password = encoding.fromlocal(password).decode('utf-8')
248 keyring.set_password(
248 keyring.set_password(
249 KEYRING_SERVICE, pwdkey, password)
249 KEYRING_SERVICE, pwdkey, password)
250
250
251 password_store = PasswordStore()
251 password_store = PasswordStore()
252
252
253
253
254 ############################################################
254 ############################################################
255 # Various utils
255 # Various utils
256 ############################################################
256 ############################################################
257
257
258 def _debug(ui, msg):
258 def _debug(ui, msg):
259 """Generic debug message"""
259 """Generic debug message"""
260 ui.debug("keyring: " + msg + "\n")
260 ui.debug("keyring: " + msg + "\n")
261
261
262
262
263 class PwdCache(object):
263 class PwdCache(object):
264 """Short term cache, used to preserve passwords
264 """Short term cache, used to preserve passwords
265 if they are used twice during a command"""
265 if they are used twice during a command"""
266 def __init__(self):
266 def __init__(self):
267 self._cache = {}
267 self._cache = {}
268
268
269 def store(self, realm, url, user, pwd):
269 def store(self, realm, url, user, pwd):
270 """Saves password"""
270 """Saves password"""
271 cache_key = (realm, url, user)
271 cache_key = (realm, url, user)
272 self._cache[cache_key] = pwd
272 self._cache[cache_key] = pwd
273
273
274 def check(self, realm, url, user):
274 def check(self, realm, url, user):
275 """Checks for cached password"""
275 """Checks for cached password"""
276 cache_key = (realm, url, user)
276 cache_key = (realm, url, user)
277 return self._cache.get(cache_key)
277 return self._cache.get(cache_key)
278
278
279
279
280 ############################################################
280 ############################################################
281 # HTTP password management
281 # HTTP password management
282 ############################################################
282 ############################################################
283
283
284
284
285 class HTTPPasswordHandler(object):
285 class HTTPPasswordHandler(object):
286 """
286 """
287 Actual implementation of password handling (user prompting,
287 Actual implementation of password handling (user prompting,
288 configuration file searching, keyring save&restore).
288 configuration file searching, keyring save&restore).
289
289
290 Object of this class is bound as passwordmgr attribute.
290 Object of this class is bound as passwordmgr attribute.
291 """
291 """
292 def __init__(self):
292 def __init__(self):
293 self.pwd_cache = PwdCache()
293 self.pwd_cache = PwdCache()
294 self.last_reply = None
294 self.last_reply = None
295
295
296 # Markers and also names used in debug notes. Password source
296 # Markers and also names used in debug notes. Password source
297 SRC_URL = "repository URL"
297 SRC_URL = "repository URL"
298 SRC_CFGAUTH = "hgrc"
298 SRC_CFGAUTH = "hgrc"
299 SRC_MEMCACHE = "temporary cache"
299 SRC_MEMCACHE = "temporary cache"
300 SRC_KEYRING = "keyring"
300 SRC_KEYRING = "keyring"
301
301
302 def get_credentials(self, pwmgr, realm, authuri, skip_caches=False):
302 def get_credentials(self, pwmgr, realm, authuri, skip_caches=False):
303 """
303 """
304 Looks up for user credentials in various places, returns them
304 Looks up for user credentials in various places, returns them
305 and information about their source.
305 and information about their source.
306
306
307 Used internally inside find_auth and inside informative
307 Used internally inside find_auth and inside informative
308 commands (thiis method doesn't cache, doesn't detect bad
308 commands (thiis method doesn't cache, doesn't detect bad
309 passwords etc, doesn't prompt interactively, doesn't store
309 passwords etc, doesn't prompt interactively, doesn't store
310 password in keyring).
310 password in keyring).
311
311
312 Returns: user, password, SRC_*, actual_url
312 Returns: user, password, SRC_*, actual_url
313
313
314 If not found, password and SRC is None, user can be given or
314 If not found, password and SRC is None, user can be given or
315 not, url is always set
315 not, url is always set
316 """
316 """
317 ui = pwmgr.ui
317 ui = pwmgr.ui
318
318
319 # Strip arguments to get actual remote repository url.
319 # Strip arguments to get actual remote repository url.
320 base_url = self.canonical_url(authuri)
320 base_url = self.canonical_url(authuri)
321
321
322 # Extract username (or password) stored directly in url
322 # Extract username (or password) stored directly in url
323 url_user, url_pwd \
323 url_user, url_pwd \
324 = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
324 = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
325 pwmgr, realm, authuri)
325 pwmgr, realm, authuri)
326 if url_user and url_pwd:
326 if url_user and url_pwd:
327 return url_user, url_pwd, self.SRC_URL, base_url
327 return url_user, url_pwd, self.SRC_URL, base_url
328
328
329 # Consult configuration to normalize url to prefix, and find username
329 # Consult configuration to normalize url to prefix, and find username
330 # (and maybe password)
330 # (and maybe password)
331 auth_user, auth_pwd, keyring_url = self.get_url_config(
331 auth_user, auth_pwd, keyring_url = self.get_url_config(
332 ui, base_url, url_user)
332 ui, base_url, url_user)
333 if auth_user and url_user and (url_user != auth_user):
333 if auth_user and url_user and (url_user != auth_user):
334 raise util.Abort(_('mercurial_keyring: username for %s specified both in repository path (%s) and in .hg/hgrc/[auth] (%s). Please, leave only one of those' % (base_url, url_user, auth_user)))
334 raise util.Abort(_('mercurial_keyring: username for %s specified both in repository path (%s) and in .hg/hgrc/[auth] (%s). Please, leave only one of those' % (base_url, url_user, auth_user)))
335 if auth_user and auth_pwd:
335 if auth_user and auth_pwd:
336 return auth_user, auth_pwd, self.SRC_CFGAUTH, keyring_url
336 return auth_user, auth_pwd, self.SRC_CFGAUTH, keyring_url
337
337
338 if skip_caches:
338 if skip_caches:
339 return auth_user, None, None, keyring_url
339 return auth_user, None, None, keyring_url
340
340
341 # Check memory cache (reuse )
341 # Check memory cache (reuse )
342 # Checking the memory cache (there may be many http calls per command)
342 # Checking the memory cache (there may be many http calls per command)
343 cached_pwd = self.pwd_cache.check(realm, keyring_url, auth_user)
343 cached_pwd = self.pwd_cache.check(realm, keyring_url, auth_user)
344 if cached_pwd:
344 if cached_pwd:
345 return auth_user, cached_pwd, self.SRC_MEMCACHE, keyring_url
345 return auth_user, cached_pwd, self.SRC_MEMCACHE, keyring_url
346
346
347 # Load from keyring.
347 # Load from keyring.
348 if auth_user:
348 if auth_user:
349 ui.debug(_("keyring: looking for password (user %s, url %s)\n") % (auth_user, keyring_url))
349 ui.debug(_("keyring: looking for password (user %s, url %s)\n") % (auth_user, keyring_url))
350 keyring_pwd = password_store.get_http_password(keyring_url, auth_user)
350 keyring_pwd = password_store.get_http_password(keyring_url, auth_user)
351 if keyring_pwd:
351 if keyring_pwd:
352 return auth_user, keyring_pwd, self.SRC_KEYRING, keyring_url
352 return auth_user, keyring_pwd, self.SRC_KEYRING, keyring_url
353
353
354 return auth_user, None, None, keyring_url
354 return auth_user, None, None, keyring_url
355
355
356
356
357 @staticmethod
357 @staticmethod
358 def prompt_interactively(ui, user, realm, url):
358 def prompt_interactively(ui, user, realm, url):
359 """Actual interactive prompt"""
359 """Actual interactive prompt"""
360 if not ui.interactive():
360 if not ui.interactive():
361 raise util.Abort(_('mercurial_keyring: http authorization required but program used in non-interactive mode'))
361 raise util.Abort(_('mercurial_keyring: http authorization required but program used in non-interactive mode'))
362
362
363 if not user:
363 if not user:
364 ui.status(_("keyring: username not specified in hgrc (or in url). Password will not be saved.\n"))
364 ui.status(_("keyring: username not specified in hgrc (or in url). Password will not be saved.\n"))
365
365
366 ui.write(_("http authorization required\n"))
366 ui.write(_("http authorization required\n"))
367 ui.status(_("realm: %s\n") % realm)
367 ui.status(_("realm: %s\n") % realm)
368 ui.status(_("url: %s\n") % url)
368 ui.status(_("url: %s\n") % url)
369 if user:
369 if user:
370 ui.write(_("user: %s (fixed in hgrc or url)\n" % user))
370 ui.write(_("user: %s (fixed in hgrc or url)\n" % user))
371 else:
371 else:
372 user = ui.prompt(_("user:"), default=None)
372 user = ui.prompt(_("user:"), default=None)
373 pwd = ui.getpass(_("password: "))
373 pwd = ui.getpass(_("password: "))
374 return user, pwd
374 return user, pwd
375
375
376 def find_auth(self, pwmgr, realm, authuri, req):
376 def find_auth(self, pwmgr, realm, authuri, req):
377 """
377 """
378 Actual implementation of find_user_password - different
378 Actual implementation of find_user_password - different
379 ways of obtaining the username and password.
379 ways of obtaining the username and password.
380
380
381 Returns pair username, password
381 Returns pair username, password
382 """
382 """
383 ui = pwmgr.ui
383 ui = pwmgr.ui
384 after_bad_auth = self._after_bad_auth(ui, realm, authuri, req)
384 after_bad_auth = self._after_bad_auth(ui, realm, authuri, req)
385
385
386 # Look in url, cache, etc
386 # Look in url, cache, etc
387 user, pwd, src, final_url = self.get_credentials(
387 user, pwd, src, final_url = self.get_credentials(
388 pwmgr, realm, authuri, skip_caches=after_bad_auth)
388 pwmgr, realm, authuri, skip_caches=after_bad_auth)
389 if pwd:
389 if pwd:
390 if src != self.SRC_MEMCACHE:
390 if src != self.SRC_MEMCACHE:
391 self.pwd_cache.store(realm, final_url, user, pwd)
391 self.pwd_cache.store(realm, final_url, user, pwd)
392 self._note_last_reply(realm, authuri, user, req)
392 self._note_last_reply(realm, authuri, user, req)
393 _debug(ui, _("Password found in " + src))
393 _debug(ui, _("Password found in " + src))
394 return user, pwd
394 return user, pwd
395
395
396 # Last resort: interactive prompt
396 # Last resort: interactive prompt
397 user, pwd = self.prompt_interactively(ui, user, realm, final_url)
397 user, pwd = self.prompt_interactively(ui, user, realm, final_url)
398
398
399 if user:
399 if user:
400 # Saving password to the keyring.
400 # Saving password to the keyring.
401 # It is done only if username is permanently set.
401 # It is done only if username is permanently set.
402 # Otherwise we won't be able to find the password so it
402 # Otherwise we won't be able to find the password so it
403 # does not make much sense to preserve it
403 # does not make much sense to preserve it
404 _debug(ui, _("Saving password for %s to keyring") % user)
404 _debug(ui, _("Saving password for %s to keyring") % user)
405 password_store.set_http_password(final_url, user, pwd)
405 password_store.set_http_password(final_url, user, pwd)
406
406
407 # Saving password to the memory cache
407 # Saving password to the memory cache
408 self.pwd_cache.store(realm, final_url, user, pwd)
408 self.pwd_cache.store(realm, final_url, user, pwd)
409 self._note_last_reply(realm, authuri, user, req)
409 self._note_last_reply(realm, authuri, user, req)
410 _debug(ui, _("Manually entered password"))
410 _debug(ui, _("Manually entered password"))
411 return user, pwd
411 return user, pwd
412
412
413 def get_url_config(self, ui, base_url, user):
413 def get_url_config(self, ui, base_url, user):
414 """
414 """
415 Checks configuration to decide whether/which username, prefix,
415 Checks configuration to decide whether/which username, prefix,
416 and password are configured for given url. Consults [auth] section.
416 and password are configured for given url. Consults [auth] section.
417
417
418 Returns tuple (username, password, prefix) containing elements
418 Returns tuple (username, password, prefix) containing elements
419 found. username and password can be None (if unset), if prefix
419 found. username and password can be None (if unset), if prefix
420 is not found, url itself is returned.
420 is not found, url itself is returned.
421 """
421 """
422 from mercurial.httpconnection import readauthforuri
422 from mercurial.httpconnection import readauthforuri
423 _debug(ui, _("Checking for hgrc info about url %s, user %s") % (base_url, user))
423 _debug(ui, _("Checking for hgrc info about url %s, user %s") % (base_url, user))
424 res = readauthforuri(ui, base_url, user)
424 res = readauthforuri(ui, base_url, user)
425 if res:
425 if res:
426 group, auth_token = res
426 group, auth_token = res
427 else:
427 else:
428 auth_token = None
428 auth_token = None
429
429
430 if auth_token:
430 if auth_token:
431 username = auth_token.get('username')
431 username = auth_token.get('username')
432 password = auth_token.get('password')
432 password = auth_token.get('password')
433 prefix = auth_token.get('prefix')
433 prefix = auth_token.get('prefix')
434 else:
434 else:
435 username = None
435 username = None
436 password = None
436 password = None
437 prefix = None
437 prefix = None
438
438
439 password_url = self.password_url(base_url, prefix)
439 password_url = self.password_url(base_url, prefix)
440 return username, password, password_url
440 return username, password, password_url
441
441
442 def _note_last_reply(self, realm, authuri, user, req):
442 def _note_last_reply(self, realm, authuri, user, req):
443 """
443 """
444 Internal helper. Saves info about auth-data obtained,
444 Internal helper. Saves info about auth-data obtained,
445 preserves them in last_reply, and returns pair user, pwd
445 preserves them in last_reply, and returns pair user, pwd
446 """
446 """
447 self.last_reply = dict(realm=realm, authuri=authuri,
447 self.last_reply = dict(realm=realm, authuri=authuri,
448 user=user, req=req)
448 user=user, req=req)
449
449
450 def _after_bad_auth(self, ui, realm, authuri, req):
450 def _after_bad_auth(self, ui, realm, authuri, req):
451 """
451 """
452 If we are called again just after identical previous
452 If we are called again just after identical previous
453 request, then the previously returned auth must have been
453 request, then the previously returned auth must have been
454 wrong. So we note this to force password prompt (and avoid
454 wrong. So we note this to force password prompt (and avoid
455 reusing bad password indefinitely).
455 reusing bad password indefinitely).
456
456
457 This routine checks for this condition.
457 This routine checks for this condition.
458 """
458 """
459 if self.last_reply:
459 if self.last_reply:
460 if (self.last_reply['realm'] == realm) \
460 if (self.last_reply['realm'] == realm) \
461 and (self.last_reply['authuri'] == authuri) \
461 and (self.last_reply['authuri'] == authuri) \
462 and (self.last_reply['req'] == req):
462 and (self.last_reply['req'] == req):
463 _debug(ui, _("Working after bad authentication, cached passwords not used %s") % str(self.last_reply))
463 _debug(ui, _("Working after bad authentication, cached passwords not used %s") % str(self.last_reply))
464 return True
464 return True
465 return False
465 return False
466
466
467 @staticmethod
467 @staticmethod
468 def password_url(base_url, prefix):
468 def password_url(base_url, prefix):
469 """Calculates actual url identifying the password. Takes
469 """Calculates actual url identifying the password. Takes
470 configured prefix under consideration (so can be shorter
470 configured prefix under consideration (so can be shorter
471 than repo url)"""
471 than repo url)"""
472 if not prefix or prefix == '*':
472 if not prefix or prefix == '*':
473 return base_url
473 return base_url
474 scheme, hostpath = base_url.split('://', 1)
474 scheme, hostpath = base_url.split('://', 1)
475 p = prefix.split('://', 1)
475 p = prefix.split('://', 1)
476 if len(p) > 1:
476 if len(p) > 1:
477 prefix_host_path = p[1]
477 prefix_host_path = p[1]
478 else:
478 else:
479 prefix_host_path = prefix
479 prefix_host_path = prefix
480 password_url = scheme + '://' + prefix_host_path
480 password_url = scheme + '://' + prefix_host_path
481 return password_url
481 return password_url
482
482
483 @staticmethod
483 @staticmethod
484 def canonical_url(authuri):
484 def canonical_url(authuri):
485 """
485 """
486 Strips query params from url. Used to convert urls like
486 Strips query params from url. Used to convert urls like
487 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
487 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
488 to
488 to
489 https://repo.machine.com/repos/apps/module
489 https://repo.machine.com/repos/apps/module
490 """
490 """
491 parsed_url = urlparse(authuri)
491 parsed_url = urlparse(authuri)
492 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
492 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
493 parsed_url.path)
493 parsed_url.path)
494
494
495 ############################################################
495 ############################################################
496 # Mercurial monkey-patching
496 # Mercurial monkey-patching
497 ############################################################
497 ############################################################
498
498
499
499
500 @monkeypatch_method(passwordmgr)
500 @monkeypatch_method(passwordmgr)
501 def find_user_password(self, realm, authuri):
501 def find_user_password(self, realm, authuri):
502 """
502 """
503 keyring-based implementation of username/password query
503 keyring-based implementation of username/password query
504 for HTTP(S) connections
504 for HTTP(S) connections
505
505
506 Passwords are saved in gnome keyring, OSX/Chain or other platform
506 Passwords are saved in gnome keyring, OSX/Chain or other platform
507 specific storage and keyed by the repository url
507 specific storage and keyed by the repository url
508 """
508 """
509 # Extend object attributes
509 # Extend object attributes
510 if not hasattr(self, '_pwd_handler'):
510 if not hasattr(self, '_pwd_handler'):
511 self._pwd_handler = HTTPPasswordHandler()
511 self._pwd_handler = HTTPPasswordHandler()
512
512
513 if hasattr(self, '_http_req'):
513 if hasattr(self, '_http_req'):
514 req = self._http_req
514 req = self._http_req
515 else:
515 else:
516 req = None
516 req = None
517
517
518 return self._pwd_handler.find_auth(self, realm, authuri, req)
518 return self._pwd_handler.find_auth(self, realm, authuri, req)
519
519
520
520
521 @monkeypatch_method(urllib2.AbstractBasicAuthHandler, "http_error_auth_reqed")
521 @monkeypatch_method(urllib2.AbstractBasicAuthHandler, "http_error_auth_reqed")
522 def basic_http_error_auth_reqed(self, authreq, host, req, headers):
522 def basic_http_error_auth_reqed(self, authreq, host, req, headers):
523 """Preserves current HTTP request so it can be consulted
523 """Preserves current HTTP request so it can be consulted
524 in find_user_password above"""
524 in find_user_password above"""
525 self.passwd._http_req = req
525 self.passwd._http_req = req
526 try:
526 try:
527 return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
527 return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
528 finally:
528 finally:
529 self.passwd._http_req = None
529 self.passwd._http_req = None
530
530
531
531
532 @monkeypatch_method(urllib2.AbstractDigestAuthHandler, "http_error_auth_reqed")
532 @monkeypatch_method(urllib2.AbstractDigestAuthHandler, "http_error_auth_reqed")
533 def digest_http_error_auth_reqed(self, authreq, host, req, headers):
533 def digest_http_error_auth_reqed(self, authreq, host, req, headers):
534 """Preserves current HTTP request so it can be consulted
534 """Preserves current HTTP request so it can be consulted
535 in find_user_password above"""
535 in find_user_password above"""
536 self.passwd._http_req = req
536 self.passwd._http_req = req
537 try:
537 try:
538 return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
538 return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
539 finally:
539 finally:
540 self.passwd._http_req = None
540 self.passwd._http_req = None
541
541
542 ############################################################
542 ############################################################
543 # SMTP support
543 # SMTP support
544 ############################################################
544 ############################################################
545
545
546
546
547 def try_smtp_login(ui, smtp_obj, username, password):
547 def try_smtp_login(ui, smtp_obj, username, password):
548 """
548 """
549 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
549 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
550 password.
550 password.
551
551
552 Returns:
552 Returns:
553 - True if login succeeded
553 - True if login succeeded
554 - False if login failed due to the wrong credentials
554 - False if login failed due to the wrong credentials
555
555
556 Throws Abort exception if login failed for any other reason.
556 Throws Abort exception if login failed for any other reason.
557
557
558 Immediately returns False if password is empty
558 Immediately returns False if password is empty
559 """
559 """
560 if not password:
560 if not password:
561 return False
561 return False
562 try:
562 try:
563 ui.note(_('(authenticating to mail server as %s)\n') %
563 ui.note(_('(authenticating to mail server as %s)\n') %
564 (username))
564 (username))
565 smtp_obj.login(username, password)
565 smtp_obj.login(username, password)
566 return True
566 return True
567 except smtplib.SMTPException, inst:
567 except smtplib.SMTPException, inst:
568 if inst.smtp_code == 535:
568 if inst.smtp_code == 535:
569 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
569 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
570 return False
570 return False
571 else:
571 else:
572 raise util.Abort(inst)
572 raise util.Abort(inst)
573
573
574
574
575 def keyring_supported_smtp(ui, username):
575 def keyring_supported_smtp(ui, username):
576 """
576 """
577 keyring-integrated replacement for mercurial.mail._smtp
577 keyring-integrated replacement for mercurial.mail._smtp
578 Used only when configuration file contains username, but
578 Used only when configuration file contains username, but
579 does not contain the password.
579 does not contain the password.
580
580
581 Most of the routine below is copied as-is from
581 Most of the routine below is copied as-is from
582 mercurial.mail._smtp. The only changed part is
582 mercurial.mail._smtp. The only changed part is
583 marked with # >>>>> and # <<<<< markers
583 marked with # >>>>> and # <<<<< markers
584 """
584 """
585 local_hostname = ui.config('smtp', 'local_hostname')
585 local_hostname = ui.config('smtp', 'local_hostname')
586 tls = ui.config('smtp', 'tls', 'none')
586 tls = ui.config('smtp', 'tls', 'none')
587 # backward compatible: when tls = true, we use starttls.
587 # backward compatible: when tls = true, we use starttls.
588 starttls = tls == 'starttls' or util.parsebool(tls)
588 starttls = tls == 'starttls' or util.parsebool(tls)
589 smtps = tls == 'smtps'
589 smtps = tls == 'smtps'
590 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
590 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
591 raise util.Abort(_("can't use TLS: Python SSL support not installed"))
591 raise util.Abort(_("can't use TLS: Python SSL support not installed"))
592 mailhost = ui.config('smtp', 'host')
592 mailhost = ui.config('smtp', 'host')
593 if not mailhost:
593 if not mailhost:
594 raise util.Abort(_('smtp.host not configured - cannot send mail'))
594 raise util.Abort(_('smtp.host not configured - cannot send mail'))
595 verifycert = ui.config('smtp', 'verifycert', 'strict')
595 verifycert = ui.config('smtp', 'verifycert', 'strict')
596 if verifycert not in ['strict', 'loose']:
596 if verifycert not in ['strict', 'loose']:
597 if util.parsebool(verifycert) is not False:
597 if util.parsebool(verifycert) is not False:
598 raise util.Abort(_('invalid smtp.verifycert configuration: %s')
598 raise util.Abort(_('invalid smtp.verifycert configuration: %s')
599 % (verifycert))
599 % (verifycert))
600 verifycert = False
600 verifycert = False
601 if (starttls or smtps) and verifycert:
601 if (starttls or smtps) and verifycert:
602 sslkwargs = sslutil.sslkwargs(ui, mailhost)
602 sslkwargs = sslutil.sslkwargs(ui, mailhost)
603 else:
603 else:
604 sslkwargs = {}
604 sslkwargs = {}
605 if smtps:
605 if smtps:
606 ui.note(_('(using smtps)\n'))
606 ui.note(_('(using smtps)\n'))
607 s = SMTPS(sslkwargs, local_hostname=local_hostname)
607 s = SMTPS(sslkwargs, local_hostname=local_hostname)
608 elif starttls:
608 elif starttls:
609 s = STARTTLS(sslkwargs, local_hostname=local_hostname)
609 s = STARTTLS(sslkwargs, local_hostname=local_hostname)
610 else:
610 else:
611 s = smtplib.SMTP(local_hostname=local_hostname)
611 s = smtplib.SMTP(local_hostname=local_hostname)
612 if smtps:
612 if smtps:
613 defaultport = 465
613 defaultport = 465
614 else:
614 else:
615 defaultport = 25
615 defaultport = 25
616 mailport = util.getport(ui.config('smtp', 'port', defaultport))
616 mailport = util.getport(ui.config('smtp', 'port', defaultport))
617 ui.note(_('sending mail: smtp host %s, port %s\n') %
617 ui.note(_('sending mail: smtp host %s, port %s\n') %
618 (mailhost, mailport))
618 (mailhost, mailport))
619 s.connect(host=mailhost, port=mailport)
619 s.connect(host=mailhost, port=mailport)
620 if starttls:
620 if starttls:
621 ui.note(_('(using starttls)\n'))
621 ui.note(_('(using starttls)\n'))
622 s.ehlo()
622 s.ehlo()
623 s.starttls()
623 s.starttls()
624 s.ehlo()
624 s.ehlo()
625 if (starttls or smtps) and verifycert:
625 if (starttls or smtps) and verifycert:
626 ui.note(_('(verifying remote certificate)\n'))
626 ui.note(_('(verifying remote certificate)\n'))
627 sslutil.validator(ui, mailhost)(s.sock, verifycert == 'strict')
627 sslutil.validator(ui, mailhost)(s.sock, verifycert == 'strict')
628
628
629 # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
629 # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
630 stored = password = password_store.get_smtp_password(
630 stored = password = password_store.get_smtp_password(
631 mailhost, mailport, username)
631 mailhost, mailport, username)
632 # No need to check whether password was found as try_smtp_login
632 # No need to check whether password was found as try_smtp_login
633 # just returns False if it is absent.
633 # just returns False if it is absent.
634 while not try_smtp_login(ui, s, username, password):
634 while not try_smtp_login(ui, s, username, password):
635 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
635 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
636
636
637 if stored != password:
637 if stored != password:
638 password_store.set_smtp_password(
638 password_store.set_smtp_password(
639 mailhost, mailport, username, password)
639 mailhost, mailport, username, password)
640 # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
640 # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
641
641
642 def send(sender, recipients, msg):
642 def send(sender, recipients, msg):
643 try:
643 try:
644 return s.sendmail(sender, recipients, msg)
644 return s.sendmail(sender, recipients, msg)
645 except smtplib.SMTPRecipientsRefused, inst:
645 except smtplib.SMTPRecipientsRefused, inst:
646 recipients = [r[1] for r in inst.recipients.values()]
646 recipients = [r[1] for r in inst.recipients.values()]
647 raise util.Abort('\n' + '\n'.join(recipients))
647 raise util.Abort('\n' + '\n'.join(recipients))
648 except smtplib.SMTPException, inst:
648 except smtplib.SMTPException, inst:
649 raise util.Abort(inst)
649 raise util.Abort(inst)
650
650
651 return send
651 return send
652
652
653 ############################################################
653 ############################################################
654 # SMTP monkeypatching
654 # SMTP monkeypatching
655 ############################################################
655 ############################################################
656
656
657
657
658 @monkeypatch_method(mail)
658 @monkeypatch_method(mail)
659 def _smtp(ui):
659 def _smtp(ui):
660 """
660 """
661 build an smtp connection and return a function to send email
661 build an smtp connection and return a function to send email
662
662
663 This is the monkeypatched version of _smtp(ui) function from
663 This is the monkeypatched version of _smtp(ui) function from
664 mercurial/mail.py. It calls the original unless username
664 mercurial/mail.py. It calls the original unless username
665 without password is given in the configuration.
665 without password is given in the configuration.
666 """
666 """
667 username = ui.config('smtp', 'username')
667 username = ui.config('smtp', 'username')
668 password = ui.config('smtp', 'password')
668 password = ui.config('smtp', 'password')
669
669
670 if username and not password:
670 if username and not password:
671 return keyring_supported_smtp(ui, username)
671 return keyring_supported_smtp(ui, username)
672 else:
672 else:
673 return _smtp.orig(ui)
673 return _smtp.orig(ui)
674
674
675
675
676 ############################################################
676 ############################################################
677 # Custom commands
677 # Custom commands
678 ############################################################
678 ############################################################
679
679
680 def cmd_keyring_check(ui, repo, *path_args, **opts):
680 def cmd_keyring_check(ui, repo, *path_args, **opts):
681 """
681 """
682 Prints basic info (whether password is currently saved, and how is
682 Prints basic info (whether password is currently saved, and how is
683 it identified) for given path or all defined repo paths.
683 it identified) for given path or for all defined repo paths which are HTTP.
684 """
684 """
685 import re
685 import re
686
686
687 re_http_url = re.compile(r'^https?://')
687 re_http_url = re.compile(r'^https?://')
688 defined_paths = [(name, url)
688 defined_paths = [(name, url)
689 for name, url in ui.configitems('paths')]
689 for name, url in ui.configitems('paths')]
690 if path_args:
690 if path_args:
691 # Maybe parameter is an alias
691 # Maybe parameter is an alias
692 defined_paths_dic = dict(defined_paths)
692 defined_paths_dic = dict(defined_paths)
693 paths = [(path_arg, defined_paths_dic.get(path_arg, path_arg))
693 paths = [(path_arg, defined_paths_dic.get(path_arg, path_arg))
694 for path_arg in path_args]
694 for path_arg in path_args]
695 else:
695 else:
696 paths = [(name, url) for name, url in defined_paths]
696 paths = [(name, url) for name, url in defined_paths]
697
697
698 if not paths:
698 if not paths:
699 ui.status(_("keyring_check: no paths defined"))
699 ui.status(_("keyring_check: no paths defined"))
700
700
701 handler = HTTPPasswordHandler()
701 handler = HTTPPasswordHandler()
702
702
703 ui.status(_("keyring password save status:\n"))
703 ui.status(_("keyring password save status:\n"))
704 for name, url in paths:
704 for name, url in paths:
705 if not re_http_url.search(url):
705 if not re_http_url.search(url):
706 if path_args:
706 if path_args:
707 ui.status(_(" %s: non-http path (%s)\n") % (name, url))
707 ui.status(_(" %s: non-http path (%s)\n") % (name, url))
708 continue
708 continue
709 user, pwd, source, final_url = handler.get_credentials(passwordmgr(ui), name, url)
709 user, pwd, source, final_url = handler.get_credentials(passwordmgr(ui), name, url)
710 if pwd:
710 if pwd:
711 ui.status(_(" %s: password available, source: %s, bound to user %s, url %s\n") % (
711 ui.status(_(" %s: password available, source: %s, bound to user %s, url %s\n") % (
712 name, source, user, final_url))
712 name, source, user, final_url))
713 elif user:
713 elif user:
714 ui.status(_(" %s: password not available, once entered, will be bound to user %s, url %s\n") % (
714 ui.status(_(" %s: password not available, once entered, will be bound to user %s, url %s\n") % (
715 name, user, final_url))
715 name, user, final_url))
716 else:
716 else:
717 ui.status(_(" %s: password not available, user unknown, url %s\n") % (
717 ui.status(_(" %s: password not available, user unknown, url %s\n") % (
718 name, final_url))
718 name, final_url))
719
719
720
720
721 def cmd_keyring_clear(ui, repo, path, **opts):
722 """
723 Drops password bound to given path (if any).
724 """
725 # TODO
726 ui.status(_("Not yet implemented"))
727
721
728
722 cmdtable = {
729 cmdtable = {
723 "keyring_check": (
730 "keyring_check": (
724 cmd_keyring_check,
731 cmd_keyring_check,
725 [],
732 [],
733 "keyring_check [PATH]",
734 ),
735 "keyring_clear": (
736 cmd_keyring_clear,
737 [],
726 "keyring_check PATH",
738 "keyring_check PATH",
727 ),
739 ),
728 }
740 }
General Comments 0
You need to be logged in to leave comments. Login now