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