
/*
  Redirecting Sametime Faux-Server
  The Meanwhile Project

  This is a tool which helps to test handling of login-redirects when
  you don't have a server which will issue them. It will listen for a
  single incoming connection, will accept a handshake message and send
  a handshake ack, and will respond to a login message with a redirect
  message to a server specified on the command line.

  Christopher O'Brien <siege@preoccupied.net>
*/


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <unistd.h>

#include <glib.h>

#include <mw_common.h>
#include <mw_message.h>


/** the server socket or the connected socket */
static int sock;

/** the io channel */
static GIOChannel *chan;

/** the listening event on the io channel */
static int chan_io;

static char *host;

static guchar *sbuf;
static gsize sbuf_size;
static gsize sbuf_recv;


static void send_msg(struct mwMessage *msg) {
  struct mwPutBuffer *b;
  struct mwOpaque o = { 0, 0 };

  b = mwPutBuffer_new();
  mwMessage_put(b, msg);
  mwPutBuffer_finalize(&o, b);

  b = mwPutBuffer_new();
  mwOpaque_put(b, &o);
  mwOpaque_clear(&o);
  mwPutBuffer_finalize(&o, b);

  if(sock) write(sock, o.data, o.len);

  mwOpaque_clear(&o);
}


static void handshake_ack() {
  struct mwMsgHandshakeAck *msg;

  msg = (struct mwMsgHandshakeAck *)
    mwMessage_new(mwMessage_HANDSHAKE_ACK);

  send_msg(MW_MESSAGE(msg));
  mwMessage_free(MW_MESSAGE(msg));
}


static void login_redir() {
  struct mwMsgLoginRedirect *msg;

  msg = (struct mwMsgLoginRedirect *)
    mwMessage_new(mwMessage_LOGIN_REDIRECT);
  msg->host = g_strdup(host);
  msg->server_id = NULL;

  send_msg(MW_MESSAGE(msg));
  mwMessage_free(MW_MESSAGE(msg));
}


static void done() {
  close(sock);
  exit(0);
}


static void side_process(const guchar *buf, gsize len) {
  struct mwOpaque o = { .len = len, .data = (guchar *) buf };
  struct mwGetBuffer *b;
  guint16 type;

  if(! len) return;

  b = mwGetBuffer_wrap(&o);
  type = guint16_peek(b);

  switch(type) {
  case mwMessage_HANDSHAKE:
    handshake_ack();
    break;

  case mwMessage_LOGIN:
    login_redir();
    break;

  case mwMessage_CHANNEL_DESTROY:
    done();
    break;

  default:
    ;
  }

  mwGetBuffer_free(b);
}


static void sbuf_free() {
  g_free(sbuf);
  sbuf = NULL;
  sbuf_size = 0;
  sbuf_recv = 0;
}


#define ADVANCE(b, n, count) { b += count; n -= count; }


/* handle input to complete an existing buffer */
static gsize side_recv_cont(const guchar *b, gsize n) {

  gsize x = sbuf_size - sbuf_recv;

  if(n < x) {
    memcpy(sbuf + sbuf_recv, b, n);
    sbuf_recv += n;
    return 0;
    
  } else {
    memcpy(sbuf + sbuf_recv, b, x);
    ADVANCE(b, n, x);
    
    if(sbuf_size == 4) {
      struct mwOpaque o = { .len = 4, .data = sbuf };
      struct mwGetBuffer *gb = mwGetBuffer_wrap(&o);
      x = guint32_peek(gb);
      mwGetBuffer_free(gb);

      if(n < x) {
	guchar *t;
	x += 4;
	t = (guchar *) g_malloc(x);
	memcpy(t, sbuf, 4);
	memcpy(t+4, b, n);
	
	sbuf_free();
	
	sbuf = t;
	sbuf_size = x;
	sbuf_recv = n + 4;
	return 0;
	
      } else {
	sbuf_free();
	side_process(b, x);
	ADVANCE(b, n, x);
      }
      
    } else {
      side_process(sbuf+4, sbuf_size-4);
      sbuf_free();
    }
  }

  return n;
}


/* handle input when there's nothing previously buffered */
static gsize side_recv_empty(const guchar *b, gsize n) {
  struct mwOpaque o = { .len = n, .data = (guchar *) b };
  struct mwGetBuffer *gb;
  gsize x;

  if(n < 4) {
    sbuf = (guchar *) g_malloc0(4);
    memcpy(sbuf, b, n);
    sbuf_size = 4;
    sbuf_recv = n;
    return 0;
  }
  
  gb = mwGetBuffer_wrap(&o);
  x = guint32_peek(gb);
  mwGetBuffer_free(gb);
  if(! x) return n - 4;

  if(n < (x + 4)) {

    x += 4;
    sbuf = (guchar *) g_malloc(x);
    memcpy(sbuf, b, n);
    sbuf_size = x;
    sbuf_recv = n;
    return 0;
    
  } else {
    ADVANCE(b, n, 4);
    side_process(b, x);
    ADVANCE(b, n, x);

    return n;
  }
}


static gsize side_recv(const guchar *b, gsize n) {

  if(n && (sbuf_size == 0) && (*b & 0x80)) {
    ADVANCE(b, n, 1);
  }

  if(n == 0) {
    return 0;

  } else if(sbuf_size > 0) {
    return side_recv_cont(b, n);

  } else {
    return side_recv_empty(b, n);
  }
}


static void feed_buf(const guchar *buf, gsize n) {
  guchar *b = (guchar *) buf;
  gsize remain = 0;

  while(n > 0) {
    remain = side_recv(b, n);
    b += (n - remain);
    n = remain;
  }
}


static int read_recv() {
  guchar buf[2048];
  int len;

  len = read(sock, buf, 2048);
  if(len > 0) feed_buf(buf, (gsize) len);

  return len;
}


static gboolean read_cb(GIOChannel *chan,
			GIOCondition cond,
			gpointer data) {
  int ret = 0;

  if(cond & G_IO_IN) {
    ret = read_recv();
    if(ret > 0) return TRUE;
  }

  if(sock) {
    g_source_remove(chan_io);
    close(sock);
    sock = 0;
    chan = NULL;
    chan_io = 0;
  }

  done();
  
  return FALSE;
}


static gboolean listen_cb(GIOChannel *chan,
			  GIOCondition cond,
			  gpointer data) {

  struct sockaddr_in rem;
  guint len = sizeof(rem);
  
  sock = accept(sock, (struct sockaddr *) &rem, &len);
  g_assert(sock > 0);

  g_source_remove(chan_io);
  chan = g_io_channel_unix_new(sock);
  chan_io = g_io_add_watch(chan, G_IO_IN | G_IO_ERR | G_IO_HUP,
			   read_cb, NULL);
  
  return FALSE;
}


static void init_socket(int port) {
  /* start listening on the local port specifier */

  struct sockaddr_in sin;

  sock = socket(PF_INET, SOCK_STREAM, 0);
  g_assert(sock >= 0);

  memset(&sin, 0, sizeof(struct sockaddr_in));
  sin.sin_family = PF_INET;
  sin.sin_port = htons(port);
  sin.sin_addr.s_addr = htonl(INADDR_ANY);

  if(bind(sock, (struct sockaddr *)&sin, sizeof(sin)) < 0)
    g_assert_not_reached();

  if(listen(sock, 1) < 0)
    g_assert_not_reached();

  chan = g_io_channel_unix_new(sock);
  chan_io = g_io_add_watch(chan, G_IO_IN | G_IO_ERR | G_IO_HUP,
			   listen_cb, NULL);
}


int main(int argc, char *argv[]) {
  int port = 0;

  if(argc > 1) {
    char *z;

    host = argv[1];
    z = host;

    host = strchr(z, ':');
    if(host) *host++ = '\0';
    port = atoi(z);
  }

  if(!host || !*host || !port) {
    fprintf(stderr,
	    ( " Usage: %s local_port:redirect_host\n"
	      " Creates a locally-running sametime proxy which redirects"
	      " logins to the\n specified host\n" ),
	    argv[0]);
    exit(1);
  }

  /* @todo create signal handlers to cleanup socket */

  init_socket(port);

  g_main_loop_run(g_main_loop_new(NULL, FALSE)); 
  return 0;
}

