/* pggpasscache.c - implementation of a passphrase cache
 *      Copyright (C) 1999 Michael Roth <mroth@gnupg.org>
 *
 * This file is part of PGG (Privacy Guard Glue).
 *
 * PGG 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.
 *
 * PGG 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
 */


#include <includes.h>
#include <pgg.h>
#include <pggpasscache.h>
#include <pggdebug.h>


#define old_pc		((PggPasscachePtr)(_old_pc))
#define pc		((PggPasscachePtr)(_pc))


static void expire(PggPasscachePtr pcptr, int force);


PggPasscache pgg_passcache_new(PggErrenv errenv)
{
    PggPasscachePtr	new_pc;
    
    PGG_CHECK_SKIP_ARG(NULL);
    
    if (!( new_pc = _malloc(PggPasscache) ))
        PGG_RETURN_ERR_ARG(RESOURCE, MEMORY, NULL);
    
    memset(new_pc, 0, _size(PggPasscache));
    
    new_pc->magic      = PggPasscacheMAGIC;
    new_pc->refcounter = 1;
    new_pc->policy     = PGG_PASSCACHE_POLICY_TIMEOUT;
    
    return _hd(PggPasscache, new_pc);
}


void pgg_passcache_addref(PggPasscache _pc, PggErrenv errenv)
{
    PGG_STD_ASSERT(PggPasscache, pc);
    pc->refcounter++;
}


PggPasscache pgg_passcache_clone(PggPasscache _old_pc, PggErrenv errenv)
{
    PggPasscachePtr	new_pc;
    PggPasscacheEntry *	old_entry;
    PggPasscacheEntry * new_entry;
    
    PGG_STD_ASSERT_ARG(PggPasscache, old_pc, NULL);
    
    expire(old_pc, 0);
    
    if (!( new_pc = _malloc(PggPasscache) ))
        PGG_RETURN_ERR_ARG(RESOURCE, MEMORY, NULL);
    
    memcpy(new_pc, old_pc, _size(PggPasscache));
    
    new_pc->magic      = PggPasscacheMAGIC;
    new_pc->refcounter = 1;
    new_pc->first = NULL;
    
    old_entry = old_pc->first;
    while (old_entry) {
        if (!( new_entry = malloc(sizeof(struct PggPasscacheEntry)) )) {
            expire(new_pc, 1);
            free(new_pc);
            PGG_RETURN_ERR_ARG(RESOURCE, MEMORY, NULL);
        }
        
        if (!( new_entry->keyid = strdup(old_entry->keyid) )) {
            free(new_entry);
            expire(new_pc, 1);
            free(new_pc);
            PGG_RETURN_ERR_ARG(RESOURCE, MEMORY, NULL);
        }
        
        if (!( new_entry->passphrase = strdup(old_entry->passphrase) )) {
            free(new_entry->keyid);
            free(new_entry);
            expire(new_pc, 1);
            free(new_pc);
            PGG_RETURN_ERR_ARG(RESOURCE, MEMORY, NULL);
        }
        
        new_entry->timestamp = old_entry->timestamp;
        new_entry->next = new_pc->first;
        new_pc->first = new_entry;
        
        old_entry = old_entry->next;
    }
    
    if (new_pc->callback && new_pc->callback((PggPasscache)new_pc, PGG_PASSCACHE_CBCMD_INIT, NULL)) {
        expire(new_pc, 1);
        free(new_pc);
        PGG_RETURN_ERR_ARG(UNKNOWN, NONE, NULL);	/* FIXME: callback failure error code */
    }
    
    return _hd(PggPasscache, new_pc);
}


static void expire(PggPasscachePtr pcptr, int force)
{
    PggPasscacheEntry * next;
    PggPasscacheEntry *	prev;
    PggPasscacheEntry *	entry;
    long		expire_timestamp = time(NULL) - pcptr->timeout;
    int			i;
    
    if (pcptr->newest_timestamp <= expire_timestamp || force) {
        /*
         * Search and remove all expired entries
         */
        entry = pcptr->first;
        prev  = NULL;
        
        while (entry)
            if (entry->timestamp <= expire_timestamp || force) {
                next = entry->next;
                
                if (!entry->keyid)
                    PGG_DEBUG(("key is null!?!?"));
                else 
                    free(entry->keyid);
                
                if (!entry->passphrase)
                    PGG_DEBUG(("passphrase is null!?!?"));
                else {
                    for (i=0; entry->passphrase[i]; entry->passphrase[i++] = 0);
                    free(entry->passphrase);
                }
                
                if (prev)
                    prev->next = next;
                else
                    pcptr->first = next;
                
                free(entry);
                
                entry = next;
            }
            else {
                prev  = entry;
                entry = entry->next;
            }
        
        /*
         * Update the newest_timestamp
         */
        entry = pcptr->first;
        
        while (entry) {
            if (entry->timestamp > pcptr->newest_timestamp)
                pcptr->newest_timestamp = entry->timestamp;
            
            entry = entry->next;
        }
    }
}


void pgg_passcache_release(PggPasscache _pc, PggErrenv errenv)
{
    PGG_STD_ASSERT(PggPasscache, pc);
    if (!--pc->refcounter) {
        if (pc->callback)
            pc->callback(_pc, PGG_PASSCACHE_CBCMD_RELEASE, NULL);
        expire(pc, 1);
        free(pc);
    }
}


void pgg_passcache_set_timeout(PggPasscache _pc, long seconds, PggErrenv errenv)
{
    PGG_STD_ASSERT(PggPasscache, pc);
    PGG_ASSERT(seconds >= 0, ARGUMENT, VALUE);
    pc->timeout = seconds;
    expire(pc, 0);
}


long pgg_passcache_get_timeout(PggPasscache _pc, PggErrenv errenv)
{
    PGG_STD_ASSERT_ARG(PggPasscache, pc, -1);
    expire(pc, 0);
    return pc->timeout;
}


void pgg_passcache_set_policy(PggPasscache _pc, PggPasscache_Policy policy, PggErrenv errenv)
{
    PGG_STD_ASSERT(PggPasscache, pc);
    PGG_ASSERT(policy >= PGG_PASSCACHE_POLICY_REFRESH, ARGUMENT, VALUE);
    PGG_ASSERT(policy <= PGG_PASSCACHE_POLICY_TIMEOUT, ARGUMENT, VALUE);
    expire(pc, 0);
    pc->policy = policy;
}


PggPasscache_Policy pgg_passcache_get_policy(PggPasscache _pc, PggErrenv errenv)
{
    PGG_STD_ASSERT_ARG(PggPasscache, pc, PGG_PASSCACHE_POLICY_ERROR);
    expire(pc, 0);
    return pc->policy;
}


static PggPasscacheEntry * search(PggPasscachePtr pcptr, const char * keyid)
{
    PggPasscacheEntry *	entry = pcptr->first;
    PggPasscacheEntry *	prev = NULL;
    
    while (entry)
        if (strcmp(entry->keyid, keyid) == 0) {
            /*
             * When we found the passphrase of a given keyid, we move this entry
             * to the front to make it easy to remove this entry if the caller
             * need to do this.
             */
            if (prev) {
                prev->next = entry->next;
                entry->next = pcptr->first;
                pcptr->first = entry;
            }
            return entry;
        }
        else {
            prev = entry;
            entry = entry->next;
        }
    
    return NULL;
}


void pgg_passcache_clear(PggPasscache _pc, PggErrenv errenv)
{
    PGG_STD_ASSERT(PggPasscache, pc);
    expire(pc, 1);
}


void pgg_passcache_expire(PggPasscache _pc, PggErrenv errenv)
{
    PGG_STD_ASSERT(PggPasscache, pc);
    expire(pc, 0);
}


void pgg_passcache_set_passphrase(PggPasscache _pc, const char * keyid, const char * passphrase, PggErrenv errenv)
{
    PggPasscacheEntry * entry;
    char *		tmp;
    int			i;
    
    PGG_STD_ASSERT(PggPasscache, pc);
    PGG_ASSERT(keyid, ARGUMENT, NULLPTR);
    PGG_ASSERT(passphrase, ARGUMENT, NULLPTR);
    
    if (strlen(keyid) < 16)
        PGG_RETURN_ERR(ARGUMENT, TOSMALL);
    if (strlen(keyid) > 16)
        PGG_RETURN_ERR(ARGUMENT, TOLARGE);
    
    expire(pc, 0);
    
    if ((entry = search(pc, keyid))) {
        if (strcmp(entry->passphrase, passphrase) != 0) {
            /*
             * An old entry should be updated
             */
            if (!( tmp = strdup(passphrase) ))
                PGG_RETURN_ERR(RESOURCE, MEMORY);
            
            for (i=0; entry->passphrase[i]; entry->passphrase[i++] = 0);
            
            free(entry->passphrase);
            
            entry->passphrase = tmp;
        }
    }
    else {
        /*
         * Create an entiry new entry
         */
         if (!( entry = (PggPasscacheEntry *)malloc(sizeof(struct PggPasscacheEntry)) ))
             PGG_RETURN_ERR(RESOURCE, MEMORY);
         
         if (!( entry->keyid = strdup(keyid) )) {
             free(entry);
             PGG_RETURN_ERR(RESOURCE, MEMORY);
         }
         
         if (!( entry->passphrase = strdup(passphrase) )) {
             free(entry->keyid);
             free(entry);
             PGG_RETURN_ERR(RESOURCE, MEMORY);
         }
         
         entry->next = pc->first;
         pc->first = entry;
    }
    
    /*
     * Adjust newest_timestamp and entry->timestamp
     */
    pc->newest_timestamp = time(NULL);
    entry->timestamp = pc->newest_timestamp;
}


void pgg_passcache_del_passphrase(PggPasscache _pc, const char * keyid, PggErrenv errenv)
{
    PggPasscacheEntry * entry;
    int			i;
    
    PGG_STD_ASSERT(PggPasscache, pc);
    PGG_ASSERT(keyid, ARGUMENT, NULLPTR);
    
    if (strlen(keyid) < 16)
        PGG_RETURN_ERR(ARGUMENT, TOSMALL);
    if (strlen(keyid) > 16)
        PGG_RETURN_ERR(ARGUMENT, TOLARGE);
    
    if (!( entry = search(pc, keyid) ))
        PGG_RETURN_ERR(REQUEST, NOTFOUND);
    
    /*
     * search() moves the found entry to the top of the list.
     * So we could easiely remove this node.
     */
    pc->first = entry->next;
    
    for (i=0; entry->passphrase[i]; entry->passphrase[i++] = 0);
    
    free(entry->keyid);
    free(entry->passphrase);
    free(entry);
    
    /*
     * Finally remove expired entries
     */
    expire(pc, 0);
}


const char * pgg_passcache_get_passphrase(PggPasscache _pc, const char * keyid, PggErrenv errenv)
{
    PggPasscacheEntry * entry;
    
    PGG_STD_ASSERT_ARG(PggPasscache, pc, NULL);
    PGG_ASSERT_ARG(keyid, ARGUMENT, NULLPTR, NULL);
    
    if (strlen(keyid) < 16)
        PGG_RETURN_ERR_ARG(ARGUMENT, TOSMALL, NULL);
    if (strlen(keyid) > 16)
        PGG_RETURN_ERR_ARG(ARGUMENT, TOLARGE, NULL);
    
    expire(pc, 0);
    
    if (!( entry = search(pc, keyid) )) {
        /*
         * The passphrase of a requested keyid isn't present in this cache.
         * When we have a callback, we ask the callback to set the passphrase.
         */
        if (!pc->callback)
            PGG_RETURN_ERR_ARG(REQUEST, NOTFOUND, NULL);
        
        if (pc->callback(_pc, PGG_PASSCACHE_CBCMD_ASK, keyid))
            PGG_RETURN_ERR_ARG(UNKNOWN, NONE, NULL);			/* FIXME: We need an error code for callback errors */
        
        if (!( entry = search(pc, keyid) ))
            PGG_RETURN_ERR_ARG(REQUEST, NOTFOUND, NULL);
    }
    
    /*
     * Refresh the timestamp value of the found entry if the 
     * appropriate policy is set.
     */
    if (pc->policy == PGG_PASSCACHE_POLICY_REFRESH) {
        pc->newest_timestamp = time(NULL);
        entry->timestamp = pc->newest_timestamp;
    }
    
    return entry->passphrase;
}


void pgg_passcache_set_callback(PggPasscache _pc, PggPasscache_Cb callback, PggErrenv errenv)
{
    PGG_STD_ASSERT(PggPasscache, pc);
    
    if (callback && callback(_pc, PGG_PASSCACHE_CBCMD_INIT, NULL))
        PGG_RETURN_ERR(UNKNOWN, NONE);
    
    if (pc->callback)
        pc->callback(_pc, PGG_PASSCACHE_CBCMD_RELEASE, NULL);
    
    pc->callback = callback;
}


