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