/* eurephiadb_session.c  --  Functions for handling sessions from eurephia-auth
 *
 *  GPLv2 only - Copyright (C) 2008 - 2012
 *               David Sommerseth <dazo@users.sourceforge.net>
 *
 *  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; version 2
 *  of the License.
 *
 *  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.
 *
 */

/**
 * @file   eurephiadb_session.c
 * @author David Sommerseth <dazo@users.sourceforge.net>
 * @date   2008-08-06
 *
 * @brief  Handles creating user sessions, which is unique per openvpn connection.
 *
 */


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define EUREPHIA_FWINTF
#include <eurephiafw_struct.h>
#include <eurephia_context.h>
#include "eurephia_nullsafe.h"
#include "eurephia_log.h"
#include "eurephiadb_session.h"
#include <eurephiadb_session_common.h>
#include <environment.h>
#include <randstr.h>
#include <sha512.h>


// Also defined in the eurephiadb_driver.h, but not as extern.
extern char *(*eDBget_sessionkey_seed) (eurephiaCTX *ctx, sessionType type, const char *sessionseed);
extern char *(*eDBget_sessionkey_macaddr) (eurephiaCTX *ctx, const char *macaddr);

extern int (*eDBcheck_sessionkey_uniqueness) (eurephiaCTX *ctx, const char *seskey);

extern int (*eDBregister_sessionkey) (eurephiaCTX *ctx, const char *seed, const char *seskey);

extern eurephiaVALUES *(*eDBload_sessiondata) (eurephiaCTX *ctx, const char *sesskey);

/**
 * Generates a new eurephia session.  Session key will be created if session seed (input params) 
 * are not known.  If session seed is known, the already generated session key will be used.
 *
 * @param ctx       eurephiaCTX
 * @param digest    Contains the clients SHA1 fingerprint / digest
 * @param cname     Contains the clients X.509 Common Name field
 * @param username  The user name of the client
 * @param vpnipaddr The IP address of the VPN connection of the client
 * @param vpnipmask The IP address' network mask
 * @param remipaddr The public IP address the client is connecting from
 * @param remport   The remote port of the client connection
 *
 * @return returns a eurephiaSESSION pointer on success, otherwise NULL.
 */
eurephiaSESSION *eDBopen_session_seed(eurephiaCTX *ctx, const char *digest,
                                      const char *cname, const char *username,
                                      const char *vpnipaddr, const char *vpnipmask,
                                      const char *remipaddr, const char *remport)
{
        eurephiaSESSION *new_session = NULL;
        char *seeddata = NULL, *seed = NULL, *ptr = NULL;
        SHA512Context sha;
        uint8_t sha_res[SHA512_HASH_SIZE];
        size_t totlen = 0, i = 0;

        DEBUG(ctx, 12, "Function call: eDBopen_session_seed(ctx, '%s', '%s', '%s', '%s', '%s', '%s', '%s')",
              digest, cname, username, vpnipaddr, vpnipmask, remipaddr, remport);

        new_session = (eurephiaSESSION *) malloc_nullsafe(ctx, sizeof(eurephiaSESSION) + 2);
        if( new_session == NULL ) {
                return NULL;
        }

        // Session type is stSESSION if we do have VPN address and/or netmask
        new_session->type = ((vpnipaddr == NULL) && (vpnipmask == NULL) ? stAUTHENTICATION : stSESSION);

        // Build up a string containing all elements for the session seed
        totlen = MAXLEN_TLSDIGEST + MAXLEN_CNAME + MAXLEN_USERNAME + MAXLEN_POOLIPADDR
                + MAXLEN_TRUSTEDIP + MAXLEN_TRUSTEDPORT + 5 + 15;
        // max length of: digest + cname + username + vpnipaddr + vpnipmask
        //                + remipaddr + remport + pid + extra buffer

        seeddata = (char *) malloc_nullsafe(ctx, totlen);
        if( seeddata == NULL ) {
                free_nullsafe(ctx, new_session);
                return NULL;
        }
        snprintf(seeddata, totlen, "%.60s%.64s%.34s%.34s%.34s%.6s%05i",
                 digest, cname, username, vpnipaddr, remipaddr, remport, getpid());

        // Generate a SHA512 version of session seed
        memset(&sha, 0, sizeof(SHA512Context));
        memset(&sha_res, 0, sizeof(sha_res));
        SHA512Init(&sha);
        SHA512Update(&sha, seeddata, totlen);
        SHA512Final(&sha, sha_res);

        seed = (char *) malloc_nullsafe(ctx, (SHA512_HASH_SIZE*2)+3);
        if( seed == NULL ) {
                free_nullsafe(ctx, seeddata);
                free_nullsafe(ctx, new_session);
                return NULL;
        }
        ptr = seed;

        for( i = 0; i < SHA512_HASH_SIZE; i++ ) {
                sprintf(ptr, "%02x", sha_res[i]);
                ptr += 2;
        }
        memset(&sha, 0, sizeof(SHA512Context));
        memset(&sha_res, 0, sizeof(sha_res));

        DEBUG(ctx, 13, "Using session seed '%s'", seed);

        // Try to retrieve the sessionkey from the database, based on the session seed
        new_session->sessionkey = eDBget_sessionkey_seed(ctx, new_session->type, seed);
        if( new_session->sessionkey == NULL ) {
                // ... if we do not find a sessionkey ... lets generate one
                int rndlen = 0;
                char *rndstr = NULL;
                char *skeydata = NULL;
                int loop = 0, uniqcheck = 0;

                DEBUG(ctx, 13, "Unknown session seed, creating new session key");

                // Loop until we get a unique sessionkey - don't loop more than 10 times
                skeydata = (char *) malloc_nullsafe(ctx, (totlen*2)+4);
                if( skeydata == NULL ) {
                        free_nullsafe(ctx, new_session->sessionkey);
                        free_nullsafe(ctx, new_session);
                        free_nullsafe(ctx, seeddata);
                        free_nullsafe(ctx, seed);
                        return NULL;
                }
                do {
                        memset(skeydata, 0, (totlen*2)+4);

		// FIXME:  Validate that we have enough random data for the session key

                        // Append some random data to our session seed
                        rndstr = (char *) malloc_nullsafe(ctx, (totlen * 2));
                        if( rndstr == NULL ) {
                                free_nullsafe(ctx, new_session->sessionkey);
                                free_nullsafe(ctx, new_session);
                                free_nullsafe(ctx, seeddata);
                                free_nullsafe(ctx, seed);
                                return NULL;
                        }
                        rndlen = ((totlen * 2) - strlen_nullsafe(seed) - 2);

                        if( !eurephia_randstring(ctx, rndstr, rndlen) ) {
                                eurephia_log(ctx, LOG_PANIC, 0,
                                             "Could not generate enough random data for session key");
                                free_nullsafe(ctx, new_session->sessionkey);
                                free_nullsafe(ctx, new_session);
                                free_nullsafe(ctx, seeddata);
                                free_nullsafe(ctx, seed);
                                return NULL;
                        }

                        // Generate SHA512 version of the session data
                        SHA512Init(&sha);
                        SHA512Update(&sha, rndstr, rndlen);
                        SHA512Final(&sha, sha_res);

                        free_nullsafe(ctx, new_session->sessionkey);
                        new_session->sessionkey = (char *) malloc_nullsafe(ctx, (SHA512_HASH_SIZE*2)+3);
                        if( new_session->sessionkey == NULL ) {
                                free_nullsafe(ctx, new_session);
                                free_nullsafe(ctx, seeddata);
                                free_nullsafe(ctx, seed);
                                return NULL;
                        }

                        ptr = new_session->sessionkey;
                        for( i = 0; i < SHA512_HASH_SIZE; i++ ) {
                                sprintf(ptr, "%02x", sha_res[i]);
                                ptr += 2;
                        }
                        memset(&sha, 0, sizeof(SHA512Context));
                        memset(&sha_res, 0, sizeof(sha_res));
                        free_nullsafe(ctx, rndstr);

                        loop++;
                        uniqcheck = eDBcheck_sessionkey_uniqueness(ctx, new_session->sessionkey);
                } while( (uniqcheck == 0) && loop < 11 );
                free_nullsafe(ctx, skeydata);

                // If we did not manage to create a unique session key (random data collection must have failed!)
                if( uniqcheck == 0 ) {
                        eurephia_log(ctx, LOG_FATAL, 0,
                                     "Did not manage to create a unique sessionkey after %i attempts", loop-1);
                        free_nullsafe(ctx, new_session->sessionkey);
                        free_nullsafe(ctx, new_session);
                        free_nullsafe(ctx, seeddata);
                        free_nullsafe(ctx, seed);
                        return NULL;
                }

                // Save this session key in the database and connect it to this session seed
                if( eDBregister_sessionkey(ctx, seed, new_session->sessionkey) == 0) {
                        eurephia_log(ctx, LOG_FATAL, 0, "Could not register sessionkey");
                        free_nullsafe(ctx, new_session->sessionkey);
                        free_nullsafe(ctx, new_session);
                        free_nullsafe(ctx, seeddata);
                        free_nullsafe(ctx, seed);
                        return NULL;
                };
                new_session->sessionstatus = SESSION_NEW;
        } else {
                new_session->sessionstatus = SESSION_EXISTING;
                DEBUG(ctx, 13, "Session seed found, using sessionkey '%s'", new_session->sessionkey);
        }
        free_nullsafe(ctx, seed);
        free_nullsafe(ctx, seeddata);

        // Load session values from the database
        new_session->sessvals = eDBload_sessiondata(ctx, new_session->sessionkey);

        // Return struct which contains session key and session variables
        return new_session;
}


/**
 * Load a session based on a known session key
 *
 * @param ctx      eurephiaCTX
 * @param sesskey  A string containing the session key
 * @param sesstype What kind of type the session data should be opend as (sessionType)
 *
 * @return Returns a pointer to the corresponding eurephiaSESSION struct on success, otherwise NULL.
 */

eurephiaSESSION *eDBsession_load(eurephiaCTX *ctx, const char *sesskey, sessionType sesstype) {
        eurephiaSESSION *new_session = NULL;

        DEBUG(ctx, 12, "Function call: eDBsession_load(ctx, '%s')", sesskey);

        new_session = (eurephiaSESSION *) malloc_nullsafe(ctx, sizeof(eurephiaSESSION) + 2);
        if( new_session == NULL ) {
                return NULL;
        }
        new_session->type = sesstype;

        // Get the sessionkey from the database
        new_session->sessionkey = strdup_nullsafe(sesskey);
        if( new_session->sessionkey == NULL ) {
                eurephia_log(ctx, LOG_CRITICAL, 0, "Failed to set the session key to '%s'",
                             sesskey);
                free_nullsafe(ctx, new_session);
                return NULL;
        }

        // Load session values from the database
        new_session->sessvals = eDBload_sessiondata(ctx, new_session->sessionkey);

        // Return struct which contains the current session
        return new_session;
}


/**
 * Open an existing eurephia session based on a MAC address.  This function is only used
 * when there is not enough information to generate a session seed and when the MAC address
 * is available.  Usually this only happens when the client has disconnected and the session
 * is about to be marked as closed.
 *
 * @param ctx     eurephiaCTX
 * @param macaddr MAC address of the client
 *
 * @return returns a eurephiaSESSION pointer on success, otherwise NULL.
 */
eurephiaSESSION *eDBopen_session_macaddr(eurephiaCTX *ctx, const char *macaddr) {
        char *sesskey = NULL;

        DEBUG(ctx, 12, "Function call: eDBopen_session_mac(ctx, '%s')", macaddr);

        // Get the sessionkey from the database
        sesskey = eDBget_sessionkey_macaddr(ctx, macaddr);
        if( sesskey == NULL ) {
                eurephia_log(ctx, LOG_CRITICAL, 0, "Could not find an active session for MAC address '%s'",
                             macaddr);
                return NULL;
        }
        DEBUG(ctx, 13, "Session seed found, using sessionkey '%s'", sesskey);

        // Open and load the session from the database
        return eDBsession_load(ctx, sesskey, stSESSION);
}
