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