/*
 * ImapProxy - a caching IMAP proxy daemon
 * Copyright (C) 2002 Steven Van Acker
 * 
 * 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.
 *
 */


#include <string.h>
#include <strings.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <database.h>
#include <output.h>
#include <splitstring.h>
#include <processing_child.h>
#include <network.h>
#include <configfile.h>


extern int Globaltime;

DB_record *DB_start = NULL;
DB_record *DB_end = NULL;

/* add a record to the linked list.
 * The record is copied.
 * DB_start and DB_end are updated.
 * the prev and next field in the new record are changed
 * the message structured are presumed empty
 */
int db_add(DB_record *rec)
{
    DB_record *p;
    
    if(!(p = (DB_record *)malloc(sizeof(DB_record))))
    {
	debug("db_add(): Could not malloc.\n");
	return -1;
    }
    
    memcpy(p,rec,sizeof(DB_record));

    if(DB_end) DB_end->next = p;
    p->prev = DB_end;
    p->next = NULL;

    if(!DB_end) DB_start = p;
    DB_end = p;
    
    return 0;
}

/* this function cleans up a record
 * it removes everything, except the parts necessary for keeping alive the connection
 *
 * what it doesn't remove :
 * 	username
 * 	password
 * 	server socket
 * 	status
 * 	next and prev pointers
 */
int db_cleanup(DB_record *rec)
{
    /* keep : 	username/password
     * 		server socket
     * 		status
     * 		next and prev pointers
     */

    /* close client socket */
    close(rec->clientsocket);
    rec->clientsocket = -1;

    /* remove pid */
    rec->pid = -1;

    /* authenticate tags */
    if(rec->clientauthtag)
    {
	free(rec->clientauthtag);
	rec->clientauthtag = NULL;
    }

    if(rec->proxyauthtag)
    {
	free(rec->proxyauthtag);
	rec->proxyauthtag = NULL;
    }

    /* remove messages */
    db_delete_client_message(rec);
    db_delete_server_message(rec);

    /* close pipe */
    close(rec->ipcfd[0]);
    close(rec->ipcfd[1]);

    rec->ipcfd[0] = -1;
    rec->ipcfd[1] = -1;

    rec->serverlastnoop = Globaltime;
    rec->clientidlesince = Globaltime;
    rec->serveridlesince = Globaltime;

    rec->clientlocaladdress.s_addr = 0;
    rec->clientlocalport = 0;

    return 0;
}

/* remove a record from the linked list
 * the record is freed
 * all resources are freed
 * DB_start and DB_end are updated
 */
int db_del(DB_record *rec)
{
    DB_record *p,*n;
    
    p = rec->prev;
    n = rec->next;

    if(p) p->next = n;
    else DB_start = n;
    if(n) n->prev = p;
    else DB_end = p;


    db_cleanup(rec);

    /* don't leave password lying around */
    memset(rec->password,0,DB_STRING_LENGTH);
    close(rec->serversocket);
    free(rec);

    return 0;
}

/* initialize a record
 */
int db_init_record(DB_record *rec)
{
    memset(rec,0,sizeof(DB_record));

    db_set_server_connection_state(rec,DB_STATE_SERVER_NOT_CONNECTED);
    db_set_client_state(rec,DB_STATE_CLIENT_NOT_AUTHENTICATED);
    db_set_server_state(rec,DB_STATE_SERVER_NONE);
    db_set_proxy_state(rec,DB_STATE_PROXY_NONE);
    db_set_misc_state(rec,DB_STATE_MISC_NONE);
    db_set_record_state(rec,DB_STATE_RECORD_NONE);
    db_set_capability_state(rec,DB_STATE_CAPABILITY_NONE);

    rec->clientsocket = -1;
    rec->serversocket = -1;
    rec->ipcfd[0] = -1;
    rec->ipcfd[1] = -1;
    rec->pid = -1;

    /* set these to 1, so we know there is no command in progress */
    rec->clientmessage.completed = 1;
    rec->servermessage.completed = 1;

    /* no messages yet, so they are unchecked. */
    rec->clientmessage.status = DB_MESSAGE_UNCHECKED;
    rec->servermessage.status = DB_MESSAGE_UNCHECKED;
   
    rec->clientidlesince = Globaltime;
    rec->serverlastnoop = Globaltime;
    rec->serveridlesince = Globaltime;

    rec->clientlocaladdress.s_addr = 0;
    rec->clientlocalport = 0;
    rec->proxylocalport = 0;
 
    rec->reuse = 0;
    
    return 0;
}

#define INDEX_FOR_SERVER	0
#define INDEX_FOR_CLIENT	1

/* when in process mode or unchecked mode :
 * 	this function appends a string to the current line in the message (client or server)
 * when in passthrough mode :
 * 	this function sends the received data to the destination, without appending it
 *
 * the function checks if the command is LOGOUT or not
 * if it is LOGOUT, the message is switched to process mode, and the message is interpreted
 * otherwise, we just send away the message
 *
 * ALL OF THIS IS ONLY DONE IN THE CHILD !!!
 * when in the parent, the message is just processed. (since we only do authentication there, and don't expect big messages)
 *
 */

int db_append_to_message(DB_record *rec,char *buf,int what)
{
    struct message_t *currmessage = NULL;
    int newsize = 0;
    char *currbuf = NULL;
    char *newbuf = NULL;
    int outfd = -1,i = 0;
    Split_string ss;

    /* decide which message we want to append to */
    if(what == INDEX_FOR_SERVER)
    {
	currmessage = &(rec->servermessage);
	outfd = rec->clientsocket;
    }
    else
    {
	currmessage = &(rec->clientmessage);
	outfd = rec->serversocket;
    }
   
    /* message is not complete */
    currmessage->completed = 0;
  
    /* only care about the state of the message if we are in a forked child */
    if(!forkedchild)
	goto db_process;
    
    /* what mode are we in ? */
    switch(currmessage->status)
    {
	case DB_MESSAGE_UNCHECKED:

	    //debug("db_append_to_buffer(%d): We are in unchecked mode ! (%s)\n",what,buf);
	    
	    init_split_string(&ss);
		
	    if(!currmessage->count)
	    {
		make_split_string_strip_crlf(buf,&ss," ");
	    }
	    else
	    {
		/* something is in the first slot of the message.
		 * check if it contains our command
		 * if it doesn't don't check the buffer !
		 * just let the buffer be appended to the message, and check the first line
		 * on the next append
		 */
		make_split_string(currmessage->lines[0],&ss," ");
	    }
		
	    if(split_string_count_parts(&ss) > 1)
	    {
		if(!strcasecmp(split_string_get(&ss,1),"LOGOUT" ))
		{
		    debug("db_append_to_buffer(%d): LOGOUT received !!! going to process mode\n",what);
		    currmessage->status = DB_MESSAGE_PROCESS;
		}
		else
		{
		    currmessage->status = DB_MESSAGE_PASSTHROUGH;

		    /* if any previous lines, send them */
		    /* there should only be 1 (one) line, otherwise there is a problem */
		    for(i = 0;i < currmessage->count;i++)
		    {
			socket_printf(outfd,"%s",currmessage->lines[i]);
		    }

		    if(currmessage->count > 1)
		    {
			debug("db_append_to_buffer(%d): There is more than one previous line ! this cannot happen ?\n");
		    }

		    /* send current buffer */
		    socket_printf(outfd,"%s",buf);
		    /* we're in passthrough mode, and everything sent so far, was passed on
		     * we can go home now
		     */
		    
		    delete_split_string(&ss);
		    return 0;
		}
	    }
	    else
	    {
		/* message is not complete so we'll check again in the next append ! */
	    }
	    
	    delete_split_string(&ss);
	    
	    break;
	case DB_MESSAGE_PASSTHROUGH:
	    //debug("db_append_to_buffer(%d): We are in passthrough mode !\n",what);
	    /* send current buffer */
	    socket_printf(outfd,"%s",buf);
	    return 0;
	    break;
	case DB_MESSAGE_PROCESS:
	    //debug("db_append_to_buffer(%d): We are in process mode !\n",what);
	    break;
    }
   
db_process:
    if(!currmessage->count)
    {
	if(db_create_new_line_in_message(rec,what) < 0)
	{
	    debug("db_append_to_buffer(): db_create_new_line_in_message() returned error !\n");
	    return -1;
	}
    }
    
    currbuf = currmessage->lines[currmessage->count - 1];
    
    /* calculate new length */
    newsize = strlen(buf);
    if(currbuf) newsize += strlen(currbuf);
    newsize++;	// \0

    /* allocate space */
    if(!(newbuf = (char *)calloc(1,newsize)))
    {
	debug("db_append_to_buffer(): Out of memory.\n");
	return -1;
    }

    /* copy and append */
    if(currbuf) strcpy(newbuf,currbuf);
    strcat(newbuf,buf);
    
    /* free */

    if(currbuf) free(currbuf);
    
    /* assign */
    currmessage->lines[currmessage->count - 1] = newbuf;
    
    return 0;
}

/* create a new line in the message = allocate an extra pointer for it */
int db_create_new_line_in_message(DB_record *rec,int what)
{
    struct message_t *currmessage = NULL;

    if(what == INDEX_FOR_SERVER)
	currmessage = &(rec->servermessage);
    else
	currmessage = &(rec->clientmessage);
   
    currmessage->completed = 0;
    currmessage->count++;
    
    if(!(currmessage->lines = (char **)realloc(currmessage->lines, sizeof(char *) * currmessage->count)))
    {
	debug("db_create_new_line_in_message(): Out of memory.\n");
	return -1;
    }

    currmessage->lines[currmessage->count - 1] = NULL;

    return 0;
}

/* delete a message and release the allocated space */
int db_delete_message(DB_record *rec, int what)
{
    struct message_t *x;
    int i;

    if(what == INDEX_FOR_SERVER)
	x = &(rec->servermessage);
    else
	x = &(rec->clientmessage);
	
    
    /* clear the messages, if any */
    if(x->count != 0)
    {
	for(i = 0;i < x->count; i++)
	    free(x->lines[i]);

	free(x->lines);
    }

    x->count = 0;
    x->completed = 1;
    x->lines = NULL;
    x->status = DB_MESSAGE_UNCHECKED;

    return 0;
}

int db_append_to_client_message(DB_record *rec,char *buf)
{
    return db_append_to_message(rec,buf,INDEX_FOR_CLIENT);
}

int db_append_to_server_message(DB_record *rec,char *buf)
{
    return db_append_to_message(rec,buf,INDEX_FOR_SERVER);
}

int db_delete_client_message(DB_record *rec)
{
    return db_delete_message(rec,INDEX_FOR_CLIENT);
}

int db_delete_server_message(DB_record *rec)
{
    return db_delete_message(rec,INDEX_FOR_SERVER);
}

int db_create_new_line_in_client_message(DB_record *rec)
{
    return db_create_new_line_in_message(rec,INDEX_FOR_CLIENT);
}

int db_create_new_line_in_server_message(DB_record *rec)
{
    return db_create_new_line_in_message(rec,INDEX_FOR_SERVER);
}

/* return the i'th line of a message
 * or NULL if i is out of range 
 */
char *db_get_message(DB_record *rec, int i,int what)
{
    struct message_t *currmessage = NULL;
    
    if(what == INDEX_FOR_SERVER)
	currmessage = &(rec->servermessage);
    else
	currmessage = &(rec->clientmessage);

    if(i < 0 || i >= currmessage->count)
	return NULL;

    return currmessage->lines[i];
}

char *db_get_server_message(DB_record *rec,int i)
{
    return db_get_message(rec,i,INDEX_FOR_SERVER);
}

char *db_get_client_message(DB_record *rec,int i)
{
    return db_get_message(rec,i,INDEX_FOR_CLIENT);
}


/* return a specific field from the status field 
 * see the headers for more information
 */
int db_get_state(DB_record *p, int mask)
{
    if(!(p->status & mask))
	debug("db_get_state(): state vs. mask mismatch ! %0.8p %0.8p\n",p->status,mask);

    return p->status & mask;
}

/* set a specific field in the status field
 */
void db_set_state(DB_record *p,int s,int mask)
{
    if(!(mask & s))
	debug("db_set_state(): state vs. mask mismatch ! %0.8p %0.8p\n",s,mask);
    
    p->status = ((mask ^ 0xffffffff) & p->status) | s;
}

int db_get_server_connection_state(DB_record *p)
{
    return db_get_state(p,DB_STATE_SERVER_CONNECTION_MASK);
}

void db_set_server_connection_state(DB_record *p,int s)
{
    db_set_state(p,s,DB_STATE_SERVER_CONNECTION_MASK);
}

int db_get_client_state(DB_record *p)
{
    return db_get_state(p,DB_STATE_CLIENT_MASK);
}

void db_set_client_state(DB_record *p,int s)
{
    db_set_state(p,s,DB_STATE_CLIENT_MASK);
}

int db_get_server_state(DB_record *p)
{
    return db_get_state(p,DB_STATE_SERVER_MASK);
}

void db_set_server_state(DB_record *p,int s)
{
    db_set_state(p,s,DB_STATE_SERVER_MASK);
}

int db_get_proxy_state(DB_record *p)
{
    return db_get_state(p,DB_STATE_PROXY_MASK);
}

void db_set_proxy_state(DB_record *p,int s)
{
    db_set_state(p,s,DB_STATE_PROXY_MASK);
}

int db_get_misc_state(DB_record *p)
{
    return db_get_state(p,DB_STATE_MISC_MASK);
}

void db_set_misc_state(DB_record *p,int s)
{
    db_set_state(p,s,DB_STATE_MISC_MASK);
}

int db_get_record_state(DB_record *p)
{
    return db_get_state(p,DB_STATE_RECORD_MASK);
}

void db_set_record_state(DB_record *p,int s)
{
    db_set_state(p,s,DB_STATE_RECORD_MASK);
}

int db_get_capability_state(DB_record *p)
{
    return db_get_state(p,DB_STATE_CAPABILITY_MASK);
}

void db_set_capability_state(DB_record *p,int s)
{
    db_set_state(p,s,DB_STATE_CAPABILITY_MASK);
}

/* find the next free inactive record
 * this record will be assigned to an authenticated temporary record
 *
 * it should :
 * 	contain the same username
 * 	contain the same password
 * 	be INACTIVE
 * 	not be in the middle of processing a message
 * 	not be "me" (the temporary record)
 */
DB_record *db_find_record(DB_record *me,char *username,char *password)
{
    DB_record *p = NULL;

    p = DB_start;
    
    while(p)
    {

	/* if username and password match and if the record is inactive, and not processing a server message, 
	 * and if has need been reused too much, return it !*/
	if(p != me && db_get_record_state(p) == DB_STATE_RECORD_INACTIVE && p->servermessage.completed &&
		(p->reuse < config_max_reuse || config_max_reuse == 0) &&
		!strcmp(p->username,username) && !strcmp(p->password,password))
	{
	    return p;
	}

	p = p->next;
    }

    return NULL;
}


char *db_get_state_string(int state)
{
    int i,max;
    static char ret[80];
    
    int intvalues[] = {
	DB_STATE_SERVER_CONNECTION_MASK,
	DB_STATE_SERVER_NOT_CONNECTED,
	DB_STATE_SERVER_CONNECTION_STARTED,
	DB_STATE_SERVER_CONNECTED,
        DB_STATE_CLIENT_MASK,
        DB_STATE_CLIENT_NOT_AUTHENTICATED,
        DB_STATE_CLIENT_SENT_AUTHENTICATE_COMMAND,
        DB_STATE_CLIENT_SENT_LOGIN_COMMAND,
        DB_STATE_CLIENT_SENT_USERNAME,
        DB_STATE_CLIENT_SENT_PASSWORD,
        DB_STATE_CLIENT_AUTHENTICATED,
        DB_STATE_CLIENT_SENT_LOGOUT_COMMAND,
        DB_STATE_SERVER_MASK,
        DB_STATE_SERVER_NONE,
        DB_STATE_SERVER_SENT_GREETING,
        DB_STATE_SERVER_SENT_USERNAME_REPLY,
        DB_STATE_SERVER_SENT_PASSWORD_REPLY,
        DB_STATE_SERVER_SENT_LOGIN_OK,
        DB_STATE_SERVER_SENT_LOGIN_FAILED,
        DB_STATE_PROXY_MASK,
        DB_STATE_PROXY_NONE,
        DB_STATE_PROXY_SENT_GREETING_TO_CLIENT,
        DB_STATE_PROXY_SENT_USERNAME_REPLY_TO_CLIENT,
        DB_STATE_PROXY_SENT_PASSWORD_REPLY_TO_CLIENT,
        DB_STATE_PROXY_SENT_AUTHENTICATE_COMMAND_TO_SERVER,
        DB_STATE_PROXY_SENT_LOGIN_COMMAND_TO_SERVER,
        DB_STATE_PROXY_SENT_USERNAME_TO_SERVER,
        DB_STATE_PROXY_SENT_PASSWORD_TO_SERVER,
        DB_STATE_PROXY_SENT_LOGIN_OK_TO_CLIENT,
        DB_STATE_PROXY_SENT_LOGIN_FAILED_TO_CLIENT,
        DB_STATE_MISC_MASK,
	DB_STATE_MISC_NONE,
        DB_STATE_MISC_REMOVE_RECORD,
	DB_STATE_RECORD_MASK,
	DB_STATE_RECORD_NONE,
	DB_STATE_RECORD_FORKED_CHILD
    };

    char *values[] = {
        "DB_STATE_SERVER_CONNECTION_MASK",
        "DB_STATE_SERVER_NOT_CONNECTED",
        "DB_STATE_SERVER_CONNECTION_STARTED",
        "DB_STATE_SERVER_CONNECTED",
        "DB_STATE_CLIENT_MASK",
        "DB_STATE_CLIENT_NOT_AUTHENTICATED",
        "DB_STATE_CLIENT_SENT_AUTHENTICATE_COMMAND",
        "DB_STATE_CLIENT_SENT_LOGIN_COMMAND",
        "DB_STATE_CLIENT_SENT_USERNAME",
        "DB_STATE_CLIENT_SENT_PASSWORD",
        "DB_STATE_CLIENT_AUTHENTICATED",
        "DB_STATE_CLIENT_SENT_LOGOUT_COMMAND",
        "DB_STATE_SERVER_MASK",
        "DB_STATE_SERVER_NONE",
        "DB_STATE_SERVER_SENT_GREETING",
        "DB_STATE_SERVER_SENT_USERNAME_REPLY",
        "DB_STATE_SERVER_SENT_PASSWORD_REPLY",
        "DB_STATE_SERVER_SENT_LOGIN_OK",
        "DB_STATE_SERVER_SENT_LOGIN_FAILED",
        "DB_STATE_PROXY_MASK",
        "DB_STATE_PROXY_NONE",
        "DB_STATE_PROXY_SENT_GREETING_TO_CLIENT",
        "DB_STATE_PROXY_SENT_USERNAME_REPLY_TO_CLIENT",
        "DB_STATE_PROXY_SENT_PASSWORD_REPLY_TO_CLIENT",
        "DB_STATE_PROXY_SENT_AUTHENTICATE_COMMAND_TO_SERVER",
        "DB_STATE_PROXY_SENT_LOGIN_COMMAND_TO_SERVER",
        "DB_STATE_PROXY_SENT_USERNAME_TO_SERVER",
        "DB_STATE_PROXY_SENT_PASSWORD_TO_SERVER",
        "DB_STATE_PROXY_SENT_LOGIN_OK_TO_CLIENT",
        "DB_STATE_PROXY_SENT_LOGIN_FAILED_TO_CLIENT",
        "DB_STATE_MISC_MASK",
	"DB_STATE_MISC_NONE",
        "DB_STATE_MISC_REMOVE_RECORD",
	"DB_STATE_RECORD_MASK",
	"DB_STATE_RECORD_NONE",
	"DB_STATE_MISC_FORKED_CHILD"
    };

    max = sizeof(intvalues) / sizeof(int);

    for(i = 0;i < max; i++)
	if(state == intvalues[i])
	    return values[i];
  
    snprintf(ret,sizeof(ret),"UNKNOWN_STATUS_%s%#0x",state?"":"0x",state);
    return ret;
}

void db_print_state(DB_record *p)
{
    debug("Server connection state: %s\n",db_get_state_string(db_get_server_connection_state(p)));
    debug("Client state: %s\n",db_get_state_string(db_get_client_state(p)));
    debug("Server state: %s\n",db_get_state_string(db_get_server_state(p)));
    debug("Proxy state: %s\n",db_get_state_string(db_get_proxy_state(p)));
    debug("Misc state: %s\n",db_get_state_string(db_get_misc_state(p)));
}

void db_log_stats()
{
    DB_record *p = NULL;
    int active = 0,inactive = 0, temporary = 0, other = 0,total = 0;
    p = DB_start;

    while(p)
    {
	switch(db_get_record_state(p))
	{
	    case DB_STATE_RECORD_INACTIVE:
		inactive++;
		break;
	    case DB_STATE_RECORD_ACTIVE:
		active++;
		break;
	    case DB_STATE_RECORD_TEMPORARY:
		temporary++;
		break;
	    default:
		other++;
		break;
	}

	total++;

	p = p->next;
    }

    log(LOG_INFO,"Status: %d temporary, %d active, %d inactive, %d other, %d total.\n",temporary,active,inactive,other,total);
}


char *db_get_record_info(DB_record *p)
{
    static char buffer[64];

    snprintf(buffer,sizeof(buffer),"[%s:%d (%d)]\n",inet_ntoa(p->clientlocaladdress),p->clientlocalport,p->proxylocalport);

    return buffer;
}


syntax highlighted by Code2HTML, v. 0.9.1