##// END OF EJS Templates
Final py3 fixes
Marcin Kasperski -
r277:79f687aa default
parent child Browse files
Show More
@@ -1,883 +1,887 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, b"")
188 188
189 189 @staticmethod
190 190 def _format_http_key(url, username):
191 191 """Construct actual key for password identification"""
192 192 # keyring expects str, mercurial feeds as here mostly with bytes
193 193 key = "%s@@%s" % (meu.pycompat.sysstr(username),
194 194 meu.pycompat.sysstr(url))
195 195 return key
196 196
197 197 @staticmethod
198 198 def _format_smtp_key(machine, port, username):
199 199 """Construct key for SMTP password identification"""
200 200 key = "%s@@%s:%s" % (meu.pycompat.sysstr(username),
201 201 meu.pycompat.sysstr(machine),
202 202 str(port))
203 203 return key
204 204
205 205 def get_smtp_password(self, machine, port, username):
206 206 """Checks for SMTP password in keyring, returns
207 207 password or None"""
208 208 return self._read_password_from_keyring(
209 209 self._format_smtp_key(machine, port, username))
210 210
211 211 def set_smtp_password(self, machine, port, username, password):
212 212 """Saves SMTP password to keyring"""
213 213 self._save_password_to_keyring(
214 214 self._format_smtp_key(machine, port, username),
215 215 password)
216 216
217 217 def clear_smtp_password(self, machine, port, username):
218 218 """Drops saved SMTP password"""
219 219 self.set_smtp_password(machine, port, username, "")
220 220
221 221 @staticmethod
222 222 def _read_password_from_keyring(pwdkey):
223 223 """Physically read from keyring"""
224 224 keyring = import_keyring()
225 225 try:
226 226 password = keyring.get_password(KEYRING_SERVICE, pwdkey)
227 227 except Exception as err:
228 228 ui = uimod.ui()
229 229 ui.warn(meu.ui_string(
230 230 "keyring: keyring backend doesn't seem to work, password can not be restored. Falling back to prompts. Error details: %s\n",
231 231 err))
232 232 return b''
233 233 # Reverse recoding from next routine
234 234 if isinstance(password, meu.pycompat.unicode):
235 235 return encoding.tolocal(password.encode('utf-8'))
236 236 return password
237 237
238 238 @staticmethod
239 239 def _save_password_to_keyring(pwdkey, password):
240 240 """Physically write to keyring"""
241 241 keyring = import_keyring()
242 242 # keyring in general expects unicode.
243 # Mercurial provides "local" encoding. See #33
244 password = encoding.fromlocal(password).decode('utf-8')
243 # Mercurial provides "local" encoding. See #33.
244 # py3 adds even more fun as we get already unicode from getpass.
245 # To handle those quirks, let go through encoding.fromlocal in case we
246 # got bytestr here, this will handle both normal py2 and emergency py3 cases.
247 if isinstance(password, bytes):
248 password = encoding.fromlocal(password).decode('utf-8')
245 249 try:
246 250 keyring.set_password(
247 251 KEYRING_SERVICE, pwdkey, password)
248 252 except Exception as err:
249 253 ui = uimod.ui()
250 254 ui.warn(meu.ui_string(
251 255 "keyring: keyring backend doesn't seem to work, password was not saved. Error details: %s\n",
252 256 err))
253 257
254 258
255 259 password_store = PasswordStore()
256 260
257 261
258 262 ############################################################
259 263 # Various utils
260 264 ############################################################
261 265
262 266 class PwdCache(object):
263 267 """Short term cache, used to preserve passwords
264 268 if they are used twice during a command"""
265 269 def __init__(self):
266 270 self._cache = {}
267 271
268 272 def store(self, realm, url, user, pwd):
269 273 """Saves password"""
270 274 cache_key = (realm, url, user)
271 275 self._cache[cache_key] = pwd
272 276
273 277 def check(self, realm, url, user):
274 278 """Checks for cached password"""
275 279 cache_key = (realm, url, user)
276 280 return self._cache.get(cache_key)
277 281
278 282
279 283 _re_http_url = re.compile(b'^https?://')
280 284
281 285
282 286 def is_http_path(url):
283 287 return bool(_re_http_url.search(url))
284 288
285 289
286 290 def make_passwordmgr(ui):
287 291 """Constructing passwordmgr in a way compatible with various mercurials"""
288 292 if hasattr(ui, 'httppasswordmgrdb'):
289 293 return passwordmgr(ui, ui.httppasswordmgrdb)
290 294 else:
291 295 return passwordmgr(ui)
292 296
293 297 ############################################################
294 298 # HTTP password management
295 299 ############################################################
296 300
297 301
298 302 class HTTPPasswordHandler(object):
299 303 """
300 304 Actual implementation of password handling (user prompting,
301 305 configuration file searching, keyring save&restore).
302 306
303 307 Object of this class is bound as passwordmgr attribute.
304 308 """
305 309 def __init__(self):
306 310 self.pwd_cache = PwdCache()
307 311 self.last_reply = None
308 312
309 313 # Markers and also names used in debug notes. Password source
310 314 SRC_URL = "repository URL"
311 315 SRC_CFGAUTH = "hgrc"
312 316 SRC_MEMCACHE = "temporary cache"
313 317 SRC_URLCACHE = "urllib temporary cache"
314 318 SRC_KEYRING = "keyring"
315 319
316 320 def get_credentials(self, pwmgr, realm, authuri, skip_caches=False):
317 321 """
318 322 Looks up for user credentials in various places, returns them
319 323 and information about their source.
320 324
321 325 Used internally inside find_auth and inside informative
322 326 commands (thiis method doesn't cache, doesn't detect bad
323 327 passwords etc, doesn't prompt interactively, doesn't store
324 328 password in keyring).
325 329
326 330 Returns: user, password, SRC_*, actual_url
327 331
328 332 If not found, password and SRC is None, user can be given or
329 333 not, url is always set
330 334 """
331 335 ui = pwmgr.ui
332 336
333 337 parsed_url, url_user, url_passwd = self.unpack_url(authuri)
334 338 base_url = bytes(parsed_url)
335 339 ui.debug(meu.ui_string('keyring: base url: %s, url user: %s, url pwd: %s\n',
336 340 base_url, url_user, url_passwd and b'******' or b''))
337 341
338 342 # Extract username (or password) stored directly in url
339 343 if url_user and url_passwd:
340 344 return url_user, url_passwd, self.SRC_URL, base_url
341 345
342 346 # Extract data from urllib (in case it was already stored)
343 347 if isinstance(pwmgr, HTTPPasswordMgrWithDefaultRealm):
344 348 urllib_user, urllib_pwd = \
345 349 HTTPPasswordMgrWithDefaultRealm.find_user_password(
346 350 pwmgr, realm, authuri)
347 351 else:
348 352 urllib_user, urllib_pwd = pwmgr.passwddb.find_user_password(
349 353 realm, authuri)
350 354 if urllib_user and urllib_pwd:
351 355 return urllib_user, urllib_pwd, self.SRC_URLCACHE, base_url
352 356
353 357 actual_user = url_user or urllib_user
354 358
355 359 # Consult configuration to normalize url to prefix, and find username
356 360 # (and maybe password)
357 361 auth_user, auth_pwd, keyring_url = self.get_url_config(
358 362 ui, parsed_url, actual_user)
359 363 if auth_user and actual_user and (actual_user != auth_user):
360 364 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)))
361 365 if auth_user and auth_pwd:
362 366 return auth_user, auth_pwd, self.SRC_CFGAUTH, keyring_url
363 367
364 368 actual_user = actual_user or auth_user
365 369
366 370 if skip_caches:
367 371 return actual_user, None, None, keyring_url
368 372
369 373 # Check memory cache (reuse )
370 374 # Checking the memory cache (there may be many http calls per command)
371 375 cached_pwd = self.pwd_cache.check(realm, keyring_url, actual_user)
372 376 if cached_pwd:
373 377 return actual_user, cached_pwd, self.SRC_MEMCACHE, keyring_url
374 378
375 379 # Load from keyring.
376 380 if actual_user:
377 381 ui.debug(meu.ui_string("keyring: looking for password (user %s, url %s)\n",
378 382 actual_user, keyring_url))
379 383 keyring_pwd = password_store.get_http_password(keyring_url, actual_user)
380 384 if keyring_pwd:
381 385 return actual_user, keyring_pwd, self.SRC_KEYRING, keyring_url
382 386
383 387 return actual_user, None, None, keyring_url
384 388
385 389 @staticmethod
386 390 def prompt_interactively(ui, user, realm, url):
387 391 """Actual interactive prompt"""
388 392 if not ui.interactive():
389 393 raise error.Abort(_('keyring: http authorization required but program used in non-interactive mode'))
390 394
391 395 if not user:
392 396 ui.status(meu.ui_string("keyring: username not specified in hgrc (or in url). Password will not be saved.\n"))
393 397
394 398 ui.write(meu.ui_string("http authorization required\n"))
395 399 ui.status(meu.ui_string("realm: %s\n",
396 400 realm))
397 401 ui.status(meu.ui_string("url: %s\n",
398 402 url))
399 403 if user:
400 404 ui.write(meu.ui_string("user: %s (fixed in hgrc or url)\n",
401 405 user))
402 406 else:
403 407 user = ui.prompt(meu.ui_string("user:"),
404 408 default=None)
405 409 pwd = ui.getpass(meu.ui_string("password: "))
406 410 return user, pwd
407 411
408 412 def find_auth(self, pwmgr, realm, authuri, req):
409 413 """
410 414 Actual implementation of find_user_password - different
411 415 ways of obtaining the username and password.
412 416
413 417 Returns pair username, password
414 418 """
415 419 ui = pwmgr.ui
416 420 after_bad_auth = self._after_bad_auth(ui, realm, authuri, req)
417 421
418 422 # Look in url, cache, etc
419 423 user, pwd, src, final_url = self.get_credentials(
420 424 pwmgr, realm, authuri, skip_caches=after_bad_auth)
421 425 if pwd:
422 426 if src != self.SRC_MEMCACHE:
423 427 self.pwd_cache.store(realm, final_url, user, pwd)
424 428 self._note_last_reply(realm, authuri, user, req)
425 429 ui.debug(meu.ui_string("keyring: Password found in %s\n",
426 430 src))
427 431 return user, pwd
428 432
429 433 # Last resort: interactive prompt
430 434 user, pwd = self.prompt_interactively(ui, user, realm, final_url)
431 435
432 436 if user:
433 437 # Saving password to the keyring.
434 438 # It is done only if username is permanently set.
435 439 # Otherwise we won't be able to find the password so it
436 440 # does not make much sense to preserve it
437 441 ui.debug(meu.ui_string("keyring: Saving password for %s to keyring\n",
438 442 user))
439 443 try:
440 444 password_store.set_http_password(final_url, user, pwd)
441 445 except Exception as e:
442 446 keyring = import_keyring()
443 447 if isinstance(e, keyring.errors.PasswordSetError):
444 448 ui.traceback()
445 449 ui.warn(meu.ui_string("warning: failed to save password in keyring\n"))
446 450 else:
447 451 raise e
448 452
449 453 # Saving password to the memory cache
450 454 self.pwd_cache.store(realm, final_url, user, pwd)
451 455 self._note_last_reply(realm, authuri, user, req)
452 456 ui.debug(meu.ui_string("keyring: Manually entered password\n"))
453 457 return user, pwd
454 458
455 459 def get_url_config(self, ui, parsed_url, user):
456 460 """
457 461 Checks configuration to decide whether/which username, prefix,
458 462 and password are configured for given url. Consults [auth] section.
459 463
460 464 Returns tuple (username, password, prefix) containing elements
461 465 found. username and password can be None (if unset), if prefix
462 466 is not found, url itself is returned.
463 467 """
464 468 from mercurial.httpconnection import readauthforuri
465 469 ui.debug(meu.ui_string("keyring: checking for hgrc info about url %s, user %s\n",
466 470 parsed_url, user))
467 471
468 472 res = readauthforuri(ui, bytes(parsed_url), user)
469 473 # If it user-less version not work, let's try with added username to handle
470 474 # both config conventions
471 475 if (not res) and user:
472 476 parsed_url.user = user
473 477 res = readauthforuri(ui, bytes(parsed_url), user)
474 478 parsed_url.user = None
475 479 if res:
476 480 group, auth_token = res
477 481 else:
478 482 auth_token = None
479 483
480 484 if auth_token:
481 485 username = auth_token.get(b'username')
482 486 password = auth_token.get(b'password')
483 487 prefix = auth_token.get(b'prefix')
484 488 else:
485 489 username = user
486 490 password = None
487 491 prefix = None
488 492
489 493 password_url = self.password_url(bytes(parsed_url), prefix)
490 494
491 495 ui.debug(meu.ui_string("keyring: Password url: %s, user: %s, password: %s (prefix: %s)\n",
492 496 password_url, username,
493 497 b'********' if password else b'',
494 498 prefix))
495 499
496 500 return username, password, password_url
497 501
498 502 def _note_last_reply(self, realm, authuri, user, req):
499 503 """
500 504 Internal helper. Saves info about auth-data obtained,
501 505 preserves them in last_reply, and returns pair user, pwd
502 506 """
503 507 self.last_reply = dict(realm=realm, authuri=authuri,
504 508 user=user, req=req)
505 509
506 510 def _after_bad_auth(self, ui, realm, authuri, req):
507 511 """
508 512 If we are called again just after identical previous
509 513 request, then the previously returned auth must have been
510 514 wrong. So we note this to force password prompt (and avoid
511 515 reusing bad password indefinitely).
512 516
513 517 This routine checks for this condition.
514 518 """
515 519 if self.last_reply:
516 520 if (self.last_reply['realm'] == realm) \
517 521 and (self.last_reply['authuri'] == authuri) \
518 522 and (self.last_reply['req'] == req):
519 523 ui.debug(meu.ui_string(
520 524 "keyring: Working after bad authentication, cached passwords not used %s\n",
521 525 str(self.last_reply)))
522 526 return True
523 527 return False
524 528
525 529 @staticmethod
526 530 def password_url(base_url, prefix):
527 531 """Calculates actual url identifying the password. Takes
528 532 configured prefix under consideration (so can be shorter
529 533 than repo url)"""
530 534 if not prefix or prefix == b'*':
531 535 return base_url
532 536 scheme, hostpath = base_url.split(b'://', 1)
533 537 p = prefix.split(b'://', 1)
534 538 if len(p) > 1:
535 539 prefix_host_path = p[1]
536 540 else:
537 541 prefix_host_path = prefix
538 542 password_url = scheme + b'://' + prefix_host_path
539 543 return password_url
540 544
541 545 @staticmethod
542 546 def unpack_url(authuri):
543 547 """
544 548 Takes original url for which authentication is attempted and:
545 549
546 550 - Strips query params from url. Used to convert urls like
547 551 https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
548 552 to
549 553 https://repo.machine.com/repos/apps/module
550 554
551 555 - Extracts username and password, if present, and removes them from url
552 556 (so prefix matching works properly)
553 557
554 558 Returns url, user, password
555 559 where url is mercurial.util.url object already stripped of all those
556 560 params.
557 561 """
558 562 # In case of py3, util.url expects bytes
559 563 authuri = meu.pycompat.bytestr(authuri)
560 564
561 565 # mercurial.util.url, rather handy url parser
562 566 parsed_url = util.url(authuri)
563 567 parsed_url.query = b''
564 568 parsed_url.fragment = None
565 569 # Strip arguments to get actual remote repository url.
566 570 # base_url = "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
567 571 # parsed_url.path)
568 572 user = parsed_url.user
569 573 passwd = parsed_url.passwd
570 574 parsed_url.user = None
571 575 parsed_url.passwd = None
572 576
573 577 return parsed_url, user, passwd
574 578
575 579
576 580 ############################################################
577 581 # Mercurial monkey-patching
578 582 ############################################################
579 583
580 584
581 585 @monkeypatch_method(passwordmgr)
582 586 def find_user_password(self, realm, authuri):
583 587 """
584 588 keyring-based implementation of username/password query
585 589 for HTTP(S) connections
586 590
587 591 Passwords are saved in gnome keyring, OSX/Chain or other platform
588 592 specific storage and keyed by the repository url
589 593 """
590 594 # In sync with hg 5.0
591 595 assert isinstance(realm, (type(None), str))
592 596 assert isinstance(authuri, str)
593 597
594 598 # Extend object attributes
595 599 if not hasattr(self, '_pwd_handler'):
596 600 self._pwd_handler = HTTPPasswordHandler()
597 601
598 602 if hasattr(self, '_http_req'):
599 603 req = self._http_req
600 604 else:
601 605 req = None
602 606
603 607 return self._pwd_handler.find_auth(self, realm, authuri, req)
604 608
605 609
606 610 @monkeypatch_method(AbstractBasicAuthHandler, "http_error_auth_reqed")
607 611 def basic_http_error_auth_reqed(self, authreq, host, req, headers):
608 612 """Preserves current HTTP request so it can be consulted
609 613 in find_user_password above"""
610 614 self.passwd._http_req = req
611 615 try:
612 616 return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
613 617 finally:
614 618 self.passwd._http_req = None
615 619
616 620
617 621 @monkeypatch_method(AbstractDigestAuthHandler, "http_error_auth_reqed")
618 622 def digest_http_error_auth_reqed(self, authreq, host, req, headers):
619 623 """Preserves current HTTP request so it can be consulted
620 624 in find_user_password above"""
621 625 self.passwd._http_req = req
622 626 try:
623 627 return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
624 628 finally:
625 629 self.passwd._http_req = None
626 630
627 631 ############################################################
628 632 # SMTP support
629 633 ############################################################
630 634
631 635
632 636 def try_smtp_login(ui, smtp_obj, username, password):
633 637 """
634 638 Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
635 639 password.
636 640
637 641 Returns:
638 642 - True if login succeeded
639 643 - False if login failed due to the wrong credentials
640 644
641 645 Throws Abort exception if login failed for any other reason.
642 646
643 647 Immediately returns False if password is empty
644 648 """
645 649 if not password:
646 650 return False
647 651 try:
648 652 ui.note(_('(authenticating to mail server as %s)\n') %
649 653 username)
650 654 smtp_obj.login(username, password)
651 655 return True
652 656 except smtplib.SMTPException as inst:
653 657 if inst.smtp_code == 535:
654 658 ui.status(meu.ui_string("SMTP login failed: %s\n\n",
655 659 inst.smtp_error))
656 660 return False
657 661 else:
658 662 raise error.Abort(inst)
659 663
660 664
661 665 def keyring_supported_smtp(ui, username):
662 666 """
663 667 keyring-integrated replacement for mercurial.mail._smtp Used only
664 668 when configuration file contains username, but does not contain
665 669 the password.
666 670
667 671 Most of the routine below is copied as-is from
668 672 mercurial.mail._smtp. The critical changed part is marked with #
669 673 >>>>> and # <<<<< markers, there are also some fixes which make
670 674 the code working on various Mercurials (like parsebool import).
671 675 """
672 676 try:
673 677 from mercurial.utils.stringutil import parsebool
674 678 except ImportError:
675 679 from mercurial.utils import parsebool
676 680
677 681 local_hostname = ui.config('smtp', 'local_hostname')
678 682 tls = ui.config('smtp', 'tls', 'none')
679 683 # backward compatible: when tls = true, we use starttls.
680 684 starttls = tls == 'starttls' or parsebool(tls)
681 685 smtps = tls == 'smtps'
682 686 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
683 687 raise error.Abort(_("can't use TLS: Python SSL support not installed"))
684 688 mailhost = ui.config('smtp', 'host')
685 689 if not mailhost:
686 690 raise error.Abort(_('smtp.host not configured - cannot send mail'))
687 691 if getattr(sslutil, 'sslkwargs', None) is None:
688 692 sslkwargs = None
689 693 elif starttls or smtps:
690 694 sslkwargs = sslutil.sslkwargs(ui, mailhost)
691 695 else:
692 696 sslkwargs = {}
693 697 if smtps:
694 698 ui.note(_('(using smtps)\n'))
695 699
696 700 # mercurial 3.8 added a mandatory host arg
697 701 if not sslkwargs:
698 702 s = SMTPS(ui, local_hostname=local_hostname, host=mailhost)
699 703 elif 'host' in SMTPS.__init__.__code__.co_varnames:
700 704 s = SMTPS(sslkwargs, local_hostname=local_hostname, host=mailhost)
701 705 else:
702 706 s = SMTPS(sslkwargs, local_hostname=local_hostname)
703 707 elif starttls:
704 708 if not sslkwargs:
705 709 s = STARTTLS(ui, local_hostname=local_hostname, host=mailhost)
706 710 elif 'host' in STARTTLS.__init__.__code__.co_varnames:
707 711 s = STARTTLS(sslkwargs, local_hostname=local_hostname, host=mailhost)
708 712 else:
709 713 s = STARTTLS(sslkwargs, local_hostname=local_hostname)
710 714 else:
711 715 s = smtplib.SMTP(local_hostname=local_hostname)
712 716 if smtps:
713 717 defaultport = 465
714 718 else:
715 719 defaultport = 25
716 720 mailport = util.getport(ui.config('smtp', 'port', defaultport))
717 721 ui.note(_('sending mail: smtp host %s, port %s\n') %
718 722 (mailhost, mailport))
719 723 s.connect(host=mailhost, port=mailport)
720 724 if starttls:
721 725 ui.note(_('(using starttls)\n'))
722 726 s.ehlo()
723 727 s.starttls()
724 728 s.ehlo()
725 729 if starttls or smtps:
726 730 if getattr(sslutil, 'validatesocket', None):
727 731 ui.note(_('(verifying remote certificate)\n'))
728 732 sslutil.validatesocket(s.sock)
729 733
730 734 # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
731 735 stored = password = password_store.get_smtp_password(
732 736 mailhost, mailport, username)
733 737 # No need to check whether password was found as try_smtp_login
734 738 # just returns False if it is absent.
735 739 while not try_smtp_login(ui, s, username, password):
736 740 password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
737 741
738 742 if stored != password:
739 743 password_store.set_smtp_password(
740 744 mailhost, mailport, username, password)
741 745 # <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
742 746
743 747 def send(sender, recipients, msg):
744 748 try:
745 749 return s.sendmail(sender, recipients, msg)
746 750 except smtplib.SMTPRecipientsRefused as inst:
747 751 recipients = [r[1] for r in inst.recipients.values()]
748 752 raise error.Abort('\n' + '\n'.join(recipients))
749 753 except smtplib.SMTPException as inst:
750 754 raise error.Abort(inst)
751 755
752 756 return send
753 757
754 758 ############################################################
755 759 # SMTP monkeypatching
756 760 ############################################################
757 761
758 762
759 763 @monkeypatch_method(mail)
760 764 def _smtp(ui):
761 765 """
762 766 build an smtp connection and return a function to send email
763 767
764 768 This is the monkeypatched version of _smtp(ui) function from
765 769 mercurial/mail.py. It calls the original unless username
766 770 without password is given in the configuration.
767 771 """
768 772 username = ui.config('smtp', 'username')
769 773 password = ui.config('smtp', 'password')
770 774
771 775 if username and not password:
772 776 return keyring_supported_smtp(ui, username)
773 777 else:
774 778 return _smtp.orig(ui)
775 779
776 780
777 781 ############################################################
778 782 # Custom commands
779 783 ############################################################
780 784
781 785 cmdtable = {}
782 786 command = meu.command(cmdtable)
783 787
784 788
785 789 @command(b'keyring_check',
786 790 [],
787 791 _("keyring_check [PATH]"),
788 792 optionalrepo=True)
789 793 def cmd_keyring_check(ui, repo, *path_args, **opts): # pylint: disable=unused-argument
790 794 """
791 795 Prints basic info (whether password is currently saved, and how is
792 796 it identified) for given path.
793 797
794 798 Can be run without parameters to show status for all (current repository) paths which
795 799 are HTTP-like.
796 800 """
797 801 defined_paths = [(name, url)
798 802 for name, url in ui.configitems(b'paths')]
799 803 if path_args:
800 804 # Maybe parameter is an alias
801 805 defined_paths_dic = dict(defined_paths)
802 806 paths = [(path_arg, defined_paths_dic.get(path_arg, path_arg))
803 807 for path_arg in path_args]
804 808 else:
805 809 if not repo:
806 810 ui.status(meu.ui_string("Url to check not specified. Either run ``hg keyring_check https://...``, or run the command inside some repository (to test all defined paths).\n"))
807 811 return
808 812 paths = [(name, url) for name, url in defined_paths]
809 813
810 814 if not paths:
811 815 ui.status(meu.ui_string("keyring_check: no paths defined\n"))
812 816 return
813 817
814 818 handler = HTTPPasswordHandler()
815 819
816 820 ui.status(meu.ui_string("keyring password save status:\n"))
817 821 for name, url in paths:
818 822 if not is_http_path(url):
819 823 if path_args:
820 824 ui.status(meu.ui_string(" %s: non-http path (%s)\n",
821 825 name, url))
822 826 continue
823 827 user, pwd, source, final_url = handler.get_credentials(
824 828 make_passwordmgr(ui), name, url)
825 829 if pwd:
826 830 ui.status(meu.ui_string(
827 831 " %s: password available, source: %s, bound to user %s, url %s\n",
828 832 name, source, user, final_url))
829 833 elif user:
830 834 ui.status(meu.ui_string(
831 835 " %s: password not available, once entered, will be bound to user %s, url %s\n",
832 836 name, user, final_url))
833 837 else:
834 838 ui.status(meu.ui_string(
835 839 " %s: password not available, user unknown, url %s\n",
836 840 name, final_url))
837 841
838 842
839 843 @command(b'keyring_clear',
840 844 [],
841 845 _('hg keyring_clear PATH-OR-ALIAS'),
842 846 optionalrepo=True)
843 847 def cmd_keyring_clear(ui, repo, path, **opts): # pylint: disable=unused-argument
844 848 """
845 849 Drops password bound to given path (if any is saved).
846 850
847 851 Parameter can be given as full url (``https://John@bitbucket.org``) or as the name
848 852 of path alias (``bitbucket``).
849 853 """
850 854 path_url = path
851 855 for name, url in ui.configitems(b'paths'):
852 856 if name == path:
853 857 path_url = url
854 858 break
855 859 if not is_http_path(path_url):
856 860 ui.status(meu.ui_string(
857 861 "%s is not a http path (and %s can't be resolved as path alias)\n",
858 862 path, path_url))
859 863 return
860 864
861 865 handler = HTTPPasswordHandler()
862 866
863 867 user, pwd, source, final_url = handler.get_credentials(
864 868 make_passwordmgr(ui), path, path_url)
865 869 if not user:
866 870 ui.status(meu.ui_string("Username not configured for url %s\n",
867 871 final_url))
868 872 return
869 873 if not pwd:
870 874 ui.status(meu.ui_string("No password is saved for user %s, url %s\n",
871 875 user, final_url))
872 876 return
873 877
874 878 if source != handler.SRC_KEYRING:
875 879 ui.status(meu.ui_string("Password for user %s, url %s is saved in %s, not in keyring\n",
876 880 user, final_url, source))
877 881
878 882 password_store.clear_http_password(final_url, user)
879 883 ui.status(meu.ui_string("Password removed for user %s, url %s\n",
880 884 user, final_url))
881 885
882 886
883 887 buglink = 'https://bitbucket.org/Mekk/mercurial_keyring/issues'
General Comments 0
You need to be logged in to leave comments. Login now