#include "ipxtund.h"

/*
  Functions to establish and control a TCP connection to other routers
*/

static void sigalrmhandler(int sig);

int send_tcp_connect_request(struct Local *local, struct Router *router)
/*
  Send a request for a TCP connection to an other router. This only happens
  if ack_timer is expired.
*/
{
  time_t t;
  
  /* check if router is willing to establish a TCP connection */
  if (router->tcp_port == 0) {
    if (debuglevel > 2) {
      fprintf(log_fp,"router %s is TCP disabled\n", router->name);
    }
    return 0;
  }
  
  t = time((time_t)0);

  /* check if req_timer is expired */
  if (t - local->tcp.req_timer < 0) {
    if (debuglevel > 2) {
      fprintf(log_fp,"request not sent: req_timer not expired\n");
    }
    return 0;
  }
  
  /* if sent, load req_timer to prohebit further request and ack_timer to
     prohebit sending an ACK to a received connect request */
  local->tcp.ack_timer = t + 2 * TCP_TIMEWAIT;
  local->tcp.req_timer = local->tcp.ack_timer + 2*(1+ random() % TCP_TIMEWAIT);

  if (send_tcp_cmd('C', router) < 0) {
    return -1;
  }

  if (debuglevel > 1) {
    log("tcp connect request sent to router %s", router->name);
  }
  return 0;
}


int process_tcp_request(struct Local *local,
                        struct Router *router_array, const int router_anz)
/*
  This is the control messages dispatcher for TCP connection handling.
*/
{
  int from_sin_len, k;
  ssize_t rec;
  u_long inaddr;
  time_t t;
  char buf[BUFSIZE];
  struct sockaddr_in from_sin;
  struct Router *router;
  
  /* receive message */  
  from_sin_len = sizeof(from_sin);
  rec = recvfrom(local->ctrl_fd, buf, BUFSIZE, 0, (struct sockaddr *)&from_sin,
               &from_sin_len);
  if (rec < 0) {
    errormsg(E_LOG,"process_tcp_request","recvfrom: %m");
    return -1;
  }
  else if (rec > 6 &&
        (strncmp(buf, "CLIENT", 6) == 0 || strncmp(buf, "SERVER", 6) == 0)) {
    errormsg(E_LOG,"process_tcp_request",
          "got ctrl-mesg with router-connect header (CLIENT or SERVER)\n");
  }

  /* check if we are willing to establish a TCP connection */
  if (local->tcp.tcp_listen_port == 0) {
    if (debuglevel > 2) {
      fprintf(log_fp,"local host is TCP disabled\n");
    }
    return 0;
  }

  t = time((time_t)0);
  
  /* check if sender is configured */
  inaddr = from_sin.sin_addr.s_addr;
  for (k = 0; k < router_anz &&
       inaddr != router_array[k].sin.sin_addr.s_addr; k++) {
  }
  if (k >= router_anz) {
    errormsg(E_LOG,"process_tcp_request",
             "packet skipped: unconfigured router");
    return 0;
  }
  router = &router_array[k];
  
  if (buf[0] == 'C' && buf[1] == '\0') {
    /* in case of a received connection request */
    if (debuglevel > 1) {
      log("tcp connect request received from router %s", router->name);
    }

    /* don't ACK if ack_timer is not expired, because in this case we are
       waiting for a ACK itself */
    if (t - local->tcp.ack_timer < 0) {
      if (debuglevel > 2) {
	fprintf(log_fp,"ACK not sent: req_timer not expired\n");
      }
      return 0;
    }
    /* accept request by sending ACK */
    if (send_tcp_cmd('A', router) < 0) {
      return -1;
    }
    if (debuglevel > 1) {
      log("tcp connect request ACK sent to router %s", router->name);
    }
    
    /* now we call "accept()" listening at the TCP-listen socket */
    if (process_tcp_accept(local, router) > 0) {
      if (debuglevel > 0) {
        log("router %s: TCP connection accept ok\n", router->name);
      }
    }
  }
  else if (buf[0] == 'A' && buf[1] == '\0') {
    /* in case of a received ACK */
    if (debuglevel > 1) {
      log("tcp connect request ACK received from router %s",
          router->name);
    }
    /* the other side has called "accept()", we call "connect()" */
    if (process_tcp_connect(router) > 0) {
      if (debuglevel > 0) {
        log("router %s: TCP connection connect ok\n", router->name);
      }
    }
  }
  else if (buf[0] == 'P' && buf[1] == '\0') {
    /* in case of a ping-reply */
    process_tcp_ping_reply(buf, router);
  }
  else {
    errormsg(E_LOG,"process_tcp_request","unknown tcp request %x %x",
             (u_char)buf[0], (u_char)buf[1]);
    return -1;
  }
  return 0;
}


int send_tcp_cmd(const char cmd, const struct Router *router)
/*
  send a command to an other router using the control channel
*/
{
  size_t len;
  u_long *ulp;
  char buf[16];
  struct sockaddr_in sin;
  
  buf[0] = cmd;
  buf[1] = '\0';
  ulp = (u_long *)&buf[2];
  *ulp = htonl((u_long)getpid());
  ulp++;
  *ulp = htonl((u_long)time((time_t)0));
  len = sizeof(router->sin);
  memcpy((void *) &sin, (void *)&router->sin, len);
  sin.sin_port = htons(router->ctrl_port);
  if (sendto(router->ctrl_fd, (void *)buf, 10, 0,
             (struct sockaddr *) &sin, len) < 0) {
    errormsg(E_LOG,"send_tcp_cmd","sendto: %m");
    return -1;
  }
  return 0;
}


int process_tcp_accept(struct Local *local, struct Router *router)
{
  int fd, n;
  size_t from_sin_len;
  struct sockaddr_in from_sin;
  struct sigaction act;
  
  /* call "accept()" and wait for a connection or until timeout */
  if (sigsetjmp(sigalrmenv, (int)TRUE) == 0) {
    sigemptyset(&act.sa_mask);
    act.sa_handler = sigalrmhandler;
    act.sa_flags = 0;
    sigaction(SIGALRM, &act, NULL);
  }
  else {
    /* Timeout */
    alarm(0);
    errormsg(E_LOG,"process_tcp_accept","accept: timeout after %ld sec",
	     TCP_TIMEWAIT);
    return 0;
  }
  close_tcp_connection(router);
  alarm(TCP_TIMEWAIT);
  from_sin_len = sizeof(from_sin);
  fd = accept(local->tcp.tcp_listen_fd, (struct sockaddr *) &from_sin,
	      &from_sin_len);
  alarm(0);
  if (fd < 0) {
    errormsg(E_LOG,"process_tcp_accept","accept: %m");
    return -1;
  }
#ifdef TCP_NODELAY_OPTION
  if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &n, sizeof(int)) < 0) {
    errormsg(E_LOG,"process_tcp_accept","setsockopt(TCP_NODELAY): %m");
    return -1;
  } 
#endif
#ifdef SO_SNDBUF_SIZE
  n = SO_SNDBUF_SIZE;
  if (setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &n, sizeof(int)) < 0) {
    errormsg(E_LOG,"process_tcp_accept","setsockopt(SO_SNDBUF): %m");
    return -1;
  }
#endif

  /* connection succesfull, check if remote is the right one */
  if (router->sin.sin_addr.s_addr != from_sin.sin_addr.s_addr) {
    errormsg(E_LOG,"process_tcp_accept","accept: wrong router");
    close(fd);
    return 0;
  }

  /* include TCP socket into the "select()" set, so we can read from,
     but until "ping" succed let com == com_udp, i.e. we sent by UDP */
  router->tcp_fd = fd;

  /* check TCP connection by sending a "ping" */
  if (send_tcp_ping(router) < 0) {
    return -1;
  }
  if (debuglevel > 1) {
    log("Ping send to router %s ...", router->name);
  }
  
  return fd;
}


int process_tcp_connect(struct Router *router)
{
  int fd, n;
  struct sockaddr_in sin;
  struct sigaction act;
  
  /* create a new socket, call "connect()" and wait until success or timeout */
  fd = socket(AF_INET, SOCK_STREAM, 0);
  if (fd < 0) {
    errormsg(E_LOG,"process_tcp_request","socket: %m");
    return -1;
  }
#ifdef TCP_NODELAY_OPTION
  if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &n, sizeof(int)) < 0) {
    errormsg(E_LOG,"process_tcp_request","setsockopt(TCP_NODELAY): %m");
    return -1; 
  }
#endif
#ifdef SO_SNDBUF_SIZE
  n = SO_SNDBUF_SIZE;
  if (setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &n, sizeof(int)) < 0) {
    errormsg(E_LOG,"process_tcp_accept","setsockopt(SO_SNDBUF): %m");
    return -1;
  }
#endif

  if (sigsetjmp(sigalrmenv, (int)TRUE) == 0) {
    sigemptyset(&act.sa_mask);
    act.sa_handler = sigalrmhandler;
    act.sa_flags = 0;
    sigaction(SIGALRM, &act, NULL);
  }
  else {
    /* Timeout */
    alarm(0);
    errormsg(E_LOG,"process_tcp_connect","connect: timeout after %ld sec",
	     TCP_TIMEWAIT);
    close(fd);
    return 0;
  }
  close_tcp_connection(router);
  alarm(TCP_TIMEWAIT);
  memcpy(&sin, &router->sin, sizeof(sin));
  sin.sin_port = htons(router->tcp_port);
  n = connect(fd, (struct sockaddr *) &sin, sizeof(sin));
  alarm(0);
  if (n < 0) {
    errormsg(E_LOG,"process_tcp_connect","connect: %m");
    return -1;
  }

  /* include TCP socket into the "select()" set, so we can read from,
     but until "ping" succed let com == com_udp, i.e. we sent by UDP */
  router->tcp_fd = fd;

  if (send_tcp_ping(router) < 0) {
    return -1;
  }
  if (debuglevel > 1) {
    log("Ping send to router %s ...", router->name);
  }
  
  return fd;
}


int send_tcp_ping(struct Router *router)
/*
  Send a "ping" over TCP to router, the reply will arrive by UDP
*/
{
  ssize_t snd;
  u_short *usp;
  static u_short id = 0;
  u_long *ulp;
  char buf[12];
  
  /* send only if last ping was replied */
  if (router->ping.pending) {
    if (debuglevel > 1) {
      log("Ping not send, because there is one pending already");
    }
    return 0;
  }
  usp = (u_short *)buf;
  *usp = htons(2);
  ulp = (u_long *)&buf[2];
  *ulp = htonl(msec_from_start());
  memcpy(&buf[6], TCP_PING_MAGIC, 4);
  usp = (u_short *)&buf[10];
  *usp = htons(id);
  snd = writen(router->tcp_fd, buf, 12);
  if (snd < 0) {
    errormsg(E_LOG,"send_tcp_ping","writen: %m");
    return -1;
  }
  /* store id and ping start time */
  router->ping.pending = TRUE;
  router->ping.pending_id = id;
  router->ping.last_ping_time = time((time_t)0);
  id++;
  return 0;  
}


int echo_tcp_ping(char *buf, struct Router *router)
/*
  Reply to a ping using UDP
*/
{
  size_t len;
  struct sockaddr_in sin;

  buf[0] = 'P';
  buf[1] = '\0';
  len = sizeof(router->sin);
  memcpy((void *) &sin, (void *)&router->sin, len);
  sin.sin_port = htons(router->ctrl_port);
  if (sendto(router->ctrl_fd, (void *)buf, 12, 0,
             (struct sockaddr *) &sin, len) < 0) {
    errormsg(E_LOG,"echo_tcp_ping","sendto: %m");
    return -1;
  }
  return 0;
}


int process_tcp_ping_reply(char *buf, struct Router *router)
{
  u_short *usp, id;
  u_long *ulp;
  
  /* check if ping is pending */
  if (! router->ping.pending) {
    errormsg(E_LOG,"process_tcp_ping_reply","no ping is pending");
    return -1;
  }
  /* check id of ping */
  usp = (u_short *)&buf[10];
  id = ntohs(*usp);
  if (id != router->ping.pending_id) {
    errormsg(E_LOG,"process_tcp_ping_reply",
             "ping reply has unexpected id %u (%u expected)", id,
	     router->ping.pending_id);
    return -1;
  }
  /* calculate roundtrip time */
  ulp = (u_long *)&buf[2];
  if (router->ping.roundtrip_time == 0) {
    router->ping.roundtrip_time = msec_from_start() - ntohl(*ulp);
  }
  else {
    router->ping.roundtrip_time = (ROUNDTRIP_LAMBDA*router->ping.roundtrip_time 
                                + msec_from_start() - ntohl(*ulp)) /
			        (1 + ROUNDTRIP_LAMBDA);
  }
  router->ping.pending = FALSE;
  if (debuglevel > 1) {
    fprintf(log_fp,
          "TCP connection to router %s verified, round trip time %ld msecs\n",
	  router->name, router->ping.roundtrip_time);
  }
  /* if com == com_udp, this ping was sent to check TCP connection, now it is
     ok and we can send data using TCP */
  if (router->com == com_udp) {
    printifverbose("TCP connection to router %s established\n", router->name);
    log("TCP connection to router %s established and verified", router->name);
    router->com = com_tcp;
  }
  printifverbose("round trip time to router %s: %ld msec (mediated)\n",
	         router->name, router->ping.roundtrip_time);
  return 0;
}


int check_tcp_connection(struct Router *router)
/*
  Check TCP connection by ping
*/
{
  time_t t;

  /* check for timed-out ping, close TCP if yes */
  t = time((time_t)0);
  if (router->ping.pending && t - router->ping.last_ping_time > TCP_TIMEWAIT) {
    close_tcp_connection(router);
    errormsg(E_LOG,"check_tcp_connection",
             "ping timeout for router %s, closing TCP connection",
	     router->name);
    return -1;
  }
  
  /* send a new ping if time to do it */
  if (t - router->ping.last_ping_time > TCP_PING_INTERVALL) {
    send_tcp_ping(router);
  }
  return 0;
}


int close_tcp_connection(struct Router *router)
{
  int n;

  n = 0;
  if (router->tcp_fd != router->udp_fd) {
    printifverbose("TCP connection to router %s closed\n", router->name);
    n = close(router->tcp_fd);
    router->tcp_fd = router->udp_fd;
  }
  router->ping.pending = FALSE;
  router->com = com_udp;
  router->flush = TRUE;
  clear_cache(&router->s_hdr_cache);
  clear_cache(&router->r_hdr_cache);
  return n;
}


static void sigalrmhandler(int sig)
{
  siglongjmp(sigalrmenv,1);
}
