#!/usr/bin/python -tt
##
# Copyright (C) 2012 by Konstantin Ryabitsev and contributors
#
# 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., 59 Temple Place - Suite 330, Boston, MA
# 02111-1307, USA.
#
import os
import re
import sys
import cgi
import syslog
import logging
import urllib2

import cgitb
cgitb.enable()

import totpcgi
import totpcgi.backends

if len(sys.argv) > 1:
    # blindly assume it's the config file
    config_file = sys.argv[1]
else:
    config_file = '/etc/totpcgi/totpcgi.conf'

import ConfigParser

from fedora.client import AuthError
from fedora.client.fasproxy import FasProxyClient

config = ConfigParser.RawConfigParser()
config.read(config_file)

require_pincode = config.getboolean('main', 'require_pincode')
success_string  = config.get('main', 'success_string')

fas_url = config.get('main', 'fas_url')
try:
    fas = FasProxyClient(fas_url)
except Exception, e:
    syslog.syslog(syslog.LOG_CRIT, 'Problem connecting to fas %s' % e)
    sys.exit(1)

backends = totpcgi.backends.Backends()

try:
    backends.load_from_config(config)
except totpcgi.backends.BackendNotSupported, ex:
    syslog.syslog(syslog.LOG_CRIT,
            'Backend engine not supported: %s' % ex)
    sys.exit(1)

syslog.openlog('totp.cgi', syslog.LOG_PID, syslog.LOG_AUTH)

### Begin custom Fedora Functions

def google_auth_fas_pincode_verify(user, pincode):
    if not fas.verify_password(user, pincode):
      raise totpcgi.UserPincodeError('User Password Error')

backends.pincode_backend.verify_user_pincode = google_auth_fas_pincode_verify

client_id = '1'

def parse_token(token):
    if token > 44:
        otp = token[-44:]
    if otp.startswith('ccccc'):
        return token[:-44], otp

    # Not a password + yubikey
    return False

class YubikeyAuthenticator(object):
    auth_regex = re.compile('^status=(?P<rc>\w{2})')
    def __init__(self, require_pincode=False):
        self.require_pincode = require_pincode

    def verify_user_token(self, user, token):
        # Parse the token apart into a password and token
        password, otp = parse_token(token)

        # Verify token against yubikey server
        server_prefix = 'http://localhost/yk-val/verify?id='
        server_url = server_prefix + client_id + "&otp=" + otp

        fh = urllib2.urlopen(server_url)

        for line in fh:
            match = self.auth_regex.search(line.strip('\n'))
            if match:
                if match.group('rc') == 'OK':
                    # Yubikey token is valid
                    break
                raise totpcgi.VerifyFailed(line.split('=')[1])
        else:
            raise totpcgi.VerifyFailed('yk-val returned malformed response')


        # Verify that the yubikey token belongs to the user
        # As a side effect, verify the password is good as well
        # if the user+password are wrong, this will raise a fedora.client.AuthError
        try:
            response = fas.send_request('/config/list/%s/yubikey' % user,
                    auth_params={'username': user, 'password': password})
        except AuthError, e:
            raise totpcgi.VerifyFailed('User Password Error: %s' % e)
        if not response[1].configs.prefix or not response[1].configs.enabled:
            raise totpcgi.VerifyFailed('Yubikey OTP unconfigured')
        elif len(response[1].configs.prefix) != 12:
            raise totpcgi.VerifyFailed('Invalid Yubikey OTP prefix')
        if not otp.startswith(response[1].configs.prefix):
            raise totpcgi.VerifyFailed('Unauthorized/Invalid OTP')

        # Okay, everything passed
        return 'Valid yubikey returned'


### End of custom Fedora Functions

def bad_request(why):
    output = 'ERR\n' + why + '\n'
    sys.stdout.write('Status: 400 BAD REQUEST\n')
    sys.stdout.write('Content-type: text/plain\n')
    sys.stdout.write('Content-Length: %s\n' % len(output))
    sys.stdout.write('\n')

    sys.stdout.write(output)
    sys.exit(0)

def cgimain():
    form = cgi.FieldStorage()

    must_keys = ('user', 'token', 'mode')

    for must_key in must_keys:
        if must_key not in form:
            bad_request("Missing field: %s" % must_key)

    user  = form.getfirst('user')
    token = form.getfirst('token')
    mode  = form.getfirst('mode')

    remote_host = os.environ['REMOTE_ADDR']

    if mode != 'PAM_SM_AUTH':
        bad_request('We only support PAM_SM_AUTH')

    if parse_token(token):
        ga = YubikeyAuthenticator(require_pincode)
    else:
        # totp/googleauth
        ga = totpcgi.GoogleAuthenticator(backends, require_pincode)

    try:
        status = ga.verify_user_token(user, token)
    except Exception, ex:
        syslog.syslog(syslog.LOG_NOTICE,
            'Failure: user=%s, mode=%s, host=%s, message=%s' % (user, mode,
                remote_host, str(ex)))
        bad_request(str(ex))

    syslog.syslog(syslog.LOG_NOTICE,
        'Success: user=%s, mode=%s, host=%s, message=%s' % (user, mode,
            remote_host, status))

    sys.stdout.write('Status: 200 OK\n')
    sys.stdout.write('Content-type: text/plain\n')
    sys.stdout.write('Content-Length: %s\n' % len(success_string))
    sys.stdout.write('\n')

    sys.stdout.write(success_string)

if __name__ == '__main__':
    cgimain()
