#!/usr/bin/env python3
#
# -*- python -*-
#
#  File: octofussd
#
#  Copyright (C) 2006-2016 Christopher R. Gabriel <cgabriel@truelite.it>
#  Copyright (C) 2008-2009 Enrico Zini <enrico@truelite.it>
#
#  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 3 of the License, or
#  (at your option) any later version.
#


import sys, os, os.path, socket
sys.path.append("/usr/share/octofussd")
from datetime import datetime
import time
import optparse
from hashlib import md5

from twisted.application import internet, service
from twisted.internet import protocol, reactor, defer
from twisted.internet.protocol import DatagramProtocol
from twisted.web import resource, server, static, xmlrpc

import octofuss
import octofuss.plugins as plugins
import octofuss.hooks
import octofussd.model as model

# Configuration
conf = None
# Octofuss tree
tree = None


def verify_ldap(user,pwd):
    from ldap3 import Server, Connection
    if not conf.has_section("LDAP"):
        return False
    if not conf.has_option("LDAP", "host"):
        return False

    server = conf.get("LDAP", "host")
    if not conf.has_option("LDAP", "basedn"):
        return False

    base_dn = conf.get("LDAP", "basedn")
    user_dn = "uid=%s,ou=Users,%s" % (user, base_dn)

    try:
        s = Server(server)
        conn = Connection(server, user_dn, pwd, auto_bind=True)
        if conn.extend.standard.who_am_i():
            return True
    except:
        pass

    return False

def authenticator(user, pwd):
    """
    authenticator function - check for mock
    """
    forceMock = conf.getboolean("General", "forceMockup")
    if forceMock:
        return user == pwd

    from octofussd.data import models

    # first, try with LDAP
    authenticated_ldap = verify_ldap(user,pwd)

    if authenticated_ldap:
        try:
            u = models.Auth.objects.get(user_name=user)
        except:
            # create the user in the db
            new_user = models.Auth.objects.create(user_name=user,
                                                  email_address=user,
                                                  display_name=user,
                                                  password="")
            new_user.password = pwd
            new_user.save()
        return True

    # if LDAP failed, continue with DB authentication
    u = models.Auth.objects.get(user_name=user)
    return u.verify_password(pwd)

class ValidatingOctofussServer(octofuss.xmlrpc.Server):
    def _validate_apikey(self, apikey):
        return apikey in list(self.apikeys.keys())

    def _validate_access(self, apikey, path):
        from octofussd.data import models

        user = self.apikeys[apikey]
        if user == 'root' or path == '/':
            return True

        rpath = path
        if rpath.startswith("/"):
            rpath = rpath[1:]
        if "/" in rpath:
            rpath = rpath.split("/")[0]
            
        r = models.AuthPath.objects.filter(user_name=user,
                                           path__startswith=rpath)
        if r.count() > 0:
            return True

        return False

    def __init__(self, authenticator, service, forest):
        octofuss.xmlrpc.Server.__init__(self, service, forest)
        self.apikeys = dict()
        self.authfunc = authenticator

    def xmlrpc_login(self, user, password):
        if self.authfunc(user, password):
            apikey = md5((user + password + str(time.process_time())).encode('utf-8')).hexdigest()
            self.apikeys[apikey] = user
            return apikey
        return ""

class LoggingTree(octofuss.Tree):
    def __init__(self, child):
        super(LoggingTree, self).__init__(child._name)
        self.tree = child
    def has(self, path):
        print("HAS", path)
        return self.tree.has(path)
    def get(self, path):
        print("GET", path)
        return self.tree.get(path)
    def list(self, path):
        print("LIST", path)
        return self.tree.list(path)
    def set(self, path, value):
        print("SET", path, repr(value).replace("\n", " ")[:100])
        return self.tree.set(path, value)
    def create(self, path, value):
        print("CREATE", path, repr(value).replace("\n", " ")[:100])
        return self.tree.create(path, value)
    def delete(self, path):
        print("DELETE", path)
        return self.tree.delete(path)
    def doc(self, path):
        print("DOC", path)
        return self.tree.doc(path)

def octofussConf(service):
    global tree
    return ValidatingOctofussServer(authenticator, service, tree)


class OctofussResource(resource.Resource):
    def __init__(self,service):
        resource.Resource.__init__(self)
        self.service = service

class OctofussService(service.Service):
    def getResource(self):
        r = OctofussResource(self)

        from octofussd.fci import FussClientInteraction
        fci = FussClientInteraction(self, tree)
        r.putChild(b'octofuss', fci)

        from octofussd.deployer import FussClientDeployer
        client_deploy = FussClientDeployer(self)
        r.putChild(b'clientdeploy', client_deploy)

        conf = octofussConf(self)
        r.putChild(b'conf', conf)

        return r

def read_config():
    global conf
    conf = octofuss.readConfig(defaults = """
[General]
allowMockup = False
forceMockup = False
""")
    model.init(conf)

def build_tree():
    global tree
    tree = octofuss.Forest(doc="""Configuration tree

This tree represent the server configuration, with no intermediate storage.

Reading from the tree reads directly from the server configuration, and
writing to the tree writes directly to the configuration.""")

    def get_forest(path):
        "Get the forest at the given path"
        path = os.path.normpath(path.strip('/')).strip('/')
        if path == '.':
            return tree
        else:
            root, child = os.path.split(path)
            root = get_forest(root)
            branch = root.branch(child)
            if branch == None:
                branch = octofuss.Forest(child)
                root.register(branch)
            return branch

    # Load plugins into the tree
    plugindir = os.environ.get("OCTOFUSS_PLUGINS", None)
    if not plugindir:
        plugindir = conf.get("General", "plugindir")
    for p in plugins.load_plugins(plugindir):
        # skip init
        if "__init__" in p.__name__: continue

        try:
            for pdata in p.init(conf = conf, tree = tree):
                branch = pdata.get("tree", None)
                root = get_forest(pdata.get("root", "/"))
                root.register(branch)
        except Exception as e:
            print("Exception caught loading plugin %s: skipping plugin" % p,
                  file=sys.stderr)
            print("Exception details:", file=sys.stderr)
            import traceback
            details = traceback.format_exc()
            print("\t"+details.rstrip().replace("\n", "\n\t"))

def getpass(prompt = "Password: "):
    import termios
    fd = sys.stdin.fileno()
    old = termios.tcgetattr(fd)
    new = termios.tcgetattr(fd)
    new[3] = new[3] & ~termios.ECHO          # lflags
    try:
        termios.tcsetattr(fd, termios.TCSADRAIN, new)
        passwd = input(prompt)
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old)
    print()
    return passwd


if __name__ == "__main__":
    class Parser(optparse.OptionParser):
        def __init__(self, *args, **kwargs):
            # Yes, in 2009 optparse from the *standard library* still uses old
            # style classes
            optparse.OptionParser.__init__(self, *args, **kwargs)

        def error(self, msg):
            sys.stderr.write("%s: error: %s\n\n" % (self.get_prog_name(), msg))
            self.print_help(sys.stderr)
            sys.exit(2)

    parser = Parser(usage="usage: %prog [options]",
                    version="%prog",
                    description="Octofuss central server")
    parser.add_option("--reset-root-password", action="store_true", help="reset the root password")
    parser.add_option("--change-password", action="store_true", help="change a user password")
    parser.add_option("--add-user", action="store_true", help="add a new octofuss user")
    parser.add_option("--del-user", action="store_true", help="delete a octofuss user")
    parser.add_option("--reset-permissions", action="store_true", help="reset all access permissions")
    parser.add_option("--random-data", action="store_true", help="populate db with random data")

    (opts, args) = parser.parse_args()
    read_config()
    from octofussd.data import models

    if opts.reset_root_password:
        if len(args) == 1:
            models.Auth.reset_root(args[0])
        else:
            while True:
                pwd1 = getpass("Root password: ")
                pwd2 = getpass("Retype password: ")
                if pwd1 != pwd2:
                    print("Passwords mismatch.", file=sys.stderr)
                elif pwd1 == "":
                    print("Passwords are empty.", file=sys.stderr)
                else:
                    models.Auth.reset_root(pwd1)
                    break

    elif opts.random_data:
        model.fillrandom()

    elif opts.change_password:
        username = input("Username: ")
        try:
            wrong_counter = 0
            u = models.Auth.objects.get(user_name=username)
            while True:
                pwd1 = getpass("%s password: " % username)
                pwd2 = getpass("Retype password: ")
                if pwd1 != pwd2:
                    print("Passwords mismatch.", file=sys.stderr)
                    wrong_counter += 1
                elif pwd1 == "":
                    print("Passwords are empty.", file=sys.stderr)
                    wrong_counter += 1
                else:
                    u = models.Auth.objects.get(user_name=username)
                    u.password = pwd1
                    break
                if wrong_counter == 3:
                    print("Too many retries.", file=sys.stderr)
                    break
        except sqlobject.SQLObjectNotFound:
            print("User not found!", file=sys.stderr)

    elif opts.reset_permissions:
        r = models.AuthPath.objects.all().delete()

    elif opts.add_user:
        username = input("Username: ")
        try:
            u = models.Auth.objects.get(user_name=username)
        except:
            wrong_counter = 0
            while True:
                pwd1 = getpass("%s password: " % username)
                pwd2 = getpass("Retype password: ")
                if pwd1 != pwd2:
                    print("Passwords mismatch.", file=sys.stderr)
                    wrong_counter += 1
                elif pwd1 == "":
                    print("Passwords are empty.", file=sys.stderr)
                    wrong_counter += 1
                else:
                    u = models.Auth.objects.create(
                        user_name = username,
                        email_address = "%s@localhost" % username,
                        display_name = username,
                        password = pwd1
                        )
                    break
                if wrong_counter == 3:
                    print("User not created, too many wrong retries",
                          file=sys.stderr)
                    break
        else:
            print("User exists", file=sys.stderr)

    elif opts.del_user:
        username = input("Username: ")
        try:
            models.Auth.objects.get(user_name=username).delete()
            print("User deleted")
        except:
            print("User not exists.", file=sys.stderr)

    else:
        parser.print_help(sys.stdout)
    sys.exit(0)
else:
    read_config()
    build_tree()

    hookdir = os.environ.get("OCTOFUSS_HOOKS", None)
    if hookdir == None:
        hookdir = conf.get("General", "hookdir")

    octofuss.hooks.configure(hookdir = hookdir)

    try:
        NETPORT = int(conf.get("General", "port"))
    except:
        # the default one
        NETPORT = 13400

    application = service.Application('octofussd')

    f = OctofussService()

    # our server discovery stuff
    # we listen of UDP to allow fuss-client to
    # discover the server on the network
    # doing a broadcast search
    class EchoUDP(DatagramProtocol):
        def datagramReceived(self,datagram,address):
            self.transport.write(datagram,address)

    reactor.listenUDP(NETPORT, EchoUDP())

    serviceCollection = service.IServiceCollection(application)
    internet.TCPServer(NETPORT, server.Site(f.getResource())
                       ).setServiceParent(serviceCollection)
