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