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