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