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