##// END OF EJS Templates
Relocated non-mercurial imports before Mercurial ones, to perform them...
Marcin Kasperski -
r225:2d48fd11 default
parent child Browse files
Show More
@@ -1,794 +1,793 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 urllib2
55 import smtplib
56 import socket
57 import os
58 import sys
59 import re
60
54 from mercurial import util, sslutil
61 from mercurial import util, sslutil
55 from mercurial.i18n import _
62 from mercurial.i18n import _
56 from mercurial.url import passwordmgr
63 from mercurial.url import passwordmgr
57 from mercurial import mail
64 from mercurial import mail
58 from mercurial.mail import SMTPS, STARTTLS
65 from mercurial.mail import SMTPS, STARTTLS
59 from mercurial import encoding
66 from mercurial import encoding
60
67
61 import urllib2
62 import smtplib
63 import socket
64 import os
65 import sys
66 import re
67
68 # pylint: disable=invalid-name, line-too-long, protected-access, too-many-arguments
68 # pylint: disable=invalid-name, line-too-long, protected-access, too-many-arguments
69
69
70 ###########################################################################
70 ###########################################################################
71 # Specific import trickery
71 # Specific import trickery
72 ###########################################################################
72 ###########################################################################
73
73
74
74
75 def import_meu():
75 def import_meu():
76 """
76 """
77 Convoluted import of mercurial_extension_utils, which helps
77 Convoluted import of mercurial_extension_utils, which helps
78 TortoiseHg/Win setups. This routine and it's use below
78 TortoiseHg/Win setups. This routine and it's use below
79 performs equivalent of
79 performs equivalent of
80 from mercurial_extension_utils import monkeypatch_method
80 from mercurial_extension_utils import monkeypatch_method
81 but looks for some non-path directories.
81 but looks for some non-path directories.
82 """
82 """
83 try:
83 try:
84 import mercurial_extension_utils
84 import mercurial_extension_utils
85 except ImportError:
85 except ImportError:
86 my_dir = os.path.dirname(__file__)
86 my_dir = os.path.dirname(__file__)
87 sys.path.extend([
87 sys.path.extend([
88 # In the same dir (manual or site-packages after pip)
88 # In the same dir (manual or site-packages after pip)
89 my_dir,
89 my_dir,
90 # Developer clone
90 # Developer clone
91 os.path.join(os.path.dirname(my_dir), "extension_utils"),
91 os.path.join(os.path.dirname(my_dir), "extension_utils"),
92 # Side clone
92 # Side clone
93 os.path.join(os.path.dirname(my_dir), "mercurial-extension_utils"),
93 os.path.join(os.path.dirname(my_dir), "mercurial-extension_utils"),
94 ])
94 ])
95 try:
95 try:
96 import mercurial_extension_utils
96 import mercurial_extension_utils
97 except ImportError:
97 except ImportError:
98 raise util.Abort(_("""Can not import mercurial_extension_utils.
98 raise util.Abort(_("""Can not import mercurial_extension_utils.
99 Please install this module in Python path.
99 Please install this module in Python path.
100 See Installation chapter in https://bitbucket.org/Mekk/mercurial_keyring/ for details
100 See Installation chapter in https://bitbucket.org/Mekk/mercurial_keyring/ for details
101 (and for info about TortoiseHG on Windows, or other bundled Python)."""))
101 (and for info about TortoiseHG on Windows, or other bundled Python)."""))
102 return mercurial_extension_utils
102 return mercurial_extension_utils
103
103
104 meu = import_meu()
104 meu = import_meu()
105 monkeypatch_method = meu.monkeypatch_method
105 monkeypatch_method = meu.monkeypatch_method
106
106
107
107
108 def import_keyring():
108 def import_keyring():
109 """
109 """
110 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
111 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
112 demandimport-related problems.
112 demandimport-related problems.
113 """
113 """
114 # mercurial.demandimport incompatibility workaround.
114 # mercurial.demandimport incompatibility workaround.
115 # various keyring backends fail as they can't properly import helper
115 # various keyring backends fail as they can't properly import helper
116 # modules (as demandimport modifies python import behaviour).
116 # modules (as demandimport modifies python import behaviour).
117 # If you get import errors with demandimport in backtrace, try
117 # If you get import errors with demandimport in backtrace, try
118 # guessing what to block and extending the list below.
118 # guessing what to block and extending the list below.
119 mod, was_imported_now = meu.direct_import_ext(
119 mod, was_imported_now = meu.direct_import_ext(
120 "keyring", [
120 "keyring", [
121 "gobject._gobject",
121 "gobject._gobject",
122 "configparser",
122 "configparser",
123 "json",
123 "json",
124 "abc",
124 "abc",
125 "io",
125 "io",
126 "keyring",
126 "keyring",
127 "gdata.docs.service",
127 "gdata.docs.service",
128 "gdata.service",
128 "gdata.service",
129 "types",
129 "types",
130 "atom.http",
130 "atom.http",
131 "atom.http_interface",
131 "atom.http_interface",
132 "atom.service",
132 "atom.service",
133 "atom.token_store",
133 "atom.token_store",
134 "ctypes",
134 "ctypes",
135 "secretstorage.exceptions",
135 "secretstorage.exceptions",
136 "fs.opener",
136 "fs.opener",
137 ])
137 ])
138 if was_imported_now:
138 if was_imported_now:
139 # Shut up warning about uninitialized logging by keyring
139 # Shut up warning about uninitialized logging by keyring
140 meu.disable_logging("keyring")
140 meu.disable_logging("keyring")
141 return mod
141 return mod
142
142
143
144 #################################################################
143 #################################################################
145 # Actual implementation
144 # Actual implementation
146 #################################################################
145 #################################################################
147
146
148 KEYRING_SERVICE = "Mercurial"
147 KEYRING_SERVICE = "Mercurial"
149
148
150
149
151 class PasswordStore(object):
150 class PasswordStore(object):
152 """
151 """
153 Helper object handling keyring usage (password save&restore,
152 Helper object handling keyring usage (password save&restore,
154 the way passwords are keyed in the keyring).
153 the way passwords are keyed in the keyring).
155 """
154 """
156 def __init__(self):
155 def __init__(self):
157 self.cache = dict()
156 self.cache = dict()
158
157
159 def get_http_password(self, url, username):
158 def get_http_password(self, url, username):
160 """
159 """
161 Checks whether password of username for url is available,
160 Checks whether password of username for url is available,
162 returns it or None
161 returns it or None
163 """
162 """
164 return self._read_password_from_keyring(
163 return self._read_password_from_keyring(
165 self._format_http_key(url, username))
164 self._format_http_key(url, username))
166
165
167 def set_http_password(self, url, username, password):
166 def set_http_password(self, url, username, password):
168 """Saves password to keyring"""
167 """Saves password to keyring"""
169 self._save_password_to_keyring(
168 self._save_password_to_keyring(
170 self._format_http_key(url, username),
169 self._format_http_key(url, username),
171 password)
170 password)
172
171
173 def clear_http_password(self, url, username):
172 def clear_http_password(self, url, username):
174 """Drops saved password"""
173 """Drops saved password"""
175 self.set_http_password(url, username, "")
174 self.set_http_password(url, username, "")
176
175
177 @staticmethod
176 @staticmethod
178 def _format_http_key(url, username):
177 def _format_http_key(url, username):
179 """Construct actual key for password identification"""
178 """Construct actual key for password identification"""
180 return "%s@@%s" % (username, url)
179 return "%s@@%s" % (username, url)
181
180
182 def get_smtp_password(self, machine, port, username):
181 def get_smtp_password(self, machine, port, username):
183 """Checks for SMTP password in keyring, returns
182 """Checks for SMTP password in keyring, returns
184 password or None"""
183 password or None"""
185 return self._read_password_from_keyring(
184 return self._read_password_from_keyring(
186 self._format_smtp_key(machine, port, username))
185 self._format_smtp_key(machine, port, username))
187
186
188 def set_smtp_password(self, machine, port, username, password):
187 def set_smtp_password(self, machine, port, username, password):
189 """Saves SMTP password to keyring"""
188 """Saves SMTP password to keyring"""
190 self._save_password_to_keyring(
189 self._save_password_to_keyring(
191 self._format_smtp_key(machine, port, username),
190 self._format_smtp_key(machine, port, username),
192 password)
191 password)
193
192
194 def clear_smtp_password(self, machine, port, username):
193 def clear_smtp_password(self, machine, port, username):
195 """Drops saved SMTP password"""
194 """Drops saved SMTP password"""
196 self.set_smtp_password(machine, port, username, "")
195 self.set_smtp_password(machine, port, username, "")
197
196
198 @staticmethod
197 @staticmethod
199 def _format_smtp_key(machine, port, username):
198 def _format_smtp_key(machine, port, username):
200 """Construct key for SMTP password identification"""
199 """Construct key for SMTP password identification"""
201 return "%s@@%s:%s" % (username, machine, str(port))
200 return "%s@@%s:%s" % (username, machine, str(port))
202
201
203 @staticmethod
202 @staticmethod
204 def _read_password_from_keyring(pwdkey):
203 def _read_password_from_keyring(pwdkey):
205 """Physically read from keyring"""
204 """Physically read from keyring"""
206 keyring = import_keyring()
205 keyring = import_keyring()
207 password = keyring.get_password(KEYRING_SERVICE, pwdkey)
206 password = keyring.get_password(KEYRING_SERVICE, pwdkey)
208 # Reverse recoding from next routine
207 # Reverse recoding from next routine
209 if isinstance(password, unicode):
208 if isinstance(password, unicode):
210 return encoding.tolocal(password.encode('utf-8'))
209 return encoding.tolocal(password.encode('utf-8'))
211 return password
210 return password
212
211
213 @staticmethod
212 @staticmethod
214 def _save_password_to_keyring(pwdkey, password):
213 def _save_password_to_keyring(pwdkey, password):
215 """Physically write to keyring"""
214 """Physically write to keyring"""
216 keyring = import_keyring()
215 keyring = import_keyring()
217 # keyring in general expects unicode.
216 # keyring in general expects unicode.
218 # Mercurial provides "local" encoding. See #33
217 # Mercurial provides "local" encoding. See #33
219 password = encoding.fromlocal(password).decode('utf-8')
218 password = encoding.fromlocal(password).decode('utf-8')
220 keyring.set_password(
219 keyring.set_password(
221 KEYRING_SERVICE, pwdkey, password)
220 KEYRING_SERVICE, pwdkey, password)
222
221
223 password_store = PasswordStore()
222 password_store = PasswordStore()
224
223
225
224
226 ############################################################
225 ############################################################
227 # Various utils
226 # Various utils
228 ############################################################
227 ############################################################
229
228
230 def _debug(ui, msg):
229 def _debug(ui, msg):
231 """Generic debug message"""
230 """Generic debug message"""
232 ui.debug("keyring: " + msg + "\n")
231 ui.debug("keyring: " + msg + "\n")
233
232
234
233
235 class PwdCache(object):
234 class PwdCache(object):
236 """Short term cache, used to preserve passwords
235 """Short term cache, used to preserve passwords
237 if they are used twice during a command"""
236 if they are used twice during a command"""
238 def __init__(self):
237 def __init__(self):
239 self._cache = {}
238 self._cache = {}
240
239
241 def store(self, realm, url, user, pwd):
240 def store(self, realm, url, user, pwd):
242 """Saves password"""
241 """Saves password"""
243 cache_key = (realm, url, user)
242 cache_key = (realm, url, user)
244 self._cache[cache_key] = pwd
243 self._cache[cache_key] = pwd
245
244
246 def check(self, realm, url, user):
245 def check(self, realm, url, user):
247 """Checks for cached password"""
246 """Checks for cached password"""
248 cache_key = (realm, url, user)
247 cache_key = (realm, url, user)
249 return self._cache.get(cache_key)
248 return self._cache.get(cache_key)
250
249
251
250
252 _re_http_url = re.compile(r'^https?://')
251 _re_http_url = re.compile(r'^https?://')
253
252
254 def is_http_path(url):
253 def is_http_path(url):
255 return bool(_re_http_url.search(url))
254 return bool(_re_http_url.search(url))
256
255
257 ############################################################
256 ############################################################
258 # HTTP password management
257 # HTTP password management
259 ############################################################
258 ############################################################
260
259
261
260
262 class HTTPPasswordHandler(object):
261 class HTTPPasswordHandler(object):
263 """
262 """
264 Actual implementation of password handling (user prompting,
263 Actual implementation of password handling (user prompting,
265 configuration file searching, keyring save&restore).
264 configuration file searching, keyring save&restore).
266
265
267 Object of this class is bound as passwordmgr attribute.
266 Object of this class is bound as passwordmgr attribute.
268 """
267 """
269 def __init__(self):
268 def __init__(self):
270 self.pwd_cache = PwdCache()
269 self.pwd_cache = PwdCache()
271 self.last_reply = None
270 self.last_reply = None
272
271
273 # Markers and also names used in debug notes. Password source
272 # Markers and also names used in debug notes. Password source
274 SRC_URL = "repository URL"
273 SRC_URL = "repository URL"
275 SRC_CFGAUTH = "hgrc"
274 SRC_CFGAUTH = "hgrc"
276 SRC_MEMCACHE = "temporary cache"
275 SRC_MEMCACHE = "temporary cache"
277 SRC_URLCACHE = "urllib temporary cache"
276 SRC_URLCACHE = "urllib temporary cache"
278 SRC_KEYRING = "keyring"
277 SRC_KEYRING = "keyring"
279
278
280 def get_credentials(self, pwmgr, realm, authuri, skip_caches=False):
279 def get_credentials(self, pwmgr, realm, authuri, skip_caches=False):
281 """
280 """
282 Looks up for user credentials in various places, returns them
281 Looks up for user credentials in various places, returns them
283 and information about their source.
282 and information about their source.
284
283
285 Used internally inside find_auth and inside informative
284 Used internally inside find_auth and inside informative
286 commands (thiis method doesn't cache, doesn't detect bad
285 commands (thiis method doesn't cache, doesn't detect bad
287 passwords etc, doesn't prompt interactively, doesn't store
286 passwords etc, doesn't prompt interactively, doesn't store
288 password in keyring).
287 password in keyring).
289
288
290 Returns: user, password, SRC_*, actual_url
289 Returns: user, password, SRC_*, actual_url
291
290
292 If not found, password and SRC is None, user can be given or
291 If not found, password and SRC is None, user can be given or
293 not, url is always set
292 not, url is always set
294 """
293 """
295 ui = pwmgr.ui
294 ui = pwmgr.ui
296
295
297 parsed_url, url_user, url_passwd = self.unpack_url(authuri)
296 parsed_url, url_user, url_passwd = self.unpack_url(authuri)
298 base_url = str(parsed_url)
297 base_url = str(parsed_url)
299 ui.debug(_('keyring: base url: %s, url user: %s, url pwd: %s\n') %
298 ui.debug(_('keyring: base url: %s, url user: %s, url pwd: %s\n') %
300 (base_url, url_user or '', url_passwd and '******' or ''))
299 (base_url, url_user or '', url_passwd and '******' or ''))
301
300
302 # Extract username (or password) stored directly in url
301 # Extract username (or password) stored directly in url
303 if url_user and url_passwd:
302 if url_user and url_passwd:
304 return url_user, url_passwd, self.SRC_URL, base_url
303 return url_user, url_passwd, self.SRC_URL, base_url
305
304
306 # Extract data from urllib (in case it was already stored)
305 # Extract data from urllib (in case it was already stored)
307 urllib_user, urllib_pwd \
306 urllib_user, urllib_pwd \
308 = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
307 = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
309 pwmgr, realm, authuri)
308 pwmgr, realm, authuri)
310 if urllib_user and urllib_pwd:
309 if urllib_user and urllib_pwd:
311 return urllib_user, urllib_pwd, self.SRC_URLCACHE, base_url
310 return urllib_user, urllib_pwd, self.SRC_URLCACHE, base_url
312
311
313 actual_user = url_user or urllib_user
312 actual_user = url_user or urllib_user
314
313
315 # Consult configuration to normalize url to prefix, and find username
314 # Consult configuration to normalize url to prefix, and find username
316 # (and maybe password)
315 # (and maybe password)
317 auth_user, auth_pwd, keyring_url = self.get_url_config(
316 auth_user, auth_pwd, keyring_url = self.get_url_config(
318 ui, parsed_url, actual_user)
317 ui, parsed_url, actual_user)
319 if auth_user and actual_user and (actual_user != auth_user):
318 if auth_user and actual_user and (actual_user != auth_user):
320 raise util.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)))
319 raise util.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)))
321 if auth_user and auth_pwd:
320 if auth_user and auth_pwd:
322 return auth_user, auth_pwd, self.SRC_CFGAUTH, keyring_url
321 return auth_user, auth_pwd, self.SRC_CFGAUTH, keyring_url
323
322
324 actual_user = actual_user or auth_user
323 actual_user = actual_user or auth_user
325
324
326 if skip_caches:
325 if skip_caches:
327 return actual_user, None, None, keyring_url
326 return actual_user, None, None, keyring_url
328
327
329 # Check memory cache (reuse )
328 # Check memory cache (reuse )
330 # Checking the memory cache (there may be many http calls per command)
329 # Checking the memory cache (there may be many http calls per command)
331 cached_pwd = self.pwd_cache.check(realm, keyring_url, actual_user)
330 cached_pwd = self.pwd_cache.check(realm, keyring_url, actual_user)
332 if cached_pwd:
331 if cached_pwd:
333 return actual_user, cached_pwd, self.SRC_MEMCACHE, keyring_url
332 return actual_user, cached_pwd, self.SRC_MEMCACHE, keyring_url
334
333
335 # Load from keyring.
334 # Load from keyring.
336 if actual_user:
335 if actual_user:
337 ui.debug(_("keyring: looking for password (user %s, url %s)\n") % (actual_user, keyring_url))
336 ui.debug(_("keyring: looking for password (user %s, url %s)\n") % (actual_user, keyring_url))
338 keyring_pwd = password_store.get_http_password(keyring_url, actual_user)
337 keyring_pwd = password_store.get_http_password(keyring_url, actual_user)
339 if keyring_pwd:
338 if keyring_pwd:
340 return actual_user, keyring_pwd, self.SRC_KEYRING, keyring_url
339 return actual_user, keyring_pwd, self.SRC_KEYRING, keyring_url
341
340
342 return actual_user, None, None, keyring_url
341 return actual_user, None, None, keyring_url
343
342
344 @staticmethod
343 @staticmethod
345 def prompt_interactively(ui, user, realm, url):
344 def prompt_interactively(ui, user, realm, url):
346 """Actual interactive prompt"""
345 """Actual interactive prompt"""
347 if not ui.interactive():
346 if not ui.interactive():
348 raise util.Abort(_('keyring: http authorization required but program used in non-interactive mode'))
347 raise util.Abort(_('keyring: http authorization required but program used in non-interactive mode'))
349
348
350 if not user:
349 if not user:
351 ui.status(_("keyring: username not specified in hgrc (or in url). Password will not be saved.\n"))
350 ui.status(_("keyring: username not specified in hgrc (or in url). Password will not be saved.\n"))
352
351
353 ui.write(_("http authorization required\n"))
352 ui.write(_("http authorization required\n"))
354 ui.status(_("realm: %s\n") % realm)
353 ui.status(_("realm: %s\n") % realm)
355 ui.status(_("url: %s\n") % url)
354 ui.status(_("url: %s\n") % url)
356 if user:
355 if user:
357 ui.write(_("user: %s (fixed in hgrc or url)\n" % user))
356 ui.write(_("user: %s (fixed in hgrc or url)\n" % user))
358 else:
357 else:
359 user = ui.prompt(_("user:"), default=None)
358 user = ui.prompt(_("user:"), default=None)
360 pwd = ui.getpass(_("password: "))
359 pwd = ui.getpass(_("password: "))
361 return user, pwd
360 return user, pwd
362
361
363 def find_auth(self, pwmgr, realm, authuri, req):
362 def find_auth(self, pwmgr, realm, authuri, req):
364 """
363 """
365 Actual implementation of find_user_password - different
364 Actual implementation of find_user_password - different
366 ways of obtaining the username and password.
365 ways of obtaining the username and password.
367
366
368 Returns pair username, password
367 Returns pair username, password
369 """
368 """
370 ui = pwmgr.ui
369 ui = pwmgr.ui
371 after_bad_auth = self._after_bad_auth(ui, realm, authuri, req)
370 after_bad_auth = self._after_bad_auth(ui, realm, authuri, req)
372
371
373 # Look in url, cache, etc
372 # Look in url, cache, etc
374 user, pwd, src, final_url = self.get_credentials(
373 user, pwd, src, final_url = self.get_credentials(
375 pwmgr, realm, authuri, skip_caches=after_bad_auth)
374 pwmgr, realm, authuri, skip_caches=after_bad_auth)
376 if pwd:
375 if pwd:
377 if src != self.SRC_MEMCACHE:
376 if src != self.SRC_MEMCACHE:
378 self.pwd_cache.store(realm, final_url, user, pwd)
377 self.pwd_cache.store(realm, final_url, user, pwd)
379 self._note_last_reply(realm, authuri, user, req)
378 self._note_last_reply(realm, authuri, user, req)
380 _debug(ui, _("Password found in " + src))
379 _debug(ui, _("Password found in " + src))
381 return user, pwd
380 return user, pwd
382
381
383 # Last resort: interactive prompt
382 # Last resort: interactive prompt
384 user, pwd = self.prompt_interactively(ui, user, realm, final_url)
383 user, pwd = self.prompt_interactively(ui, user, realm, final_url)
385
384
386 if user:
385 if user:
387 # Saving password to the keyring.
386 # Saving password to the keyring.
388 # It is done only if username is permanently set.
387 # It is done only if username is permanently set.
389 # Otherwise we won't be able to find the password so it
388 # Otherwise we won't be able to find the password so it
390 # does not make much sense to preserve it
389 # does not make much sense to preserve it
391 _debug(ui, _("Saving password for %s to keyring") % user)
390 _debug(ui, _("Saving password for %s to keyring") % user)
392 password_store.set_http_password(final_url, user, pwd)
391 password_store.set_http_password(final_url, user, pwd)
393
392
394 # Saving password to the memory cache
393 # Saving password to the memory cache
395 self.pwd_cache.store(realm, final_url, user, pwd)
394 self.pwd_cache.store(realm, final_url, user, pwd)
396 self._note_last_reply(realm, authuri, user, req)
395 self._note_last_reply(realm, authuri, user, req)
397 _debug(ui, _("Manually entered password"))
396 _debug(ui, _("Manually entered password"))
398 return user, pwd
397 return user, pwd
399
398
400 def get_url_config(self, ui, parsed_url, user):
399 def get_url_config(self, ui, parsed_url, user):
401 """
400 """
402 Checks configuration to decide whether/which username, prefix,
401 Checks configuration to decide whether/which username, prefix,
403 and password are configured for given url. Consults [auth] section.
402 and password are configured for given url. Consults [auth] section.
404
403
405 Returns tuple (username, password, prefix) containing elements
404 Returns tuple (username, password, prefix) containing elements
406 found. username and password can be None (if unset), if prefix
405 found. username and password can be None (if unset), if prefix
407 is not found, url itself is returned.
406 is not found, url itself is returned.
408 """
407 """
409 base_url = str(parsed_url)
408 base_url = str(parsed_url)
410
409
411 from mercurial.httpconnection import readauthforuri
410 from mercurial.httpconnection import readauthforuri
412 _debug(ui, _("Checking for hgrc info about url %s, user %s") % (base_url, user))
411 _debug(ui, _("Checking for hgrc info about url %s, user %s") % (base_url, user))
413 res = readauthforuri(ui, base_url, user)
412 res = readauthforuri(ui, base_url, user)
414 # If it user-less version not work, let's try with added username to handle
413 # If it user-less version not work, let's try with added username to handle
415 # both config conventions
414 # both config conventions
416 if (not res) and user:
415 if (not res) and user:
417 parsed_url.user = user
416 parsed_url.user = user
418 res = readauthforuri(ui, str(parsed_url), user)
417 res = readauthforuri(ui, str(parsed_url), user)
419 parsed_url.user = None
418 parsed_url.user = None
420 if res:
419 if res:
421 group, auth_token = res
420 group, auth_token = res
422 else:
421 else:
423 auth_token = None
422 auth_token = None
424
423
425 if auth_token:
424 if auth_token:
426 username = auth_token.get('username')
425 username = auth_token.get('username')
427 password = auth_token.get('password')
426 password = auth_token.get('password')
428 prefix = auth_token.get('prefix')
427 prefix = auth_token.get('prefix')
429 else:
428 else:
430 username = user
429 username = user
431 password = None
430 password = None
432 prefix = None
431 prefix = None
433
432
434 password_url = self.password_url(base_url, prefix)
433 password_url = self.password_url(base_url, prefix)
435
434
436 _debug(ui, _("Password url: %s, user: %s, password: %s (prefix: %s)") % (
435 _debug(ui, _("Password url: %s, user: %s, password: %s (prefix: %s)") % (
437 password_url, username, '********' if password else '', prefix))
436 password_url, username, '********' if password else '', prefix))
438
437
439 return username, password, password_url
438 return username, password, password_url
440
439
441 def _note_last_reply(self, realm, authuri, user, req):
440 def _note_last_reply(self, realm, authuri, user, req):
442 """
441 """
443 Internal helper. Saves info about auth-data obtained,
442 Internal helper. Saves info about auth-data obtained,
444 preserves them in last_reply, and returns pair user, pwd
443 preserves them in last_reply, and returns pair user, pwd
445 """
444 """
446 self.last_reply = dict(realm=realm, authuri=authuri,
445 self.last_reply = dict(realm=realm, authuri=authuri,
447 user=user, req=req)
446 user=user, req=req)
448
447
449 def _after_bad_auth(self, ui, realm, authuri, req):
448 def _after_bad_auth(self, ui, realm, authuri, req):
450 """
449 """
451 If we are called again just after identical previous
450 If we are called again just after identical previous
452 request, then the previously returned auth must have been
451 request, then the previously returned auth must have been
453 wrong. So we note this to force password prompt (and avoid
452 wrong. So we note this to force password prompt (and avoid
454 reusing bad password indefinitely).
453 reusing bad password indefinitely).
455
454
456 This routine checks for this condition.
455 This routine checks for this condition.
457 """
456 """
458 if self.last_reply:
457 if self.last_reply:
459 if (self.last_reply['realm'] == realm) \
458 if (self.last_reply['realm'] == realm) \
460 and (self.last_reply['authuri'] == authuri) \
459 and (self.last_reply['authuri'] == authuri) \
461 and (self.last_reply['req'] == req):
460 and (self.last_reply['req'] == req):
462 _debug(ui, _("Working after bad authentication, cached passwords not used %s") % str(self.last_reply))
461 _debug(ui, _("Working after bad authentication, cached passwords not used %s") % str(self.last_reply))
463 return True
462 return True
464 return False
463 return False
465
464
466 @staticmethod
465 @staticmethod
467 def password_url(base_url, prefix):
466 def password_url(base_url, prefix):
468 """Calculates actual url identifying the password. Takes
467 """Calculates actual url identifying the password. Takes
469 configured prefix under consideration (so can be shorter
468 configured prefix under consideration (so can be shorter
470 than repo url)"""
469 than repo url)"""
471 if not prefix or prefix == '*':
470 if not prefix or prefix == '*':
472 return base_url
471 return base_url
473 scheme, hostpath = base_url.split('://', 1)
472 scheme, hostpath = base_url.split('://', 1)
474 p = prefix.split('://', 1)
473 p = prefix.split('://', 1)
475 if len(p) > 1:
474 if len(p) > 1:
476 prefix_host_path = p[1]
475 prefix_host_path = p[1]
477 else:
476 else:
478 prefix_host_path = prefix
477 prefix_host_path = prefix
479 password_url = scheme + '://' + prefix_host_path
478 password_url = scheme + '://' + prefix_host_path
480 return password_url
479 return password_url
481
480
482 @staticmethod
481 @staticmethod
483 def unpack_url(authuri):
482 def unpack_url(authuri):
484 """
483 """
485 Takes original url for which authentication is attempted and:
484 Takes original url for which authentication is attempted and:
486
485
487 - Strips query params from url. Used to convert urls like
486 - Strips query params from url. Used to convert urls like
488 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
487 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
489 to
488 to
490 https://repo.machine.com/repos/apps/module
489 https://repo.machine.com/repos/apps/module
491
490
492 - Extracts username and password, if present, and removes them from url
491 - Extracts username and password, if present, and removes them from url
493 (so prefix matching works properly)
492 (so prefix matching works properly)
494
493
495 Returns url, user, password
494 Returns url, user, password
496 where url is mercurial.util.url object already stripped of all those
495 where url is mercurial.util.url object already stripped of all those
497 params.
496 params.
498 """
497 """
499 # mercurial.util.url, rather handy url parser
498 # mercurial.util.url, rather handy url parser
500 parsed_url = util.url(authuri)
499 parsed_url = util.url(authuri)
501 parsed_url.query = ''
500 parsed_url.query = ''
502 parsed_url.fragment = None
501 parsed_url.fragment = None
503 # Strip arguments to get actual remote repository url.
502 # Strip arguments to get actual remote repository url.
504 # base_url = "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
503 # base_url = "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
505 # parsed_url.path)
504 # parsed_url.path)
506 user = parsed_url.user
505 user = parsed_url.user
507 passwd = parsed_url.passwd
506 passwd = parsed_url.passwd
508 parsed_url.user = None
507 parsed_url.user = None
509 parsed_url.passwd = None
508 parsed_url.passwd = None
510
509
511 return parsed_url, user, passwd
510 return parsed_url, user, passwd
512
511
513
512
514 ############################################################
513 ############################################################
515 # Mercurial monkey-patching
514 # Mercurial monkey-patching
516 ############################################################
515 ############################################################
517
516
518
517
519 @monkeypatch_method(passwordmgr)
518 @monkeypatch_method(passwordmgr)
520 def find_user_password(self, realm, authuri):
519 def find_user_password(self, realm, authuri):
521 """
520 """
522 keyring-based implementation of username/password query
521 keyring-based implementation of username/password query
523 for HTTP(S) connections
522 for HTTP(S) connections
524
523
525 Passwords are saved in gnome keyring, OSX/Chain or other platform
524 Passwords are saved in gnome keyring, OSX/Chain or other platform
526 specific storage and keyed by the repository url
525 specific storage and keyed by the repository url
527 """
526 """
528 # Extend object attributes
527 # Extend object attributes
529 if not hasattr(self, '_pwd_handler'):
528 if not hasattr(self, '_pwd_handler'):
530 self._pwd_handler = HTTPPasswordHandler()
529 self._pwd_handler = HTTPPasswordHandler()
531
530
532 if hasattr(self, '_http_req'):
531 if hasattr(self, '_http_req'):
533 req = self._http_req
532 req = self._http_req
534 else:
533 else:
535 req = None
534 req = None
536
535
537 return self._pwd_handler.find_auth(self, realm, authuri, req)
536 return self._pwd_handler.find_auth(self, realm, authuri, req)
538
537
539
538
540 @monkeypatch_method(urllib2.AbstractBasicAuthHandler, "http_error_auth_reqed")
539 @monkeypatch_method(urllib2.AbstractBasicAuthHandler, "http_error_auth_reqed")
541 def basic_http_error_auth_reqed(self, authreq, host, req, headers):
540 def basic_http_error_auth_reqed(self, authreq, host, req, headers):
542 """Preserves current HTTP request so it can be consulted
541 """Preserves current HTTP request so it can be consulted
543 in find_user_password above"""
542 in find_user_password above"""
544 self.passwd._http_req = req
543 self.passwd._http_req = req
545 try:
544 try:
546 return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
545 return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
547 finally:
546 finally:
548 self.passwd._http_req = None
547 self.passwd._http_req = None
549
548
550
549
551 @monkeypatch_method(urllib2.AbstractDigestAuthHandler, "http_error_auth_reqed")
550 @monkeypatch_method(urllib2.AbstractDigestAuthHandler, "http_error_auth_reqed")
552 def digest_http_error_auth_reqed(self, authreq, host, req, headers):
551 def digest_http_error_auth_reqed(self, authreq, host, req, headers):
553 """Preserves current HTTP request so it can be consulted
552 """Preserves current HTTP request so it can be consulted
554 in find_user_password above"""
553 in find_user_password above"""
555 self.passwd._http_req = req
554 self.passwd._http_req = req
556 try:
555 try:
557 return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
556 return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
558 finally:
557 finally:
559 self.passwd._http_req = None
558 self.passwd._http_req = None
560
559
561 ############################################################
560 ############################################################
562 # SMTP support
561 # SMTP support
563 ############################################################
562 ############################################################
564
563
565
564
566 def try_smtp_login(ui, smtp_obj, username, password):
565 def try_smtp_login(ui, smtp_obj, username, password):
567 """
566 """
568 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
567 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
569 password.
568 password.
570
569
571 Returns:
570 Returns:
572 - True if login succeeded
571 - True if login succeeded
573 - False if login failed due to the wrong credentials
572 - False if login failed due to the wrong credentials
574
573
575 Throws Abort exception if login failed for any other reason.
574 Throws Abort exception if login failed for any other reason.
576
575
577 Immediately returns False if password is empty
576 Immediately returns False if password is empty
578 """
577 """
579 if not password:
578 if not password:
580 return False
579 return False
581 try:
580 try:
582 ui.note(_('(authenticating to mail server as %s)\n') %
581 ui.note(_('(authenticating to mail server as %s)\n') %
583 (username))
582 (username))
584 smtp_obj.login(username, password)
583 smtp_obj.login(username, password)
585 return True
584 return True
586 except smtplib.SMTPException, inst:
585 except smtplib.SMTPException, inst:
587 if inst.smtp_code == 535:
586 if inst.smtp_code == 535:
588 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
587 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
589 return False
588 return False
590 else:
589 else:
591 raise util.Abort(inst)
590 raise util.Abort(inst)
592
591
593
592
594 def keyring_supported_smtp(ui, username):
593 def keyring_supported_smtp(ui, username):
595 """
594 """
596 keyring-integrated replacement for mercurial.mail._smtp
595 keyring-integrated replacement for mercurial.mail._smtp
597 Used only when configuration file contains username, but
596 Used only when configuration file contains username, but
598 does not contain the password.
597 does not contain the password.
599
598
600 Most of the routine below is copied as-is from
599 Most of the routine below is copied as-is from
601 mercurial.mail._smtp. The only changed part is
600 mercurial.mail._smtp. The only changed part is
602 marked with # >>>>> and # <<<<< markers
601 marked with # >>>>> and # <<<<< markers
603 """
602 """
604 local_hostname = ui.config('smtp', 'local_hostname')
603 local_hostname = ui.config('smtp', 'local_hostname')
605 tls = ui.config('smtp', 'tls', 'none')
604 tls = ui.config('smtp', 'tls', 'none')
606 # backward compatible: when tls = true, we use starttls.
605 # backward compatible: when tls = true, we use starttls.
607 starttls = tls == 'starttls' or util.parsebool(tls)
606 starttls = tls == 'starttls' or util.parsebool(tls)
608 smtps = tls == 'smtps'
607 smtps = tls == 'smtps'
609 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
608 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
610 raise util.Abort(_("can't use TLS: Python SSL support not installed"))
609 raise util.Abort(_("can't use TLS: Python SSL support not installed"))
611 mailhost = ui.config('smtp', 'host')
610 mailhost = ui.config('smtp', 'host')
612 if not mailhost:
611 if not mailhost:
613 raise util.Abort(_('smtp.host not configured - cannot send mail'))
612 raise util.Abort(_('smtp.host not configured - cannot send mail'))
614 verifycert = ui.config('smtp', 'verifycert', 'strict')
613 verifycert = ui.config('smtp', 'verifycert', 'strict')
615 if verifycert not in ['strict', 'loose']:
614 if verifycert not in ['strict', 'loose']:
616 if util.parsebool(verifycert) is not False:
615 if util.parsebool(verifycert) is not False:
617 raise util.Abort(_('invalid smtp.verifycert configuration: %s')
616 raise util.Abort(_('invalid smtp.verifycert configuration: %s')
618 % (verifycert))
617 % (verifycert))
619 verifycert = False
618 verifycert = False
620 if (starttls or smtps) and verifycert:
619 if (starttls or smtps) and verifycert:
621 sslkwargs = sslutil.sslkwargs(ui, mailhost)
620 sslkwargs = sslutil.sslkwargs(ui, mailhost)
622 else:
621 else:
623 sslkwargs = {}
622 sslkwargs = {}
624 if smtps:
623 if smtps:
625 ui.note(_('(using smtps)\n'))
624 ui.note(_('(using smtps)\n'))
626 s = SMTPS(sslkwargs, local_hostname=local_hostname)
625 s = SMTPS(sslkwargs, local_hostname=local_hostname)
627 elif starttls:
626 elif starttls:
628 s = STARTTLS(sslkwargs, local_hostname=local_hostname)
627 s = STARTTLS(sslkwargs, local_hostname=local_hostname)
629 else:
628 else:
630 s = smtplib.SMTP(local_hostname=local_hostname)
629 s = smtplib.SMTP(local_hostname=local_hostname)
631 if smtps:
630 if smtps:
632 defaultport = 465
631 defaultport = 465
633 else:
632 else:
634 defaultport = 25
633 defaultport = 25
635 mailport = util.getport(ui.config('smtp', 'port', defaultport))
634 mailport = util.getport(ui.config('smtp', 'port', defaultport))
636 ui.note(_('sending mail: smtp host %s, port %s\n') %
635 ui.note(_('sending mail: smtp host %s, port %s\n') %
637 (mailhost, mailport))
636 (mailhost, mailport))
638 s.connect(host=mailhost, port=mailport)
637 s.connect(host=mailhost, port=mailport)
639 if starttls:
638 if starttls:
640 ui.note(_('(using starttls)\n'))
639 ui.note(_('(using starttls)\n'))
641 s.ehlo()
640 s.ehlo()
642 s.starttls()
641 s.starttls()
643 s.ehlo()
642 s.ehlo()
644 if (starttls or smtps) and verifycert:
643 if (starttls or smtps) and verifycert:
645 ui.note(_('(verifying remote certificate)\n'))
644 ui.note(_('(verifying remote certificate)\n'))
646 sslutil.validator(ui, mailhost)(s.sock, verifycert == 'strict')
645 sslutil.validator(ui, mailhost)(s.sock, verifycert == 'strict')
647
646
648 # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
647 # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
649 stored = password = password_store.get_smtp_password(
648 stored = password = password_store.get_smtp_password(
650 mailhost, mailport, username)
649 mailhost, mailport, username)
651 # No need to check whether password was found as try_smtp_login
650 # No need to check whether password was found as try_smtp_login
652 # just returns False if it is absent.
651 # just returns False if it is absent.
653 while not try_smtp_login(ui, s, username, password):
652 while not try_smtp_login(ui, s, username, password):
654 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
653 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
655
654
656 if stored != password:
655 if stored != password:
657 password_store.set_smtp_password(
656 password_store.set_smtp_password(
658 mailhost, mailport, username, password)
657 mailhost, mailport, username, password)
659 # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
658 # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
660
659
661 def send(sender, recipients, msg):
660 def send(sender, recipients, msg):
662 try:
661 try:
663 return s.sendmail(sender, recipients, msg)
662 return s.sendmail(sender, recipients, msg)
664 except smtplib.SMTPRecipientsRefused, inst:
663 except smtplib.SMTPRecipientsRefused, inst:
665 recipients = [r[1] for r in inst.recipients.values()]
664 recipients = [r[1] for r in inst.recipients.values()]
666 raise util.Abort('\n' + '\n'.join(recipients))
665 raise util.Abort('\n' + '\n'.join(recipients))
667 except smtplib.SMTPException, inst:
666 except smtplib.SMTPException, inst:
668 raise util.Abort(inst)
667 raise util.Abort(inst)
669
668
670 return send
669 return send
671
670
672 ############################################################
671 ############################################################
673 # SMTP monkeypatching
672 # SMTP monkeypatching
674 ############################################################
673 ############################################################
675
674
676
675
677 @monkeypatch_method(mail)
676 @monkeypatch_method(mail)
678 def _smtp(ui):
677 def _smtp(ui):
679 """
678 """
680 build an smtp connection and return a function to send email
679 build an smtp connection and return a function to send email
681
680
682 This is the monkeypatched version of _smtp(ui) function from
681 This is the monkeypatched version of _smtp(ui) function from
683 mercurial/mail.py. It calls the original unless username
682 mercurial/mail.py. It calls the original unless username
684 without password is given in the configuration.
683 without password is given in the configuration.
685 """
684 """
686 username = ui.config('smtp', 'username')
685 username = ui.config('smtp', 'username')
687 password = ui.config('smtp', 'password')
686 password = ui.config('smtp', 'password')
688
687
689 if username and not password:
688 if username and not password:
690 return keyring_supported_smtp(ui, username)
689 return keyring_supported_smtp(ui, username)
691 else:
690 else:
692 return _smtp.orig(ui)
691 return _smtp.orig(ui)
693
692
694
693
695 ############################################################
694 ############################################################
696 # Custom commands
695 # Custom commands
697 ############################################################
696 ############################################################
698
697
699 cmdtable = {}
698 cmdtable = {}
700 command = meu.command(cmdtable)
699 command = meu.command(cmdtable)
701
700
702
701
703 @command('keyring_check',
702 @command('keyring_check',
704 [],
703 [],
705 _("keyring_check [PATH]"),
704 _("keyring_check [PATH]"),
706 optionalrepo=True)
705 optionalrepo=True)
707 def cmd_keyring_check(ui, repo, *path_args, **opts): # pylint: disable=unused-argument
706 def cmd_keyring_check(ui, repo, *path_args, **opts): # pylint: disable=unused-argument
708 """
707 """
709 Prints basic info (whether password is currently saved, and how is
708 Prints basic info (whether password is currently saved, and how is
710 it identified) for given path.
709 it identified) for given path.
711
710
712 Can be run without parameters to show status for all (current repository) paths which
711 Can be run without parameters to show status for all (current repository) paths which
713 are HTTP-like.
712 are HTTP-like.
714 """
713 """
715 defined_paths = [(name, url)
714 defined_paths = [(name, url)
716 for name, url in ui.configitems('paths')]
715 for name, url in ui.configitems('paths')]
717 if path_args:
716 if path_args:
718 # Maybe parameter is an alias
717 # Maybe parameter is an alias
719 defined_paths_dic = dict(defined_paths)
718 defined_paths_dic = dict(defined_paths)
720 paths = [(path_arg, defined_paths_dic.get(path_arg, path_arg))
719 paths = [(path_arg, defined_paths_dic.get(path_arg, path_arg))
721 for path_arg in path_args]
720 for path_arg in path_args]
722 else:
721 else:
723 if not repo:
722 if not repo:
724 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"))
723 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"))
725 return
724 return
726 paths = [(name, url) for name, url in defined_paths]
725 paths = [(name, url) for name, url in defined_paths]
727
726
728 if not paths:
727 if not paths:
729 ui.status(_("keyring_check: no paths defined\n"))
728 ui.status(_("keyring_check: no paths defined\n"))
730 return
729 return
731
730
732 handler = HTTPPasswordHandler()
731 handler = HTTPPasswordHandler()
733
732
734 ui.status(_("keyring password save status:\n"))
733 ui.status(_("keyring password save status:\n"))
735 for name, url in paths:
734 for name, url in paths:
736 if not is_http_path(url):
735 if not is_http_path(url):
737 if path_args:
736 if path_args:
738 ui.status(_(" %s: non-http path (%s)\n") % (name, url))
737 ui.status(_(" %s: non-http path (%s)\n") % (name, url))
739 continue
738 continue
740 user, pwd, source, final_url = handler.get_credentials(
739 user, pwd, source, final_url = handler.get_credentials(
741 passwordmgr(ui), name, url)
740 passwordmgr(ui), name, url)
742 if pwd:
741 if pwd:
743 ui.status(_(" %s: password available, source: %s, bound to user %s, url %s\n") % (
742 ui.status(_(" %s: password available, source: %s, bound to user %s, url %s\n") % (
744 name, source, user, final_url))
743 name, source, user, final_url))
745 elif user:
744 elif user:
746 ui.status(_(" %s: password not available, once entered, will be bound to user %s, url %s\n") % (
745 ui.status(_(" %s: password not available, once entered, will be bound to user %s, url %s\n") % (
747 name, user, final_url))
746 name, user, final_url))
748 else:
747 else:
749 ui.status(_(" %s: password not available, user unknown, url %s\n") % (
748 ui.status(_(" %s: password not available, user unknown, url %s\n") % (
750 name, final_url))
749 name, final_url))
751
750
752
751
753 @command('keyring_clear',
752 @command('keyring_clear',
754 [],
753 [],
755 _('hg keyring_clear PATH-OR-ALIAS'),
754 _('hg keyring_clear PATH-OR-ALIAS'),
756 optionalrepo=True)
755 optionalrepo=True)
757 def cmd_keyring_clear(ui, repo, path, **opts): # pylint: disable=unused-argument
756 def cmd_keyring_clear(ui, repo, path, **opts): # pylint: disable=unused-argument
758 """
757 """
759 Drops password bound to given path (if any is saved).
758 Drops password bound to given path (if any is saved).
760
759
761 Parameter can be given as full url (``https://John@bitbucket.org``) or as the name
760 Parameter can be given as full url (``https://John@bitbucket.org``) or as the name
762 of path alias (``bitbucket``).
761 of path alias (``bitbucket``).
763 """
762 """
764 path_url = path
763 path_url = path
765 for name, url in ui.configitems('paths'):
764 for name, url in ui.configitems('paths'):
766 if name == path:
765 if name == path:
767 path_url = url
766 path_url = url
768 break
767 break
769 if not is_http_path(path_url):
768 if not is_http_path(path_url):
770 ui.status(_("%s is not a http path (and %s can't be resolved as path alias)\n") % (path, path_url))
769 ui.status(_("%s is not a http path (and %s can't be resolved as path alias)\n") % (path, path_url))
771 return
770 return
772
771
773 handler = HTTPPasswordHandler()
772 handler = HTTPPasswordHandler()
774
773
775 user, pwd, source, final_url = handler.get_credentials(
774 user, pwd, source, final_url = handler.get_credentials(
776 passwordmgr(ui), path, path_url)
775 passwordmgr(ui), path, path_url)
777 if not user:
776 if not user:
778 ui.status(_("Username not configured for url %s\n") % final_url)
777 ui.status(_("Username not configured for url %s\n") % final_url)
779 return
778 return
780 if not pwd:
779 if not pwd:
781 ui.status(_("No password is saved for user %s, url %s\n") % (
780 ui.status(_("No password is saved for user %s, url %s\n") % (
782 user, final_url))
781 user, final_url))
783 return
782 return
784
783
785 if source != handler.SRC_KEYRING:
784 if source != handler.SRC_KEYRING:
786 ui.status(_("Password for user %s, url %s is saved in %s, not in keyring\n") % (
785 ui.status(_("Password for user %s, url %s is saved in %s, not in keyring\n") % (
787 user, final_url, source))
786 user, final_url, source))
788
787
789 password_store.clear_http_password(final_url, user)
788 password_store.clear_http_password(final_url, user)
790 ui.status(_("Password removed for user %s, url %s\n") % (
789 ui.status(_("Password removed for user %s, url %s\n") % (
791 user, final_url))
790 user, final_url))
792
791
793
792
794 buglink = 'https://bitbucket.org/Mekk/mercurial_keyring/issues'
793 buglink = 'https://bitbucket.org/Mekk/mercurial_keyring/issues'
General Comments 0
You need to be logged in to leave comments. Login now