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