/*
 * $Id: mirror.c,v 1.18 1999/04/21 14:22:54 labovit Exp $
 * originally Id: mirror.c,v 1.26 1998/08/03 21:55:23 gerald Exp 
 */

#include <stdio.h>
#include <string.h>
#include "mrt.h"
#include "trace.h"
/*#include "interface.h"*/
#include <time.h>
#include <signal.h>
#include "config_file.h"
#include "select.h"
#include <sys/time.h>
#include <ctype.h>
#include "irrd.h"
#include "irrd_prototypes.h"

extern trace_t *default_trace;

static int mirror_read_data ();
int dump_serial_updates (int sockfd, char * dbname, int journal_ext, 
			 u_long from, u_long to);
int socket_send_updates (char *file, int sockfd, FILE *fd, u_long curr_serial, 
			 u_long to);
int valid_start_line (irr_database_t *db, FILE *fd, u_long *serial_num);
int parse_mirror_request_line (int sockfd, char *command, irr_database_t **database, 
			       u_long *currserial, u_long *from, u_long *to);
int range_errors (int sockfd, u_long oldestserial, u_long currentserial, 
		  u_long from, u_long to);

int request_mirror (irr_database_t *db, uii_connection_t *uii, int last) {
  char tmp[MAXLINE], name[MAXLINE], logfile[MAXLINE];
  struct timeval	tv;
  fd_set		write_fds;
  int			n, i, ret;

  if ((db->fd == (void *) -1) || (db->fd == (void*) -11)) {
    trace (NORM, default_trace, 
	   "Aborting mirror -- we don't have a valid database socket for %s (%d)\n",
	   db->name, db->fd);
    return (-1);
  }

  if (last > 0 &&
     (db->serial_number + 1) > last) {
    trace (NORM, default_trace,
	   "ERROR: user supplied serial # last (%d) is <= current serial (%d)\n", 
	   last, db->serial_number);
    return (-1);
  }

  db->mirror_error_message[0] = '\0';

  /* we might be processing the mirror results, lock to make sure */
  /*irr_lock (db);*/

  /* 
   * Check if already mirroring. 
   * If so, check if we want to time out old attempt 
   */
  if (db->mirror_fd != -1) {
    if ( (time (NULL) - db->mirror_started) < 60*10) {
      trace (NORM, default_trace, "**** ERROR **** %s already mirroring...!\n",
	     db->name);
      /*irr_unlock (db);*/
      return (-1);
    }

    /*irr_unlock (db);*/
    trace (NORM, default_trace, "**** TIMING OUT old mirror attempt to %s\n",
	   db->name);
    strcpy (db->mirror_error_message, "Mirroring timed out...");
    select_delete_fd (db->mirror_fd);
    fclose (db->mirror_disk_fd);
    db->mirror_disk_fd = NULL;
    db->mirror_fd = -1;
    return (-1);
  }

  /*irr_unlock (db);*/

  if ((db->mirror_fd = socket (AF_INET, SOCK_STREAM, 0)) < 0) {
    trace (NORM, default_trace, "**** ERROR **** Could not get socket: %s!\n",
	   strerror (errno));
    db->mirror_fd = -1;
    return(0);
  }

  /* non blocking connect */
  trace (NORM, default_trace, "Connecting to Mirror %s:%d\n",
	 prefix_toa (db->mirror_prefix), db->mirror_port);
  n =  nonblock_connect (default_trace, db->mirror_prefix, 
			 db->mirror_port, db->mirror_fd);

  if (n !=1 ) {
    /*select_delete_fd (db->mirror_fd);*/
    close (db->mirror_fd);
    db->mirror_fd = -1;
    trace (NORM, default_trace, "*ERROR* Connect to mirror %s:%d failed\n",
	   prefix_toa (db->mirror_prefix), db->mirror_port);
    return (-1);
  }

  
  strcpy (name, db->name);
  convert_toupper(name);
  if (last == 0)
    sprintf (tmp, "-g %s:1:%ld-LAST\n", name, db->serial_number+1);
  else
    sprintf (tmp, "-g %s:1:%ld-%d\n", name, db->serial_number+1, last);

  trace (NORM, default_trace, "  Requesting mirror (%d): %s\n", 
	 db->mirror_fd, tmp);

  sprintf (logfile, "%s/.%s.mirror", IRR.database_dir, db->name);
  trace (NORM, default_trace, "  Saving updates to %s\n", logfile);
  if ((db->mirror_disk_fd = fopen (logfile, "w+")) == NULL) {
    trace (NORM, default_trace, "  Error opening %s (%s)\n", logfile, strerror (errno));
    /*select_delete_fd (db->mirror_fd);  No! We haven't added it to select! */
    close (db->mirror_fd);
    db->mirror_fd = -1;
    return(0);
  }


  /* check if mirror server is ready for writing */
  tv.tv_sec = 1; /* one second is plenty */
  tv.tv_usec = 0;
  FD_ZERO(&write_fds);
  FD_SET(db->mirror_fd, &write_fds);
  ret = select (FD_SETSIZE, NULL, &write_fds, NULL, &tv);
  if (ret <= 0) {
    trace (NORM, default_trace, "  Error writing request (timeout) to %s:%d\n", 
	   prefix_toa (db->mirror_prefix), db->mirror_port);
    close (db->mirror_fd);
    fclose (db->mirror_disk_fd); /* and delete tmp file??? */
    db->mirror_fd = -1;	   
    return 0;
  }

  /* write request to mirror server */
  if (write (db->mirror_fd, tmp, strlen (tmp)) != strlen (tmp)) {
    trace (NORM, default_trace, "  Error writing request (write failed) to %s:%d\n", 
	   prefix_toa (db->mirror_prefix), db->mirror_port);
    close (db->mirror_fd);
    fclose (db->mirror_disk_fd); /* and delete tmp file??? */
    db->mirror_fd = -1;	   
    return 0;
  }


  /* reset statistics */
  db->num_changes = 0;
  for (i=0; i<= IRR_MAX_KEYS; i++) {
    db->num_objects_deleted[i] = 0;
    db->num_objects_changed[i] = 0;
    db->num_objects_notfound[i] = 0;
  }
  db->mirror_update_size = 0;

  /* save when we started, so we can check for a timeout */
  db->mirror_started = time (NULL);

  select_add_fd (db->mirror_fd, SELECT_READ, (void_fn_t) mirror_read_data, db);
  return (1);
}


static int mirror_read_data (irr_database_t *database) {
  static struct timeval	tv;
  static fd_set		read_fds;
  static char		tmp[MIRROR_BUFFER];
  int			ret, n;


  tv.tv_sec = 0;
  tv.tv_usec = 0;

  FD_ZERO(&read_fds);
  FD_SET(database->mirror_fd, &read_fds);

  while (1) {
    ret = select (FD_SETSIZE, &read_fds, NULL, NULL, &tv);
    if (ret <= 0) {
      select_enable_fd (database->mirror_fd);
      return 0;
    }

    /* If read = 0, connection is close and we are done */
    if ((n = read (database->mirror_fd, tmp, MIRROR_BUFFER)) <= 0) break;

    database->mirror_update_size += n;
    /*JW this produces mucho output, do we really want it?
    trace (NORM, default_trace, "  Mirrored %d  (%d total)\n",
	   n, database->mirror_update_size);
	   */
    /* need to check for errors !! */
    fwrite (tmp, 1, n, database->mirror_disk_fd);
    select_enable_fd (database->mirror_fd);
  }

  trace (NORM, default_trace, "  Read %d bytes\n", database->mirror_update_size);

  /* reset fast buffer stuff 
  database->read_cur_ptr = database->read_buf;
  database->read_end_ptr = database->read_buf + IRRD_FAST_BUF_SIZE;
  */

  if (valid_start_line (database, database->mirror_disk_fd, 
                        &database->new_serial_number) > 0) {
    irr_lock (database);
#if (defined(USE_GDBM))
    dbm_fast (database);
#endif

    if (scan_irr_file (database, "mirror", 2, database->mirror_disk_fd) != NULL) {
      trace (NORM, default_trace, 
	     "Mirroring Error: Serial number unchanged: %d\n",
	     database->serial_number);
      irr_unlock (database);
      fclose (database->mirror_disk_fd);
#if (defined(USE_GDBM))
      dbm_slow (database);
#endif
      select_delete_fd (database->mirror_fd);
      database->mirror_fd = -1;
      return (-1);
    }
    
    select_delete_fd (database->mirror_fd);
    database->mirror_fd = -1;
#if (defined(USE_GDBM))
    dbm_slow (database);
#endif
    database->mirror_error_message[0] = '\0';
    trace (NORM, default_trace, 
	   "Serial number now %d, Mirroring header said: %d\n", 
	   database->serial_number, database->new_serial_number);
    
    irr_unlock (database);
    database->last_mirrored = time (NULL);

    trace (NORM, default_trace, "Route Objects    %4d changed, %4d deleted\n",
	   database->num_objects_changed[ROUTE], 
	   database->num_objects_deleted[ROUTE]);
    trace (NORM, default_trace, "AutNum Objects   %4d changed, %4d deleted\n",
	   database->num_objects_changed[AUT_NUM], 
	   database->num_objects_deleted[AUT_NUM]);
  }
  else {
     trace (NORM, default_trace, "Mirroring failed... no valid START line\n");
     select_delete_fd (database->mirror_fd);
     database->mirror_fd = -1;
  }
  
  fclose (database->mirror_disk_fd);
  
  return (1);
}

int valid_start_line (irr_database_t *database, FILE *fd, u_long *serial_num) {
  char buffer[MAXLINE], *cp, *last;
  int state = BLANK_LINE;

  fseek (fd, 0L, SEEK_SET);

  while (state != DB_EOF) {
    cp = fgets (buffer, sizeof (buffer) - 1, fd);

    state = get_state (buffer, cp, state);
    /* dump comment lines and lines that exceed the buffer size */
    if (state == OVRFLW     || 
	state == OVRFLW_END ||
	state == COMMENT    ||
	state == DB_EOF)
      continue;
    
    if (state == START_F) {
      if (!strncmp ("%START Version: 1", buffer, 17)) {
	strtok_r (buffer, "-", &last);
	if ((cp = strtok_r (NULL, "-", &last)) != NULL) {
	  *serial_num = atol (cp);
	  trace (NORM, default_trace,"Mirror SERIAL Last-(%d)\n", *serial_num);
	  return (1);
	}
      }
      else if (buffer[0] != '%')
	break;
    }
  }

  if (state == START_F) {
    trace (NORM, default_trace,"Invalid mirror header line: %s", buffer);
    memcpy (database->mirror_error_message, buffer, 60);
  }
  else
    trace (NORM, default_trace,"WARN: mirror.c: valid_start_line(): No mirror header line found!\n");

  return (-1);
}

void irr_mirror_timer (mtimer_t *timer, irr_database_t *db) {
  request_mirror (db, NULL, 0);
}


int irr_service_mirror_request (irr_connection_t *irr, char *command) {
  char buffer[MAXLINE];
  irr_database_t *database;
  u_long oldestserial, currentserial, first_in_new;
  u_long from, to;
  int old_journal_exists, new_journal_exists;
  int ret_code = 1;
  fd_set write_fds;
  struct timeval  tv;

  FD_ZERO(&write_fds);
  FD_SET(irr->sockfd, &write_fds);
  tv.tv_sec = 2;
  tv.tv_usec = 0;

  /* Parse a valid -g mirror request line and set request "from" - "to" */
  if (parse_mirror_request_line (irr->sockfd, command, &database, 
                                 &currentserial, &from, &to) < 0)
    return (-1);


  trace (NORM, default_trace, "Processing mirror request from %s for %s %d - %d\n",
	 prefix_toa (irr->from), database->name, from, to);


  /* check acls */
  if (!apply_access_list (database->mirror_access_list, irr->from)) {
    trace (NORM | INFO, default_trace, "IRR DENIED to %s\n",
	   prefix_toa (irr->from));
    irr_send_error (irr, NULL);
    return (-1);
  }

  /* find the oldest serial we have in *.JOURNAL.1 */
  old_journal_exists = find_oldest_serial (database->name, JOURNAL_OLD, &oldestserial);

  /* now find the first value in the *.JOURNAL file */
  if ((new_journal_exists = 
       find_oldest_serial (database->name, JOURNAL_NEW, &first_in_new)) == 0) {
      sprintf (buffer, "WARN: No serials to mirror yet or the first serial is corrupted!\n" );
      write (irr->sockfd, buffer, strlen (buffer));
      return (-1);
  }

  /* oldestserial is the first in *.JOURNAL if *.JOURNAL.1 doesn't exist */
  if (!old_journal_exists)
    oldestserial = first_in_new;

  /* check and see if there are any "from-to" range errors in the request */
  if (range_errors (irr->sockfd, oldestserial, currentserial, from, to) < 0)
    return (-1);

  /* okay, lets do it !*/
  irr_lock (database);

  sprintf (buffer, "%%START Version: 1 %s %lu-%lu\n\n", database->name, from, to);
  /*JW: I think we only need to set once 
  FD_SET(irr->sockfd, &write_fds);
  */
  if (select (irr->sockfd + 1, 0, &write_fds, 0, &tv) < 1) {
    trace (NORM, default_trace, "Write send %%START timed out during mirror\n");
    irr_unlock (database);
    return (-1);
  }
  if (write (irr->sockfd, buffer, strlen (buffer)) < 0) {
    trace (NORM, default_trace, "Mirror send %%START write error during mirror (%s)\n", strerror (errno));
    irr_unlock (database);
    return -1;
  }


  if (old_journal_exists && from < first_in_new)
    ret_code = dump_serial_updates (irr->sockfd, database->name, 
                                    JOURNAL_OLD, from, to);

  if (ret_code < 0) {
    irr_unlock (database);
    return -1;
  }

  if (to >= first_in_new)
    ret_code = dump_serial_updates (irr->sockfd, database->name, 
                                    JOURNAL_NEW, from, to);

  if (ret_code < 0) {
    irr_unlock (database);
    return -1;
  }

  sprintf (buffer, "%%END %s\n", database->name);
  /* traditional perl clients only accept 'DB name'
   * in upper case
   */
  convert_toupper (buffer);
  /*JW I think we only need to set once
  FD_SET(irr->sockfd, &write_fds);
  */
  if (select (irr->sockfd + 1, 0, &write_fds, 0, &tv) < 1) {
    trace (NORM, default_trace, "Write send %%END timed out during mirror\n");
    irr_unlock (database);
    return -1;
  }
  if (write (irr->sockfd, buffer, strlen (buffer)) < 0) {
      trace (NORM, default_trace, "Mirror send %%END write error during mirror (%s)\n", strerror (errno));
      irr_unlock (database);
      return -1;
  }

  irr_unlock (database);
  return (0);
}

int dump_serial_updates (int sockfd, char * dbname, int journal_ext, 
			 u_long from, u_long to) {
  char buf[MAXLINE], file[MAXLINE];
  u_long serial;
  int ret_code = 1;
  FILE *journal_fd;

  make_journal_name (dbname, journal_ext, file);
  
  if ((journal_fd = fopen (file, "r")) == NULL) 
    return (-1);

  /* find the correct spot in the journal file, then pump the updates onto the socket */
  while (fgets (buf, MAXLINE - 1, journal_fd) != NULL) {
    if (sscanf (buf, "%% SERIAL %lu", &serial) == 1) {
      if (serial >= from) {
	ret_code = socket_send_updates (file, sockfd, journal_fd, serial, to);
	break;
      }
    }
  }
  fclose (journal_fd);

  return ret_code;
}

int socket_send_updates (char *file, int sockfd, FILE *fd, u_long curr_serial, 
			 u_long to) {
  char buf[MAXLINE];
  u_long chk_serial;
  fd_set write_fds;
  struct timeval  tv;

  FD_ZERO(&write_fds);
  FD_SET(sockfd, &write_fds);
  tv.tv_sec = 2;
  tv.tv_usec = 0;
  
  while ((fgets (buf, MAXLINE - 1, fd) != NULL) &&
	 (curr_serial <= to)) {

    if (sscanf (buf, "%% SERIAL %lu", &chk_serial) == 1) {
      if (++curr_serial != chk_serial) {
	trace (NORM, default_trace, "ERROR: Skipped sequence mirror update, should be (%lu) (%s) says (%lu)\n", curr_serial, file, chk_serial);
	return -1;
      }
      continue;
    }
    
    /* JW: I think we only need to set once
    FD_SET(sockfd, &write_fds);
    */
    if (select (sockfd + 1, 0, &write_fds, 0, &tv) < 1) {
      trace (NORM, default_trace, "Write timed out during mirror\n");
      return (-1);
    }
    if (write (sockfd, buf, strlen (buf)) < 0) {
      trace (NORM, default_trace, "Mirror send serial transaction write error %s \n", strerror (errno));
      return -1;
    }
  }
  
  return 1;
}

int parse_mirror_request_line (int sockfd, char *command, irr_database_t **database, u_long *currserial, u_long *from, u_long *to) {
  char name[MAXLINE], buffer[MAXLINE], buffer1[MAXLINE];
  
  if (sscanf (command, " %[^:]:1:%[^-]-%s", name, buffer1, buffer) != 3) {
    sprintf (buffer, "\n\n\n%% ERROR: syntax error in -g command: (%s)\n", command);
    write (sockfd, buffer, strlen (buffer));
    return (-1);
  }
  else {
    if (convert_to_lu (buffer1, from) < 0) {
      sprintf (buffer, "\n\n\n%% ERROR: syntax error in 'from' value (%s)\n", buffer1);
      write (sockfd, buffer, strlen (buffer));
      return (-1);
    }
    if (strncasecmp (buffer, "LAST", 4) != 0 && 
        convert_to_lu (buffer, to) < 0) {
      sprintf (buffer1, "\n\n\n%% ERROR: syntax error in 'to' value (%s)\n", buffer);
      write (sockfd, buffer1, strlen (buffer1));
      return (-1);
    }
  }
  
  /* See if the db name is one we know about and is mirrored */
  if ((*database = find_database (name)) == NULL) {
    sprintf (buffer, "\n\n\n%% ERROR: Unknown db (%s) in mirror request\n", name);
    write (sockfd, buffer, strlen (buffer));
    return (-1);
  }
  else if ((*database)->mirror_prefix == NULL &&
	   !((*database)->flags & IRR_AUTHORITATIVE)) {
    sprintf (buffer, "\n\n\n%% ERROR: Database (%s) is a non-mirrored db!\n", (*database)->name);
    write (sockfd, buffer, strlen (buffer));
    return (-1);
  }
  
  if (get_current_serial (name, currserial) < 0) {
    if ((*database)->db_syntax != EMPTY_DB) {
      sprintf (buffer, "\n\n\n%% ERROR: (%s).CURRENTSERIAL file missing or corrupted!\n", name);
      write (sockfd, buffer, strlen (buffer));
      return (-1);
    }
    /* DB is empty so there are no updates to mirror */
    *currserial = 0;
  }

  if (*currserial == 0) {
    sprintf (buffer, "\n\n\n%%Warning: No updates yet, journal file is empty!\n");
    write (sockfd, buffer, strlen (buffer));
    return (-1);
  }
  
  if (!strncasecmp (buffer, "LAST", 4)) {
    *to = *currserial;
    if (*from == (*currserial + 1)) {
      sprintf (buffer, "\n\n\n%% Warning: there are no newer updates available!\n");
      write (sockfd, buffer, strlen (buffer));
      return (-1);
    }
  }

  return (1);
}

/*
 * return 1 if user request (ie, from-to) is "in range" (ie, contained
 * in oldestserial-currentserial)
 * otherwise return -1
 */
int range_errors (int sockfd, u_long oldestserial, u_long currentserial, 
		  u_long from, u_long to) {
  char buffer[MAXLINE];
  
  if (from > to) {
    sprintf (buffer, "\n\n\n%% ERROR: range error 'from > to' (%lu > %lu)\n", from, to);
    write (sockfd, buffer, strlen (buffer));
    return (-1);
  }
  
  if (from < oldestserial) {
    sprintf (buffer, "\n\n\n%% ERROR: serials (%lu - %lu) don't exist!\n", from, oldestserial - 1);
    write (sockfd, buffer, strlen (buffer));
    return (-1);
  }
  
  if (to > currentserial) {
    sprintf (buffer, "\n\n\n%% ERROR: serials (%lu - %lu) don't exist!\n", currentserial + 1, to);
    write (sockfd, buffer, strlen (buffer));
    return (-1);
  }
  
  return (1);
}

