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