##// END OF EJS Templates
merge
Marcin Kasperski -
r173:817841ec merge default
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (1256 lines changed) Show them Hide them
@@ -1,611 +1,645 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 authentication details
32 ''' securely save HTTP and SMTP authentication details
33 mercurial_keyring is a Mercurial extension used to securely save
33 mercurial_keyring is a Mercurial extension used to securely save
34 HTTP and SMTP authentication passwords in password databases (Gnome
34 HTTP and SMTP authentication passwords in password databases (Gnome
35 Keyring, KDE KWallet, OSXKeyChain, specific solutions for Win32 and
35 Keyring, KDE KWallet, OSXKeyChain, specific solutions for Win32 and
36 command line). This extension uses and wraps services of the keyring
36 command line). This extension uses and wraps services of the keyring
37 library.
37 library.
38 '''
38 '''
39
39
40 from mercurial import util, sslutil
40 from mercurial import util, sslutil
41 from mercurial.i18n import _
41 from mercurial.i18n import _
42 try:
42 try:
43 from mercurial.url import passwordmgr
43 from mercurial.url import passwordmgr
44 except:
44 except:
45 from mercurial.httprepo import passwordmgr
45 from mercurial.httprepo import passwordmgr
46 from mercurial import mail
46 from mercurial import mail
47 from mercurial.mail import SMTPS, STARTTLS
47 from mercurial.mail import SMTPS, STARTTLS
48 from mercurial import encoding
48 from mercurial import encoding
49
49
50 from urlparse import urlparse
50 from urlparse import urlparse
51 import urllib2
51 import urllib2
52 import smtplib
52 import smtplib
53 import socket
53 import socket
54 import os
54 import os
55 import sys
55 import sys
56
56
57 def import_meu():
57 # pylint:disable=invalid-name
58 """Imports and returns mercurial_extension_utils module.
58
59 Workaround for TortoiseHg/Win environment, where this module
59 ###########################################################################
60 can'b be easily put on PYTHONPATH"""
60 # Specific import trickery
61 try:
61 ###########################################################################
62 import mercurial_extension_utils
62
63 except ImportError:
63
64 my_dir = os.path.dirname(__file__)
64 def import_meu():
65 sys.path.extend([
65 """
66 # In the same dir (manual or site-packages after pip)
66 Convoluted import of mercurial_extension_utils, which helps
67 my_dir,
67 TortoiseHg/Win setups. This routine and it's use below
68 # Developer clone
68 performs equivalent of
69 os.path.join(os.path.dirname(my_dir), "extension_utils"),
69 from mercurial_extension_utils import monkeypatch_method
70 # Side clone
70 but looks for some non-path directories.
71 os.path.join(os.path.dirname(my_dir), "mercurial-extension_utils"),
71 """
72 ])
72 try:
73 try:
73 import mercurial_extension_utils
74 import mercurial_extension_utils
74 except ImportError:
75 except ImportError:
75 my_dir = os.path.dirname(__file__)
76 raise util.Abort(_("""Can not import mercurial_extension_utils.
76 sys.path.extend([
77 Please install this module in Python path.
77 # In the same dir (manual or site-packages after pip)
78 See Installation chapter in https://bitbucket.org/Mekk/mercurial-dynamic_username/ for details
78 my_dir,
79 (and for info about TortoiseHG on Windows, or other bundled Python)."""))
79 # Developer clone
80 return mercurial_extension_utils
80 os.path.join(os.path.dirname(my_dir), "extension_utils"),
81
81 # Side clone
82 meu = import_meu()
82 os.path.join(os.path.dirname(my_dir), "mercurial-extension_utils"),
83 monkeypatch_method = meu.monkeypatch_method
83 ])
84
84 try:
85
85 import mercurial_extension_utils
86 def import_keyring():
86 except ImportError:
87 """
87 raise util.Abort(_("""Can not import mercurial_extension_utils.
88 Importing keyring happens to be costly if wallet is slow, so we delay it
88 Please install this module in Python path.
89 until really needed.
89 See Installation chapter in https://bitbucket.org/Mekk/mercurial-dynamic_username/ for details
90 """
90 (and for info about TortoiseHG on Windows, or other bundled Python)."""))
91 if 'keyring' in sys.modules:
91 return mercurial_extension_utils
92 return sys.modules['keyring']
92
93 # mercurial.demandimport incompatibility workaround.
93 meu = import_meu()
94 # various keyring backends fail as they can't properly import helper
94 monkeypatch_method = meu.monkeypatch_method
95 # modules (as demandimport modifies python import behaviour).
95
96 # If you get import errors with demandimport in backtrace, try
96
97 # guessing what to block and extending the list below.
97 def import_keyring():
98 from mercurial import demandimport
98 """
99 for blocked_module in [
99 Importing keyring happens to be costly if wallet is slow, so we delay it
100 "gobject._gobject",
100 until it is really needed. The routine below also works around various
101 "configparser",
101 demandimport-related problems.
102 "json",
102 """
103 "abc",
103 if 'keyring' in sys.modules:
104 "io",
104 return sys.modules['keyring']
105 "keyring",
105 # mercurial.demandimport incompatibility workaround.
106 "gdata.docs.service",
106 # various keyring backends fail as they can't properly import helper
107 "gdata.service",
107 # modules (as demandimport modifies python import behaviour).
108 "types",
108 # If you get import errors with demandimport in backtrace, try
109 "atom.http",
109 # guessing what to block and extending the list below.
110 "atom.http_interface",
110 from mercurial import demandimport
111 "atom.service",
111 for blocked_module in [
112 "atom.token_store",
112 "gobject._gobject",
113 "ctypes",
113 "configparser",
114 "secretstorage.exceptions",
114 "json",
115 "fs.opener",
115 "abc",
116 ]:
116 "io",
117 if blocked_module not in demandimport.ignore:
117 "keyring",
118 demandimport.ignore.append(blocked_module)
118 "gdata.docs.service",
119 try:
119 "gdata.service",
120 is_demandimport_enabled = demandimport.isenabled
120 "types",
121 except AttributeError:
121 "atom.http",
122 # Mercurial < 2.9.1
122 "atom.http_interface",
123 try:
123 "atom.service",
124 orig_demandimport = demandimport.demandimport
124 "atom.token_store",
125 except AttributeError:
125 "ctypes",
126 orig_demandimport = demandimport._demandimport
126 "secretstorage.exceptions",
127 def is_demandimport_enabled():
127 "fs.opener",
128 """Checks whether demandimport is enabled at the moment"""
128 ]:
129 return __import__ == orig_demandimport
129 if blocked_module not in demandimport.ignore:
130
130 demandimport.ignore.append(blocked_module)
131 # Shut up warning about uninitialized logging for new keyring versions.
131
132 # But beware 2.6…
132 # Various attempts to define is_demandimport_enabled
133 try:
133 try:
134 import logging
134 is_demandimport_enabled = demandimport.isenabled
135 logging.getLogger("keyring").addHandler(logging.NullHandler())
135 except AttributeError:
136 except:
136 # Mercurial < 2.9.1
137 pass
137 try:
138
138 orig_demandimport = demandimport.demandimport
139 # Temporarily disable demandimport to make the need of extending
139 except AttributeError:
140 # the list above less likely.
140 orig_demandimport = demandimport._demandimport
141 if is_demandimport_enabled():
141
142 demandimport.disable()
142 def is_demandimport_enabled():
143 try:
143 """Checks whether demandimport is enabled at the moment"""
144 import keyring
144 return __import__ == orig_demandimport
145 finally:
145
146 demandimport.enable()
146 # Shut up warning about uninitialized logging for new keyring versions.
147 else:
147 # But beware 2.6…
148 import keyring
148 try:
149 return keyring
149 import logging
150
150 logging.getLogger("keyring").addHandler(logging.NullHandler())
151 KEYRING_SERVICE = "Mercurial"
151 except:
152
152 pass
153 ############################################################
153
154
154 # Temporarily disable demandimport to make the need of extending
155 class PasswordStore(object):
155 # the list above less likely.
156 """
156 if is_demandimport_enabled():
157 Helper object handling keyring usage (password save&restore,
157 demandimport.disable()
158 the way passwords are keyed in the keyring).
158 try:
159 """
159 import keyring
160 def __init__(self):
160 finally:
161 self.cache = dict()
161 demandimport.enable()
162
162 else:
163 def get_http_password(self, url, username):
163 import keyring
164 """
164 return keyring
165 Checks whether password of username for url is available,
165
166 returns it or None
166 #################################################################
167 """
167 # Actual implementation
168 return self._read_password_from_keyring(
168 #################################################################
169 self._format_http_key(url, username))
169
170
170 KEYRING_SERVICE = "Mercurial"
171 def set_http_password(self, url, username, password):
171
172 """Saves password to keyring"""
172
173 self._save_password_to_keyring(
173 class PasswordStore(object):
174 self._format_http_key(url, username),
174 """
175 password)
175 Helper object handling keyring usage (password save&restore,
176
176 the way passwords are keyed in the keyring).
177 def clear_http_password(self, url, username):
177 """
178 """Drops saved password"""
178 def __init__(self):
179 self.set_http_password(url, username, "")
179 self.cache = dict()
180
180
181 def _format_http_key(self, url, username):
181 def get_http_password(self, url, username):
182 """Construct actual key for password identification"""
182 """
183 return "%s@@%s" % (username, url)
183 Checks whether password of username for url is available,
184
184 returns it or None
185 def get_smtp_password(self, machine, port, username):
185 """
186 """Checks for SMTP password in keyring, returns
186 return self._read_password_from_keyring(
187 password or None"""
187 self._format_http_key(url, username))
188 return self._read_password_from_keyring(
188
189 self._format_smtp_key(machine, port, username))
189 def set_http_password(self, url, username, password):
190
190 """Saves password to keyring"""
191 def set_smtp_password(self, machine, port, username, password):
191 self._save_password_to_keyring(
192 """Saves SMTP password to keyring"""
192 self._format_http_key(url, username),
193 self._save_password_to_keyring(
193 password)
194 self._format_smtp_key(machine, port, username),
194
195 password)
195 def clear_http_password(self, url, username):
196
196 """Drops saved password"""
197 def clear_smtp_password(self, machine, port, username):
197 self.set_http_password(url, username, "")
198 """Drops saved SMTP password"""
198
199 self.set_smtp_password(machine, port, username, "")
199 def _format_http_key(self, url, username):
200
200 """Construct actual key for password identification"""
201 def _format_smtp_key(self, machine, port, username):
201 return "%s@@%s" % (username, url)
202 """Construct key for SMTP password identification"""
202
203 return "%s@@%s:%s" % (username, machine, str(port))
203 def get_smtp_password(self, machine, port, username):
204
204 """Checks for SMTP password in keyring, returns
205 def _read_password_from_keyring(self, pwdkey):
205 password or None"""
206 """Physically read from keyring"""
206 return self._read_password_from_keyring(
207 keyring = import_keyring()
207 self._format_smtp_key(machine, port, username))
208 password = keyring.get_password(KEYRING_SERVICE, pwdkey)
208
209 # Reverse recoding from next routine
209 def set_smtp_password(self, machine, port, username, password):
210 if isinstance(password, unicode):
210 """Saves SMTP password to keyring"""
211 return encoding.tolocal(password.encode('utf-8'))
211 self._save_password_to_keyring(
212 return password
212 self._format_smtp_key(machine, port, username),
213
213 password)
214 def _save_password_to_keyring(self, pwdkey, password):
214
215 """Physically write to keyring"""
215 def clear_smtp_password(self, machine, port, username):
216 keyring = import_keyring()
216 """Drops saved SMTP password"""
217 # keyring in general expects unicode.
217 self.set_smtp_password(machine, port, username, "")
218 # Mercurial provides "local" encoding. See #33
218
219 password = encoding.fromlocal(password).decode('utf-8')
219 def _format_smtp_key(self, machine, port, username):
220 keyring.set_password(
220 """Construct key for SMTP password identification"""
221 KEYRING_SERVICE, pwdkey, password)
221 return "%s@@%s:%s" % (username, machine, str(port))
222
222
223 password_store = PasswordStore()
223 def _read_password_from_keyring(self, pwdkey):
224
224 """Physically read from keyring"""
225 ############################################################
225 keyring = import_keyring()
226
226 password = keyring.get_password(KEYRING_SERVICE, pwdkey)
227 def _debug(ui, msg):
227 # Reverse recoding from next routine
228 ui.debug("keyring: " + msg + "\n")
228 if isinstance(password, unicode):
229
229 return encoding.tolocal(password.encode('utf-8'))
230 def _debug_reply(ui, msg, url, user, pwd):
230 return password
231 _debug(ui, "%s. Url: %s, user: %s, passwd: %s" % (
231
232 msg, url, user, pwd and '*' * len(pwd) or 'not set'))
232 def _save_password_to_keyring(self, pwdkey, password):
233
233 """Physically write to keyring"""
234
234 keyring = import_keyring()
235 ############################################################
235 # keyring in general expects unicode.
236
236 # Mercurial provides "local" encoding. See #33
237 class HTTPPasswordHandler(object):
237 password = encoding.fromlocal(password).decode('utf-8')
238 """
238 keyring.set_password(
239 Actual implementation of password handling (user prompting,
239 KEYRING_SERVICE, pwdkey, password)
240 configuration file searching, keyring save&restore).
240
241
241 password_store = PasswordStore()
242 Object of this class is bound as passwordmgr attribute.
242
243 """
243
244 def __init__(self):
244 ############################################################
245 self.pwd_cache = {}
245 # Tiny utils
246 self.last_reply = None
246 ############################################################
247
247
248 def find_auth(self, pwmgr, realm, authuri, req):
248 def _debug(ui, msg):
249 """
249 """Generic debug message"""
250 Actual implementation of find_user_password - different
250 ui.debug("keyring: " + msg + "\n")
251 ways of obtaining the username and password.
251
252 """
252
253 ui = pwmgr.ui
253 def _debug_reply(ui, msg, url, user, pwd):
254
254 """Debugging used to note info about data given"""
255 # If we are called again just after identical previous
255 _debug(ui, "%s. Url: %s, user: %s, passwd: %s" % (
256 # request, then the previously returned auth must have been
256 msg, url, user, pwd and '*' * len(pwd) or 'not set'))
257 # wrong. So we note this to force password prompt (and avoid
257
258 # reusing bad password indifinitely).
258
259 after_bad_auth = (self.last_reply \
259 ############################################################
260 and (self.last_reply['realm'] == realm) \
260 # Mercurial modifications
261 and (self.last_reply['authuri'] == authuri) \
261 ############################################################
262 and (self.last_reply['req'] == req))
262
263 if after_bad_auth:
263
264 _debug(ui, _("Working after bad authentication, cached passwords not used %s") % str(self.last_reply))
264 class HTTPPasswordHandler(object):
265
265 """
266 # Strip arguments to get actual remote repository url.
266 Actual implementation of password handling (user prompting,
267 base_url = self.canonical_url(authuri)
267 configuration file searching, keyring save&restore).
268
268
269 # Extracting possible username (or password)
269 Object of this class is bound as passwordmgr attribute.
270 # stored directly in repository url
270 """
271 user, pwd = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
271 def __init__(self):
272 pwmgr, realm, authuri)
272 self.pwd_cache = {}
273 if user and pwd:
273 self.last_reply = None
274 _debug_reply(ui, _("Auth data found in repository URL"),
274
275 base_url, user, pwd)
275 def _return_pwd(self, ui, msg, base_url, realm, authuri, user, req, pwd):
276 self.last_reply = dict(
276 """
277 realm=realm, authuri=authuri, user=user,req=req)
277 Internal helper. Saves info about auth-data obtained, preserves them in last_reply
278 return user, pwd
278 and returns pair user,pwd
279
279 """
280 # Loading .hg/hgrc [auth] section contents. If prefix is given,
280 _debug_reply(ui, _(msg), base_url, user, pwd)
281 # it will be used as a key to lookup password in the keyring.
281 self.last_reply = dict(realm=realm, authuri=authuri,
282 auth_user, pwd, prefix_url = self.load_hgrc_auth(ui, base_url, user)
282 user=user, req=req)
283 if prefix_url:
283 return user, pwd
284 keyring_url = prefix_url
284
285 else:
285 def find_auth(self, pwmgr, realm, authuri, req):
286 keyring_url = base_url
286 """
287 _debug(ui, _("Keyring URL: %s") % keyring_url)
287 Actual implementation of find_user_password - different
288
288 ways of obtaining the username and password.
289 # Checking the memory cache (there may be many http calls per command)
289 """
290 cache_key = (realm, keyring_url)
290 ui = pwmgr.ui
291 if not after_bad_auth:
291
292 cached_auth = self.pwd_cache.get(cache_key)
292 # If we are called again just after identical previous
293 if cached_auth:
293 # request, then the previously returned auth must have been
294 user, pwd = cached_auth
294 # wrong. So we note this to force password prompt (and avoid
295 _debug_reply(ui, _("Cached auth data found"),
295 # reusing bad password indifinitely).
296 base_url, user, pwd)
296 after_bad_auth = (self.last_reply
297 self.last_reply = dict(realm=realm,authuri=authuri,user=user,req=req)
297 and (self.last_reply['realm'] == realm)
298 return user, pwd
298 and (self.last_reply['authuri'] == authuri)
299
299 and (self.last_reply['req'] == req))
300 if auth_user:
300 if after_bad_auth:
301 if user and (user != auth_user):
301 _debug(ui, _("Working after bad authentication, cached passwords not used %s") % str(self.last_reply))
302 raise util.Abort(_('mercurial_keyring: username for %s specified both in repository path (%s) and in .hg/hgrc/[auth] (%s). Please, leave only one of those' % (base_url, user, auth_user)))
302
303 user = auth_user
303 # Strip arguments to get actual remote repository url.
304 if pwd:
304 base_url = self.canonical_url(authuri)
305 self.pwd_cache[cache_key] = user, pwd
305
306 _debug_reply(ui, _("Auth data set in .hg/hgrc"),
306 # Extracting possible username (or password)
307 base_url, user, pwd)
307 # stored directly in repository url
308 self.last_reply = dict(realm=realm,authuri=authuri,user=user,req=req)
308 user, pwd = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
309 return user, pwd
309 pwmgr, realm, authuri)
310 else:
310 if user and pwd:
311 _debug(ui, _("Username found in .hg/hgrc: %s") % user)
311 return self._return_pwd(ui, "Auth data present in repository URL", base_url,
312
312 realm, authuri, user, req, pwd)
313 # Loading password from keyring.
313
314 # Only if username is known (so we know the key) and we are
314 # Loading .hg/hgrc [auth] section contents. If prefix is given,
315 # not after failure (so we don't reuse the bad password).
315 # it will be used as a key to lookup password in the keyring.
316 if user and not after_bad_auth:
316 auth_user, pwd, prefix_url = self.load_hgrc_auth(ui, base_url, user)
317 _debug(ui, _("Looking for password for user %s and url %s") % (user, keyring_url))
317 if prefix_url:
318 pwd = password_store.get_http_password(keyring_url, user)
318 keyring_url = prefix_url
319 if pwd:
319 else:
320 self.pwd_cache[cache_key] = user, pwd
320 keyring_url = base_url
321 _debug_reply(ui, _("Keyring password found"),
321 _debug(ui, _("Keyring URL: %s") % keyring_url)
322 base_url, user, pwd)
322
323 self.last_reply = dict(realm=realm,authuri=authuri,user=user,req=req)
323 # Checking the memory cache (there may be many http calls per command)
324 return user, pwd
324 cache_key = (realm, keyring_url)
325 else:
325 if not after_bad_auth:
326 _debug(ui, _("Password not present in the keyring"))
326 cached_auth = self.pwd_cache.get(cache_key)
327
327 if cached_auth:
328 # Is the username permanently set?
328 user, pwd = cached_auth
329 fixed_user = (user and True or False)
329 return self._return_pwd(ui, "Cached auth data found", base_url,
330
330 realm, authuri, user, req, pwd)
331 # Last resort: interactive prompt
331
332 if not ui.interactive():
332 if auth_user:
333 raise util.Abort(_('mercurial_keyring: http authorization required but program used in non-interactive mode'))
333 if user and (user != auth_user):
334
334 raise util.Abort(_('mercurial_keyring: username for %s specified both in repository path (%s) and in .hg/hgrc/[auth] (%s). Please, leave only one of those' % (base_url, user, auth_user)))
335 if not fixed_user:
335 user = auth_user
336 ui.status(_("Username not specified in .hg/hgrc. Keyring will not be used.\n"))
336 if pwd:
337
337 self.pwd_cache[cache_key] = user, pwd
338 ui.write(_("http authorization required\n"))
338 return self._return_pwd(ui, "Auth data set in .hg/hgrc", base_url,
339 ui.status(_("realm: %s\n") % realm)
339 realm, authuri, user, req, pwd)
340 if fixed_user:
340 else:
341 ui.write(_("user: %s (fixed in .hg/hgrc)\n" % user))
341 _debug(ui, _("Username found in .hg/hgrc: %s") % user)
342 else:
342
343 user = ui.prompt(_("user:"), default=None)
343 # Loading password from keyring.
344 pwd = ui.getpass(_("password: "))
344 # Only if username is known (so we know the key) and we are
345
345 # not after failure (so we don't reuse the bad password).
346 if fixed_user:
346 if user and not after_bad_auth:
347 # Saving password to the keyring.
347 _debug(ui, _("Looking for password for user %s and url %s") % (user, keyring_url))
348 # It is done only if username is permanently set.
348 pwd = password_store.get_http_password(keyring_url, user)
349 # Otherwise we won't be able to find the password so it
349 if pwd:
350 # does not make much sense to preserve it
350 self.pwd_cache[cache_key] = user, pwd
351 _debug(ui, _("Saving password for %s to keyring") % user)
351 return self._return_pwd(ui, "Password found in keyring", base_url,
352 password_store.set_http_password(keyring_url, user, pwd)
352 realm, authuri, user, req, pwd)
353
353 else:
354 # Saving password to the memory cache
354 _debug(ui, _("Password not present in the keyring"))
355 self.pwd_cache[cache_key] = user, pwd
355
356
356 # Is the username permanently set?
357 _debug_reply(ui, _("Manually entered password"),
357 fixed_user = (user and True or False)
358 base_url, user, pwd)
358
359 self.last_reply = dict(realm=realm,authuri=authuri,user=user,req=req)
359 # Last resort: interactive prompt
360 return user, pwd
360 if not ui.interactive():
361
361 raise util.Abort(_('mercurial_keyring: http authorization required but program used in non-interactive mode'))
362 def load_hgrc_auth(self, ui, base_url, user):
362
363 """
363 if not fixed_user:
364 Loading [auth] section contents from local .hgrc
364 ui.status(_("Username not specified in .hg/hgrc. Keyring will not be used.\n"))
365
365
366 Returns (username, password, prefix) tuple (every
366 ui.write(_("http authorization required\n"))
367 element can be None)
367 ui.status(_("realm: %s\n") % realm)
368 """
368 if fixed_user:
369 # Theoretically 3 lines below should do:
369 ui.write(_("user: %s (fixed in .hg/hgrc)\n" % user))
370
370 else:
371 #auth_token = self.readauthtoken(base_url)
371 user = ui.prompt(_("user:"), default=None)
372 #if auth_token:
372 pwd = ui.getpass(_("password: "))
373 # user, pwd = auth.get('username'), auth.get('password')
373
374
374 if fixed_user:
375 # Unfortunately they do not work, readauthtoken always return
375 # Saving password to the keyring.
376 # None. Why? Because ui (self.ui of passwordmgr) describes the
376 # It is done only if username is permanently set.
377 # *remote* repository, so does *not* contain any option from
377 # Otherwise we won't be able to find the password so it
378 # local .hg/hgrc.
378 # does not make much sense to preserve it
379
379 _debug(ui, _("Saving password for %s to keyring") % user)
380 # TODO: mercurial 1.4.2 is claimed to resolve this problem
380 password_store.set_http_password(keyring_url, user, pwd)
381 # (thanks to: http://hg.xavamedia.nl/mercurial/crew/rev/fb45c1e4396f)
381
382 # so since this version workaround implemented below should
382 # Saving password to the memory cache
383 # not be necessary. As it will take some time until people
383 self.pwd_cache[cache_key] = user, pwd
384 # migrate to >= 1.4.2, it would be best to implement
384
385 # workaround conditionally.
385 return self._return_pwd(ui, "Manually entered password", base_url,
386
386 realm, authuri, user, req, pwd)
387 # Workaround: we recreate the repository object
387
388 repo_root = ui.config("bundle", "mainreporoot")
388 def load_hgrc_auth(self, ui, base_url, user):
389
389 """
390 from mercurial.ui import ui as _ui
390 Loading [auth] section contents from local .hgrc
391 local_ui = _ui(ui)
391
392 if repo_root:
392 Returns (username, password, prefix) tuple (every
393 local_ui.readconfig(os.path.join(repo_root, ".hg", "hgrc"))
393 element can be None)
394 try:
394 """
395 local_passwordmgr = passwordmgr(local_ui)
395 # Theoretically 3 lines below should do:
396 auth_token = local_passwordmgr.readauthtoken(base_url)
396 # auth_token = self.readauthtoken(base_url)
397 except AttributeError:
397 # if auth_token:
398 try:
398 # user, pwd = auth.get('username'), auth.get('password')
399 # hg 1.8
399 # Unfortunately they do not work, readauthtoken always return
400 import mercurial.url
400 # None. Why? Because ui (self.ui of passwordmgr) describes the
401 readauthforuri = mercurial.url.readauthforuri
401 # *remote* repository, so does *not* contain any option from
402 except (ImportError, AttributeError):
402 # local .hg/hgrc.
403 # hg 1.9
403
404 import mercurial.httpconnection
404 # TODO: mercurial 1.4.2 is claimed to resolve this problem
405 readauthforuri = mercurial.httpconnection.readauthforuri
405 # (thanks to: http://hg.xavamedia.nl/mercurial/crew/rev/fb45c1e4396f)
406 if readauthforuri.func_code.co_argcount == 3:
406 # so since this version workaround implemented below should
407 # Since hg.0593e8f81c71
407 # not be necessary. As it will take some time until people
408 res = readauthforuri(local_ui, base_url, user)
408 # migrate to >= 1.4.2, it would be best to implement
409 else:
409 # workaround conditionally.
410 res = readauthforuri(local_ui, base_url)
410
411 if res:
411 # Workaround: we recreate the repository object
412 group, auth_token = res
412 repo_root = ui.config("bundle", "mainreporoot")
413 else:
413
414 auth_token = None
414 from mercurial.ui import ui as _ui
415 if auth_token:
415 local_ui = _ui(ui)
416 username = auth_token.get('username')
416 if repo_root:
417 password = auth_token.get('password')
417 local_ui.readconfig(os.path.join(repo_root, ".hg", "hgrc"))
418 prefix = auth_token.get('prefix')
418 try:
419 shortest_url = self.shortest_url(base_url, prefix)
419 local_passwordmgr = passwordmgr(local_ui)
420 return username, password, shortest_url
420 auth_token = local_passwordmgr.readauthtoken(base_url)
421
421 except AttributeError:
422 return None, None, None
422 try:
423
423 # hg 1.8
424 def shortest_url(self, base_url, prefix):
424 import mercurial.url
425 if not prefix or prefix == '*':
425 readauthforuri = mercurial.url.readauthforuri
426 return base_url
426 except (ImportError, AttributeError):
427 scheme, hostpath = base_url.split('://', 1)
427 # hg 1.9
428 p = prefix.split('://', 1)
428 import mercurial.httpconnection
429 if len(p) > 1:
429 readauthforuri = mercurial.httpconnection.readauthforuri
430 prefix_host_path = p[1]
430 if readauthforuri.func_code.co_argcount == 3:
431 else:
431 # Since hg.0593e8f81c71
432 prefix_host_path = prefix
432 res = readauthforuri(local_ui, base_url, user)
433 shortest_url = scheme + '://' + prefix_host_path
433 else:
434 return shortest_url
434 res = readauthforuri(local_ui, base_url)
435
435 if res:
436 def canonical_url(self, authuri):
436 group, auth_token = res
437 """
437 else:
438 Strips query params from url. Used to convert urls like
438 auth_token = None
439 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
439 if auth_token:
440 to
440 username = auth_token.get('username')
441 https://repo.machine.com/repos/apps/module
441 password = auth_token.get('password')
442 """
442 prefix = auth_token.get('prefix')
443 parsed_url = urlparse(authuri)
443 shortest_url = self.shortest_url(base_url, prefix)
444 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
444 return username, password, shortest_url
445 parsed_url.path)
445
446
446 return None, None, None
447 ############################################################
447
448
448 def shortest_url(self, base_url, prefix):
449 @monkeypatch_method(passwordmgr)
449 if not prefix or prefix == '*':
450 def find_user_password(self, realm, authuri):
450 return base_url
451 """
451 scheme, hostpath = base_url.split('://', 1)
452 keyring-based implementation of username/password query
452 p = prefix.split('://', 1)
453 for HTTP(S) connections
453 if len(p) > 1:
454
454 prefix_host_path = p[1]
455 Passwords are saved in gnome keyring, OSX/Chain or other platform
455 else:
456 specific storage and keyed by the repository url
456 prefix_host_path = prefix
457 """
457 shortest_url = scheme + '://' + prefix_host_path
458 # Extend object attributes
458 return shortest_url
459 if not hasattr(self, '_pwd_handler'):
459
460 self._pwd_handler = HTTPPasswordHandler()
460 def canonical_url(self, authuri):
461
461 """
462 if hasattr(self, '_http_req'):
462 Strips query params from url. Used to convert urls like
463 req = self._http_req
463 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
464 else:
464 to
465 req = None
465 https://repo.machine.com/repos/apps/module
466
466 """
467 return self._pwd_handler.find_auth(self, realm, authuri, req)
467 parsed_url = urlparse(authuri)
468
468 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
469 @monkeypatch_method(urllib2.AbstractBasicAuthHandler, "http_error_auth_reqed")
469 parsed_url.path)
470 def basic_http_error_auth_reqed(self, authreq, host, req, headers):
470
471 self.passwd._http_req = req
471 ############################################################
472 try:
472 # Monkeypatching
473 return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
473 ############################################################
474 finally:
474
475 self.passwd._http_req = None
475
476
476 @monkeypatch_method(passwordmgr)
477 @monkeypatch_method(urllib2.AbstractDigestAuthHandler, "http_error_auth_reqed")
477 def find_user_password(self, realm, authuri):
478 def digest_http_error_auth_reqed(self, authreq, host, req, headers):
478 """
479 self.passwd._http_req = req
479 keyring-based implementation of username/password query
480 try:
480 for HTTP(S) connections
481 return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
481
482 finally:
482 Passwords are saved in gnome keyring, OSX/Chain or other platform
483 self.passwd._http_req = None
483 specific storage and keyed by the repository url
484
484 """
485 ############################################################
485 # Extend object attributes
486
486 if not hasattr(self, '_pwd_handler'):
487 def try_smtp_login(ui, smtp_obj, username, password):
487 self._pwd_handler = HTTPPasswordHandler()
488 """
488
489 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
489 if hasattr(self, '_http_req'):
490 password.
490 req = self._http_req
491
491 else:
492 Returns:
492 req = None
493 - True if login succeeded
493
494 - False if login failed due to the wrong credentials
494 return self._pwd_handler.find_auth(self, realm, authuri, req)
495
495
496 Throws Abort exception if login failed for any other reason.
496
497
497 @monkeypatch_method(urllib2.AbstractBasicAuthHandler, "http_error_auth_reqed")
498 Immediately returns False if password is empty
498 def basic_http_error_auth_reqed(self, authreq, host, req, headers):
499 """
499 self.passwd._http_req = req
500 if not password:
500 try:
501 return False
501 return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
502 try:
502 finally:
503 ui.note(_('(authenticating to mail server as %s)\n') %
503 self.passwd._http_req = None
504 (username))
504
505 smtp_obj.login(username, password)
505
506 return True
506 @monkeypatch_method(urllib2.AbstractDigestAuthHandler, "http_error_auth_reqed")
507 except smtplib.SMTPException, inst:
507 def digest_http_error_auth_reqed(self, authreq, host, req, headers):
508 if inst.smtp_code == 535:
508 self.passwd._http_req = req
509 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
509 try:
510 return False
510 return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
511 else:
511 finally:
512 raise util.Abort(inst)
512 self.passwd._http_req = None
513
513
514 def keyring_supported_smtp(ui, username):
514 ############################################################
515 """
515 # SMTP support
516 keyring-integrated replacement for mercurial.mail._smtp
516 ############################################################
517 Used only when configuration file contains username, but
517
518 does not contain the password.
518
519
519 def try_smtp_login(ui, smtp_obj, username, password):
520 Most of the routine below is copied as-is from
520 """
521 mercurial.mail._smtp. The only changed part is
521 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
522 marked with #>>>>> and #<<<<< markers
522 password.
523 """
523
524 local_hostname = ui.config('smtp', 'local_hostname')
524 Returns:
525 tls = ui.config('smtp', 'tls', 'none')
525 - True if login succeeded
526 # backward compatible: when tls = true, we use starttls.
526 - False if login failed due to the wrong credentials
527 starttls = tls == 'starttls' or util.parsebool(tls)
527
528 smtps = tls == 'smtps'
528 Throws Abort exception if login failed for any other reason.
529 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
529
530 raise util.Abort(_("can't use TLS: Python SSL support not installed"))
530 Immediately returns False if password is empty
531 mailhost = ui.config('smtp', 'host')
531 """
532 if not mailhost:
532 if not password:
533 raise util.Abort(_('smtp.host not configured - cannot send mail'))
533 return False
534 verifycert = ui.config('smtp', 'verifycert', 'strict')
534 try:
535 if verifycert not in ['strict', 'loose']:
535 ui.note(_('(authenticating to mail server as %s)\n') %
536 if util.parsebool(verifycert) is not False:
536 (username))
537 raise util.Abort(_('invalid smtp.verifycert configuration: %s')
537 smtp_obj.login(username, password)
538 % (verifycert))
538 return True
539 verifycert = False
539 except smtplib.SMTPException, inst:
540 if (starttls or smtps) and verifycert:
540 if inst.smtp_code == 535:
541 sslkwargs = sslutil.sslkwargs(ui, mailhost)
541 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
542 else:
542 return False
543 sslkwargs = {}
543 else:
544 if smtps:
544 raise util.Abort(inst)
545 ui.note(_('(using smtps)\n'))
545
546 s = SMTPS(sslkwargs, local_hostname=local_hostname)
546
547 elif starttls:
547 def keyring_supported_smtp(ui, username):
548 s = STARTTLS(sslkwargs, local_hostname=local_hostname)
548 """
549 else:
549 keyring-integrated replacement for mercurial.mail._smtp
550 s = smtplib.SMTP(local_hostname=local_hostname)
550 Used only when configuration file contains username, but
551 if smtps:
551 does not contain the password.
552 defaultport = 465
552
553 else:
553 Most of the routine below is copied as-is from
554 defaultport = 25
554 mercurial.mail._smtp. The only changed part is
555 mailport = util.getport(ui.config('smtp', 'port', defaultport))
555 marked with # >>>>> and # <<<<< markers
556 ui.note(_('sending mail: smtp host %s, port %s\n') %
556 """
557 (mailhost, mailport))
557 local_hostname = ui.config('smtp', 'local_hostname')
558 s.connect(host=mailhost, port=mailport)
558 tls = ui.config('smtp', 'tls', 'none')
559 if starttls:
559 # backward compatible: when tls = true, we use starttls.
560 ui.note(_('(using starttls)\n'))
560 starttls = tls == 'starttls' or util.parsebool(tls)
561 s.ehlo()
561 smtps = tls == 'smtps'
562 s.starttls()
562 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
563 s.ehlo()
563 raise util.Abort(_("can't use TLS: Python SSL support not installed"))
564 if (starttls or smtps) and verifycert:
564 mailhost = ui.config('smtp', 'host')
565 ui.note(_('(verifying remote certificate)\n'))
565 if not mailhost:
566 sslutil.validator(ui, mailhost)(s.sock, verifycert == 'strict')
566 raise util.Abort(_('smtp.host not configured - cannot send mail'))
567
567 verifycert = ui.config('smtp', 'verifycert', 'strict')
568 #>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
568 if verifycert not in ['strict', 'loose']:
569 stored = password = password_store.get_smtp_password(
569 if util.parsebool(verifycert) is not False:
570 mailhost, mailport, username)
570 raise util.Abort(_('invalid smtp.verifycert configuration: %s')
571 # No need to check whether password was found as try_smtp_login
571 % (verifycert))
572 # just returns False if it is absent.
572 verifycert = False
573 while not try_smtp_login(ui, s, username, password):
573 if (starttls or smtps) and verifycert:
574 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
574 sslkwargs = sslutil.sslkwargs(ui, mailhost)
575
575 else:
576 if stored != password:
576 sslkwargs = {}
577 password_store.set_smtp_password(
577 if smtps:
578 mailhost, mailport, username, password)
578 ui.note(_('(using smtps)\n'))
579 #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
579 s = SMTPS(sslkwargs, local_hostname=local_hostname)
580
580 elif starttls:
581 def send(sender, recipients, msg):
581 s = STARTTLS(sslkwargs, local_hostname=local_hostname)
582 try:
582 else:
583 return s.sendmail(sender, recipients, msg)
583 s = smtplib.SMTP(local_hostname=local_hostname)
584 except smtplib.SMTPRecipientsRefused, inst:
584 if smtps:
585 recipients = [r[1] for r in inst.recipients.values()]
585 defaultport = 465
586 raise util.Abort('\n' + '\n'.join(recipients))
586 else:
587 except smtplib.SMTPException, inst:
587 defaultport = 25
588 raise util.Abort(inst)
588 mailport = util.getport(ui.config('smtp', 'port', defaultport))
589
589 ui.note(_('sending mail: smtp host %s, port %s\n') %
590 return send
590 (mailhost, mailport))
591
591 s.connect(host=mailhost, port=mailport)
592 ############################################################
592 if starttls:
593
593 ui.note(_('(using starttls)\n'))
594 orig_smtp = mail._smtp
594 s.ehlo()
595
595 s.starttls()
596 @monkeypatch_method(mail)
596 s.ehlo()
597 def _smtp(ui):
597 if (starttls or smtps) and verifycert:
598 """
598 ui.note(_('(verifying remote certificate)\n'))
599 build an smtp connection and return a function to send email
599 sslutil.validator(ui, mailhost)(s.sock, verifycert == 'strict')
600
600
601 This is the monkeypatched version of _smtp(ui) function from
601 # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
602 mercurial/mail.py. It calls the original unless username
602 stored = password = password_store.get_smtp_password(
603 without password is given in the configuration.
603 mailhost, mailport, username)
604 """
604 # No need to check whether password was found as try_smtp_login
605 username = ui.config('smtp', 'username')
605 # just returns False if it is absent.
606 password = ui.config('smtp', 'password')
606 while not try_smtp_login(ui, s, username, password):
607
607 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
608 if username and not password:
608
609 return keyring_supported_smtp(ui, username)
609 if stored != password:
610 else:
610 password_store.set_smtp_password(
611 return orig_smtp(ui)
611 mailhost, mailport, username, password)
612 # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
613
614 def send(sender, recipients, msg):
615 try:
616 return s.sendmail(sender, recipients, msg)
617 except smtplib.SMTPRecipientsRefused, inst:
618 recipients = [r[1] for r in inst.recipients.values()]
619 raise util.Abort('\n' + '\n'.join(recipients))
620 except smtplib.SMTPException, inst:
621 raise util.Abort(inst)
622
623 return send
624
625 ############################################################
626 # SMTP monkeypatching
627 ############################################################
628
629
630 @monkeypatch_method(mail)
631 def _smtp(ui):
632 """
633 build an smtp connection and return a function to send email
634
635 This is the monkeypatched version of _smtp(ui) function from
636 mercurial/mail.py. It calls the original unless username
637 without password is given in the configuration.
638 """
639 username = ui.config('smtp', 'username')
640 password = ui.config('smtp', 'password')
641
642 if username and not password:
643 return keyring_supported_smtp(ui, username)
644 else:
645 return _smtp.orig(ui)
General Comments 0
You need to be logged in to leave comments. Login now