/*
 * File: dpip.c
 *
 * Copyright 2005-2015 Jorge Arellano Cid <jcid@dillo.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 */

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>   /* for close */
#include <fcntl.h>    /* for fcntl */

#include "dpip.h"
#include "d_size.h"

#define RBUF_SZ 16*1024
//#define RBUF_SZ 1

#define DPIP_TAG_END            " '>"
#define DPIP_MODE_SWITCH_TAG    "cmd='start_send_page' "
#define MSG_ERR(...)            fprintf(stderr, "[dpip]: " __VA_ARGS__)

/*
 * Local variables
 */
static const char Quote = '\'';

/** @file
 * Library for dealing with dpip tags (dillo plugin protocol tags).
 *
 * Basically the syntax of a dpip tag is:
 *
 * @code
 * "<"[*alpha] *(<name>"="Quote<escaped_value>Quote) " "Quote">"
 * @endcode
 *   1. No space is allowed around the "=" sign between a name and its value.
 *   2. The Quote character is not allowed in <name>.
 *   3. Attribute values stuff Quote as QuoteQuote.
 *
 * e.g. (with ' as Quote):
 *
 * @code
 *   <a='b' b='c' '>                 OK
 *   <dpi a='b i' b='12' '>          OK
 *   <a='>' '>                       OK
 *   <a='ain''t no doubt' '>         OK
 *   <a='ain''t b=''no'' b='' doubt' '>    OK
 *   <a = '>' '>                     Wrong
 * @endcode
 *
 * Notes:
 *
 * - Restriction #1 is for easy finding of end of tag (EOT=Space+Quote+>).
 * - Restriction #2 can be removed, but what for? ;)
 * - The functions here provide for this functionality.
 */

typedef enum {
   SEEK_NAME,
   MATCH_NAME,
   SKIP_VALUE,
   SKIP_QUOTE,
   FOUND
} DpipTagParsingState;

/* ------------------------------------------------------------------------- */

/**
 * Printf like function for building dpip commands.
 * It takes care of dpip escaping of its arguments.
 * NOTE : It ONLY accepts string parameters, and
 *        only one %s per parameter.
 */
char *a_Dpip_build_cmd(const char *format, ...)
{
   va_list argp;
   char *p, *q, *s;
   Dstr *cmd;

   /* Don't allow Quote characters in attribute names */
   if (strchr(format, Quote))
      return NULL;

   cmd = dStr_sized_new(64);
   dStr_append_c(cmd, '<');
   va_start(argp, format);
   for (p = q = (char*)format; *q;  ) {
      p = strstr(q, "%s");
      if (!p) {
         dStr_append(cmd, q);
         break;
      } else {
         /* Copy format's part */
         while (q != p)
            dStr_append_c(cmd, *q++);
         q += 2;

         dStr_append_c(cmd, Quote);
         /* Stuff-copy of argument */
         s = va_arg (argp, char *);
         for (  ; *s; ++s) {
            dStr_append_c(cmd, *s);
            if (*s == Quote)
               dStr_append_c(cmd, *s);
         }
         dStr_append_c(cmd, Quote);
      }
   }
   va_end(argp);
   dStr_append_c(cmd, ' ');
   dStr_append_c(cmd, Quote);
   dStr_append_c(cmd, '>');

   p = cmd->str;
   dStr_free(cmd, FALSE);
   return p;
}

/**
 * Task: given a tag, its size and an attribute name, return the
 * attribute value (stuffing of ' is removed here).
 *
 * Return value: the attribute value, or NULL if not present or malformed.
 */
char *a_Dpip_get_attr_l(const char *tag, size_t tagsize, const char *attrname)
{
   uint_t i, n = 0, found = 0;
   const char *p, *q, *start;
   char *r, *s, *val = NULL;
   DpipTagParsingState state = SEEK_NAME;

   if (!tag || !tagsize || !attrname || !*attrname)
      return NULL;

   for (i = 1; i < tagsize && !found; ++i) {
      switch (state) {
      case SEEK_NAME:
         if (tag[i] == attrname[0] && (tag[i-1] == ' ' || tag[i-1] == '<')) {
            n = 1;
            state = MATCH_NAME;
         } else if (tag[i] == Quote && tag[i-1] == '=')
            state = SKIP_VALUE;
         break;
      case MATCH_NAME:
         if (tag[i] == attrname[n])
            ++n;
         else if (tag[i] == '=' && !attrname[n])
            state = FOUND;
         else
            state = SEEK_NAME;
         break;
      case SKIP_VALUE:
         if (tag[i] == Quote)
            state = (tag[i+1] == Quote) ? SKIP_QUOTE : SEEK_NAME;
         break;
      case SKIP_QUOTE:
         state = SKIP_VALUE;
         break;
      case FOUND:
         found = 1;
         break;
      }
   }

   if (found) {
      p = start = tag + i;
      while ((q = strchr(p, Quote)) && q[1] == Quote)
         p = q + 2;
      if (q && q[1] == ' ') {
         val = dStrndup(start, (uint_t)(q - start));
         for (r = s = val; (*r = *s); ++r, ++s)
            if (s[0] == Quote && s[0] == s[1])
               ++s;
      }
   }
   return val;
}

/**
 * Task: given a tag and an attribute name, return its value.
 * Return value: the attribute value, or NULL if not present or malformed.
 */
char *a_Dpip_get_attr(const char *tag, const char *attrname)
{
   return (tag ? a_Dpip_get_attr_l(tag, strlen(tag), attrname) : NULL);
}

/**
 * Check whether the given 'auth' string equals what dpid saved.
 * Return value: 1 if equal, -1 otherwise
 */
int a_Dpip_check_auth(const char *auth_tag)
{
   char SharedSecret[32];
   FILE *In;
   char *fname, *rcline = NULL, *tail, *cmd, *msg;
   int i, port, ret = -1;

   /* sanity checks */
   if (!auth_tag ||
       !(cmd = a_Dpip_get_attr(auth_tag, "cmd")) || strcmp(cmd, "auth") ||
       !(msg = a_Dpip_get_attr(auth_tag, "msg"))) {
      return ret;
   }

   fname = dStrconcat(dGethomedir(), "/.dillo/dpid_comm_keys", NULL);
   if ((In = fopen(fname, "r")) == NULL) {
      MSG_ERR("[a_Dpip_check_auth] %s\n", dStrerror(errno));
   } else if ((rcline = dGetline(In)) == NULL) {
      MSG_ERR("[a_Dpip_check_auth] empty file: %s\n", fname);
   } else {
      port = strtol(rcline, &tail, 10);
      if (tail && port != 0) {
         for (i = 0; *tail && isxdigit(tail[i+1]); ++i)
            SharedSecret[i] = tail[i+1];
         SharedSecret[i] = 0;
         if (strcmp(msg, SharedSecret) == 0)
            ret = 1;
      }
   }
   if (In)
      fclose(In);
   dFree(rcline);
   dFree(fname);
   dFree(msg);
   dFree(cmd);

   return ret;
}

/* --------------------------------------------------------------------------
 * Dpip socket API ----------------------------------------------------------
 */

/**
 * Create and initialize a dpip socket handler.
 */
Dsh *a_Dpip_dsh_new(int fd_in, int fd_out, int flush_sz)
{
   Dsh *dsh = dNew(Dsh, 1);

   /* init descriptors and streams */
   dsh->fd_in  = fd_in;
   dsh->fd_out = fd_out;

   /* init buffer */
   dsh->wrbuf = dStr_sized_new(8 *1024);
   dsh->rdbuf = dStr_sized_new(8 *1024);
   dsh->flush_sz = flush_sz;
   dsh->mode = DPIP_TAG;
   if (fcntl(dsh->fd_in, F_GETFL) & O_NONBLOCK)
      dsh->mode |= DPIP_NONBLOCK;
   dsh->status = 0;

   return dsh;
}

/*
 * Return value: 1..DataSize sent, -1 eagain, or -3 on big Error
 */
static int Dpip_dsh_write(Dsh *dsh, int nb, const char *Data, int DataSize)
{
   int req_mode, old_flags = 0, st, ret = -3, sent = 0;

   req_mode = (nb) ? DPIP_NONBLOCK : 0;
   if ((dsh->mode & DPIP_NONBLOCK) != req_mode) {
      /* change mode temporarily... */
      old_flags = fcntl(dsh->fd_out, F_GETFL);
      fcntl(dsh->fd_out, F_SETFL,
            (nb) ? O_NONBLOCK | old_flags : old_flags & ~O_NONBLOCK);
   }

   while (1) {
      st = write(dsh->fd_out, Data + sent, DataSize - sent);
      if (st < 0) {
         if (errno == EINTR) {
            continue;
         } else if (errno == EAGAIN) {
            dsh->status = DPIP_EAGAIN;
            ret = -1;
            break;
         } else {
            MSG_ERR("[Dpip_dsh_write] %s\n", dStrerror(errno));
            dsh->status = DPIP_ERROR;
            break;
         }
      } else {
         sent += st;
         if (nb || sent == DataSize) {
            ret = sent;
            break;
         }
      }
   }

   if ((dsh->mode & DPIP_NONBLOCK) != req_mode) {
      /* restore old mode */
      fcntl(dsh->fd_out, F_SETFL, old_flags);
   }

   return ret;
}

/**
 * Streamed write to socket.
 * Return: 0 on success, 1 on error.
 */
int a_Dpip_dsh_write(Dsh *dsh, int flush, const char *Data, int DataSize)
{
   int ret = 1;

   /* append to buf */
   dStr_append_l(dsh->wrbuf, Data, DataSize);

   if (!flush || dsh->wrbuf->len == 0)
      return 0;

   ret = Dpip_dsh_write(dsh, 0, dsh->wrbuf->str, dsh->wrbuf->len);
   if (ret == dsh->wrbuf->len) {
      dStr_truncate(dsh->wrbuf, 0);
      ret = 0;
   }

   return ret;
}

/*
 * Return value: 0 on success or empty buffer,
 *               1..DataSize sent, -1 eagain, or -3 on big Error
 */
int a_Dpip_dsh_tryflush(Dsh *dsh)
{
   int st;

   if (dsh->wrbuf->len == 0) {
      st = 0;
   } else {
      st = Dpip_dsh_write(dsh, 1, dsh->wrbuf->str, dsh->wrbuf->len);
      if (st > 0) {
         /* update internal buffer */
         dStr_erase(dsh->wrbuf, 0, st);
      }
   }
   return (dsh->wrbuf->len == 0) ? 0 : st;
}

/*
 * Return value: 1..DataSize sent, -1 eagain, or -3 on big Error
 */
int a_Dpip_dsh_trywrite(Dsh *dsh, const char *Data, int DataSize)
{
   int st;

   if ((st = Dpip_dsh_write(dsh, 1, Data, DataSize)) > 0) {
      /* update internal buffer */
      if (st < DataSize)
         dStr_append_l(dsh->wrbuf, Data + st, DataSize - st);
   }
   return st;
}

/**
 * Convenience function.
 */
int a_Dpip_dsh_write_str(Dsh *dsh, int flush, const char *str)
{
   return a_Dpip_dsh_write(dsh, flush, str, (int)strlen(str));
}

/**
 * Read raw data from the socket into our buffer in
 * either BLOCKING or NONBLOCKING mode.
 */
static void Dpip_dsh_read(Dsh *dsh, int blocking)
{
   char buf[RBUF_SZ];
   int req_mode, old_flags = 0, st, nb = !blocking;

   dReturn_if (dsh->status == DPIP_ERROR || dsh->status == DPIP_EOF);

   req_mode = (nb) ? DPIP_NONBLOCK : 0;
   if ((dsh->mode & DPIP_NONBLOCK) != req_mode) {
      /* change mode temporarily... */
      old_flags = fcntl(dsh->fd_in, F_GETFL);
      fcntl(dsh->fd_in, F_SETFL,
            (nb) ? O_NONBLOCK | old_flags : old_flags & ~O_NONBLOCK);
   }

   while (1) {
      st = read(dsh->fd_in, buf, RBUF_SZ);
      if (st < 0) {
         if (errno == EINTR) {
            continue;
         } else if (errno == EAGAIN) {
            dsh->status = DPIP_EAGAIN;
            break;
         } else {
            MSG_ERR("[Dpip_dsh_read] %s\n", dStrerror(errno));
            dsh->status = DPIP_ERROR;
            break;
         }
      } else if (st == 0) {
         dsh->status = DPIP_EOF;
         break;
      } else {
         /* append to buf */
         dStr_append_l(dsh->rdbuf, buf, st);
         if (blocking)
            break;
      }
   }

   if ((dsh->mode & DPIP_NONBLOCK) != req_mode) {
      /* restore old mode */
      fcntl(dsh->fd_out, F_SETFL, old_flags);
   }

   /* assert there's no more data in the wire...
    * (st < buf upon interrupt || st == buf and no more data) */
   if (blocking)
      Dpip_dsh_read(dsh, 0);
}

/**
 * Return a newlly allocated string with the next dpip token in the socket.
 * Return value: token string and length on success, NULL otherwise.
 * (useful for handling null characters in the data stream)
 */
char *a_Dpip_dsh_read_token2(Dsh *dsh, int blocking, int *DataSize)
{
   char *p, *ret = NULL;
   *DataSize = 0;

   /* Read all available data without blocking */
   Dpip_dsh_read(dsh, 0);

   /* switch mode upon request */
   if (dsh->mode & DPIP_LAST_TAG)
      dsh->mode = DPIP_RAW;

   if (blocking) {
      if (dsh->mode & DPIP_TAG) {
         /* Only wait for data when the tag is incomplete */
         if (!strstr(dsh->rdbuf->str, DPIP_TAG_END)) {
             do {
                Dpip_dsh_read(dsh, 1);
                p = strstr(dsh->rdbuf->str, DPIP_TAG_END);
             } while (!p && dsh->status == EAGAIN);
         }

      } else if (dsh->mode & DPIP_RAW) {
         /* Wait for data when the buffer is empty and there's no ERR/EOF */
         while (dsh->rdbuf->len == 0 &&
                dsh->status != DPIP_ERROR && dsh->status != DPIP_EOF)
            Dpip_dsh_read(dsh, 1);
      }
   }

   if (dsh->mode & DPIP_TAG) {
      /* return a full tag */
      if ((p = strstr(dsh->rdbuf->str, DPIP_TAG_END))) {
         ret = dStrndup(dsh->rdbuf->str, p - dsh->rdbuf->str + 3);
         *DataSize = p - dsh->rdbuf->str + 3;
         dStr_erase(dsh->rdbuf, 0, p - dsh->rdbuf->str + 3);
         if (strstr(ret, DPIP_MODE_SWITCH_TAG))
            dsh->mode |= DPIP_LAST_TAG;
      }
   } else {
      /* raw mode, return what we have "as is" */
      if (dsh->rdbuf->len > 0) {
         ret = dStrndup(dsh->rdbuf->str, dsh->rdbuf->len);
         *DataSize = dsh->rdbuf->len;
         dStr_truncate(dsh->rdbuf, 0);
      }
   }

   return ret;
}

/**
 * Return a newlly allocated string with the next dpip token in the socket.
 * Return value: token string on success, NULL otherwise
 */
char *a_Dpip_dsh_read_token(Dsh *dsh, int blocking)
{
   int token_size;

   return a_Dpip_dsh_read_token2(dsh, blocking, &token_size);
}

/**
 * Close this socket for reading and writing.
 * (flush pending data)
 */
void a_Dpip_dsh_close(Dsh *dsh)
{
   int st;

   /* flush internal buffer */
   a_Dpip_dsh_write(dsh, 1, "", 0);

   /* close fds */
   st = dClose(dsh->fd_in);
   if (st < 0)
      MSG_ERR("[a_Dpip_dsh_close] close: %s\n", dStrerror(errno));
   if (dsh->fd_out != dsh->fd_in) {
      st = dClose(dsh->fd_out);
      if (st < 0)
         MSG_ERR("[a_Dpip_dsh_close] close: %s\n", dStrerror(errno));
   }
}

/**
 * Free the SockHandler structure
 */
void a_Dpip_dsh_free(Dsh *dsh)
{
   dReturn_if (dsh == NULL);

   dStr_free(dsh->wrbuf, 1);
   dStr_free(dsh->rdbuf, 1);
   dFree(dsh);
}

