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