##// END OF EJS Templates
Add help text to output for hg help.
Levi Bard -
r92:1068018d default
parent child Browse files
Show More
@@ -1,444 +1,452 b''
1 1 # -*- coding: utf-8 -*-
2 2 #
3 3 # mercurial_keyring: save passwords in password database
4 4 #
5 5 # Copyright 2009 Marcin Kasperski <Marcin.Kasperski@mekk.waw.pl>
6 6 #
7 7 # This software may be used and distributed according to the terms
8 8 # of the GNU General Public License, incorporated herein by reference.
9 9 #
10 10 # See README.txt for more details.
11 11
12 ''' securely save HTTP and SMTP authentication details
13 mercurial_keyring is a Mercurial extension used to securely save
14 HTTP and SMTP authentication passwords in password databases (Gnome
15 Keyring, KDE KWallet, OSXKeyChain, specific solutions for Win32 and
16 command line). This extension uses and wraps services of the keyring
17 library.
18 '''
19
12 20 from mercurial import hg, repo, util
13 21 from mercurial.i18n import _
14 22 try:
15 23 from mercurial.url import passwordmgr
16 24 except:
17 25 from mercurial.httprepo import passwordmgr
18 26 from mercurial.httprepo import httprepository
19 27 from mercurial import mail
20 28 from urllib2 import AbstractBasicAuthHandler, AbstractDigestAuthHandler
21 29
22 30 # mercurial.demandimport incompatibility workaround,
23 31 # would cause gnomekeyring, one of the possible
24 32 # keyring backends, not to work.
25 33 from mercurial.demandimport import ignore
26 34 if "gobject._gobject" not in ignore:
27 35 ignore.append("gobject._gobject")
28 36
29 37 import keyring
30 38 from urlparse import urlparse
31 39 import urllib2
32 40 import smtplib, socket
33 41 import os
34 42
35 43 KEYRING_SERVICE = "Mercurial"
36 44
37 45 ############################################################
38 46
39 47 def monkeypatch_method(cls,fname=None):
40 48 def decorator(func):
41 49 local_fname = fname
42 50 if local_fname is None:
43 51 local_fname = func.__name__
44 52 setattr(func, "orig", getattr(cls, local_fname, None))
45 53 setattr(cls, local_fname, func)
46 54 return func
47 55 return decorator
48 56
49 57 ############################################################
50 58
51 59 class PasswordStore(object):
52 60 """
53 61 Helper object handling keyring usage (password save&restore,
54 62 the way passwords are keyed in the keyring).
55 63 """
56 64 def __init__(self):
57 65 self.cache = dict()
58 66 def get_http_password(self, url, username):
59 67 return keyring.get_password(KEYRING_SERVICE,
60 68 self._format_http_key(url, username))
61 69 def set_http_password(self, url, username, password):
62 70 keyring.set_password(KEYRING_SERVICE,
63 71 self._format_http_key(url, username),
64 72 password)
65 73 def clear_http_password(self, url, username):
66 74 self.set_http_password(url, username, "")
67 75 def _format_http_key(self, url, username):
68 76 return "%s@@%s" % (username, url)
69 77 def get_smtp_password(self, machine, port, username):
70 78 return keyring.get_password(
71 79 KEYRING_SERVICE,
72 80 self._format_smtp_key(machine, port, username))
73 81 def set_smtp_password(self, machine, port, username, password):
74 82 keyring.set_password(
75 83 KEYRING_SERVICE,
76 84 self._format_smtp_key(machine, port, username),
77 85 password)
78 86 def clear_smtp_password(self, machine, port, username):
79 87 self.set_smtp_password(url, username, "")
80 88 def _format_smtp_key(self, machine, port, username):
81 89 return "%s@@%s:%s" % (username, machine, str(port))
82 90
83 91 password_store = PasswordStore()
84 92
85 93 ############################################################
86 94
87 95 def _debug(ui, msg):
88 96 ui.debug("[HgKeyring] " + msg + "\n")
89 97
90 98 def _debug_reply(ui, msg, url, user, pwd):
91 99 _debug(ui, "%s. Url: %s, user: %s, passwd: %s" % (
92 100 msg, url, user, pwd and '*' * len(pwd) or 'not set'))
93 101
94 102
95 103 ############################################################
96 104
97 105 class HTTPPasswordHandler(object):
98 106 """
99 107 Actual implementation of password handling (user prompting,
100 108 configuration file searching, keyring save&restore).
101 109
102 110 Object of this class is bound as passwordmgr attribute.
103 111 """
104 112 def __init__(self):
105 113 self.pwd_cache = {}
106 114 self.last_reply = None
107 115
108 116 def find_auth(self, pwmgr, realm, authuri, req):
109 117 """
110 118 Actual implementation of find_user_password - different
111 119 ways of obtaining the username and password.
112 120 """
113 121 ui = pwmgr.ui
114 122
115 123 # If we are called again just after identical previous
116 124 # request, then the previously returned auth must have been
117 125 # wrong. So we note this to force password prompt (and avoid
118 126 # reusing bad password indifinitely).
119 127 after_bad_auth = (self.last_reply \
120 128 and (self.last_reply['realm'] == realm) \
121 129 and (self.last_reply['authuri'] == authuri) \
122 130 and (self.last_reply['req'] == req))
123 131 if after_bad_auth:
124 132 _debug(ui, _("Working after bad authentication, cached passwords not used %s") % str(self.last_reply))
125 133
126 134 # Strip arguments to get actual remote repository url.
127 135 base_url = self.canonical_url(authuri)
128 136
129 137 # Extracting possible username (or password)
130 138 # stored directly in repository url
131 139 user, pwd = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
132 140 pwmgr, realm, authuri)
133 141 if user and pwd:
134 142 _debug_reply(ui, _("Auth data found in repository URL"),
135 143 base_url, user, pwd)
136 144 self.last_reply = dict(realm=realm,authuri=authuri,user=user,req=req)
137 145 return user, pwd
138 146
139 147 # Loading .hg/hgrc [auth] section contents. If prefix is given,
140 148 # it will be used as a key to lookup password in the keyring.
141 149 auth_user, pwd, prefix_url = self.load_hgrc_auth(ui, base_url, user)
142 150 if prefix_url:
143 151 keyring_url = prefix_url
144 152 else:
145 153 keyring_url = base_url
146 154 _debug(ui, _("Keyring URL: %s") % keyring_url)
147 155
148 156 # Checking the memory cache (there may be many http calls per command)
149 157 cache_key = (realm, keyring_url)
150 158 if not after_bad_auth:
151 159 cached_auth = self.pwd_cache.get(cache_key)
152 160 if cached_auth:
153 161 user, pwd = cached_auth
154 162 _debug_reply(ui, _("Cached auth data found"),
155 163 base_url, user, pwd)
156 164 self.last_reply = dict(realm=realm,authuri=authuri,user=user,req=req)
157 165 return user, pwd
158 166
159 167 if auth_user:
160 168 if user and (user != auth_user):
161 169 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)))
162 170 user = auth_user
163 171 if pwd:
164 172 self.pwd_cache[cache_key] = user, pwd
165 173 _debug_reply(ui, _("Auth data set in .hg/hgrc"),
166 174 base_url, user, pwd)
167 175 self.last_reply = dict(realm=realm,authuri=authuri,user=user,req=req)
168 176 return user, pwd
169 177 else:
170 178 _debug(ui, _("Username found in .hg/hgrc: %s") % user)
171 179
172 180 # Loading password from keyring.
173 181 # Only if username is known (so we know the key) and we are
174 182 # not after failure (so we don't reuse the bad password).
175 183 if user and not after_bad_auth:
176 184 _debug(ui, _("Looking for password for user %s and url %s") % (user, keyring_url))
177 185 pwd = password_store.get_http_password(keyring_url, user)
178 186 if pwd:
179 187 self.pwd_cache[cache_key] = user, pwd
180 188 _debug_reply(ui, _("Keyring password found"),
181 189 base_url, user, pwd)
182 190 self.last_reply = dict(realm=realm,authuri=authuri,user=user,req=req)
183 191 return user, pwd
184 192 else:
185 193 _debug(ui, _("Password not present in the keyring"))
186 194
187 195 # Is the username permanently set?
188 196 fixed_user = (user and True or False)
189 197
190 198 # Last resort: interactive prompt
191 199 if not ui.interactive():
192 200 raise util.Abort(_('mercurial_keyring: http authorization required but program used in non-interactive mode'))
193 201
194 202 if not fixed_user:
195 203 ui.status(_("Username not specified in .hg/hgrc. Keyring will not be used.\n"))
196 204
197 205 ui.write(_("http authorization required\n"))
198 206 ui.status(_("realm: %s\n") % realm)
199 207 if fixed_user:
200 208 ui.write(_("user: %s (fixed in .hg/hgrc)\n" % user))
201 209 else:
202 210 user = ui.prompt(_("user:"), default=None)
203 211 pwd = ui.getpass(_("password: "))
204 212
205 213 if fixed_user:
206 214 # Saving password to the keyring.
207 215 # It is done only if username is permanently set.
208 216 # Otherwise we won't be able to find the password so it
209 217 # does not make much sense to preserve it
210 218 _debug(ui, _("Saving password for %s to keyring") % user)
211 219 password_store.set_http_password(keyring_url, user, pwd)
212 220
213 221 # Saving password to the memory cache
214 222 self.pwd_cache[cache_key] = user, pwd
215 223
216 224 _debug_reply(ui, _("Manually entered password"),
217 225 base_url, user, pwd)
218 226 self.last_reply = dict(realm=realm,authuri=authuri,user=user,req=req)
219 227 return user, pwd
220 228
221 229 def load_hgrc_auth(self, ui, base_url, user):
222 230 """
223 231 Loading [auth] section contents from local .hgrc
224 232
225 233 Returns (username, password, prefix) tuple (every
226 234 element can be None)
227 235 """
228 236 # Theoretically 3 lines below should do:
229 237
230 238 #auth_token = self.readauthtoken(base_url)
231 239 #if auth_token:
232 240 # user, pwd = auth.get('username'), auth.get('password')
233 241
234 242 # Unfortunately they do not work, readauthtoken always return
235 243 # None. Why? Because ui (self.ui of passwordmgr) describes the
236 244 # *remote* repository, so does *not* contain any option from
237 245 # local .hg/hgrc.
238 246
239 247 # TODO: mercurial 1.4.2 is claimed to resolve this problem
240 248 # (thanks to: http://hg.xavamedia.nl/mercurial/crew/rev/fb45c1e4396f)
241 249 # so since this version workaround implemented below should
242 250 # not be necessary. As it will take some time until people
243 251 # migrate to >= 1.4.2, it would be best to implement
244 252 # workaround conditionally.
245 253
246 254 # Workaround: we recreate the repository object
247 255 repo_root = ui.config("bundle", "mainreporoot")
248 256
249 257 from mercurial.ui import ui as _ui
250 258 local_ui = _ui(ui)
251 259 if repo_root:
252 260 local_ui.readconfig(os.path.join(repo_root, ".hg", "hgrc"))
253 261 try:
254 262 local_passwordmgr = passwordmgr(local_ui)
255 263 auth_token = local_passwordmgr.readauthtoken(base_url)
256 264 except AttributeError:
257 265 try:
258 266 # hg 1.8
259 267 import mercurial.url
260 268 readauthforuri = mercurial.url.readauthforuri
261 269 except (ImportError, AttributeError):
262 270 # hg 1.9
263 271 import mercurial.httpconnection
264 272 readauthforuri = mercurial.httpconnection.readauthforuri
265 273 if readauthforuri.func_code.co_argcount == 3:
266 274 # Since hg.0593e8f81c71
267 275 res = readauthforuri(local_ui, base_url, user)
268 276 else:
269 277 res = readauthforuri(local_ui, base_url)
270 278 if res:
271 279 group, auth_token = res
272 280 else:
273 281 auth_token = None
274 282 if auth_token:
275 283 username = auth_token.get('username')
276 284 password = auth_token.get('password')
277 285 prefix = auth_token.get('prefix')
278 286 shortest_url = self.shortest_url(base_url, prefix)
279 287 return username, password, shortest_url
280 288
281 289 return None, None, None
282 290
283 291 def shortest_url(self, base_url, prefix):
284 292 if not prefix or prefix == '*':
285 293 return base_url
286 294 scheme, hostpath = base_url.split('://', 1)
287 295 p = prefix.split('://', 1)
288 296 if len(p) > 1:
289 297 prefix_host_path = p[1]
290 298 else:
291 299 prefix_host_path = prefix
292 300 shortest_url = scheme + '://' + prefix_host_path
293 301 return shortest_url
294 302
295 303 def canonical_url(self, authuri):
296 304 """
297 305 Strips query params from url. Used to convert urls like
298 306 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
299 307 to
300 308 https://repo.machine.com/repos/apps/module
301 309 """
302 310 parsed_url = urlparse(authuri)
303 311 return "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
304 312 parsed_url.path)
305 313
306 314 ############################################################
307 315
308 316 @monkeypatch_method(passwordmgr)
309 317 def find_user_password(self, realm, authuri):
310 318 """
311 319 keyring-based implementation of username/password query
312 320 for HTTP(S) connections
313 321
314 322 Passwords are saved in gnome keyring, OSX/Chain or other platform
315 323 specific storage and keyed by the repository url
316 324 """
317 325 # Extend object attributes
318 326 if not hasattr(self, '_pwd_handler'):
319 327 self._pwd_handler = HTTPPasswordHandler()
320 328
321 329 if hasattr(self, '_http_req'):
322 330 req = self._http_req
323 331 else:
324 332 req = None
325 333
326 334 return self._pwd_handler.find_auth(self, realm, authuri, req)
327 335
328 336 @monkeypatch_method(AbstractBasicAuthHandler, "http_error_auth_reqed")
329 337 def basic_http_error_auth_reqed(self, authreq, host, req, headers):
330 338 self.passwd._http_req = req
331 339 try:
332 340 return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
333 341 finally:
334 342 self.passwd._http_req = None
335 343
336 344 @monkeypatch_method(AbstractDigestAuthHandler, "http_error_auth_reqed")
337 345 def digest_http_error_auth_reqed(self, authreq, host, req, headers):
338 346 self.passwd._http_req = req
339 347 try:
340 348 return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
341 349 finally:
342 350 self.passwd._http_req = None
343 351
344 352 ############################################################
345 353
346 354 def try_smtp_login(ui, smtp_obj, username, password):
347 355 """
348 356 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
349 357 password.
350 358
351 359 Returns:
352 360 - True if login succeeded
353 361 - False if login failed due to the wrong credentials
354 362
355 363 Throws Abort exception if login failed for any other reason.
356 364
357 365 Immediately returns False if password is empty
358 366 """
359 367 if not password:
360 368 return False
361 369 try:
362 370 ui.note(_('(authenticating to mail server as %s)\n') %
363 371 (username))
364 372 smtp_obj.login(username, password)
365 373 return True
366 374 except smtplib.SMTPException, inst:
367 375 if inst.smtp_code == 535:
368 376 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
369 377 return False
370 378 else:
371 379 raise util.Abort(inst)
372 380
373 381 def keyring_supported_smtp(ui, username):
374 382 """
375 383 keyring-integrated replacement for mercurial.mail._smtp
376 384 Used only when configuration file contains username, but
377 385 does not contain the password.
378 386
379 387 Most of the routine below is copied as-is from
380 388 mercurial.mail._smtp. The only changed part is
381 389 marked with #>>>>> and #<<<<< markers
382 390 """
383 391 local_hostname = ui.config('smtp', 'local_hostname')
384 392 s = smtplib.SMTP(local_hostname=local_hostname)
385 393 mailhost = ui.config('smtp', 'host')
386 394 if not mailhost:
387 395 raise util.Abort(_('no [smtp]host in hgrc - cannot send mail'))
388 396 mailport = int(ui.config('smtp', 'port', 25))
389 397 ui.note(_('sending mail: smtp host %s, port %s\n') %
390 398 (mailhost, mailport))
391 399 s.connect(host=mailhost, port=mailport)
392 400 if ui.configbool('smtp', 'tls'):
393 401 if not hasattr(socket, 'ssl'):
394 402 raise util.Abort(_("can't use TLS: Python SSL support "
395 403 "not installed"))
396 404 ui.note(_('(using tls)\n'))
397 405 s.ehlo()
398 406 s.starttls()
399 407 s.ehlo()
400 408
401 409 #>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
402 410 stored = password = password_store.get_smtp_password(
403 411 mailhost, mailport, username)
404 412 # No need to check whether password was found as try_smtp_login
405 413 # just returns False if it is absent.
406 414 while not try_smtp_login(ui, s, username, password):
407 415 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
408 416
409 417 if stored != password:
410 418 password_store.set_smtp_password(
411 419 mailhost, mailport, username, password)
412 420 #<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
413 421
414 422 def send(sender, recipients, msg):
415 423 try:
416 424 return s.sendmail(sender, recipients, msg)
417 425 except smtplib.SMTPRecipientsRefused, inst:
418 426 recipients = [r[1] for r in inst.recipients.values()]
419 427 raise util.Abort('\n' + '\n'.join(recipients))
420 428 except smtplib.SMTPException, inst:
421 429 raise util.Abort(inst)
422 430
423 431 return send
424 432
425 433 ############################################################
426 434
427 435 orig_smtp = mail._smtp
428 436
429 437 @monkeypatch_method(mail)
430 438 def _smtp(ui):
431 439 """
432 440 build an smtp connection and return a function to send email
433 441
434 442 This is the monkeypatched version of _smtp(ui) function from
435 443 mercurial/mail.py. It calls the original unless username
436 444 without password is given in the configuration.
437 445 """
438 446 username = ui.config('smtp', 'username')
439 447 password = ui.config('smtp', 'password')
440 448
441 449 if username and not password:
442 450 return keyring_supported_smtp(ui, username)
443 451 else:
444 452 return orig_smtp(ui)
General Comments 0
You need to be logged in to leave comments. Login now