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