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