/*
 * Secure X Message Passing Library v2 examples.
 *
 * (c) Originally written by somebody else ...
 * (c) Alexander Vdolainen 2013-2015,2016 <avdolainen@zoho.com>
 *
 * libsxmp is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * libsxmp 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.";
 *
 */

/*
 * This is an example of sxmp usage.
 * NOTE: It was developed quite fast within one day,
 * btw - this the reason of some ugly code here.
 *
 * This is a master or simply - daemon part, it will
 * listen a requests and create a stream with directory entries,
 * which ridden by the client.
 * This implements a simple client-server topology, to see
 * more advanced technics check out other examples.
 *
 * NOTE(win32): don't have a time to test it or fix it to
 * make it works on windows, if you can - u're welcome.
 */

#include <stdio.h>
#include <dirent.h>
#define __USE_GNU
#include <stdlib.h>
#include <stdarg.h>
#include <sys/types.h>
#include <limits.h>
#include <sys/select.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/resource.h>
#include <netdb.h>
#include <unistd.h>
#include <uuid/uuid.h>
#include <execinfo.h>
#include <netinet/in.h>

#include <tdata/usrtc.h>
#include <sexpr/sexp.h>
#include <sxmp/limits.h>
#include <sxmp/sxmp.h>

#include <pthread.h>
#include <stdint.h>
#include <getopt.h>
#include <errno.h>

#include "filelist.h"
#include "helpers.h"

/*
 * type used to stream a directory contents
 * in case of lot of files we cannot send it
 * within one message.
 */
typedef struct __datastream_type {
  int dsid;
  usrtc_node_t node;
  DIR *dp;
  struct dirent dent;
  struct dirent *dres;
} datastream_t;

/* ugly, but no so important here */
static usrtc_t *_rd_streams;
static int _rd_last_id = 0;
static pthread_rwlock_t _lock;

/* some helper functions, not any serios stuff */
static long __cmp_int(const void *a, const void *b)
{
  return *(int *)a - *(int *)b;
}

static inline void dump_dirent(struct dirent *d, char *buf)
{
  if (d == NULL) return;
  strcat(buf, "\"");
  strcat(buf, d->d_name);
  strcat(buf, "\" ");

  switch(d->d_type) {
  case DT_REG:    strcat(buf, "\"regular\"");    break;
  case DT_DIR:    strcat(buf, "\"directory\"");    break;
  case DT_BLK:    strcat(buf, "\"block\"");    break;
  case DT_CHR:    strcat(buf, "\"char\"");    break;
  case DT_FIFO:    strcat(buf, "\"fifo\"");    break;
  case DT_LNK:    strcat(buf, "\"link\"");    break;
  case DT_UNKNOWN:    strcat(buf, "\"unknown\"");    break;
  default:    strcat(buf, "\"another\"");    break;
  }
}

/* firstly we need to take an rpc calls data,
 * sxmp uses usrtc from libtdata to store it
 */
static usrtc_t *fulist;

/* ok, here we will define out sxmp specific functions */

/*
 * RPC list contain groups of RPC calls called channel types
 * for each sxmp link depends on access rules you can change
 * available channel types.
 * To do so you must provide a special callback returning this list.
 * Since we haven't any access restrictions in our example, just
 * create a fake.
 */
static usrtc_t *__rettlist(sxlink_t *l)
{
  return fulist;
}

/*
 * Each sxmp link validated by certificate ID,
 * i.e. when basic checking is done, you can also
 * review the certificate ID, for example you need
 * to disable certificate for a time being.
 *
 * Here we don't have those checks, just always accept
 */
static int __validate_sslpem(sxlink_t *l)
{
  return 0;
}

/*
 * To be more paranoic, sxmp also allow to
 * check up username and password.
 * We don't overview this tuff here, just
 * always allow.
 */
static int __secure_check(sxlink_t *l)
{
  return SXE_SUCCESS;
}

/*
 * This function might be used to output log in any format you like.
 * In this case it is a prefix with date and time marks before a message body.
 */
static int __log(const sxlogtype_t type, const char *fmt, ...)
{
  int r = 0;

  time_t timer;
  char buffer[26];
  struct tm* tm_info;

  time(&timer);
  tm_info = localtime(&timer);

  strftime(buffer, 26, "%Y/%m/%d %H:%M:%S", tm_info);
  r += fprintf(stderr, "[%s] ", buffer);

  va_list arglist;
  va_start(arglist, fmt);
  r += vfprintf(stderr, fmt, arglist);
  va_end(arglist);
  return r;
}

/* Our RPC functions it used to get a message and reply */
static int __dir_open(void *m, sexp_t *sx)
{
  sexp_t *lsx = NULL;
  sxmsg_t *msg = (sxmsg_t *)m;
  char *buf;
  size_t ln = 0;

  if(sexp_list_cdr(sx, &lsx) || !sx->list->next || !sx->list->next->val) {
    fprintf(stderr, "Invalid protocol\n");
    return sxmsg_return(msg, SXE_BADPROTO);
  }

  char *dir_name = strdup(sx->list->next->val);

  if ( usrtc_isfull(_rd_streams) ) {
    return sxmsg_return(msg, SXE_TOOLONG);
  }

  datastream_t *item;
  if ( !(item = malloc(sizeof(datastream_t))) ) {
    return sxmsg_return(msg, SXE_ENOMEM);
  }

  /* find free id */
  pthread_rwlock_wrlock(&_lock);
  do {
    if (_rd_last_id == INT_MAX)
      _rd_last_id = 1;
    else
      ++_rd_last_id;
  } while (usrtc_lookup(_rd_streams, &_rd_last_id));

  /* create rd stream */
  DIR *dp = NULL;
  if ( !(dp = opendir(dir_name)) ) {
    pthread_rwlock_unlock(&_lock);
    FREE(item);
    FREE(dir_name);
    return sxmsg_return(msg, SXE_FAILED);
  }

  FREE(dir_name);

  /* init stream */
  item->dsid = _rd_last_id;
  item->dp = dp;
  item->dres = NULL;
  usrtc_node_init(&(item->node), item);
  usrtc_insert(_rd_streams, &(item->node), &item->dsid);

  buf = sxmsg_rapidbuf(msg);
  ln = snprintf(buf, MAX_RBBUF_LEN, "(dir-stream %d)", item->dsid);

  pthread_rwlock_unlock(&_lock);

  return sxmsg_rreply(msg, ln + 1);
}

static int __dir_read(void *m, sexp_t *sx)
{
  sexp_t *lsx = NULL;
  sxmsg_t *msg = (sxmsg_t *)m;
  char *buf;
  size_t ln = 0;
  int stid = -1;
  datastream_t *item;
  usrtc_node_t *node;
  char dump[2048];

  if (sexp_list_cdr(sx, &lsx) || !sx->list->next || !sx->list->next->val
      || (0 >= (stid = atoi(sx->list->next->val) )) )
  {
    fprintf(stderr, "Invalid protocol\n");
    return sxmsg_return(msg, SXE_BADPROTO);
  }

  /* get stream item */
  pthread_rwlock_rdlock(&_lock);
  if ( !(node = usrtc_lookup(_rd_streams, &stid)) ) {
    pthread_rwlock_unlock(&_lock);
    return sxmsg_return(msg, SXE_INVALINDEX);
  }
  pthread_rwlock_unlock(&_lock);

  pthread_rwlock_wrlock(&_lock);
  item = usrtc_node_getdata(node);
  if (readdir_r(item->dp, &(item->dent), &(item->dres))) {
    pthread_rwlock_unlock(&_lock);
    return sxmsg_return(msg, SXE_FAILED);
  }

  if (!item->dres) {
    buf = sxmsg_rapidbuf(msg);
    ln = snprintf(buf, MAX_RBBUF_LEN, "(dir-end %d)", item->dsid);
    goto __finish;
  }

  sprintf(dump, "(dir-entry (");
  dump_dirent(item->dres, dump);
  strcat(dump, "))");

  buf = sxmsg_rapidbuf(msg);
  ln = snprintf(buf, MAX_RBBUF_LEN, "%s", dump);

__finish:
  pthread_rwlock_unlock(&_lock);
  return sxmsg_rreply(msg, ln + 1);
}

static int __dir_close(void *m, sexp_t *sx)
{
  sexp_t *lsx = NULL;
  sxmsg_t *msg = (sxmsg_t *)m;
  int stid = -1;
  datastream_t *item;
  usrtc_node_t *node;

  if (sexp_list_cdr(sx, &lsx) || !sx->list->next || !sx->list->next->val
      || (0 >= (stid = atoi(sx->list->next->val) )) )
  {
    fprintf(stderr, "Invalid protocol\n");
    return sxmsg_return(msg, SXE_BADPROTO);
  }

  /* get stream item */
  pthread_rwlock_rdlock(&_lock);
  if ( !(node = usrtc_lookup(_rd_streams, &stid)) ) {
    pthread_rwlock_unlock(&_lock);
    return sxmsg_return(msg, ENOENT);
  }
  pthread_rwlock_unlock(&_lock);

  pthread_rwlock_wrlock(&_lock);
  item = usrtc_node_getdata(node);
  closedir(item->dp);
  usrtc_delete(_rd_streams, node);
  FREE(item);
  pthread_rwlock_unlock(&_lock);

  return sxmsg_return(msg, SXE_SUCCESS);
}

int main(int argc, char **argv)
{
  char *rootca = NULL, *cert = NULL;
  sxhub_t *sxmphub = sxhub_create();
  int port = DEFAULT_PORT;
  int opt;
  int is_daemon = 0;

  while((opt = getopt(argc, argv, "dp:r:u:")) != -1) {
    switch(opt) {
    case 'd':
      is_daemon = 1;
      break;
    case 'p':
      port = atoi(optarg);
      break;
    case 'r':
      rootca = strdup(optarg);
      break;
    case 'u':
      cert = strdup(optarg);
      break;
    default:
      fprintf(stderr, "usage: %s [-p <PORTNUM>] -r <PATH to Root CA> -u <PATH"
              " to SSL certificate> [-d]\n", argv[0]);
      return EINVAL;
    }
  }

  if(!rootca) {
    fprintf(stderr, "Root CA not pointed.\n Failure.\n");
    return EINVAL;
  }

  if(!cert) {
    fprintf(stderr, "User certificate not pointed.\n Failure.\n");
    return EINVAL;
  }

  /* become daemon if it's necessary */
  if (is_daemon && (opt = daemonize())) {
    fprintf(stderr, "daemonize() failed (%d).\n Failure.\n", opt);
    return EINVAL;
  }

  sxmp_init();

  /* all is fine let's init connection subsystem */
  if(!sxmphub) {
    fprintf(stderr, "Subsystem init failed: %d\n", opt);
    return 2;
  }

  /* set sxhub log function */
  sxhub_set_logops(sxmphub, __log);

  /* set working certificates */
  opt = sxhub_setsslserts(sxmphub, rootca, cert, cert);
  if(opt) {
    fprintf(stderr, "Subsystem init failed (set SSL x.509 pems): %d\n", opt);
    return opt;
  }

  /* clean up */
  free(rootca);
  free(cert);

  /* set important callbacks to do the security checking */
  sxhub_set_authcheck(sxmphub, __secure_check);
  sxhub_set_sslvalidate(sxmphub, __validate_sslpem);

  /* ok, now we need to construct RPC lists (channels) */
  if(!(fulist = malloc(sizeof(usrtc_t)))) {
    fprintf(stderr, "Cannot allocate memory for RPC lists\n Failure.\n");
    return ENOMEM;
  }
  opt = sxmp_rpclist_init(fulist);
  if(opt) {
    fprintf(stderr, "Failed to init rpc list\n Failure.\n");
    return opt;
  }

  /* we will add one channel with type id READONLY_CHANNEL for read only operations */
  opt = sxmp_rpclist_add(fulist, READONLY_CHANNEL, "Read only operations", NULL);
  if(opt) {
    fprintf(stderr, "Failed to add typed RPC channel\n Failure.\n");
    return opt;
  }
  opt = sxmp_rpclist_add(fulist, 1, "Public", NULL);
  if(opt) {
    fprintf(stderr, "Failed to add typed RPC channel\n Failure.\n");
    return opt;
  }

  /* ok, let's add stream functions */
  opt = sxmp_rpclist_add_function(fulist, READONLY_CHANNEL, "dir-open", __dir_open);
  if(opt) {
  __fail:
    fprintf(stderr, "Failed to add functions to typed RPC channel\n Failure.\n");
    return opt;
  }
  opt = sxmp_rpclist_add_function(fulist, READONLY_CHANNEL, "dir-read", __dir_read);
  if(opt) goto __fail;
  opt = sxmp_rpclist_add_function(fulist, READONLY_CHANNEL, "dir-close", __dir_close);
  if(opt) goto __fail;

  /* ok, setup it */
  sxhub_set_rpcvalidator(sxmphub, __rettlist);

  /* create stream tree */
  if(!(_rd_streams = malloc(sizeof(usrtc_t)) ))
    return ENOMEM;
  usrtc_init(_rd_streams, USRTC_REDBLACK, MAX_STREAMS, __cmp_int);

  signal(SIGPIPE, SIG_IGN);

  /* now we're ready to run the listen process */
  int srv = openlistener_socket(port);

  while(1) {
    struct sockaddr_in addr;
    socklen_t len = sizeof(addr);
    sxlink_t *link;

    int client = accept(srv, (struct sockaddr*)&addr, &len); /* accept connection as usual */

    link = sxlink_master_accept(sxmphub, client, NULL);

    if(!link) fprintf(stderr, "Cannot create sxmp link (%d)\n", errno);
  }

  sxhub_destroy(sxmphub);

  sxmp_finalize();

  return 0;
}