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