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