[PATCH 3 of 3] hgkeyring: add new extension hgkeyring

Markus Zapke-Gründemann markuszapke at gmx.net
Fri Aug 24 04:40:19 CDT 2012


# HG changeset patch
# User Markus Zapke-Gründemann <markus at keimlink.de>
# Date 1345799209 -7200
# Node ID bf1c189b5eb8bcfdfe7434081e3da19b0539d2bd
# Parent  eb40ccf52cf171cc111d64418fbe81b7ecf42143
hgkeyring: add new extension hgkeyring

hgkeyring is an improved version of the mercurial_keyting extension. It
can be used to securely save HTTP authentication details.

diff --git a/hgext/hgkeyring.py b/hgext/hgkeyring.py
new file mode 100644
--- /dev/null
+++ b/hgext/hgkeyring.py
@@ -0,0 +1,434 @@
+# hgkeyring.py - secure password databases for mercurial
+#
+# Based on the mercurial_keyring extension.
+#
+# Copyright 2009 Marcin Kasperski <Marcin.Kasperski at mekk.waw.pl>
+# Copyright 2012 Markus Zapke-Gruendemann <markus at keimlink.de>
+"""securely save HTTP authentication details
+
+hgkeyring is a Mercurial extension used to securely save HTTP
+authentication passwords in password databases (GNOME Keyring, KDE
+KWallet, OS X Keychain, specific solutions for Win32 and command line).
+This extension uses and wraps services of the python keyring library.
+
+How it works
+------------
+
+The extension prompts for the password on the first pull/push, just like
+it is done by default, but saves the password. On successive runs it
+checks for the username in the configuration, then for the suitable
+password in the password database, and uses those credentials (if
+found).
+
+In case password turns out to be incorrect (either because it was
+invalid, or because it was changed on the server) or missing it just
+prompts the user again.
+
+Passwords are identified by the combination of username and remote
+address, so they can be reused between repositories if they access the
+same remote repository.
+
+Installation
+------------
+
+Prerequisites
+.............
+
+Install the keyring library using ``pip``::
+
+    pip install keyring
+
+or ``easy_install``::
+
+    easy_install keyring
+
+On Debian the library can be also installed from the official archive
+(packages ``python-keyring`` and either ``python-keyring-gnome`` or
+``python-keyring-kwallet``).
+
+Note: keyring >= 0.3 is strongly recommended, especially in case text
+backend is to be used.
+
+Extension installation
+......................
+
+Enable the extension in the configuration::
+
+    [extensions]
+    hgkeyring =
+
+Password backend configuration
+------------------------------
+
+The library should usually pick the most appropriate password backend
+without configuration. Still, if necessary, it can be configured using
+the ``~/keyringrc.cfg`` file (``keyringrc.cfg`` in the home directory of
+the current user). Refer to keyring documentation for more details.
+
+Repository configuration
+------------------------
+
+Edit repository-local ``hgrc`` and save there the remote repository path
+and the username, but do not save the password. For example::
+
+    [paths]
+    default = https://hg.example.com/repo/someproject
+
+    [auth]
+    myremote.schemes = http https
+    myremote.prefix = hg.example.com/repo
+    myremote.username = alice
+
+Simpler form with url-embedded name can also be used::
+
+    [paths]
+    bitbucket = https://bob@bitbucket.org/bob/project_name/
+
+If prefix is specified, it is used to identify the password (so all
+repositories with the same prefix and the same username will share the
+same password). Otherwise the full repository URL is used for this
+purpose.
+
+Note: if both username and password are given in the configuration, the
+extension will use them without using the password database. If username
+is not given, extension will prompt for credentials every time, also
+without saving the password.
+
+Finally, if you are consistent about remote repository nicknames, you
+can configure the username in your global configuration. For example,
+write there::
+
+    [auth]
+    acme.schemes = http https
+    acme.prefix = hg.acme.com/repositories
+    acme.username = clara
+
+As long as you will be using alias `acme` for repositories like
+`https://hg.acme.com/repositories/my_beautiful_app`, username `clara`
+will be used, and the same password reused.
+
+The advantage of this method is that it works also for :hg:`clone`.
+
+Usage
+-----
+
+Configure the repository as above, then just :hg:`pull`, :hg:`push`,
+etc. You should be asked for the password only once (per every username
+and remote repository prefix or url combination).
+"""
+import urllib2
+
+from mercurial import httpconnection, url, util
+from mercurial.i18n import _
+# mercurial.demandimport incompatibility workaround, otherwise
+# gnomekeyring, one of the possible keyring backends, would not to work.
+from mercurial.demandimport import ignore
+if 'gobject._gobject' not in ignore:
+    ignore.append('gobject._gobject')
+import keyring
+
+
+class passwordstore(object):
+    """Helper object handling keyring usage.
+
+    Controls how passwords are saved and read and the way they are keyed
+    in the keyring.
+    """
+    KEYRING_SERVICE = 'Mercurial'
+
+    @staticmethod
+    def _formatkey(uri, username):
+        """Creates a key for http passwords."""
+        return '%s@@%s' % (username, uri)
+
+    def getpassword(self, uri, username):
+        """Reads the password from the keyring."""
+        return keyring.get_password(self.KEYRING_SERVICE,
+            self._formatkey(uri, username))  # pragma: no cover
+
+    def setpassword(self, uri, username, password):
+        """Saves the password in the keyring."""
+        keyring.set_password(self.KEYRING_SERVICE,
+            self._formatkey(uri, username), password)  # pragma: no cover
+
+    def clearpassword(self, uri, username):
+        """Clears a user's password for an uri."""
+        self.setpassword(uri, username, '')  # pragma: no cover
+
+password_store = passwordstore()
+
+
+def canonicalurl(uri):
+    """Strips query and fragment from uri.
+
+    Example:
+
+    >>> url = 'https://hg.example.com/repos/module?cmd=capabilities'
+    >>> canonicalurl(url)
+    'https://hg.example.com/repos/module'
+    """
+    u = util.url(uri)
+    u.query = None
+    u.fragment = None
+    return str(u)
+
+
+def expandprefix(prefix, uri):
+    """Expands auth.prefix.
+
+    Examples:
+
+    >>> prefix = '*'
+    >>> url = 'https://hg.example.com/repo'
+    >>> expandprefix(prefix, url)
+    'https://hg.example.com/repo'
+    >>> prefix = None
+    >>> expandprefix(prefix, url)
+    'https://hg.example.com/repo'
+    >>> prefix= 'hg.example.com'
+    >>> expandprefix(prefix, url)
+    'https://hg.example.com'
+    >>> expandprefix(prefix, 'example.com')
+    'http://hg.example.com'
+    >>> prefix= 'https://hg.example.com'
+    >>> expandprefix(prefix, url)
+    'https://hg.example.com'
+    """
+    if not prefix or prefix == '*':
+        return uri
+    if util.hasscheme(prefix):
+        return prefix
+    try:
+        scheme, hostpath = uri.split('://')
+    except ValueError:
+        scheme = 'http'
+    return '%s://%s' % (scheme, prefix)
+
+
+class requesthistory(object):
+    """Stores information about different requests.
+
+    Use it to store and compare request information.
+
+    Examples:
+
+    >>> from urllib2 import Request
+    >>> uri = 'https://hg.example.com/'
+    >>> r = Request(uri)
+    >>> h = requesthistory()
+    >>> h == (uri, None, r)
+    False
+    >>> h.update(uri, None, r)
+    >>> h == (uri, None, r)
+    True
+    >>> h == (uri, 'auth', r)
+    False
+    >>> r.add_header('User-agent', 'mercurial/proto-1.0')
+    >>> h == (uri, None, r)
+    False
+    >>> h.update(uri)
+    >>> h == (uri, None, None)
+    True
+    """
+    def __init__(self):
+        self.uri = self.realm = self.headers = None
+
+    @staticmethod
+    def getheaders(request):
+        """Extracts headers from a Request object."""
+        try:
+            headers = request.header_items()
+        except AttributeError:
+            headers = None
+        return headers
+
+    def update(self, uri, realm=None, request=None):
+        """Updates requesthistory instance."""
+        self.uri = uri
+        self.realm = realm
+        self.headers = self.getheaders(request)
+
+    def __eq__(self, data):
+        """Compares if requesthistory instance and a data tuple are equal.
+
+        The tuple must have three elements: (uri, realm, request)
+        """
+        return (self.uri == data[0] and self.realm == data[1]
+            and self.headers == self.getheaders(data[2]))
+
+
+class httppasswordhandler(urllib2.HTTPPasswordMgrWithDefaultRealm):
+    """Actual implementation of password handling.
+
+    An instance of this class can be used as passwordmgr in mercurial.url.
+    """
+    def __init__(self, ui):
+        urllib2.HTTPPasswordMgrWithDefaultRealm.__init__(self)
+        self.pwdcache = {}
+        self.req = None
+        self.reqhistory = requesthistory()
+        self.ui = ui
+
+    def _writedebug(self, msg, uri=None, user=None, pwd=None):
+        """Writes debugging message to stdout.
+
+        If one or all of uri, user or pwd are set they are appended
+        to the debug message on a new line.
+        """
+        extramsg = []
+        if uri:
+            extramsg.append('url: %s' % util.hidepassword(uri))
+        if user:
+            extramsg.append('user: %s' % user)
+        if pwd:
+            extramsg.append('password: %s' % '***')
+        self.ui.debug('%s\n' % msg)
+        if len(extramsg):
+            self.ui.debug('\t%s\n' % ', '.join(extramsg))
+
+    def _debugbadauth(self, uri, realm):
+        """Print debugging information after bad authentication."""
+        self._writedebug(
+            'detected bad authentication, cached passwords not used')
+        self._writedebug('current request:')
+        self._writedebug('\turi: %s' % uri)
+        if self.req:
+            self._writedebug('\trequest headers:')
+            for key, value in self.req.header_items():
+                if key == 'Authorization':
+                    value = '***'
+                self._writedebug('\t\t%s: %s' % (key, value))
+        self._writedebug('\trealm: %s' % realm)
+        self._writedebug('previous request:')
+        self._writedebug('\turi: %s' % self.reqhistory.uri)
+        if self.reqhistory.headers:
+            self._writedebug('\trequest headers:')
+            for key, value in self.reqhistory.headers:
+                if key == 'Authorization':
+                    value = '***'
+                self._writedebug('\t\t%s: %s' % (key, value))
+        self._writedebug('\trealm: %s' % self.reqhistory.realm)
+
+    def setrequest(self, request):
+        """Stores a Request instance for later analysis."""
+        self.req = request
+
+    def find_stored_password(self, authuri):
+        return self.find_user_password(None, authuri)  # pragma: no cover
+
+    def find_user_password(self, realm, authuri):
+        """keyring-based implementation of username/password query.
+
+        Passwords are saved in GNOME Keyring, KDE KWallet, OS X Keychain
+        or other platform specific storage and keyed by the repository
+        url.
+        """
+        # 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 indifinitely).
+        afterbadauth = self.reqhistory == (authuri, realm, self.req)
+        if afterbadauth:
+            self._debugbadauth(authuri, realm)
+        baseurl = canonicalurl(authuri)
+        # Extracting possible username (and password if available)
+        # stored directly in repository url.
+        user, pwd = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
+            self, realm, authuri)
+        if user and pwd:
+            self._writedebug('auth data found in repository url', baseurl,
+                user, pwd)
+            self.reqhistory.update(authuri, realm, self.req)
+            return user, pwd
+        if user:
+            self._writedebug('stripped username "%s" from url' % user)
+        cleanurl = util.removeauth(baseurl)
+        group, authuser, pwd, prefix = self.loadauthconf(cleanurl, user)
+        keyringurl = expandprefix(prefix, cleanurl)
+        self._writedebug('keyring url: %s' % keyringurl)
+        # Checking the memory cache (there may be many http calls per command)
+        cachekey = (realm, keyringurl)
+        if not afterbadauth:
+            cachedauth = self.pwdcache.get(cachekey)
+            if cachedauth:
+                user, pwd = cachedauth
+                self._writedebug('cached auth data found', baseurl, user, pwd)
+                self.reqhistory.update(authuri, realm, self.req)
+                return user, pwd
+        if authuser:
+            if user and (user == authuser):
+                msg = 'hgkeyring: username %s for %s specified both in '
+                msg += 'url/paths and in auth - please, leave only one of those'
+                raise util.Abort(_(msg % (user, keyringurl)))
+            user = authuser
+            self._writedebug('using auth.%s.* for authentication' % group)
+            if pwd:
+                self.pwdcache[cachekey] = user, pwd
+                self.reqhistory.update(authuri, realm, self.req)
+                return user, pwd
+        # Loading password from keyring. Only if username is known (so
+        # we know the key) and we are not after failure (so we don't
+        # reuse the bad password).
+        if user and not afterbadauth:
+            self._writedebug('looking for password for user %s and url %s'
+                % (user, keyringurl))
+            pwd = password_store.getpassword(keyringurl, user)
+            if pwd:
+                self.pwdcache[cachekey] = user, pwd
+                self._writedebug('password found on keyring', baseurl, user,
+                    pwd)
+                self.reqhistory.update(authuri, realm, self.req)
+                return user, pwd
+            else:
+                self._writedebug('password not present in keyring')
+        # Is the username permanently set?
+        fixeduser = (user and True or False)
+        # Last resort: interactive prompt
+        if not self.ui.interactive():
+            msg = 'hgkeyring: http authorization required but program '
+            msg += 'used in non-interactive mode'
+            raise util.Abort(_(msg))
+        if not fixeduser:
+            msg = 'username not set in configuration - keyring is not used\n'
+            self.ui.status(_(msg))
+        self.ui.write(_('http authorization required\n'))
+        self.ui.status(_('realm: %s\n') % realm)
+        if fixeduser:
+            self.ui.write(_('user: %s (fixed in configuration)\n') % user)
+        else:
+            user = self.ui.prompt(_('user:'), default=None)
+        pwd = self.ui.getpass(_('password: '))
+        if fixeduser:
+            # Saving password to the keyring. It is done only if the
+            # username is permanently set. Otherwise we won't be able to
+            # find the password so it does not make much sense to
+            # preserve it.
+            self._writedebug('saving password for %s to keyring' % user)
+            password_store.setpassword(keyringurl, user, pwd)
+        # Saving password to the memory cache
+        self.pwdcache[cachekey] = user, pwd
+        self._writedebug('manually entered password', baseurl, user, pwd)
+        self.reqhistory.update(authuri, realm, self.req)
+        return user, pwd
+
+    def loadauthconf(self, uri, user):
+        """Loading auth section contents from local configuration.
+
+        Returns (group, username, password, prefix) tuple (every element
+        can be None).
+        """
+        authinfo = httpconnection.readauthforuri(self.ui, uri, user)
+        if authinfo:
+            group, auth = authinfo
+            username = auth.get('username')
+            pwd = auth.get('password')
+            prefix = auth.get('prefix')
+        else:
+            group = username = pwd = prefix = None
+        return group, username, pwd, prefix
+
+
+def uisetup(ui):
+    url.pwmgrclass = httppasswordhandler
+
+testedwith = '2.3'
diff --git a/tests/hghave.py b/tests/hghave.py
--- a/tests/hghave.py
+++ b/tests/hghave.py
@@ -107,6 +107,13 @@ def has_inotify():
     os.unlink(name)
     return True
 
+def has_keyring():
+    try:
+        import keyring
+        return True
+    except ImportError:
+        return False
+
 def has_fifo():
     if getattr(os, "mkfifo", None) is None:
         return False
@@ -286,6 +293,7 @@ checks = {
     "hardlink": (has_hardlink, "hardlinks"),
     "icasefs": (has_icasefs, "case insensitive file system"),
     "inotify": (has_inotify, "inotify extension support"),
+    "keyring": (has_keyring, "python keyring library"),
     "lsprof": (has_lsprof, "python lsprof module"),
     "mtn": (has_mtn, "monotone client (>= 1.0)"),
     "outer-repo": (has_outer_repo, "outer repo"),
diff --git a/tests/test-hgkeyring.py b/tests/test-hgkeyring.py
new file mode 100644
--- /dev/null
+++ b/tests/test-hgkeyring.py
@@ -0,0 +1,228 @@
+"""Unit tests and doctests for the hgkeyring extension."""
+import os
+from urllib2 import Request
+import sys
+
+from mercurial import ui, util
+
+from hgext import hgkeyring
+
+
+class passwordstorefake(hgkeyring.passwordstore):
+    """A passwordstore fake using an internal cache."""
+    def __init__(self):
+        self.cache = {}
+
+    def getpassword(self, uri, username):
+        testclient.debug('reading password from keyring for %s\n' % uri)
+        cache_key = self._formatkey(uri, username)
+        try:
+            password = self.cache[cache_key]
+        except KeyError:
+            password = ''
+        return password
+
+    def setpassword(self, uri, username, password):
+        cache_key = self._formatkey(uri, username)
+        self.cache[cache_key] = password
+        testclient.debug('stored username %s and password %s\n'
+            % (username, password))
+
+
+hgkeyring.password_store = passwordstorefake()
+
+
+class uifake(ui.ui):
+    """A ui fake class for authentication tests."""
+    def __init__(self, user, password, interactive):
+        """Creates a new ui instance.
+
+        Set user and password to the desired values.
+        interactive is a boolean to control the interactive shell.
+        """
+        self._user = user
+        self._password = password
+        self._interactive = interactive
+        super(uifake, self).__init__()
+
+    def interactive(self):
+        """Returns True if the ui instance is interactive."""
+        return self._interactive
+
+    def prompt(self, msg, default='y'):
+        """"Prompt the user for a username.
+
+        Because this is a SUT the method returns always the username set
+        at instantiation.
+        """
+        return self._user
+
+    def getpass(self, prompt=None, default=None):
+        """"Prompt the user for a password.
+
+        Because this is a SUT the method returns always the password set
+        at instantiation.
+        """
+        return self._password
+
+
+class testclient(object):
+    """Test client for hgkeyring."""
+    def __init__(self, user, password=None, interactive=True):
+        """Creates a test client.
+
+        You must set a username. If no password is set a default
+        password is used. The interactive shell is enabled by default.
+        """
+        self.user = user
+        if password is None:
+            self.password = 'secret'
+        else:
+            self.password = password
+        self.ui = uifake(self.user, self.password, interactive=interactive)
+        self.pwhandler = hgkeyring.httppasswordhandler(self.ui)
+
+    @staticmethod
+    def debug(msg):
+        sys.stdout.write('[test] %s' % msg)
+
+    def testuri(self, uri, msg=None, headers=None, realm=None, debug=True):
+        """Tests an uri with username and password.
+
+        Use msg to print a message when the test starts.
+        Add additional headers using the headers kwarg.
+        Set the realm using the realm kwarg.
+        Set debug to False to disable hgkeyring debug output.
+        """
+        if headers is None:
+            headers = {}
+        if msg is not None:
+            self.debug(msg + '\n')
+        if debug:
+            debug_orig = self.ui.config('ui', 'debug')
+            self.ui.setconfig('ui', 'debug', 'True')
+        self.debug('testing user %s at %s\n' % (self.user, uri))
+        request = Request(uri, headers=headers)
+        if headers:
+            self.debug('request headers:\n')
+            for key, value in headers.items():
+                self.debug('\t%s: %s\n' % (key, value))
+        self.pwhandler.setrequest(request)
+        try:
+            uriobj = util.url(uri)
+            if uriobj.user:
+                self.pwhandler.add_password(realm, uri, uriobj.user,
+                    uriobj.passwd)
+            user, pwd = self.pwhandler.find_user_password(realm, uri)
+            assert user == self.user, 'user "%s" did not match "%s"' \
+                % (user, self.user)
+            assert pwd == self.password, 'password "%s" did not match "%s"' \
+                % (pwd, self.password)
+        except util.Abort, err:
+            self.debug('abort: %s\n' % err)
+        except AssertionError, err:
+            self.debug('warning: %s\n' % err)
+        sys.stdout.write('\n')
+        if debug:
+            self.ui.setconfig('ui', 'debug', debug_orig)
+
+
+t = testclient('alice')
+t.testuri('https://hg.example.com/repo', msg='ask for password',
+    headers={'Authorization': 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==',
+        'User-agent': 'mercurial/proto-1.0'})
+t.testuri('https://hg.example.com/repo',
+    msg='ignore password cache because of identical request',
+    headers={'Authorization': 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==',
+        'User-agent': 'mercurial/proto-1.0'})
+t.testuri('https://alice@hg.example.com/repo',
+    msg='read username from URI, use cached password')
+t.testuri('https://alice:secret@hg.example.com/repo',
+    msg='read username and password from URI, cache is not used')
+t.testuri('https://hg.example.com/repo', msg='use cached password')
+
+# add auth section to .hgrc
+hgrc = open(os.environ["HGRCPATH"], 'w')
+hgrc.write('[auth]\n')
+hgrc.write('myremote.prefix = hg.example.com\n')
+hgrc.write('myremote.username = bob\n')
+hgrc.close()
+
+t = testclient('bob')
+t.testuri('https://hg.example.com/repo', msg='read password from keyring')
+t.testuri('https://hg.example.com/repo/module?cmd=capabilities',
+        msg='use cached password and strip off query params')
+
+pwd = 'cookies'
+# add password to .hgrc
+hgrc = open(os.environ["HGRCPATH"], 'a')
+hgrc.write('myremote.password = %s\n' % pwd)
+hgrc.close()
+
+t = testclient('bob', pwd)
+t.testuri('https://bob@hg.example.com/repo',
+    msg='aborts because username is set in url and .hgrc')
+t.testuri('https://hg.example.com/repo',
+    msg='password is read from .hgrc')
+t.testuri('https://hg.example.com/repo?cmd=capabilities',
+    msg='use cached auth data')
+t.testuri('https://clara@hg.example.com/repo',
+    msg='store password for clara on keyring as user bob')
+
+# delete password from .hgrc
+hgrc = open(os.environ["HGRCPATH"], 'a')
+hgrc.write('myremote.password =\n')
+hgrc.close()
+
+t = testclient('bob')
+t.testuri('https://hg.example.com/repo',
+    msg='username is taken from .hgrc and password is read from keyring')
+
+t = testclient('clara', pwd)
+t.testuri('https://clara@hg.example.com/repo',
+    msg='read password for clara on keyring')
+
+# add catch all prefix to .hgrc
+hgrc = open(os.environ["HGRCPATH"], 'a')
+hgrc.write('myremote.prefix = *\n')
+hgrc.close()
+
+t = testclient('bob')
+t.testuri('https://hg.example.com/repo',
+    msg='read password from keyring, use full URI because of catch all prefix')
+
+# add not matching prefix to .hgrc
+hgrc = open(os.environ["HGRCPATH"], 'a')
+hgrc.write('myremote.prefix = code.example.com\n')
+hgrc.close()
+
+t = testclient('bob')
+t.testuri('https://hg.example.com/repo',
+    msg='will prompt for password because prefix does not match')
+
+t = testclient('bob', interactive=False)
+t.testuri('https://hg.example.com/repo',
+    msg='will abort because terminal is non-interactive', debug=False)
+
+# test (simulated) clone operation
+t = testclient('bob')
+t.testuri('https://hg.example.com/repo/?cmd=capabilities',
+    msg='test with different http headers')
+t.testuri('https://hg.example.com/repo/?cmd=batch',
+    headers={'x-hgarg-1': 'cmds=heads+%3Bknown+nodes%3D'})
+revs = ('0000000000000000000000000000000000000000',
+    'b066b1ec09fb7b604b548375170e78bc008bb6b2')
+t.testuri('https://hg.example.com/repo/?cmd=getbundle',
+    headers={'x-hgarg-1': 'common=%s&heads=%s' % revs})
+t.testuri('https://hg.example.com/repo/?cmd=listkeys',
+    headers={'x-hgarg-1': 'namespace=phases'})
+t.testuri('https://hg.example.com/repo/?cmd=listkeys',
+    headers={'x-hgarg-1': 'namespace=bookmarks'})
+
+# this is hack to make sure no escape characters are inserted into the
+# output
+if 'TERM' in os.environ:
+    del os.environ['TERM']
+
+import doctest
+doctest.testmod(hgkeyring)
diff --git a/tests/test-hgkeyring.py.out b/tests/test-hgkeyring.py.out
new file mode 100644
--- /dev/null
+++ b/tests/test-hgkeyring.py.out
@@ -0,0 +1,192 @@
+[test] ask for password
+[test] testing user alice at https://hg.example.com/repo
+[test] request headers:
+[test] 	Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
+[test] 	User-agent: mercurial/proto-1.0
+keyring url: https://hg.example.com/repo
+username not set in configuration - keyring is not used
+http authorization required
+realm: None
+manually entered password
+	url: https://hg.example.com/repo, user: alice, password: ***
+
+[test] ignore password cache because of identical request
+[test] testing user alice at https://hg.example.com/repo
+[test] request headers:
+[test] 	Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
+[test] 	User-agent: mercurial/proto-1.0
+detected bad authentication, cached passwords not used
+current request:
+	uri: https://hg.example.com/repo
+	request headers:
+		Authorization: ***
+		User-agent: mercurial/proto-1.0
+	realm: None
+previous request:
+	uri: https://hg.example.com/repo
+	request headers:
+		Authorization: ***
+		User-agent: mercurial/proto-1.0
+	realm: None
+keyring url: https://hg.example.com/repo
+username not set in configuration - keyring is not used
+http authorization required
+realm: None
+manually entered password
+	url: https://hg.example.com/repo, user: alice, password: ***
+
+[test] read username from URI, use cached password
+[test] testing user alice at https://alice@hg.example.com/repo
+stripped username "alice" from url
+keyring url: https://hg.example.com/repo
+cached auth data found
+	url: https://alice@hg.example.com/repo, user: alice, password: ***
+
+[test] read username and password from URI, cache is not used
+[test] testing user alice at https://alice:secret@hg.example.com/repo
+auth data found in repository url
+	url: https://alice:***@hg.example.com/repo, user: alice, password: ***
+
+[test] use cached password
+[test] testing user alice at https://hg.example.com/repo
+keyring url: https://hg.example.com/repo
+cached auth data found
+	url: https://hg.example.com/repo, user: alice, password: ***
+
+[test] read password from keyring
+[test] testing user bob at https://hg.example.com/repo
+keyring url: https://hg.example.com
+using auth.myremote.* for authentication
+looking for password for user bob and url https://hg.example.com
+[test] reading password from keyring for https://hg.example.com
+password not present in keyring
+http authorization required
+realm: None
+user: bob (fixed in configuration)
+saving password for bob to keyring
+[test] stored username bob and password secret
+manually entered password
+	url: https://hg.example.com/repo, user: bob, password: ***
+
+[test] use cached password and strip off query params
+[test] testing user bob at https://hg.example.com/repo/module?cmd=capabilities
+keyring url: https://hg.example.com
+cached auth data found
+	url: https://hg.example.com/repo/module, user: bob, password: ***
+
+[test] aborts because username is set in url and .hgrc
+[test] testing user bob at https://bob@hg.example.com/repo
+stripped username "bob" from url
+keyring url: https://hg.example.com
+[test] abort: hgkeyring: username bob for https://hg.example.com specified both in url/paths and in auth - please, leave only one of those
+
+[test] password is read from .hgrc
+[test] testing user bob at https://hg.example.com/repo
+keyring url: https://hg.example.com
+using auth.myremote.* for authentication
+
+[test] use cached auth data
+[test] testing user bob at https://hg.example.com/repo?cmd=capabilities
+keyring url: https://hg.example.com
+cached auth data found
+	url: https://hg.example.com/repo, user: bob, password: ***
+
+[test] store password for clara on keyring as user bob
+[test] testing user bob at https://clara@hg.example.com/repo
+stripped username "clara" from url
+keyring url: https://hg.example.com/repo
+looking for password for user clara and url https://hg.example.com/repo
+[test] reading password from keyring for https://hg.example.com/repo
+password not present in keyring
+http authorization required
+realm: None
+user: clara (fixed in configuration)
+saving password for clara to keyring
+[test] stored username clara and password cookies
+manually entered password
+	url: https://clara@hg.example.com/repo, user: clara, password: ***
+[test] warning: user "clara" did not match "bob"
+
+[test] username is taken from .hgrc and password is read from keyring
+[test] testing user bob at https://hg.example.com/repo
+keyring url: https://hg.example.com
+using auth.myremote.* for authentication
+looking for password for user bob and url https://hg.example.com
+[test] reading password from keyring for https://hg.example.com
+password found on keyring
+	url: https://hg.example.com/repo, user: bob, password: ***
+
+[test] read password for clara on keyring
+[test] testing user clara at https://clara@hg.example.com/repo
+stripped username "clara" from url
+keyring url: https://hg.example.com/repo
+looking for password for user clara and url https://hg.example.com/repo
+[test] reading password from keyring for https://hg.example.com/repo
+password found on keyring
+	url: https://clara@hg.example.com/repo, user: clara, password: ***
+
+[test] read password from keyring, use full URI because of catch all prefix
+[test] testing user bob at https://hg.example.com/repo
+keyring url: https://hg.example.com/repo
+using auth.myremote.* for authentication
+looking for password for user bob and url https://hg.example.com/repo
+[test] reading password from keyring for https://hg.example.com/repo
+password not present in keyring
+http authorization required
+realm: None
+user: bob (fixed in configuration)
+saving password for bob to keyring
+[test] stored username bob and password secret
+manually entered password
+	url: https://hg.example.com/repo, user: bob, password: ***
+
+[test] will prompt for password because prefix does not match
+[test] testing user bob at https://hg.example.com/repo
+keyring url: https://hg.example.com/repo
+username not set in configuration - keyring is not used
+http authorization required
+realm: None
+manually entered password
+	url: https://hg.example.com/repo, user: bob, password: ***
+
+[test] will abort because terminal is non-interactive
+[test] testing user bob at https://hg.example.com/repo
+[test] abort: hgkeyring: http authorization required but program used in non-interactive mode
+
+[test] test with different http headers
+[test] testing user bob at https://hg.example.com/repo/?cmd=capabilities
+keyring url: https://hg.example.com/repo/
+username not set in configuration - keyring is not used
+http authorization required
+realm: None
+manually entered password
+	url: https://hg.example.com/repo/, user: bob, password: ***
+
+[test] testing user bob at https://hg.example.com/repo/?cmd=batch
+[test] request headers:
+[test] 	x-hgarg-1: cmds=heads+%3Bknown+nodes%3D
+keyring url: https://hg.example.com/repo/
+cached auth data found
+	url: https://hg.example.com/repo/, user: bob, password: ***
+
+[test] testing user bob at https://hg.example.com/repo/?cmd=getbundle
+[test] request headers:
+[test] 	x-hgarg-1: common=0000000000000000000000000000000000000000&heads=b066b1ec09fb7b604b548375170e78bc008bb6b2
+keyring url: https://hg.example.com/repo/
+cached auth data found
+	url: https://hg.example.com/repo/, user: bob, password: ***
+
+[test] testing user bob at https://hg.example.com/repo/?cmd=listkeys
+[test] request headers:
+[test] 	x-hgarg-1: namespace=phases
+keyring url: https://hg.example.com/repo/
+cached auth data found
+	url: https://hg.example.com/repo/, user: bob, password: ***
+
+[test] testing user bob at https://hg.example.com/repo/?cmd=listkeys
+[test] request headers:
+[test] 	x-hgarg-1: namespace=bookmarks
+keyring url: https://hg.example.com/repo/
+cached auth data found
+	url: https://hg.example.com/repo/, user: bob, password: ***
+
diff --git a/tests/test-hgkeyring.t b/tests/test-hgkeyring.t
new file mode 100644
--- /dev/null
+++ b/tests/test-hgkeyring.t
@@ -0,0 +1,79 @@
+  $ "$TESTDIR/hghave" serve || exit 80
+  $ "$TESTDIR/hghave" keyring || exit 80
+
+  $ hg init test
+  $ cd test
+  $ echo foo>foo
+  $ mkdir foo.d foo.d/bAr.hg.d foo.d/baR.d.hg
+  $ echo foo>foo.d/foo
+  $ echo bar>foo.d/bAr.hg.d/BaR
+  $ echo bar>foo.d/baR.d.hg/bAR
+  $ hg commit -A -m 1
+  adding foo
+  adding foo.d/bAr.hg.d/BaR
+  adding foo.d/baR.d.hg/bAR
+  adding foo.d/foo
+  $ hg phase -fpr 0:tip
+
+test http authentication with hgkeyring extension
+
+  $ cat << EOT > userpass.py
+  > import base64
+  > from mercurial.hgweb import common
+  > def perform_authentication(hgweb, req, op):
+  >     auth = req.env.get('HTTP_AUTHORIZATION')
+  >     if not auth:
+  >         raise common.ErrorResponse(common.HTTP_UNAUTHORIZED, 'who',
+  >             [('WWW-Authenticate', 'Basic Realm="mercurial"')])
+  >     authinfo = base64.b64decode(auth.split()[1]).split(':', 1)
+  >     if authinfo != ['user', 'pass']:
+  >         raise common.ErrorResponse(common.HTTP_FORBIDDEN, 'no')
+  > def extsetup():
+  >     common.permhooks.insert(0, perform_authentication)
+  > EOT
+  $ hg --config extensions.x=userpass.py serve -p $HGPORT -d --pid-file=pid \
+  >    --config server.preferuncompressed=True
+  $ cat pid >> $DAEMON_PIDS
+
+  $ echo '[extensions]' >> .hg/hgrc
+  $ echo 'hgkeyring=' >> .hg/hgrc
+  $ hg id http://localhost:$HGPORT/
+  abort: hgkeyring: http authorization required but program used in non-interactive mode
+  [255]
+  $ hg id http://user@localhost:$HGPORT/
+  abort: hgkeyring: http authorization required but program used in non-interactive mode
+  [255]
+  $ hg id http://user:pass@localhost:$HGPORT/
+  8b6053c928fe
+  $ echo '[auth]' >> .hg/hgrc
+  $ echo 'l.schemes=http' >> .hg/hgrc
+  $ echo 'l.prefix=*' >> .hg/hgrc
+  $ echo 'l.username=user' >> .hg/hgrc
+  $ echo 'l.password=pass' >> .hg/hgrc
+  $ hg id http://localhost:$HGPORT/
+  8b6053c928fe
+  $ hg id http://user@localhost:$HGPORT/
+  abort: hgkeyring: username user for http://localhost:$HGPORT/ specified both in url/paths and in auth - please, leave only one of those
+  [255]
+  $ hg id http://user:pass@localhost:$HGPORT/
+  8b6053c928fe
+  $ hg id http://user2@localhost:$HGPORT/
+  abort: hgkeyring: http authorization required but program used in non-interactive mode
+  [255]
+  $ hg clone http://user:pass@localhost:$HGPORT/ dest
+  streaming all changes
+  6 files to transfer, 606 bytes of data
+  transferred 606 bytes in * seconds (* KB/sec) (glob)
+  updating to branch default
+  4 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ cd dest
+  $ cp ../.hg/hgrc .hg
+  $ hg pull http://localhost:$HGPORT/
+  pulling from http://localhost:$HGPORT/
+  searching for changes
+  no changes found
+  $ hg push http://localhost:$HGPORT/
+  pushing to http://localhost:$HGPORT/
+  searching for changes
+  no changes found
+  [1]


More information about the Mercurial-devel mailing list