##// END OF EJS Templates
In case username (and maybe password) is present in url, they are removed...
Marcin Kasperski -
r198:ae4c2c2b default
parent child Browse files
Show More
@@ -1,792 +1,797 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-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_URLCACHE = "urllib temporary cache"
300 SRC_URLCACHE = "urllib temporary cache"
301 SRC_KEYRING = "keyring"
301 SRC_KEYRING = "keyring"
302
302
303 def get_credentials(self, pwmgr, realm, authuri, skip_caches=False):
303 def get_credentials(self, pwmgr, realm, authuri, skip_caches=False):
304 """
304 """
305 Looks up for user credentials in various places, returns them
305 Looks up for user credentials in various places, returns them
306 and information about their source.
306 and information about their source.
307
307
308 Used internally inside find_auth and inside informative
308 Used internally inside find_auth and inside informative
309 commands (thiis method doesn't cache, doesn't detect bad
309 commands (thiis method doesn't cache, doesn't detect bad
310 passwords etc, doesn't prompt interactively, doesn't store
310 passwords etc, doesn't prompt interactively, doesn't store
311 password in keyring).
311 password in keyring).
312
312
313 Returns: user, password, SRC_*, actual_url
313 Returns: user, password, SRC_*, actual_url
314
314
315 If not found, password and SRC is None, user can be given or
315 If not found, password and SRC is None, user can be given or
316 not, url is always set
316 not, url is always set
317 """
317 """
318 ui = pwmgr.ui
318 ui = pwmgr.ui
319
319
320 base_url, url_user, url_passwd = self.unpack_url(authuri)
320 base_url, url_user, url_passwd = self.unpack_url(authuri)
321 ui.debug(_('keyring: base url: %s, url user: %s, url pwd: %s\n') %
321 ui.debug(_('keyring: base url: %s, url user: %s, url pwd: %s\n') %
322 (base_url, url_user or '', url_passwd and '******' or ''))
322 (base_url, url_user or '', url_passwd and '******' or ''))
323
323
324 # Extract username (or password) stored directly in url
324 # Extract username (or password) stored directly in url
325 if url_user and url_passwd:
325 if url_user and url_passwd:
326 return url_user, url_passwd, self.SRC_URL, base_url
326 return url_user, url_passwd, self.SRC_URL, base_url
327
327
328 # Extract data from urllib (in case it was already stored)
328 # Extract data from urllib (in case it was already stored)
329 urllib_user, urllib_pwd \
329 urllib_user, urllib_pwd \
330 = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
330 = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
331 pwmgr, realm, authuri)
331 pwmgr, realm, authuri)
332 if urllib_user and urllib_pwd:
332 if urllib_user and urllib_pwd:
333 return urllib_user, urllib_pwd, self.SRC_URLCACHE, base_url
333 return urllib_user, urllib_pwd, self.SRC_URLCACHE, base_url
334
334
335 # Consult configuration to normalize url to prefix, and find username
335 # Consult configuration to normalize url to prefix, and find username
336 # (and maybe password)
336 # (and maybe password)
337 auth_user, auth_pwd, keyring_url = self.get_url_config(
337 auth_user, auth_pwd, keyring_url = self.get_url_config(
338 ui, base_url, url_user)
338 ui, base_url, url_user)
339 if auth_user and url_user and (url_user != auth_user):
339 if auth_user and url_user and (url_user != auth_user):
340 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, url_user, auth_user)))
340 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, url_user, auth_user)))
341 if auth_user and auth_pwd:
341 if auth_user and auth_pwd:
342 return auth_user, auth_pwd, self.SRC_CFGAUTH, keyring_url
342 return auth_user, auth_pwd, self.SRC_CFGAUTH, keyring_url
343
343
344 if skip_caches:
344 if skip_caches:
345 return auth_user, None, None, keyring_url
345 return auth_user, None, None, keyring_url
346
346
347 # Check memory cache (reuse )
347 # Check memory cache (reuse )
348 # Checking the memory cache (there may be many http calls per command)
348 # Checking the memory cache (there may be many http calls per command)
349 cached_pwd = self.pwd_cache.check(realm, keyring_url, auth_user)
349 cached_pwd = self.pwd_cache.check(realm, keyring_url, auth_user)
350 if cached_pwd:
350 if cached_pwd:
351 return auth_user, cached_pwd, self.SRC_MEMCACHE, keyring_url
351 return auth_user, cached_pwd, self.SRC_MEMCACHE, keyring_url
352
352
353 # Load from keyring.
353 # Load from keyring.
354 if auth_user:
354 if auth_user:
355 ui.debug(_("keyring: looking for password (user %s, url %s)\n") % (auth_user, keyring_url))
355 ui.debug(_("keyring: looking for password (user %s, url %s)\n") % (auth_user, keyring_url))
356 keyring_pwd = password_store.get_http_password(keyring_url, auth_user)
356 keyring_pwd = password_store.get_http_password(keyring_url, auth_user)
357 if keyring_pwd:
357 if keyring_pwd:
358 return auth_user, keyring_pwd, self.SRC_KEYRING, keyring_url
358 return auth_user, keyring_pwd, self.SRC_KEYRING, keyring_url
359
359
360 return auth_user, None, None, keyring_url
360 return auth_user, None, None, keyring_url
361
361
362 @staticmethod
362 @staticmethod
363 def prompt_interactively(ui, user, realm, url):
363 def prompt_interactively(ui, user, realm, url):
364 """Actual interactive prompt"""
364 """Actual interactive prompt"""
365 if not ui.interactive():
365 if not ui.interactive():
366 raise util.Abort(_('keyring: http authorization required but program used in non-interactive mode'))
366 raise util.Abort(_('keyring: http authorization required but program used in non-interactive mode'))
367
367
368 if not user:
368 if not user:
369 ui.status(_("keyring: username not specified in hgrc (or in url). Password will not be saved.\n"))
369 ui.status(_("keyring: username not specified in hgrc (or in url). Password will not be saved.\n"))
370
370
371 ui.write(_("http authorization required\n"))
371 ui.write(_("http authorization required\n"))
372 ui.status(_("realm: %s\n") % realm)
372 ui.status(_("realm: %s\n") % realm)
373 ui.status(_("url: %s\n") % url)
373 ui.status(_("url: %s\n") % url)
374 if user:
374 if user:
375 ui.write(_("user: %s (fixed in hgrc or url)\n" % user))
375 ui.write(_("user: %s (fixed in hgrc or url)\n" % user))
376 else:
376 else:
377 user = ui.prompt(_("user:"), default=None)
377 user = ui.prompt(_("user:"), default=None)
378 pwd = ui.getpass(_("password: "))
378 pwd = ui.getpass(_("password: "))
379 return user, pwd
379 return user, pwd
380
380
381 def find_auth(self, pwmgr, realm, authuri, req):
381 def find_auth(self, pwmgr, realm, authuri, req):
382 """
382 """
383 Actual implementation of find_user_password - different
383 Actual implementation of find_user_password - different
384 ways of obtaining the username and password.
384 ways of obtaining the username and password.
385
385
386 Returns pair username, password
386 Returns pair username, password
387 """
387 """
388 ui = pwmgr.ui
388 ui = pwmgr.ui
389 after_bad_auth = self._after_bad_auth(ui, realm, authuri, req)
389 after_bad_auth = self._after_bad_auth(ui, realm, authuri, req)
390
390
391 # Look in url, cache, etc
391 # Look in url, cache, etc
392 user, pwd, src, final_url = self.get_credentials(
392 user, pwd, src, final_url = self.get_credentials(
393 pwmgr, realm, authuri, skip_caches=after_bad_auth)
393 pwmgr, realm, authuri, skip_caches=after_bad_auth)
394 if pwd:
394 if pwd:
395 if src != self.SRC_MEMCACHE:
395 if src != self.SRC_MEMCACHE:
396 self.pwd_cache.store(realm, final_url, user, pwd)
396 self.pwd_cache.store(realm, final_url, user, pwd)
397 self._note_last_reply(realm, authuri, user, req)
397 self._note_last_reply(realm, authuri, user, req)
398 _debug(ui, _("Password found in " + src))
398 _debug(ui, _("Password found in " + src))
399 return user, pwd
399 return user, pwd
400
400
401 # Last resort: interactive prompt
401 # Last resort: interactive prompt
402 user, pwd = self.prompt_interactively(ui, user, realm, final_url)
402 user, pwd = self.prompt_interactively(ui, user, realm, final_url)
403
403
404 if user:
404 if user:
405 # Saving password to the keyring.
405 # Saving password to the keyring.
406 # It is done only if username is permanently set.
406 # It is done only if username is permanently set.
407 # Otherwise we won't be able to find the password so it
407 # Otherwise we won't be able to find the password so it
408 # does not make much sense to preserve it
408 # does not make much sense to preserve it
409 _debug(ui, _("Saving password for %s to keyring") % user)
409 _debug(ui, _("Saving password for %s to keyring") % user)
410 password_store.set_http_password(final_url, user, pwd)
410 password_store.set_http_password(final_url, user, pwd)
411
411
412 # Saving password to the memory cache
412 # Saving password to the memory cache
413 self.pwd_cache.store(realm, final_url, user, pwd)
413 self.pwd_cache.store(realm, final_url, user, pwd)
414 self._note_last_reply(realm, authuri, user, req)
414 self._note_last_reply(realm, authuri, user, req)
415 _debug(ui, _("Manually entered password"))
415 _debug(ui, _("Manually entered password"))
416 return user, pwd
416 return user, pwd
417
417
418 def get_url_config(self, ui, base_url, user):
418 def get_url_config(self, ui, base_url, user):
419 """
419 """
420 Checks configuration to decide whether/which username, prefix,
420 Checks configuration to decide whether/which username, prefix,
421 and password are configured for given url. Consults [auth] section.
421 and password are configured for given url. Consults [auth] section.
422
422
423 Returns tuple (username, password, prefix) containing elements
423 Returns tuple (username, password, prefix) containing elements
424 found. username and password can be None (if unset), if prefix
424 found. username and password can be None (if unset), if prefix
425 is not found, url itself is returned.
425 is not found, url itself is returned.
426 """
426 """
427 from mercurial.httpconnection import readauthforuri
427 from mercurial.httpconnection import readauthforuri
428 _debug(ui, _("Checking for hgrc info about url %s, user %s") % (base_url, user))
428 _debug(ui, _("Checking for hgrc info about url %s, user %s") % (base_url, user))
429 res = readauthforuri(ui, base_url, user)
429 res = readauthforuri(ui, base_url, user)
430 if res:
430 if res:
431 group, auth_token = res
431 group, auth_token = res
432 else:
432 else:
433 auth_token = None
433 auth_token = None
434
434
435 if auth_token:
435 if auth_token:
436 username = auth_token.get('username')
436 username = auth_token.get('username')
437 password = auth_token.get('password')
437 password = auth_token.get('password')
438 prefix = auth_token.get('prefix')
438 prefix = auth_token.get('prefix')
439 else:
439 else:
440 username = user
440 username = user
441 password = None
441 password = None
442 prefix = None
442 prefix = None
443
443
444 password_url = self.password_url(base_url, prefix)
444 password_url = self.password_url(base_url, prefix)
445
445
446 _debug(ui, _("Password url: %s, user: %s, password: %s (prefix: %s)") % (
446 _debug(ui, _("Password url: %s, user: %s, password: %s (prefix: %s)") % (
447 password_url, username, '********' if password else '', prefix))
447 password_url, username, '********' if password else '', prefix))
448
448
449 return username, password, password_url
449 return username, password, password_url
450
450
451 def _note_last_reply(self, realm, authuri, user, req):
451 def _note_last_reply(self, realm, authuri, user, req):
452 """
452 """
453 Internal helper. Saves info about auth-data obtained,
453 Internal helper. Saves info about auth-data obtained,
454 preserves them in last_reply, and returns pair user, pwd
454 preserves them in last_reply, and returns pair user, pwd
455 """
455 """
456 self.last_reply = dict(realm=realm, authuri=authuri,
456 self.last_reply = dict(realm=realm, authuri=authuri,
457 user=user, req=req)
457 user=user, req=req)
458
458
459 def _after_bad_auth(self, ui, realm, authuri, req):
459 def _after_bad_auth(self, ui, realm, authuri, req):
460 """
460 """
461 If we are called again just after identical previous
461 If we are called again just after identical previous
462 request, then the previously returned auth must have been
462 request, then the previously returned auth must have been
463 wrong. So we note this to force password prompt (and avoid
463 wrong. So we note this to force password prompt (and avoid
464 reusing bad password indefinitely).
464 reusing bad password indefinitely).
465
465
466 This routine checks for this condition.
466 This routine checks for this condition.
467 """
467 """
468 if self.last_reply:
468 if self.last_reply:
469 if (self.last_reply['realm'] == realm) \
469 if (self.last_reply['realm'] == realm) \
470 and (self.last_reply['authuri'] == authuri) \
470 and (self.last_reply['authuri'] == authuri) \
471 and (self.last_reply['req'] == req):
471 and (self.last_reply['req'] == req):
472 _debug(ui, _("Working after bad authentication, cached passwords not used %s") % str(self.last_reply))
472 _debug(ui, _("Working after bad authentication, cached passwords not used %s") % str(self.last_reply))
473 return True
473 return True
474 return False
474 return False
475
475
476 @staticmethod
476 @staticmethod
477 def password_url(base_url, prefix):
477 def password_url(base_url, prefix):
478 """Calculates actual url identifying the password. Takes
478 """Calculates actual url identifying the password. Takes
479 configured prefix under consideration (so can be shorter
479 configured prefix under consideration (so can be shorter
480 than repo url)"""
480 than repo url)"""
481 if not prefix or prefix == '*':
481 if not prefix or prefix == '*':
482 return base_url
482 return base_url
483 scheme, hostpath = base_url.split('://', 1)
483 scheme, hostpath = base_url.split('://', 1)
484 p = prefix.split('://', 1)
484 p = prefix.split('://', 1)
485 if len(p) > 1:
485 if len(p) > 1:
486 prefix_host_path = p[1]
486 prefix_host_path = p[1]
487 else:
487 else:
488 prefix_host_path = prefix
488 prefix_host_path = prefix
489 password_url = scheme + '://' + prefix_host_path
489 password_url = scheme + '://' + prefix_host_path
490 return password_url
490 return password_url
491
491
492 @staticmethod
492 @staticmethod
493 def unpack_url(authuri):
493 def unpack_url(authuri):
494 """
494 """
495 Does two things:
495 Takes original url for which authentication is attempted and:
496
496
497 1. Strips query params from url. Used to convert urls like
497 - Strips query params from url. Used to convert urls like
498 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
498 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
499 to
499 to
500 https://repo.machine.com/repos/apps/module
500 https://repo.machine.com/repos/apps/module
501
501
502 2. Extracts username and password, if present.
502 - Extracts username and password, if present, and removes them from url
503 (so prefix matching works properly)
503
504
504 Returns url, user, password
505 Returns url, user, password
505 """
506 """
506 # mercurial.util.url, rather handy url parser
507 # mercurial.util.url, rather handy url parser
507 parsed_url = util.url(authuri)
508 parsed_url = util.url(authuri)
508 parsed_url.query = ''
509 parsed_url.query = ''
509 parsed_url.fragment = None
510 parsed_url.fragment = None
510 # Strip arguments to get actual remote repository url.
511 # Strip arguments to get actual remote repository url.
511 # base_url = "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
512 # base_url = "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
512 # parsed_url.path)
513 # parsed_url.path)
514 user = parsed_url.user
515 passwd = parsed_url.passwd
516 parsed_url.user = None
517 parsed_url.passwd = None
513
518
514 return str(parsed_url), parsed_url.user, parsed_url.passwd
519 return str(parsed_url), user, passwd
515
520
516
521
517 ############################################################
522 ############################################################
518 # Mercurial monkey-patching
523 # Mercurial monkey-patching
519 ############################################################
524 ############################################################
520
525
521
526
522 @monkeypatch_method(passwordmgr)
527 @monkeypatch_method(passwordmgr)
523 def find_user_password(self, realm, authuri):
528 def find_user_password(self, realm, authuri):
524 """
529 """
525 keyring-based implementation of username/password query
530 keyring-based implementation of username/password query
526 for HTTP(S) connections
531 for HTTP(S) connections
527
532
528 Passwords are saved in gnome keyring, OSX/Chain or other platform
533 Passwords are saved in gnome keyring, OSX/Chain or other platform
529 specific storage and keyed by the repository url
534 specific storage and keyed by the repository url
530 """
535 """
531 # Extend object attributes
536 # Extend object attributes
532 if not hasattr(self, '_pwd_handler'):
537 if not hasattr(self, '_pwd_handler'):
533 self._pwd_handler = HTTPPasswordHandler()
538 self._pwd_handler = HTTPPasswordHandler()
534
539
535 if hasattr(self, '_http_req'):
540 if hasattr(self, '_http_req'):
536 req = self._http_req
541 req = self._http_req
537 else:
542 else:
538 req = None
543 req = None
539
544
540 return self._pwd_handler.find_auth(self, realm, authuri, req)
545 return self._pwd_handler.find_auth(self, realm, authuri, req)
541
546
542
547
543 @monkeypatch_method(urllib2.AbstractBasicAuthHandler, "http_error_auth_reqed")
548 @monkeypatch_method(urllib2.AbstractBasicAuthHandler, "http_error_auth_reqed")
544 def basic_http_error_auth_reqed(self, authreq, host, req, headers):
549 def basic_http_error_auth_reqed(self, authreq, host, req, headers):
545 """Preserves current HTTP request so it can be consulted
550 """Preserves current HTTP request so it can be consulted
546 in find_user_password above"""
551 in find_user_password above"""
547 self.passwd._http_req = req
552 self.passwd._http_req = req
548 try:
553 try:
549 return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
554 return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
550 finally:
555 finally:
551 self.passwd._http_req = None
556 self.passwd._http_req = None
552
557
553
558
554 @monkeypatch_method(urllib2.AbstractDigestAuthHandler, "http_error_auth_reqed")
559 @monkeypatch_method(urllib2.AbstractDigestAuthHandler, "http_error_auth_reqed")
555 def digest_http_error_auth_reqed(self, authreq, host, req, headers):
560 def digest_http_error_auth_reqed(self, authreq, host, req, headers):
556 """Preserves current HTTP request so it can be consulted
561 """Preserves current HTTP request so it can be consulted
557 in find_user_password above"""
562 in find_user_password above"""
558 self.passwd._http_req = req
563 self.passwd._http_req = req
559 try:
564 try:
560 return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
565 return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
561 finally:
566 finally:
562 self.passwd._http_req = None
567 self.passwd._http_req = None
563
568
564 ############################################################
569 ############################################################
565 # SMTP support
570 # SMTP support
566 ############################################################
571 ############################################################
567
572
568
573
569 def try_smtp_login(ui, smtp_obj, username, password):
574 def try_smtp_login(ui, smtp_obj, username, password):
570 """
575 """
571 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
576 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
572 password.
577 password.
573
578
574 Returns:
579 Returns:
575 - True if login succeeded
580 - True if login succeeded
576 - False if login failed due to the wrong credentials
581 - False if login failed due to the wrong credentials
577
582
578 Throws Abort exception if login failed for any other reason.
583 Throws Abort exception if login failed for any other reason.
579
584
580 Immediately returns False if password is empty
585 Immediately returns False if password is empty
581 """
586 """
582 if not password:
587 if not password:
583 return False
588 return False
584 try:
589 try:
585 ui.note(_('(authenticating to mail server as %s)\n') %
590 ui.note(_('(authenticating to mail server as %s)\n') %
586 (username))
591 (username))
587 smtp_obj.login(username, password)
592 smtp_obj.login(username, password)
588 return True
593 return True
589 except smtplib.SMTPException, inst:
594 except smtplib.SMTPException, inst:
590 if inst.smtp_code == 535:
595 if inst.smtp_code == 535:
591 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
596 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
592 return False
597 return False
593 else:
598 else:
594 raise util.Abort(inst)
599 raise util.Abort(inst)
595
600
596
601
597 def keyring_supported_smtp(ui, username):
602 def keyring_supported_smtp(ui, username):
598 """
603 """
599 keyring-integrated replacement for mercurial.mail._smtp
604 keyring-integrated replacement for mercurial.mail._smtp
600 Used only when configuration file contains username, but
605 Used only when configuration file contains username, but
601 does not contain the password.
606 does not contain the password.
602
607
603 Most of the routine below is copied as-is from
608 Most of the routine below is copied as-is from
604 mercurial.mail._smtp. The only changed part is
609 mercurial.mail._smtp. The only changed part is
605 marked with # >>>>> and # <<<<< markers
610 marked with # >>>>> and # <<<<< markers
606 """
611 """
607 local_hostname = ui.config('smtp', 'local_hostname')
612 local_hostname = ui.config('smtp', 'local_hostname')
608 tls = ui.config('smtp', 'tls', 'none')
613 tls = ui.config('smtp', 'tls', 'none')
609 # backward compatible: when tls = true, we use starttls.
614 # backward compatible: when tls = true, we use starttls.
610 starttls = tls == 'starttls' or util.parsebool(tls)
615 starttls = tls == 'starttls' or util.parsebool(tls)
611 smtps = tls == 'smtps'
616 smtps = tls == 'smtps'
612 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
617 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
613 raise util.Abort(_("can't use TLS: Python SSL support not installed"))
618 raise util.Abort(_("can't use TLS: Python SSL support not installed"))
614 mailhost = ui.config('smtp', 'host')
619 mailhost = ui.config('smtp', 'host')
615 if not mailhost:
620 if not mailhost:
616 raise util.Abort(_('smtp.host not configured - cannot send mail'))
621 raise util.Abort(_('smtp.host not configured - cannot send mail'))
617 verifycert = ui.config('smtp', 'verifycert', 'strict')
622 verifycert = ui.config('smtp', 'verifycert', 'strict')
618 if verifycert not in ['strict', 'loose']:
623 if verifycert not in ['strict', 'loose']:
619 if util.parsebool(verifycert) is not False:
624 if util.parsebool(verifycert) is not False:
620 raise util.Abort(_('invalid smtp.verifycert configuration: %s')
625 raise util.Abort(_('invalid smtp.verifycert configuration: %s')
621 % (verifycert))
626 % (verifycert))
622 verifycert = False
627 verifycert = False
623 if (starttls or smtps) and verifycert:
628 if (starttls or smtps) and verifycert:
624 sslkwargs = sslutil.sslkwargs(ui, mailhost)
629 sslkwargs = sslutil.sslkwargs(ui, mailhost)
625 else:
630 else:
626 sslkwargs = {}
631 sslkwargs = {}
627 if smtps:
632 if smtps:
628 ui.note(_('(using smtps)\n'))
633 ui.note(_('(using smtps)\n'))
629 s = SMTPS(sslkwargs, local_hostname=local_hostname)
634 s = SMTPS(sslkwargs, local_hostname=local_hostname)
630 elif starttls:
635 elif starttls:
631 s = STARTTLS(sslkwargs, local_hostname=local_hostname)
636 s = STARTTLS(sslkwargs, local_hostname=local_hostname)
632 else:
637 else:
633 s = smtplib.SMTP(local_hostname=local_hostname)
638 s = smtplib.SMTP(local_hostname=local_hostname)
634 if smtps:
639 if smtps:
635 defaultport = 465
640 defaultport = 465
636 else:
641 else:
637 defaultport = 25
642 defaultport = 25
638 mailport = util.getport(ui.config('smtp', 'port', defaultport))
643 mailport = util.getport(ui.config('smtp', 'port', defaultport))
639 ui.note(_('sending mail: smtp host %s, port %s\n') %
644 ui.note(_('sending mail: smtp host %s, port %s\n') %
640 (mailhost, mailport))
645 (mailhost, mailport))
641 s.connect(host=mailhost, port=mailport)
646 s.connect(host=mailhost, port=mailport)
642 if starttls:
647 if starttls:
643 ui.note(_('(using starttls)\n'))
648 ui.note(_('(using starttls)\n'))
644 s.ehlo()
649 s.ehlo()
645 s.starttls()
650 s.starttls()
646 s.ehlo()
651 s.ehlo()
647 if (starttls or smtps) and verifycert:
652 if (starttls or smtps) and verifycert:
648 ui.note(_('(verifying remote certificate)\n'))
653 ui.note(_('(verifying remote certificate)\n'))
649 sslutil.validator(ui, mailhost)(s.sock, verifycert == 'strict')
654 sslutil.validator(ui, mailhost)(s.sock, verifycert == 'strict')
650
655
651 # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
656 # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
652 stored = password = password_store.get_smtp_password(
657 stored = password = password_store.get_smtp_password(
653 mailhost, mailport, username)
658 mailhost, mailport, username)
654 # No need to check whether password was found as try_smtp_login
659 # No need to check whether password was found as try_smtp_login
655 # just returns False if it is absent.
660 # just returns False if it is absent.
656 while not try_smtp_login(ui, s, username, password):
661 while not try_smtp_login(ui, s, username, password):
657 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
662 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
658
663
659 if stored != password:
664 if stored != password:
660 password_store.set_smtp_password(
665 password_store.set_smtp_password(
661 mailhost, mailport, username, password)
666 mailhost, mailport, username, password)
662 # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
667 # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
663
668
664 def send(sender, recipients, msg):
669 def send(sender, recipients, msg):
665 try:
670 try:
666 return s.sendmail(sender, recipients, msg)
671 return s.sendmail(sender, recipients, msg)
667 except smtplib.SMTPRecipientsRefused, inst:
672 except smtplib.SMTPRecipientsRefused, inst:
668 recipients = [r[1] for r in inst.recipients.values()]
673 recipients = [r[1] for r in inst.recipients.values()]
669 raise util.Abort('\n' + '\n'.join(recipients))
674 raise util.Abort('\n' + '\n'.join(recipients))
670 except smtplib.SMTPException, inst:
675 except smtplib.SMTPException, inst:
671 raise util.Abort(inst)
676 raise util.Abort(inst)
672
677
673 return send
678 return send
674
679
675 ############################################################
680 ############################################################
676 # SMTP monkeypatching
681 # SMTP monkeypatching
677 ############################################################
682 ############################################################
678
683
679
684
680 @monkeypatch_method(mail)
685 @monkeypatch_method(mail)
681 def _smtp(ui):
686 def _smtp(ui):
682 """
687 """
683 build an smtp connection and return a function to send email
688 build an smtp connection and return a function to send email
684
689
685 This is the monkeypatched version of _smtp(ui) function from
690 This is the monkeypatched version of _smtp(ui) function from
686 mercurial/mail.py. It calls the original unless username
691 mercurial/mail.py. It calls the original unless username
687 without password is given in the configuration.
692 without password is given in the configuration.
688 """
693 """
689 username = ui.config('smtp', 'username')
694 username = ui.config('smtp', 'username')
690 password = ui.config('smtp', 'password')
695 password = ui.config('smtp', 'password')
691
696
692 if username and not password:
697 if username and not password:
693 return keyring_supported_smtp(ui, username)
698 return keyring_supported_smtp(ui, username)
694 else:
699 else:
695 return _smtp.orig(ui)
700 return _smtp.orig(ui)
696
701
697
702
698 ############################################################
703 ############################################################
699 # Custom commands
704 # Custom commands
700 ############################################################
705 ############################################################
701
706
702 _re_http_url = re.compile(r'^https?://')
707 _re_http_url = re.compile(r'^https?://')
703
708
704 def is_http_path(url):
709 def is_http_path(url):
705 return bool(_re_http_url.search(url))
710 return bool(_re_http_url.search(url))
706
711
707
712
708 def cmd_keyring_check(ui, repo, *path_args, **opts): # pylint: disable=unused-argument
713 def cmd_keyring_check(ui, repo, *path_args, **opts): # pylint: disable=unused-argument
709 """
714 """
710 Prints basic info (whether password is currently saved, and how is
715 Prints basic info (whether password is currently saved, and how is
711 it identified) for given path or for all defined repo paths which are HTTP.
716 it identified) for given path or for all defined repo paths which are HTTP.
712 """
717 """
713 defined_paths = [(name, url)
718 defined_paths = [(name, url)
714 for name, url in ui.configitems('paths')]
719 for name, url in ui.configitems('paths')]
715 if path_args:
720 if path_args:
716 # Maybe parameter is an alias
721 # Maybe parameter is an alias
717 defined_paths_dic = dict(defined_paths)
722 defined_paths_dic = dict(defined_paths)
718 paths = [(path_arg, defined_paths_dic.get(path_arg, path_arg))
723 paths = [(path_arg, defined_paths_dic.get(path_arg, path_arg))
719 for path_arg in path_args]
724 for path_arg in path_args]
720 else:
725 else:
721 paths = [(name, url) for name, url in defined_paths]
726 paths = [(name, url) for name, url in defined_paths]
722
727
723 if not paths:
728 if not paths:
724 ui.status(_("keyring_check: no paths defined\n"))
729 ui.status(_("keyring_check: no paths defined\n"))
725
730
726 handler = HTTPPasswordHandler()
731 handler = HTTPPasswordHandler()
727
732
728 ui.status(_("keyring password save status:\n"))
733 ui.status(_("keyring password save status:\n"))
729 for name, url in paths:
734 for name, url in paths:
730 if not is_http_path(url):
735 if not is_http_path(url):
731 if path_args:
736 if path_args:
732 ui.status(_(" %s: non-http path (%s)\n") % (name, url))
737 ui.status(_(" %s: non-http path (%s)\n") % (name, url))
733 continue
738 continue
734 user, pwd, source, final_url = handler.get_credentials(
739 user, pwd, source, final_url = handler.get_credentials(
735 passwordmgr(ui), name, url)
740 passwordmgr(ui), name, url)
736 if pwd:
741 if pwd:
737 ui.status(_(" %s: password available, source: %s, bound to user %s, url %s\n") % (
742 ui.status(_(" %s: password available, source: %s, bound to user %s, url %s\n") % (
738 name, source, user, final_url))
743 name, source, user, final_url))
739 elif user:
744 elif user:
740 ui.status(_(" %s: password not available, once entered, will be bound to user %s, url %s\n") % (
745 ui.status(_(" %s: password not available, once entered, will be bound to user %s, url %s\n") % (
741 name, user, final_url))
746 name, user, final_url))
742 else:
747 else:
743 ui.status(_(" %s: password not available, user unknown, url %s\n") % (
748 ui.status(_(" %s: password not available, user unknown, url %s\n") % (
744 name, final_url))
749 name, final_url))
745
750
746
751
747 def cmd_keyring_clear(ui, repo, path, **opts): # pylint: disable=unused-argument
752 def cmd_keyring_clear(ui, repo, path, **opts): # pylint: disable=unused-argument
748 """
753 """
749 Drops password bound to given path (if any is saved).
754 Drops password bound to given path (if any is saved).
750 """
755 """
751 path_url = path
756 path_url = path
752 for name, url in ui.configitems('paths'):
757 for name, url in ui.configitems('paths'):
753 if name == path:
758 if name == path:
754 path_url = url
759 path_url = url
755 break
760 break
756 if not is_http_path(path_url):
761 if not is_http_path(path_url):
757 ui.warn(_("%s is not a http path (%s)") % (path, path_url))
762 ui.warn(_("%s is not a http path (%s)") % (path, path_url))
758 return
763 return
759
764
760 handler = HTTPPasswordHandler()
765 handler = HTTPPasswordHandler()
761
766
762 user, pwd, source, final_url = handler.get_credentials(
767 user, pwd, source, final_url = handler.get_credentials(
763 passwordmgr(ui), path, path_url)
768 passwordmgr(ui), path, path_url)
764 if not user:
769 if not user:
765 ui.status(_("Username not configured for url %s\n") % final_url)
770 ui.status(_("Username not configured for url %s\n") % final_url)
766 return
771 return
767 if not pwd:
772 if not pwd:
768 ui.status(_("No password is saved for user %s, url %s\n") % (
773 ui.status(_("No password is saved for user %s, url %s\n") % (
769 user, final_url))
774 user, final_url))
770 return
775 return
771
776
772 if source != handler.SRC_KEYRING:
777 if source != handler.SRC_KEYRING:
773 ui.status(_("Password for user %s, url %s is saved in %s, not in keyring\n") % (
778 ui.status(_("Password for user %s, url %s is saved in %s, not in keyring\n") % (
774 user, final_url, source))
779 user, final_url, source))
775
780
776 password_store.clear_http_password(final_url, user)
781 password_store.clear_http_password(final_url, user)
777 ui.status(_("Password removed for user %s, url %s\n") % (
782 ui.status(_("Password removed for user %s, url %s\n") % (
778 user, final_url))
783 user, final_url))
779
784
780
785
781 cmdtable = {
786 cmdtable = {
782 "keyring_check": (
787 "keyring_check": (
783 cmd_keyring_check,
788 cmd_keyring_check,
784 [],
789 [],
785 "keyring_check [PATH]",
790 "keyring_check [PATH]",
786 ),
791 ),
787 "keyring_clear": (
792 "keyring_clear": (
788 cmd_keyring_clear,
793 cmd_keyring_clear,
789 [],
794 [],
790 "keyring_check PATH",
795 "keyring_check PATH",
791 ),
796 ),
792 }
797 }
General Comments 0
You need to be logged in to leave comments. Login now