/* Customer of a Swissbank
 */

#include <netinet/ip.h>
#include <syslog.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <db.h>
#include <time.h>
#include <errno.h>
#include <stdio.h>
#include <fcntl.h>

#include "swisscrypt.h"
#include "swissdef.h"


#define SC_DB_FILENAME "swissclient.db"
#define SC_PWD_ITERATIONS 100




typedef struct {
      int accid; //account id at bank
      char seed[SWISS_PWDLEN];
      int i_left;
      char next_auth[SWISS_PWDLEN];
} sc_account;

struct sockaddr_in serv_addr;
int sd = -1;
pid_t child = 0;


void swiss_sigchld(int sig){
      openlog("swissclient" , 0, LOG_USER);
      syslog(LOG_WARNING|LOG_USER, "exiting client because time is up! (return=%d)\n", SW_RET_REMOTE_ERR*(-1));
      closelog();

      if(0 <= sd)
	    close(sd);
      exit(SW_RET_REMOTE_ERR*(-1));
}


static void swiss_err_log(char *errstr){
      openlog("swissclient", LOG_PERROR, LOG_USER);
      syslog(LOG_ERR||LOG_USER, errstr);
      closelog();
}

static void swiss_info_log(char *errstr){
      openlog("swissclient", LOG_PERROR, LOG_USER);
      syslog(LOG_INFO||LOG_USER, errstr);
      closelog();
}


static void swiss_exit(int ret){
      openlog("swissclient" , 0, LOG_USER);
      syslog(LOG_INFO|LOG_USER, "exiting client with result %d\n", ret*(-1));
      closelog();
      if(0 != child)
	    kill(child, SIGTERM);
      if(0 <= sd)
	    close(sd);
      exit(ret*(-1));
}



// returns socket fd
static int connect_to_bank(struct sockaddr_in *saddr, char *ipstr, int port){
      int ret = 0;
      int sockd = 0;
      
      sockd = SWISSSOCK;

      ret = inet_pton(AF_INET, ipstr, &(saddr->sin_addr));
      if(ret <= 0){
	    swiss_err_log("connect_to_bank: parameter error in Address or -family\n");
	    close(sd);
	    return SW_RET_INT_ERR;
      }
      saddr->sin_port = htons(port);
      saddr->sin_family = AF_INET;
      
      if(0 > connect(sockd, (struct sockaddr*)saddr, sizeof(struct sockaddr_in))){
	    //	    swiss_err_log("connect_to_bank: Connecting to Bank at failed\n");
	    openlog("swissclient", LOG_PERROR, LOG_USER);
	    syslog(LOG_ERR||LOG_USER, "connect_to_bank: Connecting to Bank at %s:%d failed\n", ipstr, port);
	    closelog();
	    close(sd);
	    return SW_RET_REMOTE_ERR;
      }
      
      return sockd;
}




static DB *sc_initdb(char* vault_file){
      u_int32_t flags; /* database open flags */
      int ret;         /* function return value */
      DB *db;
      
      //db = (DB*)malloc(sizeof(DB));
      
      ret = db_create(&db, 0, 0);
      if (ret != 0) {
	    swiss_err_log("Couldn't create the Vault.");
	    free(db);
	    swiss_exit(SW_RET_INT_ERR);	    
      }
      /* Database open flags */
      flags = DB_CREATE;     /* If the database does not exist,
			      * create it.*/
      /* open the database */
      ret = db->open(db,         /* DB structure pointer */
		     NULL,        /* Transaction pointer */
		     vault_file,  /* On-disk file that holds the database. */
		     "ClientAccounts",        /* Optional logical database name */
		     DB_HASH,    /* Database access method */
		     flags,       /* Open flags */
		     DB_FILE_PERMS);         /* File mode (using defaults) */
      if (ret != 0) {
	    swiss_err_log("client account list could not be opened for you!");
	    free(db);
	    swiss_exit(SW_RET_INT_ERR);
      }
      return db;
}


// returns account id or error-code
static int swissc_send_store(const int accid, char * data, char * key){
      char *msg;
      void *b;
      struct account acc;
      int ret;
      
      acc.acc_id = accid;
      acc.auth = key;
      acc.asize = SWISS_PWDLEN;
      acc.data = data;
      acc.dsize = strlen(data)+1;
      
      b = msg = (char*)swiss_mallocstoremsg(&acc);
      if(!msg){
	    swiss_err_log("couldn't allocate memory");
	    return SW_RET_INT_ERR;
      }
      
      *msg = SW_MSG_STORE;
      b = (void*)sw_marsh_cred(&acc, ++b);
      b = (void*)sw_marsh_dsize_data(&acc,b);
      

      
      if(b-(void*)msg > (ret = send(sd, msg, b-(void*)msg, 0))){
	    swiss_err_log("swissc_send_store: couldn't send");
	    free(msg);
	    return SW_RET_REMOTE_ERR;
      }
      
      //get answer
      msg = (char*)realloc(msg, 1+sizeof(uint32_t));
      ret = recv(sd, msg, 1+sizeof(uint32_t), MSG_WAITALL);
      if(ret < 1+sizeof(uint32_t)){
	    if(ret < 0){
		  swiss_err_log("swissc_send_store: error while receiving");
		  free(msg);
		  return(SW_RET_INT_ERR);
	    } else {
		  swiss_err_log("swissc_send_store: answer too short");
		  free(msg);
		  return(SW_RET_REMOTE_ERR);
	    }
      }
      if(*msg == SW_MSG_SREP){
	    acc.acc_id = ntohl(*(uint32_t*)(msg+1));
      }
      else if(*msg == SW_MSG_ERR){
	    acc.acc_id = SW_RET_REMOTE_ERR;
      } else {
	    acc.acc_id = SW_RET_REMOTE_ERR;
      }
      
      free(msg);
      return acc.acc_id;
}


// returns account id or error code
static int swissc_send_store_new(char * data,  char * key){
      return swissc_send_store(0, data, key);
}

//returns flag
static char *swissc_send_retrieve_acc(int aid, char *key){
      int ret = 0, dreceived = 0;
      char *msg = 0, *b = 0;
      struct account acc;
      char *flag = 0;
      
      
      acc.acc_id = aid;
      acc.auth = key;
      acc.asize = SWISS_PWDLEN;
      acc.data = 0;
      acc.dsize = 0;
      
      b = msg = (char*)swiss_mallocretrmsg();
      if(!msg){
	    swiss_err_log("couldn't allocate memory");
	    return (char*)SW_RET_INT_ERR;
      }

      

      *msg = SW_MSG_RETR;
      ++b;
      b = (char*)sw_marsh_cred(&acc, b);

      if(b-msg > send(sd, msg, b-msg, 0)){
	    swiss_err_log("swissc_send_retrieve_acc: couldn't send");
	    free(msg);
	    return (char*)SW_RET_REMOTE_ERR;
      }

      //get answer
      msg = (char*)realloc(msg, 1+sizeof(uint32_t));

      memset(msg, 0, 1+sizeof(uint32_t));
      ret = recv(sd, msg, 1+sizeof(uint32_t), MSG_WAITALL);
      
      if(ret < 1+sizeof(uint32_t)){
	    if(ret < 0){
		  swiss_err_log("swissc_send_retrieve_acc: error while receiving");
		  free(msg);
		  return (char*)SW_RET_INT_ERR;
	    } else {
		  swiss_err_log("swissc_send_retrieve_acc: answer too short");
		  free(msg);
		  return (char*)SW_RET_REMOTE_ERR;
	    }
      }
      
      if(*msg == SW_MSG_RREP){
	    sw_parse_dsize(&acc, msg+1);
	    flag = (char*)malloc(acc.dsize);
	    dreceived = recv(sd, flag, acc.dsize, MSG_WAITALL);
	    if(dreceived < acc.dsize){
		  swiss_err_log("send_retreieve: retrieved insufficient number of bytes from server\n");
		  //no abort
	    }
      }
      else if(*msg == (char)SW_MSG_ERR){
	    flag = (char*)SW_RET_NOFLAG;
      } else {
	    swiss_err_log("retrieved garbled message\n");
	    flag = (char*)SW_RET_REMOTE_ERR;
      }
      
      
      free(msg);

      return flag;
}
      


/* noncom functions *************************************************
 */
static int swissc_store_flag(char *ip, char *id, char *flag){
      DB *db;
      DBT key, data, rdata;
      char init_auth[SWISS_PWDLEN];
      sc_account acc;
      int dbret, ret;
      
      memset(&key, 0, sizeof(key));
      memset(&data, 0, sizeof(data));
      memset(&rdata, 0, sizeof(data));
      memset(&acc, 0, sizeof(sc_account));

      //init db
      if(0 > (db = sc_initdb(SC_DB_FILENAME))){
	    swiss_err_log("store: error initialiting db\n");
	    return SW_RET_INT_ERR;
      }

      key.size = strlen(id) + strlen(ip);
      key.data=(void*)malloc(key.size);
      memcpy(key.data, id, strlen(id));
      memcpy(key.data+strlen(id), ip, strlen(ip));

      
      dbret = db->get(db, NULL, &key, &data, 0);
      if(dbret == DB_NOTFOUND){
	    //create authentication hash chain, initiate own db-entry
	    time((time_t*)acc.seed);
	    swiss_pwd_hash_iterate(acc.next_auth,SC_PWD_ITERATIONS, (char*)acc.seed);
	    swiss_pwd_hash(init_auth, acc.next_auth);
	    acc.i_left = SC_PWD_ITERATIONS;
	    
	    //store flag at bank get account number
	    
	    acc.accid = swissc_send_store_new(flag, init_auth);
	    
	    if(0 > acc.accid){
		  swiss_err_log("store_flag: Couldn't create account at server");
		  db->close(db,0);
		  return SW_RET_REMOTE_ERR;
	    }
	    rdata.data = (void*)malloc(sizeof(sc_account));
	    memcpy(rdata.data, &acc, sizeof(sc_account));
	    rdata.size = sizeof(sc_account);
	    
      } else if (dbret >= 0){
	    //check if there is enough hashes left (leave one for auth for new chain)
	    memcpy(&acc, data.data, sizeof(sc_account));
	    if(1 >= acc.i_left){
		  //TODO: probably setup new hash-chain
		  swiss_err_log("not enough hash chain left");
		  db->close(db, 0);
		  return SW_RET_INT_ERR;
	    }
	    //store in accound with id acc.accid
	    ret = swissc_send_store(acc.accid, flag, acc.next_auth);
	    if(ret <= 0){
		  swiss_err_log("failed to store flag at bank");
		  db->close(db,0);
		  return ret;
	    }
	    acc.accid = ret;
	    
	    //calculate next_auth
	    swiss_pwd_hash_iterate(acc.next_auth, --acc.i_left, (char*)acc.seed);
	    
	    memcpy(rdata.data, &acc, sizeof(sc_account));
	    rdata.size = sizeof(sc_account);
	    
      } else {
	    //db ERROR
	    db->err(db, dbret, "swissc_store_flag: db->get");
	    db->close(db,0);
	    return SW_RET_INT_ERR;
      }
      
      
      //store account number, hash chain under id in db
      dbret = db->put(db, 0, &key, &rdata, 0);
      if(dbret < 0){
	    db->err(db, dbret, "swissc_store_flag: db->put");
	    db->close(db, 0);
	    return SW_RET_INT_ERR;
      }
      
      db->close(db, 0);
      return -2;
}


/* gets the flag from the server
   @return a new char[] containing the flag (or StdErrorcodes*(-1))
*/
static char *swissc_retrieve_flag(char *ip, char *id){
      DB *db;
      DBT key, data, rdata;
      sc_account acc;
      char *flag;
      int ret;
      
      if(0 > (db = sc_initdb(SC_DB_FILENAME))){
	    swiss_err_log("retrieve: error initiating db\n");
	    return (char*)SW_RET_INT_ERR;
      }
      
      memset(&key, 0, sizeof(key));
      memset(&data, 0, sizeof(data));
      memset(&rdata, 0, sizeof(rdata));
      memset(&acc, 0, sizeof(sc_account));
      
      //get account number from db
      key.size = strlen(id) + strlen(ip);
      key.data = (void*)malloc(key.size);
      memcpy(key.data, id, strlen(id));
      memcpy(key.data+strlen(id), ip, strlen(ip));
      

      ret = db->get(db, 0, &key, &data, 0);
      if(ret != 0){
	    if(ret == DB_NOTFOUND){
		  swiss_err_log("accid not found in db during retrieve\n");
	    } else {
		  swiss_err_log("unspecified error while getting key from db\n");
	    }
	    db->close(db, 0);	    
	    return (char*)SW_RET_INT_ERR;
      }

      //printf("retrieve: data.size=%d\n", data.size);
      
      memcpy(&acc, data.data, data.size);
      
      //check if there is enough hashes left (leave one for auth for new chain)
      if(1 >= acc.i_left){
	    //TODO: setup new hash-chain
	    swiss_err_log("not enough hash chain left\n");
	    db->close(db, 0);
	    return (char*)SW_RET_INT_ERR;
      }

      //send retrieve to bank
      

      flag = swissc_send_retrieve_acc(acc.accid, acc.next_auth);

      if(0 >= (int)flag){
	    swiss_err_log("couldn't retrieve account from server\n");
	    db->close(db, 0);
	    return (char*)0;
      }

      
      // calculate next_auth
      acc.i_left--; 
      swiss_pwd_hash_iterate(acc.next_auth, acc.i_left, (char*)acc.seed);
      
      
      
      rdata.data = (char*)malloc(sizeof(sc_account));
      rdata.size = sizeof(sc_account);
      memcpy(rdata.data, &acc, sizeof(sc_account));
      ret = db->put(db, NULL, &key, &rdata, 0);
      if (ret) {
	    db->err(db, ret, "retrieve db->put");
	    db->close(db, 0);
	    free(data.data);
	    free(flag);
	    return (char*)SW_RET_INT_ERR;
      }

      //return flag

      free(rdata.data);
      db->close(db,0);
      return flag;}
      


/* Params:
   $1 (store|receive)
   $2 IP
   $3 ID
   $4 Flag
*/
int main(int argc, char** argv){
      char *cmd_str, *ip_str, *id_str, *flag;
      int ret = 2;
      char *sflag;

      //parse params
      if(argc != 5){
	    swiss_err_log("inproper number of arguments(expected 4)\n");
	    swiss_exit(SW_RET_INT_ERR);
      }
      
      cmd_str = argv[1];
      ip_str = argv[2];
      id_str = argv[3];
      flag = argv[4];
      

      // fork child that kills us after 59 secs

      if(0 ==  (child = fork())){
	    // is child
	    sleep(58);
	    // vater abschießen über SIGCHLD
	    exit(0);
      } else {
	    // is father
	    signal(SIGCHLD, swiss_sigchld);
            sd = connect_to_bank(&serv_addr, ip_str, SWISSPORT);
	    if(sd < 0){
		  swiss_err_log("connection to bank failed");
		  swiss_exit(sd);
	    }
      
	    if('s' == cmd_str[0]){
		  ret = swissc_store_flag(ip_str, id_str, flag);
		  // ret*=(-1);
	    } else if('r' == cmd_str[0]){
		  sflag = swissc_retrieve_flag(ip_str, id_str);
		  //compare stored flag and expected flag
		  if((int)sflag > 0){
			if(0 == strcmp(sflag,flag)){
			      ret = -2;
			} else {
			      ret = -1;
			}
			free(sflag);
		  } else {
			ret = (int)sflag;
		  }
	    }
	    close(sd);
      }
      
      swiss_exit(ret);
      return ret;
}
