##// END OF EJS Templates
Patching various links bitbucket → heptapod
Patching various links bitbucket → heptapod

File last commit:

r283:e95e300e default
r283:e95e300e default
Show More
mercurial_keyring.py
887 lines | 32.5 KiB | text/x-python | PythonLexer
/ mercurial_keyring.py
Marcin Kasperski
eol
r175 # -*- coding: utf-8 -*-
#
# mercurial_keyring: save passwords in password database
#
# Copyright (c) 2009 Marcin Kasperski <Marcin.Kasperski@mekk.waw.pl>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# 3. The name of the author may not be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
Marcin Kasperski
Renaming README, so heptapod recognizes it
r282 # See README.rst for more details.
Marcin Kasperski
eol
r175
Marcin Kasperski
Dropping support for hg < 2.0 (old readauthoforuri)
r178 '''securely save HTTP and SMTP passwords to encrypted storage
mercurial_keyring securely saves HTTP and SMTP passwords in password
databases (Gnome Keyring, KDE KWallet, OSXKeyChain, Win32 crypto
services).
The process is automatic. Whenever bare Mercurial just prompts for
the password, Mercurial with mercurial_keyring enabled checks whether
saved password is available first. If so, it is used. If not, you
will be prompted for the password, but entered password will be
saved for the future use.
In case saved password turns out to be invalid (HTTP or SMTP login
fails) it is dropped, and you are asked for current password.
Actual password storage is implemented by Python keyring library, this
extension glues those services to Mercurial. Consult keyring
documentation for information how to configure actual password
backend (by default keyring guesses, usually correctly, for example
you get KDE Wallet under KDE, and Gnome Keyring under Gnome or Unity).
Marcin Kasperski
eol
r175 '''
Marcin Kasperski
Relocated non-mercurial imports before Mercurial ones, to perform them...
r225 import socket
import os
import sys
import re
Marcin Kasperski
More efficient logging entries. Some effort towards py3+hg5
r269 if sys.version_info[0] < 3:
from urllib2 import (
HTTPPasswordMgrWithDefaultRealm, AbstractBasicAuthHandler, AbstractDigestAuthHandler)
else:
from urllib.request import (
HTTPPasswordMgrWithDefaultRealm, AbstractBasicAuthHandler, AbstractDigestAuthHandler)
import smtplib
Marcin Kasperski
Relocated non-mercurial imports before Mercurial ones, to perform them...
r225
Marcin Kasperski
4.6 fix (one of…)
r250 from mercurial import util, sslutil, error
Marcin Kasperski
eol
r175 from mercurial.i18n import _
Marcin Kasperski
Dropping workaround necessary for Mercurial <1.1 or sth like that...
r176 from mercurial.url import passwordmgr
Marcin Kasperski
eol
r175 from mercurial import mail
from mercurial.mail import SMTPS, STARTTLS
from mercurial import encoding
Marcin Kasperski
#61 In case keyring library raises exceptions (like in recent cases when...
r258 from mercurial import ui as uimod
Marcin Kasperski
eol
r175
Marcin Kasperski
reworking readme
r181 # pylint: disable=invalid-name, line-too-long, protected-access, too-many-arguments
Marcin Kasperski
eol
r175
###########################################################################
# Specific import trickery
###########################################################################
def import_meu():
"""
Convoluted import of mercurial_extension_utils, which helps
TortoiseHg/Win setups. This routine and it's use below
performs equivalent of
from mercurial_extension_utils import monkeypatch_method
but looks for some non-path directories.
"""
try:
import mercurial_extension_utils
except ImportError:
my_dir = os.path.dirname(__file__)
sys.path.extend([
# In the same dir (manual or site-packages after pip)
my_dir,
# Developer clone
os.path.join(os.path.dirname(my_dir), "extension_utils"),
# Side clone
os.path.join(os.path.dirname(my_dir), "mercurial-extension_utils"),
])
try:
import mercurial_extension_utils
except ImportError:
Marcin Kasperski
4.6 fix (one of…)
r250 raise error.Abort(_("""Can not import mercurial_extension_utils.
Marcin Kasperski
eol
r175 Please install this module in Python path.
Marcin Kasperski
Patching various links bitbucket → heptapod
r283 See Installation chapter in https://foss.heptapod.net/mercurial/mercurial_keyring/ for details
Marcin Kasperski
eol
r175 (and for info about TortoiseHG on Windows, or other bundled Python)."""))
return mercurial_extension_utils
Marcin Kasperski
PEP
r256
Marcin Kasperski
eol
r175 meu = import_meu()
monkeypatch_method = meu.monkeypatch_method
def import_keyring():
"""
Importing keyring happens to be costly if wallet is slow, so we delay it
until it is really needed. The routine below also works around various
demandimport-related problems.
"""
# mercurial.demandimport incompatibility workaround.
# various keyring backends fail as they can't properly import helper
# modules (as demandimport modifies python import behaviour).
# If you get import errors with demandimport in backtrace, try
# guessing what to block and extending the list below.
Marcin Kasperski
Migrated demandimport and logging support to meu. Functionally:...
r200 mod, was_imported_now = meu.direct_import_ext(
"keyring", [
Marcin Kasperski
eol
r175 "gobject._gobject",
"configparser",
"json",
"abc",
"io",
"keyring",
"gdata.docs.service",
"gdata.service",
"types",
"atom.http",
"atom.http_interface",
"atom.service",
"atom.token_store",
"ctypes",
"secretstorage.exceptions",
"fs.opener",
Marcin Kasperski
Various py3 incompatibilities (syntax errors so far)
r268 "win32ctypes.pywin32",
"win32ctypes.pywin32.pywintypes",
"win32ctypes.pywin32.win32cred",
"pywintypes",
"win32cred",
Marcin Kasperski
Migrated demandimport and logging support to meu. Functionally:...
r200 ])
if was_imported_now:
# Shut up warning about uninitialized logging by keyring
meu.disable_logging("keyring")
return mod
Marcin Kasperski
eol
r175
Marcin Kasperski
PEP
r256
Marcin Kasperski
eol
r175 #################################################################
# Actual implementation
#################################################################
KEYRING_SERVICE = "Mercurial"
class PasswordStore(object):
"""
Helper object handling keyring usage (password save&restore,
the way passwords are keyed in the keyring).
"""
def __init__(self):
self.cache = dict()
def get_http_password(self, url, username):
"""
Checks whether password of username for url is available,
returns it or None
"""
return self._read_password_from_keyring(
self._format_http_key(url, username))
def set_http_password(self, url, username, password):
"""Saves password to keyring"""
self._save_password_to_keyring(
self._format_http_key(url, username),
password)
def clear_http_password(self, url, username):
"""Drops saved password"""
Marcin Kasperski
Preliminarly works under py3
r276 self.set_http_password(url, username, b"")
Marcin Kasperski
eol
r175
@staticmethod
def _format_http_key(url, username):
"""Construct actual key for password identification"""
Marcin Kasperski
Preliminarly works under py3
r276 # keyring expects str, mercurial feeds as here mostly with bytes
key = "%s@@%s" % (meu.pycompat.sysstr(username),
meu.pycompat.sysstr(url))
return key
@staticmethod
def _format_smtp_key(machine, port, username):
"""Construct key for SMTP password identification"""
key = "%s@@%s:%s" % (meu.pycompat.sysstr(username),
meu.pycompat.sysstr(machine),
str(port))
return key
Marcin Kasperski
eol
r175
def get_smtp_password(self, machine, port, username):
"""Checks for SMTP password in keyring, returns
password or None"""
return self._read_password_from_keyring(
self._format_smtp_key(machine, port, username))
def set_smtp_password(self, machine, port, username, password):
"""Saves SMTP password to keyring"""
self._save_password_to_keyring(
self._format_smtp_key(machine, port, username),
password)
def clear_smtp_password(self, machine, port, username):
"""Drops saved SMTP password"""
self.set_smtp_password(machine, port, username, "")
@staticmethod
def _read_password_from_keyring(pwdkey):
"""Physically read from keyring"""
keyring = import_keyring()
Marcin Kasperski
#61 In case keyring library raises exceptions (like in recent cases when...
r258 try:
password = keyring.get_password(KEYRING_SERVICE, pwdkey)
except Exception as err:
ui = uimod.ui()
Marcin Kasperski
Preliminarly works under py3
r276 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",
err))
return b''
Marcin Kasperski
eol
r175 # Reverse recoding from next routine
Marcin Kasperski
Various py3 incompatibilities
r272 if isinstance(password, meu.pycompat.unicode):
Marcin Kasperski
eol
r175 return encoding.tolocal(password.encode('utf-8'))
return password
@staticmethod
def _save_password_to_keyring(pwdkey, password):
"""Physically write to keyring"""
keyring = import_keyring()
# keyring in general expects unicode.
Marcin Kasperski
Final py3 fixes
r277 # Mercurial provides "local" encoding. See #33.
# py3 adds even more fun as we get already unicode from getpass.
# To handle those quirks, let go through encoding.fromlocal in case we
# got bytestr here, this will handle both normal py2 and emergency py3 cases.
if isinstance(password, bytes):
password = encoding.fromlocal(password).decode('utf-8')
Marcin Kasperski
#61 In case keyring library raises exceptions (like in recent cases when...
r258 try:
keyring.set_password(
KEYRING_SERVICE, pwdkey, password)
except Exception as err:
ui = uimod.ui()
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.warn(meu.ui_string(
"keyring: keyring backend doesn't seem to work, password was not saved. Error details: %s\n",
err))
Marcin Kasperski
eol
r175
Marcin Kasperski
PEP
r256
Marcin Kasperski
eol
r175 password_store = PasswordStore()
############################################################
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 # Various utils
Marcin Kasperski
eol
r175 ############################################################
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 class PwdCache(object):
"""Short term cache, used to preserve passwords
if they are used twice during a command"""
def __init__(self):
self._cache = {}
def store(self, realm, url, user, pwd):
"""Saves password"""
cache_key = (realm, url, user)
self._cache[cache_key] = pwd
def check(self, realm, url, user):
"""Checks for cached password"""
cache_key = (realm, url, user)
return self._cache.get(cache_key)
Marcin Kasperski
some subtle py3-related fixes (bytes vs str in config-related actions)
r275 _re_http_url = re.compile(b'^https?://')
Marcin Kasperski
Modifying pull request to be safer on old mercurials....
r206
Marcin Kasperski
PEP
r256
Marcin Kasperski
Modifying pull request to be safer on old mercurials....
r206 def is_http_path(url):
return bool(_re_http_url.search(url))
Marcin Kasperski
#52 First attempt on fixing keyring_check/keyring_clear problems on hg 4.0
r240
def make_passwordmgr(ui):
"""Constructing passwordmgr in a way compatible with various mercurials"""
if hasattr(ui, 'httppasswordmgrdb'):
return passwordmgr(ui, ui.httppasswordmgrdb)
else:
return passwordmgr(ui)
Marcin Kasperski
eol
r175 ############################################################
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 # HTTP password management
Marcin Kasperski
eol
r175 ############################################################
class HTTPPasswordHandler(object):
"""
Actual implementation of password handling (user prompting,
configuration file searching, keyring save&restore).
Object of this class is bound as passwordmgr attribute.
"""
def __init__(self):
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 self.pwd_cache = PwdCache()
Marcin Kasperski
eol
r175 self.last_reply = None
Marcin Kasperski
Various cleanups and refactorings.
r185 # Markers and also names used in debug notes. Password source
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 SRC_URL = "repository URL"
SRC_CFGAUTH = "hgrc"
SRC_MEMCACHE = "temporary cache"
Marcin Kasperski
Partial bugfix for ignoring usernames specified in url (not yet finished)....
r196 SRC_URLCACHE = "urllib temporary cache"
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 SRC_KEYRING = "keyring"
Marcin Kasperski
Various cleanups and refactorings.
r185 def get_credentials(self, pwmgr, realm, authuri, skip_caches=False):
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 """
Looks up for user credentials in various places, returns them
and information about their source.
Marcin Kasperski
Various cleanups and refactorings.
r185 Used internally inside find_auth and inside informative
commands (thiis method doesn't cache, doesn't detect bad
passwords etc, doesn't prompt interactively, doesn't store
password in keyring).
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184
Returns: user, password, SRC_*, actual_url
Marcin Kasperski
Various cleanups and refactorings.
r185
If not found, password and SRC is None, user can be given or
not, url is always set
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 """
ui = pwmgr.ui
Marcin Kasperski
Prefix lookup attempts to handle both version with user and without it...
r199 parsed_url, url_user, url_passwd = self.unpack_url(authuri)
Marcin Kasperski
Various py3 incompatibilities
r272 base_url = bytes(parsed_url)
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.debug(meu.ui_string('keyring: base url: %s, url user: %s, url pwd: %s\n',
base_url, url_user, url_passwd and b'******' or b''))
Marcin Kasperski
In case username (and maybe password) is present in url, they are removed...
r198
Marcin Kasperski
Partial bugfix for ignoring usernames specified in url (not yet finished)....
r196 # Extract username (or password) stored directly in url
if url_user and url_passwd:
return url_user, url_passwd, self.SRC_URL, base_url
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184
Marcin Kasperski
Partial bugfix for ignoring usernames specified in url (not yet finished)....
r196 # Extract data from urllib (in case it was already stored)
Marcin Kasperski
More efficient logging entries. Some effort towards py3+hg5
r269 if isinstance(pwmgr, HTTPPasswordMgrWithDefaultRealm):
Henrik Stuart
Support new Mercurial password database...
r232 urllib_user, urllib_pwd = \
Marcin Kasperski
More efficient logging entries. Some effort towards py3+hg5
r269 HTTPPasswordMgrWithDefaultRealm.find_user_password(
Henrik Stuart
Support new Mercurial password database...
r232 pwmgr, realm, authuri)
else:
urllib_user, urllib_pwd = pwmgr.passwddb.find_user_password(
realm, authuri)
Marcin Kasperski
Partial bugfix for ignoring usernames specified in url (not yet finished)....
r196 if urllib_user and urllib_pwd:
return urllib_user, urllib_pwd, self.SRC_URLCACHE, base_url
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184
Marcin Kasperski
#49 Url-stored usernames should work again.
r212 actual_user = url_user or urllib_user
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 # Consult configuration to normalize url to prefix, and find username
# (and maybe password)
auth_user, auth_pwd, keyring_url = self.get_url_config(
Marcin Kasperski
#49 Url-stored usernames should work again.
r212 ui, parsed_url, actual_user)
if auth_user and actual_user and (actual_user != auth_user):
Marcin Kasperski
4.6 fix (one of…)
r250 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)))
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 if auth_user and auth_pwd:
return auth_user, auth_pwd, self.SRC_CFGAUTH, keyring_url
Marcin Kasperski
#49 Url-stored usernames should work again.
r212 actual_user = actual_user or auth_user
Marcin Kasperski
Various cleanups and refactorings.
r185 if skip_caches:
Marcin Kasperski
#49 Url-stored usernames should work again.
r212 return actual_user, None, None, keyring_url
Marcin Kasperski
Various cleanups and refactorings.
r185
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 # Check memory cache (reuse )
# Checking the memory cache (there may be many http calls per command)
Marcin Kasperski
#49 Url-stored usernames should work again.
r212 cached_pwd = self.pwd_cache.check(realm, keyring_url, actual_user)
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 if cached_pwd:
Marcin Kasperski
#49 Url-stored usernames should work again.
r212 return actual_user, cached_pwd, self.SRC_MEMCACHE, keyring_url
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184
# Load from keyring.
Marcin Kasperski
#49 Url-stored usernames should work again.
r212 if actual_user:
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.debug(meu.ui_string("keyring: looking for password (user %s, url %s)\n",
actual_user, keyring_url))
Marcin Kasperski
#49 Url-stored usernames should work again.
r212 keyring_pwd = password_store.get_http_password(keyring_url, actual_user)
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 if keyring_pwd:
Marcin Kasperski
#49 Url-stored usernames should work again.
r212 return actual_user, keyring_pwd, self.SRC_KEYRING, keyring_url
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184
Marcin Kasperski
#49 Url-stored usernames should work again.
r212 return actual_user, None, None, keyring_url
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184
Marcin Kasperski
Various cleanups and refactorings.
r185 @staticmethod
def prompt_interactively(ui, user, realm, url):
"""Actual interactive prompt"""
Marcin Kasperski
eol
r175 if not ui.interactive():
Marcin Kasperski
4.6 fix (one of…)
r250 raise error.Abort(_('keyring: http authorization required but program used in non-interactive mode'))
Marcin Kasperski
eol
r175
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 if not user:
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.status(meu.ui_string("keyring: username not specified in hgrc (or in url). Password will not be saved.\n"))
Marcin Kasperski
eol
r175
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.write(meu.ui_string("http authorization required\n"))
ui.status(meu.ui_string("realm: %s\n",
realm))
ui.status(meu.ui_string("url: %s\n",
url))
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 if user:
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.write(meu.ui_string("user: %s (fixed in hgrc or url)\n",
user))
Marcin Kasperski
eol
r175 else:
Marcin Kasperski
Fixes toward py3.0 support
r274 user = ui.prompt(meu.ui_string("user:"),
default=None)
pwd = ui.getpass(meu.ui_string("password: "))
Marcin Kasperski
Various cleanups and refactorings.
r185 return user, pwd
def find_auth(self, pwmgr, realm, authuri, req):
"""
Actual implementation of find_user_password - different
ways of obtaining the username and password.
Returns pair username, password
"""
ui = pwmgr.ui
after_bad_auth = self._after_bad_auth(ui, realm, authuri, req)
# Look in url, cache, etc
user, pwd, src, final_url = self.get_credentials(
pwmgr, realm, authuri, skip_caches=after_bad_auth)
if pwd:
if src != self.SRC_MEMCACHE:
self.pwd_cache.store(realm, final_url, user, pwd)
self._note_last_reply(realm, authuri, user, req)
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.debug(meu.ui_string("keyring: Password found in %s\n",
src))
Marcin Kasperski
Various cleanups and refactorings.
r185 return user, pwd
# Last resort: interactive prompt
user, pwd = self.prompt_interactively(ui, user, realm, final_url)
Marcin Kasperski
eol
r175
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 if user:
Marcin Kasperski
eol
r175 # Saving password to the keyring.
# It is done only if username is permanently set.
# Otherwise we won't be able to find the password so it
# does not make much sense to preserve it
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.debug(meu.ui_string("keyring: Saving password for %s to keyring\n",
user))
Dan Villiom Podlaski Christiansen
gracefully handle failures to save passwords...
r227 try:
password_store.set_http_password(final_url, user, pwd)
Marcin Kasperski
Various py3 incompatibilities (syntax errors so far)
r268 except Exception as e:
ataumoefolau
Fix a NameError in find_auth.
r236 keyring = import_keyring()
if isinstance(e, keyring.errors.PasswordSetError):
ui.traceback()
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.warn(meu.ui_string("warning: failed to save password in keyring\n"))
ataumoefolau
Fix a NameError in find_auth.
r236 else:
raise e
Marcin Kasperski
eol
r175
# Saving password to the memory cache
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 self.pwd_cache.store(realm, final_url, user, pwd)
Marcin Kasperski
Various cleanups and refactorings.
r185 self._note_last_reply(realm, authuri, user, req)
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.debug(meu.ui_string("keyring: Manually entered password\n"))
Marcin Kasperski
Various cleanups and refactorings.
r185 return user, pwd
Marcin Kasperski
eol
r175
Marcin Kasperski
Prefix lookup attempts to handle both version with user and without it...
r199 def get_url_config(self, ui, parsed_url, user):
Marcin Kasperski
eol
r175 """
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 Checks configuration to decide whether/which username, prefix,
and password are configured for given url. Consults [auth] section.
Marcin Kasperski
eol
r175
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 Returns tuple (username, password, prefix) containing elements
found. username and password can be None (if unset), if prefix
is not found, url itself is returned.
Marcin Kasperski
reworking readme
r181 """
Marcin Kasperski
Dropping support for hg < 2.0 (old readauthoforuri)
r178 from mercurial.httpconnection import readauthforuri
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.debug(meu.ui_string("keyring: checking for hgrc info about url %s, user %s\n",
parsed_url, user))
Marcin Kasperski
some subtle py3-related fixes (bytes vs str in config-related actions)
r275
res = readauthforuri(ui, bytes(parsed_url), user)
Marcin Kasperski
Prefix lookup attempts to handle both version with user and without it...
r199 # If it user-less version not work, let's try with added username to handle
# both config conventions
if (not res) and user:
parsed_url.user = user
Marcin Kasperski
some subtle py3-related fixes (bytes vs str in config-related actions)
r275 res = readauthforuri(ui, bytes(parsed_url), user)
Marcin Kasperski
Prefix lookup attempts to handle both version with user and without it...
r199 parsed_url.user = None
Marcin Kasperski
Dropping support for hg < 2.0 (old readauthoforuri)
r178 if res:
group, auth_token = res
else:
auth_token = None
Marcin Kasperski
eol
r175 if auth_token:
Marcin Kasperski
some subtle py3-related fixes (bytes vs str in config-related actions)
r275 username = auth_token.get(b'username')
password = auth_token.get(b'password')
prefix = auth_token.get(b'prefix')
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 else:
Marcin Kasperski
Further work on username in url
r197 username = user
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 password = None
prefix = None
Marcin Kasperski
eol
r175
Marcin Kasperski
some subtle py3-related fixes (bytes vs str in config-related actions)
r275 password_url = self.password_url(bytes(parsed_url), prefix)
Marcin Kasperski
Further work on username in url
r197
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.debug(meu.ui_string("keyring: Password url: %s, user: %s, password: %s (prefix: %s)\n",
password_url, username,
b'********' if password else b'',
prefix))
Marcin Kasperski
Further work on username in url
r197
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 return username, password, password_url
Marcin Kasperski
eol
r175
Marcin Kasperski
Various cleanups and refactorings.
r185 def _note_last_reply(self, realm, authuri, user, req):
"""
Internal helper. Saves info about auth-data obtained,
preserves them in last_reply, and returns pair user, pwd
"""
self.last_reply = dict(realm=realm, authuri=authuri,
user=user, req=req)
def _after_bad_auth(self, ui, realm, authuri, req):
"""
If we are called again just after identical previous
request, then the previously returned auth must have been
wrong. So we note this to force password prompt (and avoid
reusing bad password indefinitely).
This routine checks for this condition.
"""
if self.last_reply:
if (self.last_reply['realm'] == realm) \
and (self.last_reply['authuri'] == authuri) \
and (self.last_reply['req'] == req):
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.debug(meu.ui_string(
"keyring: Working after bad authentication, cached passwords not used %s\n",
str(self.last_reply)))
Marcin Kasperski
Various cleanups and refactorings.
r185 return True
return False
Marcin Kasperski
eol
r175 @staticmethod
Marcin Kasperski
History update, internal tweaks.
r183 def password_url(base_url, prefix):
Marcin Kasperski
Various cleanups and refactorings.
r185 """Calculates actual url identifying the password. Takes
configured prefix under consideration (so can be shorter
than repo url)"""
Marcin Kasperski
some subtle py3-related fixes (bytes vs str in config-related actions)
r275 if not prefix or prefix == b'*':
Marcin Kasperski
eol
r175 return base_url
Marcin Kasperski
some subtle py3-related fixes (bytes vs str in config-related actions)
r275 scheme, hostpath = base_url.split(b'://', 1)
p = prefix.split(b'://', 1)
Marcin Kasperski
eol
r175 if len(p) > 1:
prefix_host_path = p[1]
else:
prefix_host_path = prefix
Marcin Kasperski
some subtle py3-related fixes (bytes vs str in config-related actions)
r275 password_url = scheme + b'://' + prefix_host_path
Marcin Kasperski
History update, internal tweaks.
r183 return password_url
Marcin Kasperski
eol
r175
@staticmethod
Marcin Kasperski
Partial bugfix for ignoring usernames specified in url (not yet finished)....
r196 def unpack_url(authuri):
Marcin Kasperski
eol
r175 """
Marcin Kasperski
In case username (and maybe password) is present in url, they are removed...
r198 Takes original url for which authentication is attempted and:
Marcin Kasperski
Partial bugfix for ignoring usernames specified in url (not yet finished)....
r196
Marcin Kasperski
In case username (and maybe password) is present in url, they are removed...
r198 - Strips query params from url. Used to convert urls like
https://repo.machine.com/repos/apps/module?pairs=0000000000000000000000000000000000000000-0000000000000000000000000000000000000000&cmd=between
to
https://repo.machine.com/repos/apps/module
Marcin Kasperski
Partial bugfix for ignoring usernames specified in url (not yet finished)....
r196
Marcin Kasperski
In case username (and maybe password) is present in url, they are removed...
r198 - Extracts username and password, if present, and removes them from url
(so prefix matching works properly)
Marcin Kasperski
Partial bugfix for ignoring usernames specified in url (not yet finished)....
r196
Returns url, user, password
Marcin Kasperski
Prefix lookup attempts to handle both version with user and without it...
r199 where url is mercurial.util.url object already stripped of all those
params.
Marcin Kasperski
eol
r175 """
Marcin Kasperski
Various py3 incompatibilities
r272 # In case of py3, util.url expects bytes
authuri = meu.pycompat.bytestr(authuri)
Marcin Kasperski
A few pro=py3 changes
r271
Marcin Kasperski
Partial bugfix for ignoring usernames specified in url (not yet finished)....
r196 # mercurial.util.url, rather handy url parser
parsed_url = util.url(authuri)
Marcin Kasperski
Various py3 incompatibilities
r272 parsed_url.query = b''
Marcin Kasperski
Further work on username in url
r197 parsed_url.fragment = None
Marcin Kasperski
Partial bugfix for ignoring usernames specified in url (not yet finished)....
r196 # Strip arguments to get actual remote repository url.
# base_url = "%s://%s%s" % (parsed_url.scheme, parsed_url.netloc,
# parsed_url.path)
Marcin Kasperski
In case username (and maybe password) is present in url, they are removed...
r198 user = parsed_url.user
passwd = parsed_url.passwd
parsed_url.user = None
parsed_url.passwd = None
Marcin Kasperski
Partial bugfix for ignoring usernames specified in url (not yet finished)....
r196
Marcin Kasperski
Prefix lookup attempts to handle both version with user and without it...
r199 return parsed_url, user, passwd
Marcin Kasperski
Partial bugfix for ignoring usernames specified in url (not yet finished)....
r196
Marcin Kasperski
eol
r175
############################################################
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 # Mercurial monkey-patching
Marcin Kasperski
eol
r175 ############################################################
@monkeypatch_method(passwordmgr)
def find_user_password(self, realm, authuri):
"""
keyring-based implementation of username/password query
for HTTP(S) connections
Passwords are saved in gnome keyring, OSX/Chain or other platform
specific storage and keyed by the repository url
"""
Marcin Kasperski
A few pro=py3 changes
r271 # In sync with hg 5.0
assert isinstance(realm, (type(None), str))
assert isinstance(authuri, str)
Marcin Kasperski
eol
r175 # Extend object attributes
if not hasattr(self, '_pwd_handler'):
self._pwd_handler = HTTPPasswordHandler()
if hasattr(self, '_http_req'):
req = self._http_req
else:
req = None
return self._pwd_handler.find_auth(self, realm, authuri, req)
Marcin Kasperski
More efficient logging entries. Some effort towards py3+hg5
r269 @monkeypatch_method(AbstractBasicAuthHandler, "http_error_auth_reqed")
Marcin Kasperski
eol
r175 def basic_http_error_auth_reqed(self, authreq, host, req, headers):
Marcin Kasperski
Dropping support for hg < 2.0 (old readauthoforuri)
r178 """Preserves current HTTP request so it can be consulted
in find_user_password above"""
Marcin Kasperski
eol
r175 self.passwd._http_req = req
try:
return basic_http_error_auth_reqed.orig(self, authreq, host, req, headers)
finally:
self.passwd._http_req = None
Marcin Kasperski
More efficient logging entries. Some effort towards py3+hg5
r269 @monkeypatch_method(AbstractDigestAuthHandler, "http_error_auth_reqed")
Marcin Kasperski
eol
r175 def digest_http_error_auth_reqed(self, authreq, host, req, headers):
Marcin Kasperski
Dropping support for hg < 2.0 (old readauthoforuri)
r178 """Preserves current HTTP request so it can be consulted
in find_user_password above"""
Marcin Kasperski
eol
r175 self.passwd._http_req = req
try:
return digest_http_error_auth_reqed.orig(self, authreq, host, req, headers)
finally:
self.passwd._http_req = None
############################################################
# SMTP support
############################################################
def try_smtp_login(ui, smtp_obj, username, password):
"""
Attempts smtp login on smtp_obj (smtplib.SMTP) using username and
password.
Returns:
- True if login succeeded
- False if login failed due to the wrong credentials
Throws Abort exception if login failed for any other reason.
Immediately returns False if password is empty
"""
if not password:
return False
try:
Marcin Kasperski
Reverting ui. change, it doesn't work after all (but uniformizing the way status entries are written)
r270 ui.note(_('(authenticating to mail server as %s)\n') %
Marcin Kasperski
More efficient logging entries. Some effort towards py3+hg5
r269 username)
Marcin Kasperski
eol
r175 smtp_obj.login(username, password)
return True
Marcin Kasperski
Various py3 incompatibilities (syntax errors so far)
r268 except smtplib.SMTPException as inst:
Marcin Kasperski
eol
r175 if inst.smtp_code == 535:
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.status(meu.ui_string("SMTP login failed: %s\n\n",
inst.smtp_error))
Marcin Kasperski
eol
r175 return False
else:
Marcin Kasperski
4.6 fix (one of…)
r250 raise error.Abort(inst)
Marcin Kasperski
eol
r175
def keyring_supported_smtp(ui, username):
"""
Marcin Kasperski
Refreshing smtp monkeypatch to be a bit more like modern mercurials (in particular obligatory cert falidaton instead of cfg)
r257 keyring-integrated replacement for mercurial.mail._smtp Used only
when configuration file contains username, but does not contain
the password.
Marcin Kasperski
eol
r175
Most of the routine below is copied as-is from
Marcin Kasperski
Refreshing smtp monkeypatch to be a bit more like modern mercurials (in particular obligatory cert falidaton instead of cfg)
r257 mercurial.mail._smtp. The critical changed part is marked with #
>>>>> and # <<<<< markers, there are also some fixes which make
the code working on various Mercurials (like parsebool import).
Marcin Kasperski
eol
r175 """
Marcin Kasperski
Refreshing smtp monkeypatch to be a bit more like modern mercurials (in particular obligatory cert falidaton instead of cfg)
r257 try:
from mercurial.utils.stringutil import parsebool
except ImportError:
from mercurial.utils import parsebool
Marcin Kasperski
eol
r175 local_hostname = ui.config('smtp', 'local_hostname')
tls = ui.config('smtp', 'tls', 'none')
# backward compatible: when tls = true, we use starttls.
Marcin Kasperski
Refreshing smtp monkeypatch to be a bit more like modern mercurials (in particular obligatory cert falidaton instead of cfg)
r257 starttls = tls == 'starttls' or parsebool(tls)
Marcin Kasperski
eol
r175 smtps = tls == 'smtps'
if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
Marcin Kasperski
4.6 fix (one of…)
r250 raise error.Abort(_("can't use TLS: Python SSL support not installed"))
Marcin Kasperski
eol
r175 mailhost = ui.config('smtp', 'host')
if not mailhost:
Marcin Kasperski
4.6 fix (one of…)
r250 raise error.Abort(_('smtp.host not configured - cannot send mail'))
Dan Villiom Podlaski Christiansen
fix smtp with mercurial tip...
r228 if getattr(sslutil, 'sslkwargs', None) is None:
sslkwargs = None
Marcin Kasperski
Refreshing smtp monkeypatch to be a bit more like modern mercurials (in particular obligatory cert falidaton instead of cfg)
r257 elif starttls or smtps:
Marcin Kasperski
eol
r175 sslkwargs = sslutil.sslkwargs(ui, mailhost)
else:
sslkwargs = {}
if smtps:
ui.note(_('(using smtps)\n'))
Sean Farley
mercurial: update for 3.8 changes
r221
# mercurial 3.8 added a mandatory host arg
Dan Villiom Podlaski Christiansen
fix smtp with mercurial tip...
r228 if not sslkwargs:
s = SMTPS(ui, local_hostname=local_hostname, host=mailhost)
elif 'host' in SMTPS.__init__.__code__.co_varnames:
Sean Farley
mercurial: update for 3.8 changes
r221 s = SMTPS(sslkwargs, local_hostname=local_hostname, host=mailhost)
else:
s = SMTPS(sslkwargs, local_hostname=local_hostname)
Marcin Kasperski
eol
r175 elif starttls:
Dan Villiom Podlaski Christiansen
fix smtp with mercurial tip...
r228 if not sslkwargs:
s = STARTTLS(ui, local_hostname=local_hostname, host=mailhost)
elif 'host' in STARTTLS.__init__.__code__.co_varnames:
Sean Farley
mercurial: update for 3.8 changes
r221 s = STARTTLS(sslkwargs, local_hostname=local_hostname, host=mailhost)
else:
s = STARTTLS(sslkwargs, local_hostname=local_hostname)
Marcin Kasperski
eol
r175 else:
s = smtplib.SMTP(local_hostname=local_hostname)
if smtps:
defaultport = 465
else:
defaultport = 25
mailport = util.getport(ui.config('smtp', 'port', defaultport))
Marcin Kasperski
Reverting ui. change, it doesn't work after all (but uniformizing the way status entries are written)
r270 ui.note(_('sending mail: smtp host %s, port %s\n') %
(mailhost, mailport))
Marcin Kasperski
eol
r175 s.connect(host=mailhost, port=mailport)
if starttls:
ui.note(_('(using starttls)\n'))
s.ehlo()
s.starttls()
s.ehlo()
Marcin Kasperski
Refreshing smtp monkeypatch to be a bit more like modern mercurials (in particular obligatory cert falidaton instead of cfg)
r257 if starttls or smtps:
Dan Villiom Podlaski Christiansen
fix smtp with mercurial tip...
r228 if getattr(sslutil, 'validatesocket', None):
Marcin Kasperski
Refreshing smtp monkeypatch to be a bit more like modern mercurials (in particular obligatory cert falidaton instead of cfg)
r257 ui.note(_('(verifying remote certificate)\n'))
Dan Villiom Podlaski Christiansen
fix smtp with mercurial tip...
r228 sslutil.validatesocket(s.sock)
Marcin Kasperski
eol
r175
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
stored = password = password_store.get_smtp_password(
mailhost, mailport, username)
# No need to check whether password was found as try_smtp_login
# just returns False if it is absent.
while not try_smtp_login(ui, s, username, password):
password = ui.getpass(_("Password for %s on %s:%d: ") % (username, mailhost, mailport))
if stored != password:
password_store.set_smtp_password(
mailhost, mailport, username, password)
# <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
def send(sender, recipients, msg):
try:
return s.sendmail(sender, recipients, msg)
Marcin Kasperski
Various py3 incompatibilities (syntax errors so far)
r268 except smtplib.SMTPRecipientsRefused as inst:
Marcin Kasperski
eol
r175 recipients = [r[1] for r in inst.recipients.values()]
Marcin Kasperski
4.6 fix (one of…)
r250 raise error.Abort('\n' + '\n'.join(recipients))
Marcin Kasperski
Various py3 incompatibilities (syntax errors so far)
r268 except smtplib.SMTPException as inst:
Marcin Kasperski
4.6 fix (one of…)
r250 raise error.Abort(inst)
Marcin Kasperski
eol
r175
return send
############################################################
# SMTP monkeypatching
############################################################
@monkeypatch_method(mail)
def _smtp(ui):
"""
build an smtp connection and return a function to send email
This is the monkeypatched version of _smtp(ui) function from
mercurial/mail.py. It calls the original unless username
without password is given in the configuration.
"""
username = ui.config('smtp', 'username')
password = ui.config('smtp', 'password')
if username and not password:
return keyring_supported_smtp(ui, username)
else:
return _smtp.orig(ui)
Marcin Kasperski
Dropping support for hg < 2.0 (old readauthoforuri)
r178
############################################################
# Custom commands
############################################################
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184
Marcin Kasperski
Modifying pull request to be safer on old mercurials....
r206 cmdtable = {}
command = meu.command(cmdtable)
Marcin Kasperski
#45 Added hg keyring_clear
r191
Marcin Kasperski
More efficient logging entries. Some effort towards py3+hg5
r269 @command(b'keyring_check',
Romain DEP.
fix for mercurial 3.8 new dispatch mechanism storing norepo/optionalrepo/...
r205 [],
Marcin Kasperski
keyring_check and keyring_clear can be run outside repository...
r217 _("keyring_check [PATH]"),
optionalrepo=True)
Marcin Kasperski
#45 Added hg keyring_clear
r191 def cmd_keyring_check(ui, repo, *path_args, **opts): # pylint: disable=unused-argument
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 """
Marcin Kasperski
Various cleanups and refactorings.
r185 Prints basic info (whether password is currently saved, and how is
Marcin Kasperski
keyring_check and keyring_clear can be run outside repository...
r217 it identified) for given path.
Can be run without parameters to show status for all (current repository) paths which
are HTTP-like.
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184 """
Marcin Kasperski
keyring_check command. Initially works.
r187 defined_paths = [(name, url)
Marcin Kasperski
some subtle py3-related fixes (bytes vs str in config-related actions)
r275 for name, url in ui.configitems(b'paths')]
Marcin Kasperski
keyring_check handles a few args
r188 if path_args:
Marcin Kasperski
keyring_check command. Initially works.
r187 # Maybe parameter is an alias
defined_paths_dic = dict(defined_paths)
Marcin Kasperski
keyring_check handles a few args
r188 paths = [(path_arg, defined_paths_dic.get(path_arg, path_arg))
for path_arg in path_args]
Marcin Kasperski
notes
r186 else:
Marcin Kasperski
keyring_check and keyring_clear can be run outside repository...
r217 if not repo:
Marcin Kasperski
Fixes toward py3.0 support
r274 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"))
Marcin Kasperski
keyring_check and keyring_clear can be run outside repository...
r217 return
Marcin Kasperski
keyring_check handles a few args
r188 paths = [(name, url) for name, url in defined_paths]
Marcin Kasperski
keyring_check command. Initially works.
r187
if not paths:
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.status(meu.ui_string("keyring_check: no paths defined\n"))
Marcin Kasperski
keyring_check and keyring_clear can be run outside repository...
r217 return
Marcin Kasperski
keyring_check command. Initially works.
r187
handler = HTTPPasswordHandler()
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.status(meu.ui_string("keyring password save status:\n"))
Marcin Kasperski
keyring_check command. Initially works.
r187 for name, url in paths:
Marcin Kasperski
#45 Added hg keyring_clear
r191 if not is_http_path(url):
Marcin Kasperski
keyring_check handles a few args
r188 if path_args:
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.status(meu.ui_string(" %s: non-http path (%s)\n",
name, url))
Marcin Kasperski
keyring_check handles a few args
r188 continue
Marcin Kasperski
#45 Added hg keyring_clear
r191 user, pwd, source, final_url = handler.get_credentials(
Marcin Kasperski
#52 First attempt on fixing keyring_check/keyring_clear problems on hg 4.0
r240 make_passwordmgr(ui), name, url)
Marcin Kasperski
keyring_check command. Initially works.
r187 if pwd:
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.status(meu.ui_string(
" %s: password available, source: %s, bound to user %s, url %s\n",
name, source, user, final_url))
Marcin Kasperski
keyring_check command. Initially works.
r187 elif user:
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.status(meu.ui_string(
" %s: password not available, once entered, will be bound to user %s, url %s\n",
name, user, final_url))
Marcin Kasperski
keyring_check command. Initially works.
r187 else:
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.status(meu.ui_string(
" %s: password not available, user unknown, url %s\n",
name, final_url))
Marcin Kasperski
in progress: refactoring, work on command keyring_check
r184
Marcin Kasperski
More efficient logging entries. Some effort towards py3+hg5
r269 @command(b'keyring_clear',
Romain DEP.
fix for mercurial 3.8 new dispatch mechanism storing norepo/optionalrepo/...
r205 [],
Marcin Kasperski
keyring_check and keyring_clear can be run outside repository...
r217 _('hg keyring_clear PATH-OR-ALIAS'),
optionalrepo=True)
Marcin Kasperski
#45 Added hg keyring_clear
r191 def cmd_keyring_clear(ui, repo, path, **opts): # pylint: disable=unused-argument
"""
Drops password bound to given path (if any is saved).
Marcin Kasperski
keyring_check and keyring_clear can be run outside repository...
r217
Parameter can be given as full url (``https://John@bitbucket.org``) or as the name
of path alias (``bitbucket``).
Marcin Kasperski
#45 Initial work on keyring_clear
r189 """
Marcin Kasperski
#45 Added hg keyring_clear
r191 path_url = path
Marcin Kasperski
Preliminarly works under py3
r276 for name, url in ui.configitems(b'paths'):
Marcin Kasperski
#45 Added hg keyring_clear
r191 if name == path:
path_url = url
break
if not is_http_path(path_url):
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.status(meu.ui_string(
"%s is not a http path (and %s can't be resolved as path alias)\n",
path, path_url))
Marcin Kasperski
#45 Added hg keyring_clear
r191 return
handler = HTTPPasswordHandler()
user, pwd, source, final_url = handler.get_credentials(
Marcin Kasperski
#52 First attempt on fixing keyring_check/keyring_clear problems on hg 4.0
r240 make_passwordmgr(ui), path, path_url)
Marcin Kasperski
#45 Added hg keyring_clear
r191 if not user:
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.status(meu.ui_string("Username not configured for url %s\n",
final_url))
Marcin Kasperski
#45 Added hg keyring_clear
r191 return
if not pwd:
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.status(meu.ui_string("No password is saved for user %s, url %s\n",
user, final_url))
Marcin Kasperski
#45 Added hg keyring_clear
r191 return
if source != handler.SRC_KEYRING:
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.status(meu.ui_string("Password for user %s, url %s is saved in %s, not in keyring\n",
user, final_url, source))
Marcin Kasperski
#45 Added hg keyring_clear
r191
password_store.clear_http_password(final_url, user)
Marcin Kasperski
Fixes toward py3.0 support
r274 ui.status(meu.ui_string("Password removed for user %s, url %s\n",
user, final_url))
Marcin Kasperski
Added buglink
r209
Marcin Kasperski
Patching various links bitbucket → heptapod
r283 buglink = 'https://foss.heptapod.net/mercurial/mercurial_keyring/issues'