Source

hg-textauth / textauth.py

# textauth.py - HTTP basic authentication for Mercurial
#
# Copyright 2010 Henrik Stuart <hg@hstuart.dk>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

'''Provides HTTP basic authentication for Mercurial

The basic authentication uses a server-side stored password file.

Note that this extension is not compatible with htpasswd generated files as
they use a custom and non-standardised format. Instead we define our own
format below.

The configuration for this extension is read from the usual hgrc
configuration mechanism:

textauth
--------

Section that controls the behavior of the textauth extension.

Supported arguments:

``file``
    The path to the file containing authentication information. The
    contents of this file should be as described below.

file format
-----------

The format for the file is a list of ``username:password`` pairs, where the
password field encodes what type of password we are using. The following
would be a typical authentication file::

    user1:$plain$thisismypassword
    user2:$md5$31435008693ce6976f45dedc5532e2c1
    user3:$sha1$4b2c5a6d33c70caa171639d1e5a76a81f83c3cfb
    user4:$sha224$8607c28d53f2c41d101c3fdbe87210adbd0584c21fcd5ddd7237f039
    user5:$sha256$1da9133ab9dbd11d2937ec8d312e1e2569857059e73cc72df92e670928983ab5
    user6:$sha384$f2e4be47d9642bdd1d208f0738a55feca0c7bc42a445f1a0233c28260141ad6bb432810dafc7ffb5891f96ad65a3206f
    user7:$sha512$e9670bc9fb8d3a578736c31390205b1d9fef50adf941b3b349076d27e483e42b00c38998cac4370a362ffe5966d45012b95f8eb279b2cecfe1760cdf25d5ecca

This extension also doubles as a management interface to this file that can be
launched by executing the file through python::

    python textauth.py

This will display usage details of working with it. Its interface greatly
mirrors that of htpasswd, but its storage format is markedly different.

'''

import hashlib

algos = {
        'plain': lambda x: '$plain$%s' % (x,),
        'md5': lambda x: '$md5$%s' % (hashlib.md5(x).hexdigest(),),
        'sha1': lambda x: '$sha1$%s' % (hashlib.sha1(x).hexdigest(),),
        'sha224': lambda x: '$sha224$%s' % (hashlib.sha224(x).hexdigest(),),
        'sha256': lambda x: '$sha256$%s' % (hashlib.sha256(x).hexdigest(),),
        'sha384': lambda x: '$sha384$%s' % (hashlib.sha384(x).hexdigest(),),
        'sha512': lambda x: '$sha512$%s' % (hashlib.sha512(x).hexdigest(),),
        }

if __name__ == '__main__':
    import sys
    import os
    import getpass
    from optparse import OptionParser

    parser = OptionParser(usage='usage: %prog [options] PASSWORDFILE USERNAME')
    parser.add_option('-p', '--plain', help='write plaintext password',
            default=False, action='store_true')
    parser.add_option('-m', '--md5', help='write password in md5 digest format',
            default=False, action='store_true')
    parser.add_option('', '--sha1', help='write password in sha1 format',
            default=False, action='store_true')
    parser.add_option('', '--sha224', help='write password in sha224 format',
            default=False, action='store_true')
    parser.add_option('', '--sha256', help='write password in sha256 format',
            default=False, action='store_true')
    parser.add_option('', '--sha384', help='write password in sha384 format',
            default=False, action='store_true')
    parser.add_option('', '--sha512', action='store_true',
            help='write password in sha512 format (default)', default=False)
    parser.add_option('-c', '--create', help='create password if it does not '
            'exist', default=False, action='store_true')
    parser.add_option('-d', '--delete', help='delete specified user from file',
            default=False, action='store_true')

    (options, args) = parser.parse_args()

    algo = [None]

    def _exit_if_set(new_algo):
        if algo[0]:
            sys.stderr.write('Cannot specify both %s and md5. exiting.\n' %
                    (algo[0],))
            sys.exit(1)
        algo[0] = new_algo

    if options.plain: _exit_if_set('plain')
    if options.md5: _exit_if_set('md5')
    if options.sha1: _exit_if_set('sha1')
    if options.sha224: _exit_if_set('sha224')
    if options.sha256: _exit_if_set('sha256')
    if options.sha384: _exit_if_set('sha384')
    if options.sha512: _exit_if_set('sha512')

    algo = algo[0]
    if not algo:
        algo = 'sha512'

    if len(args) != 2:
        parser.print_help()
        sys.exit(1)

    fname = args[0]
    uname = args[1]

    if ':' in uname:
        sys.stderr.write('username may not contain :\n')
        sys.exit(1)

    if not options.create and not os.path.exists(fname):
        sys.stderr.write('use -c to create password file\n')
        sys.exit(1)

    users = set()
    entries = []
    if os.path.exists(fname):
        with open(fname, 'r') as fh:
            for l in fh:
                l = l.strip()
                if not l:
                    continue
                dbuname, dbphash = l.split(':', 1)
                entries.append((dbuname, dbphash))
                users.add(dbuname)

    if uname in users and not options.delete:
        sys.stderr.write('user already in file. aborting.\n')
        sys.exit(1)
    elif uname not in users and options.delete:
        sys.stderr.write('user not in file. aborting.\n')
        sys.exit(1)

    if options.delete:
        with open(fname, 'w') as fh:
            for dbuser, dbphash in entries:
                if dbuser == uname:
                    continue
                fh.write('%s:%s\n' % (dbuser, dbphash))
    else:
        with open(fname, 'a') as fh:
            passwd = getpass.getpass('Enter password:')
            verify = getpass.getpass('Verify password:')
            if passwd != verify:
                sys.stderr.write('password does not verify.\n')
                sys.exit(1)

            fh.write('%s:%s\n' % (uname, algos[algo](passwd)))

    sys.exit(0)

import base64
import re
import os.path

from mercurial.hgweb import common

def perform_authentication(hgweb, req, op):
    def fail(msg):
        raise common.ErrorResponse(common.HTTP_UNAUTHORIZED, msg,
                [('WWW-Authenticate', 'Basic Realm="mercurial"')])

    auth = req.env.get('HTTP_AUTHORIZATION')
    if not auth:
        fail('enter authorization')

    try:
        user, passwd = base64.b64decode(auth.split()[1]).split(':', 1)
        if not passwd:
            fail('invalid username or password')
    except (IndexError, TypeError):
        fail('invalid username or password')

    do_auth(hgweb, req, op, user, passwd)

def _decode_db_password(dbpasswd):
    regex = re.compile(r'^\$([^\$]+)\$(.*)$')
    m = regex.match(dbpasswd)
    if m:
        return m.group(1), m.group(2)
    return None, None

def do_auth(hgweb, req, op, user, passwd):
    fname = hgweb.config('textauth', 'file')
    fname = os.path.expanduser(fname)
    if not file or not os.path.exists(fname):
        raise common.ErrorResponse(common.HTTP_SERVER_ERROR,
                'no authentication file is specified on server')

    f = open(fname)
    try:
        db = [line.rstrip('\r\n').split(':', 1) for line in f]
        for dbuser, dbpasswd in db:
            if user == dbuser:
                algo, rdbpasswd = _decode_db_password(dbpasswd)
                if algo is None:
                    raise common.ErrorResponse(common.HTTP_SERVER_ERROR,
                            'invalid authentication file on server')
                if algos[algo](passwd) == dbpasswd:
                    req.env['REMOTE_USER'] = user
                    return
                else:
                    break
        raise common.ErrorResponse(common.HTTP_FORBIDDEN,
                'invalid username or password')
    finally:
        f.close()

def extsetup():
    common.permhooks.insert(0, perform_authentication)

cmdtable = {}
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.