##// END OF EJS Templates
Support new Mercurial password database...
Henrik Stuart -
r232:8b2977bc default
parent child Browse files
Show More
@@ -1,814 +1,818 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 urllib_user, urllib_pwd \
307 = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
308 pwmgr, realm, authuri)
306 if isinstance(pwmgr, urllib2.HTTPPasswordMgrWithDefaultRealm):
307 urllib_user, urllib_pwd = \
308 urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
309 pwmgr, realm, authuri)
310 else:
311 urllib_user, urllib_pwd = pwmgr.passwddb.find_user_password(
312 realm, authuri)
309 313 if urllib_user and urllib_pwd:
310 314 return urllib_user, urllib_pwd, self.SRC_URLCACHE, base_url
311 315
312 316 actual_user = url_user or urllib_user
313 317
314 318 # Consult configuration to normalize url to prefix, and find username
315 319 # (and maybe password)
316 320 auth_user, auth_pwd, keyring_url = self.get_url_config(
317 321 ui, parsed_url, actual_user)
318 322 if auth_user and actual_user and (actual_user != auth_user):
319 323 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 324 if auth_user and auth_pwd:
321 325 return auth_user, auth_pwd, self.SRC_CFGAUTH, keyring_url
322 326
323 327 actual_user = actual_user or auth_user
324 328
325 329 if skip_caches:
326 330 return actual_user, None, None, keyring_url
327 331
328 332 # Check memory cache (reuse )
329 333 # Checking the memory cache (there may be many http calls per command)
330 334 cached_pwd = self.pwd_cache.check(realm, keyring_url, actual_user)
331 335 if cached_pwd:
332 336 return actual_user, cached_pwd, self.SRC_MEMCACHE, keyring_url
333 337
334 338 # Load from keyring.
335 339 if actual_user:
336 340 ui.debug(_("keyring: looking for password (user %s, url %s)\n") % (actual_user, keyring_url))
337 341 keyring_pwd = password_store.get_http_password(keyring_url, actual_user)
338 342 if keyring_pwd:
339 343 return actual_user, keyring_pwd, self.SRC_KEYRING, keyring_url
340 344
341 345 return actual_user, None, None, keyring_url
342 346
343 347 @staticmethod
344 348 def prompt_interactively(ui, user, realm, url):
345 349 """Actual interactive prompt"""
346 350 if not ui.interactive():
347 351 raise util.Abort(_('keyring: http authorization required but program used in non-interactive mode'))
348 352
349 353 if not user:
350 354 ui.status(_("keyring: username not specified in hgrc (or in url). Password will not be saved.\n"))
351 355
352 356 ui.write(_("http authorization required\n"))
353 357 ui.status(_("realm: %s\n") % realm)
354 358 ui.status(_("url: %s\n") % url)
355 359 if user:
356 360 ui.write(_("user: %s (fixed in hgrc or url)\n" % user))
357 361 else:
358 362 user = ui.prompt(_("user:"), default=None)
359 363 pwd = ui.getpass(_("password: "))
360 364 return user, pwd
361 365
362 366 def find_auth(self, pwmgr, realm, authuri, req):
363 367 """
364 368 Actual implementation of find_user_password - different
365 369 ways of obtaining the username and password.
366 370
367 371 Returns pair username, password
368 372 """
369 373 ui = pwmgr.ui
370 374 after_bad_auth = self._after_bad_auth(ui, realm, authuri, req)
371 375
372 376 # Look in url, cache, etc
373 377 user, pwd, src, final_url = self.get_credentials(
374 378 pwmgr, realm, authuri, skip_caches=after_bad_auth)
375 379 if pwd:
376 380 if src != self.SRC_MEMCACHE:
377 381 self.pwd_cache.store(realm, final_url, user, pwd)
378 382 self._note_last_reply(realm, authuri, user, req)
379 383 _debug(ui, _("Password found in " + src))
380 384 return user, pwd
381 385
382 386 # Last resort: interactive prompt
383 387 user, pwd = self.prompt_interactively(ui, user, realm, final_url)
384 388
385 389 if user:
386 390 # Saving password to the keyring.
387 391 # It is done only if username is permanently set.
388 392 # Otherwise we won't be able to find the password so it
389 393 # does not make much sense to preserve it
390 394 _debug(ui, _("Saving password for %s to keyring") % user)
391 395 try:
392 396 password_store.set_http_password(final_url, user, pwd)
393 397 except keyring.errors.PasswordSetError, e:
394 398 ui.traceback()
395 399 ui.warn(_("warning: failed to save password in keyring\n"))
396 400
397 401 # Saving password to the memory cache
398 402 self.pwd_cache.store(realm, final_url, user, pwd)
399 403 self._note_last_reply(realm, authuri, user, req)
400 404 _debug(ui, _("Manually entered password"))
401 405 return user, pwd
402 406
403 407 def get_url_config(self, ui, parsed_url, user):
404 408 """
405 409 Checks configuration to decide whether/which username, prefix,
406 410 and password are configured for given url. Consults [auth] section.
407 411
408 412 Returns tuple (username, password, prefix) containing elements
409 413 found. username and password can be None (if unset), if prefix
410 414 is not found, url itself is returned.
411 415 """
412 416 base_url = str(parsed_url)
413 417
414 418 from mercurial.httpconnection import readauthforuri
415 419 _debug(ui, _("Checking for hgrc info about url %s, user %s") % (base_url, user))
416 420 res = readauthforuri(ui, base_url, user)
417 421 # If it user-less version not work, let's try with added username to handle
418 422 # both config conventions
419 423 if (not res) and user:
420 424 parsed_url.user = user
421 425 res = readauthforuri(ui, str(parsed_url), user)
422 426 parsed_url.user = None
423 427 if res:
424 428 group, auth_token = res
425 429 else:
426 430 auth_token = None
427 431
428 432 if auth_token:
429 433 username = auth_token.get('username')
430 434 password = auth_token.get('password')
431 435 prefix = auth_token.get('prefix')
432 436 else:
433 437 username = user
434 438 password = None
435 439 prefix = None
436 440
437 441 password_url = self.password_url(base_url, prefix)
438 442
439 443 _debug(ui, _("Password url: %s, user: %s, password: %s (prefix: %s)") % (
440 444 password_url, username, '********' if password else '', prefix))
441 445
442 446 return username, password, password_url
443 447
444 448 def _note_last_reply(self, realm, authuri, user, req):
445 449 """
446 450 Internal helper. Saves info about auth-data obtained,
447 451 preserves them in last_reply, and returns pair user, pwd
448 452 """
449 453 self.last_reply = dict(realm=realm, authuri=authuri,
450 454 user=user, req=req)
451 455
452 456 def _after_bad_auth(self, ui, realm, authuri, req):
453 457 """
454 458 If we are called again just after identical previous
455 459 request, then the previously returned auth must have been
456 460 wrong. So we note this to force password prompt (and avoid
457 461 reusing bad password indefinitely).
458 462
459 463 This routine checks for this condition.
460 464 """
461 465 if self.last_reply:
462 466 if (self.last_reply['realm'] == realm) \
463 467 and (self.last_reply['authuri'] == authuri) \
464 468 and (self.last_reply['req'] == req):
465 469 _debug(ui, _("Working after bad authentication, cached passwords not used %s") % str(self.last_reply))
466 470 return True
467 471 return False
468 472
469 473 @staticmethod
470 474 def password_url(base_url, prefix):
471 475 """Calculates actual url identifying the password. Takes
472 476 configured prefix under consideration (so can be shorter
473 477 than repo url)"""
474 478 if not prefix or prefix == '*':
475 479 return base_url
476 480 scheme, hostpath = base_url.split('://', 1)
477 481 p = prefix.split('://', 1)
478 482 if len(p) > 1:
479 483 prefix_host_path = p[1]
480 484 else:
481 485 prefix_host_path = prefix
482 486 password_url = scheme + '://' + prefix_host_path
483 487 return password_url
484 488
485 489 @staticmethod
486 490 def unpack_url(authuri):
487 491 """
488 492 Takes original url for which authentication is attempted and:
489 493
490 494 - Strips query params from url. Used to convert urls like
491 495 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
492 496 to
493 497 https://repo.machine.com/repos/apps/module
494 498
495 499 - Extracts username and password, if present, and removes them from url
496 500 (so prefix matching works properly)
497 501
498 502 Returns url, user, password
499 503 where url is mercurial.util.url object already stripped of all those
500 504 params.
501 505 """
502 506 # mercurial.util.url, rather handy url parser
503 507 parsed_url = util.url(authuri)
504 508 parsed_url.query = ''
505 509 parsed_url.fragment = None
506 510 # Strip arguments to get actual remote repository url.
507 511 # base_url = "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
508 512 # parsed_url.path)
509 513 user = parsed_url.user
510 514 passwd = parsed_url.passwd
511 515 parsed_url.user = None
512 516 parsed_url.passwd = None
513 517
514 518 return parsed_url, user, passwd
515 519
516 520
517 521 ############################################################
518 522 # Mercurial monkey-patching
519 523 ############################################################
520 524
521 525
522 526 @monkeypatch_method(passwordmgr)
523 527 def find_user_password(self, realm, authuri):
524 528 """
525 529 keyring-based implementation of username/password query
526 530 for HTTP(S) connections
527 531
528 532 Passwords are saved in gnome keyring, OSX/Chain or other platform
529 533 specific storage and keyed by the repository url
530 534 """
531 535 # Extend object attributes
532 536 if not hasattr(self, '_pwd_handler'):
533 537 self._pwd_handler = HTTPPasswordHandler()
534 538
535 539 if hasattr(self, '_http_req'):
536 540 req = self._http_req
537 541 else:
538 542 req = None
539 543
540 544 return self._pwd_handler.find_auth(self, realm, authuri, req)
541 545
542 546
543 547 @monkeypatch_method(urllib2.AbstractBasicAuthHandler, "http_error_auth_reqed")
544 548 def basic_http_error_auth_reqed(self, authreq, host, req, headers):
545 549 """Preserves current HTTP request so it can be consulted
546 550 in find_user_password above"""
547 551 self.passwd._http_req = req
548 552 try:
549 553 return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
550 554 finally:
551 555 self.passwd._http_req = None
552 556
553 557
554 558 @monkeypatch_method(urllib2.AbstractDigestAuthHandler, "http_error_auth_reqed")
555 559 def digest_http_error_auth_reqed(self, authreq, host, req, headers):
556 560 """Preserves current HTTP request so it can be consulted
557 561 in find_user_password above"""
558 562 self.passwd._http_req = req
559 563 try:
560 564 return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
561 565 finally:
562 566 self.passwd._http_req = None
563 567
564 568 ############################################################
565 569 # SMTP support
566 570 ############################################################
567 571
568 572
569 573 def try_smtp_login(ui, smtp_obj, username, password):
570 574 """
571 575 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
572 576 password.
573 577
574 578 Returns:
575 579 - True if login succeeded
576 580 - False if login failed due to the wrong credentials
577 581
578 582 Throws Abort exception if login failed for any other reason.
579 583
580 584 Immediately returns False if password is empty
581 585 """
582 586 if not password:
583 587 return False
584 588 try:
585 589 ui.note(_('(authenticating to mail server as %s)\n') %
586 590 (username))
587 591 smtp_obj.login(username, password)
588 592 return True
589 593 except smtplib.SMTPException, inst:
590 594 if inst.smtp_code == 535:
591 595 ui.status(_("SMTP login failed: %s\n\n") % inst.smtp_error)
592 596 return False
593 597 else:
594 598 raise util.Abort(inst)
595 599
596 600
597 601 def keyring_supported_smtp(ui, username):
598 602 """
599 603 keyring-integrated replacement for mercurial.mail._smtp
600 604 Used only when configuration file contains username, but
601 605 does not contain the password.
602 606
603 607 Most of the routine below is copied as-is from
604 608 mercurial.mail._smtp. The only changed part is
605 609 marked with # >>>>> and # <<<<< markers
606 610 """
607 611 local_hostname = ui.config('smtp', 'local_hostname')
608 612 tls = ui.config('smtp', 'tls', 'none')
609 613 # backward compatible: when tls = true, we use starttls.
610 614 starttls = tls == 'starttls' or util.parsebool(tls)
611 615 smtps = tls == 'smtps'
612 616 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
613 617 raise util.Abort(_("can't use TLS: Python SSL support not installed"))
614 618 mailhost = ui.config('smtp', 'host')
615 619 if not mailhost:
616 620 raise util.Abort(_('smtp.host not configured - cannot send mail'))
617 621 verifycert = ui.config('smtp', 'verifycert', 'strict')
618 622 if verifycert not in ['strict', 'loose']:
619 623 if util.parsebool(verifycert) is not False:
620 624 raise util.Abort(_('invalid smtp.verifycert configuration: %s')
621 625 % (verifycert))
622 626 verifycert = False
623 627 if getattr(sslutil, 'sslkwargs', None) is None:
624 628 sslkwargs = None
625 629 elif (starttls or smtps) and verifycert:
626 630 sslkwargs = sslutil.sslkwargs(ui, mailhost)
627 631 else:
628 632 sslkwargs = {}
629 633 if smtps:
630 634 ui.note(_('(using smtps)\n'))
631 635
632 636 # mercurial 3.8 added a mandatory host arg
633 637 if not sslkwargs:
634 638 s = SMTPS(ui, local_hostname=local_hostname, host=mailhost)
635 639 elif 'host' in SMTPS.__init__.__code__.co_varnames:
636 640 s = SMTPS(sslkwargs, local_hostname=local_hostname, host=mailhost)
637 641 else:
638 642 s = SMTPS(sslkwargs, local_hostname=local_hostname)
639 643 elif starttls:
640 644 if not sslkwargs:
641 645 s = STARTTLS(ui, local_hostname=local_hostname, host=mailhost)
642 646 elif 'host' in STARTTLS.__init__.__code__.co_varnames:
643 647 s = STARTTLS(sslkwargs, local_hostname=local_hostname, host=mailhost)
644 648 else:
645 649 s = STARTTLS(sslkwargs, local_hostname=local_hostname)
646 650 else:
647 651 s = smtplib.SMTP(local_hostname=local_hostname)
648 652 if smtps:
649 653 defaultport = 465
650 654 else:
651 655 defaultport = 25
652 656 mailport = util.getport(ui.config('smtp', 'port', defaultport))
653 657 ui.note(_('sending mail: smtp host %s, port %s\n') %
654 658 (mailhost, mailport))
655 659 s.connect(host=mailhost, port=mailport)
656 660 if starttls:
657 661 ui.note(_('(using starttls)\n'))
658 662 s.ehlo()
659 663 s.starttls()
660 664 s.ehlo()
661 665 if (starttls or smtps) and verifycert:
662 666 ui.note(_('(verifying remote certificate)\n'))
663 667 if getattr(sslutil, 'validatesocket', None):
664 668 sslutil.validatesocket(s.sock)
665 669 else:
666 670 validator(ui, mailhost)(s.sock, verifycert == 'strict')
667 671
668 672 # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
669 673 stored = password = password_store.get_smtp_password(
670 674 mailhost, mailport, username)
671 675 # No need to check whether password was found as try_smtp_login
672 676 # just returns False if it is absent.
673 677 while not try_smtp_login(ui, s, username, password):
674 678 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
675 679
676 680 if stored != password:
677 681 password_store.set_smtp_password(
678 682 mailhost, mailport, username, password)
679 683 # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
680 684
681 685 def send(sender, recipients, msg):
682 686 try:
683 687 return s.sendmail(sender, recipients, msg)
684 688 except smtplib.SMTPRecipientsRefused, inst:
685 689 recipients = [r[1] for r in inst.recipients.values()]
686 690 raise util.Abort('\n' + '\n'.join(recipients))
687 691 except smtplib.SMTPException, inst:
688 692 raise util.Abort(inst)
689 693
690 694 return send
691 695
692 696 ############################################################
693 697 # SMTP monkeypatching
694 698 ############################################################
695 699
696 700
697 701 @monkeypatch_method(mail)
698 702 def _smtp(ui):
699 703 """
700 704 build an smtp connection and return a function to send email
701 705
702 706 This is the monkeypatched version of _smtp(ui) function from
703 707 mercurial/mail.py. It calls the original unless username
704 708 without password is given in the configuration.
705 709 """
706 710 username = ui.config('smtp', 'username')
707 711 password = ui.config('smtp', 'password')
708 712
709 713 if username and not password:
710 714 return keyring_supported_smtp(ui, username)
711 715 else:
712 716 return _smtp.orig(ui)
713 717
714 718
715 719 ############################################################
716 720 # Custom commands
717 721 ############################################################
718 722
719 723 cmdtable = {}
720 724 command = meu.command(cmdtable)
721 725
722 726
723 727 @command('keyring_check',
724 728 [],
725 729 _("keyring_check [PATH]"),
726 730 optionalrepo=True)
727 731 def cmd_keyring_check(ui, repo, *path_args, **opts): # pylint: disable=unused-argument
728 732 """
729 733 Prints basic info (whether password is currently saved, and how is
730 734 it identified) for given path.
731 735
732 736 Can be run without parameters to show status for all (current repository) paths which
733 737 are HTTP-like.
734 738 """
735 739 defined_paths = [(name, url)
736 740 for name, url in ui.configitems('paths')]
737 741 if path_args:
738 742 # Maybe parameter is an alias
739 743 defined_paths_dic = dict(defined_paths)
740 744 paths = [(path_arg, defined_paths_dic.get(path_arg, path_arg))
741 745 for path_arg in path_args]
742 746 else:
743 747 if not repo:
744 748 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"))
745 749 return
746 750 paths = [(name, url) for name, url in defined_paths]
747 751
748 752 if not paths:
749 753 ui.status(_("keyring_check: no paths defined\n"))
750 754 return
751 755
752 756 handler = HTTPPasswordHandler()
753 757
754 758 ui.status(_("keyring password save status:\n"))
755 759 for name, url in paths:
756 760 if not is_http_path(url):
757 761 if path_args:
758 762 ui.status(_(" %s: non-http path (%s)\n") % (name, url))
759 763 continue
760 764 user, pwd, source, final_url = handler.get_credentials(
761 765 passwordmgr(ui), name, url)
762 766 if pwd:
763 767 ui.status(_(" %s: password available, source: %s, bound to user %s, url %s\n") % (
764 768 name, source, user, final_url))
765 769 elif user:
766 770 ui.status(_(" %s: password not available, once entered, will be bound to user %s, url %s\n") % (
767 771 name, user, final_url))
768 772 else:
769 773 ui.status(_(" %s: password not available, user unknown, url %s\n") % (
770 774 name, final_url))
771 775
772 776
773 777 @command('keyring_clear',
774 778 [],
775 779 _('hg keyring_clear PATH-OR-ALIAS'),
776 780 optionalrepo=True)
777 781 def cmd_keyring_clear(ui, repo, path, **opts): # pylint: disable=unused-argument
778 782 """
779 783 Drops password bound to given path (if any is saved).
780 784
781 785 Parameter can be given as full url (``https://John@bitbucket.org``) or as the name
782 786 of path alias (``bitbucket``).
783 787 """
784 788 path_url = path
785 789 for name, url in ui.configitems('paths'):
786 790 if name == path:
787 791 path_url = url
788 792 break
789 793 if not is_http_path(path_url):
790 794 ui.status(_("%s is not a http path (and %s can't be resolved as path alias)\n") % (path, path_url))
791 795 return
792 796
793 797 handler = HTTPPasswordHandler()
794 798
795 799 user, pwd, source, final_url = handler.get_credentials(
796 800 passwordmgr(ui), path, path_url)
797 801 if not user:
798 802 ui.status(_("Username not configured for url %s\n") % final_url)
799 803 return
800 804 if not pwd:
801 805 ui.status(_("No password is saved for user %s, url %s\n") % (
802 806 user, final_url))
803 807 return
804 808
805 809 if source != handler.SRC_KEYRING:
806 810 ui.status(_("Password for user %s, url %s is saved in %s, not in keyring\n") % (
807 811 user, final_url, source))
808 812
809 813 password_store.clear_http_password(final_url, user)
810 814 ui.status(_("Password removed for user %s, url %s\n") % (
811 815 user, final_url))
812 816
813 817
814 818 buglink = 'https://bitbucket.org/Mekk/mercurial_keyring/issues'
General Comments 0
You need to be logged in to leave comments. Login now