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