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