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