/* NessusClient -- the Nessus Client
 * Copyright (C) 2005, 2006 Renaud Deraison
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2,
 * as published by the Free Software Foundation
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * In addition, as a special exception, Renaud Deraison
 * gives permission to link the code of this program with any
 * version of the OpenSSL library which is distributed under a
 * license identical to that listed in the included COPYING.OpenSSL
 * file, and distribute linked combinations including the two.
 * You must obey the GNU General Public License in all respects
 * for all of the code used other than OpenSSL.  If you modify
 * this file, you may extend this exception to your version of the
 * file, but you are not obligated to do so.  If you do not wish to
 * do so, delete this exception statement from your version.
 */

/**
 * \file
 * Cache for the plugin information read from the server.
 *
 * @section plugincache-fileformat File format
 *
 * A plugin cache file consists of a number of lines, each holding a
 * number of fields.  The fields are separated by vertical bar
 * characters ('|').  The first field is a keyword looking like a
 * C-language identifier describing the type of the line, the rest of
 * the fields are %-escaped strings.  Percent escaped means that
 * non-printable ascii characters, i.e. anything with ascii code < 32 or
 * >= 127, as well as % and | are replaced with a % followed by two
 * hexadecimal digits giving the ascii code of the original character.
 * E.g. "%" is turned into "%25".
 *
 * Since the | is escaped too, the only |-characters occurring in a line
 * are the field separators.  So decoding can be done by first replacing
 * all | with NUL and then %-unescaping each field.  Unescaping never
 * makes the string longer and thus can be done in place.
 * An example is given below.
 *
 * There are several kinds of lines, distinguished by keywords.
 * For a healthy cache file they appear in the following order:
 *
 *   OpenVASNVTDescCache
 *
 *     This is only and always used on the first line and also serves as
 *     a magic string identifying the file.  The following fields are in
 *     order:
 *
 *      - the version number of the file format in decimal.  Currently
 *        there is only version 0.
 *
 *      - the hex-encoded md5sum that was reported by the server for the
 *        set of plugins saved in the cache.
 *
 *   nvt
 *
 *     NVT desription.  The rest of the fields are: oid, md5sum, name,
 *     category, copyright, description, summary, family, version, cve,
 *     bid, xrefs, sign_key_ids.
 *
 *   dependency
 *
 *     Description of a dependency. Currently, in this line the plugins
 *     are refered to by their respective name. Indication is that the
 *     first plugin depends on the later.
 *
 *   end
 *
 *     No further fields.  This line marks the end of the file.  If it's
 *     missing, the cache hasn't been written properly and should be
 *     considered outdated.
 *
 *
 * @section plugincache-files-used Files used
 *
 * A cache file is specific for a given context and is stored in the
 * same directory as the openvasrc file for the context.  The cache for
 * the global context is ~/.openvas_nvt_cache.  If an alternate
 * openvas file was given on the command line, no caching is done.
 * 
 * @section plugincache-example Examplary first two lines of a cache file
 * 
 * OpenVASNVTDescCache|2|fc8902a32651d93ab136e75977f664ec
 * nvt|1.3.6.1.4.1.25623.1.0.11219|8e58495bf0130d4da02340a505b14041|SYN Scan|scanner|Copyright (C) Renaud Deraison \<deraison@cvs.nessus.org\>|%0aThis plugins performs a supposedly fast SYN port scan%0aIt does so by computing the RTT (round trip time) of the packets%0acoming back and forth between the openvasd host and the target,%0athen it uses that to quicky send SYN packets to the remote host%0a|Performs a TCP SYN scan|Port scanners|$Revision: 1266 $|NOCVE|NOBID|NOXREF|NOSIGNKEYS|NOTAG
 *
 */

#include <includes.h>

#include "nessus_plugin.h"
#include "error_dlg.h"
#include "context.h"
#include "comm.h"
#include "preferences.h"
#include "globals.h"
#include "plugin_cache.h"
#include "nessus_i18n.h"

/* file format constants */
#define MAX_HEADER_ITEMS 3
#define MAX_LINE_ITEMS 15
#define HEADER_MAGIC "OpenVASNVTDescCache"
#define FILE_FORMAT_VERSION 2
#define NVT_KEYWORD "nvt"
#define DEP_KEYWORD "dependency"
#define END_KEYWORD "end"

/**
 * @brief Determine the cache file to use for the context.
 *
 * The cache file will be in the same directory as the nessusrc file of
 * the context.  The return value has to be free'd with efree.
 */
gchar *
plugin_cache_get_filename(struct context *context)
{
  char *filename = NULL;

  if (context->dir)
  {
    filename = g_build_filename (context->dir, "openvas_nvt_cache", NULL);
  }
  else
  {
    char *home = prefs_get_nessushome();
    filename = g_build_filename (home, ".openvas_nvt_cache", NULL);
  }

  return filename;
}


/**
 * @brief Write a string to the cache file.
 *
 * The parameter FILE is the stream to be used to write to the cache
 * file.  S is the NUL-terminated string to be written.  The bytes of S
 * are percent escaped (see file format description above).
 *
 * @return Returns 0 on success and != 0 otherwise.
 */
static int
write_string(FILE *file, const char *s)
{
  const char *p;

  for (p = s; *p; p++)
  {
    if (*p >= 0x20 && *p < 127 && *p != '|' && *p != '%')
      putc(*p, file);
    else
      fprintf(file, "%%%02x", *(unsigned char*)p);
  }

  return ferror(file) ? -1 : 0;
}

/**
 * @brief Write a record to the cache file.
 *
 * The parameter FILE is the stream to be used to write to the cache
 * file.  FORMAT is a string containing one character for each of the
 * fields following the format parameter.  Two field types are
 * supported:
 *
 *   i   (int) The value must be an int.  It is written in a decimal.
 *   s   (string) The value must be a NUL-terminated string.
 *       The string is percent-escaped while being written.
 *   k   (keyword) The value must be a NUL-terminated string.
 *       The string is copied literally to the file
 *       and is not percent escaped.
 *
 * The values are written in percent-escaped form, separated by vertical
 * bars, followed by a newline to end the line/record.
 *
 * @return Returns 0 on success and != 0 otherwise.
 */
/* TODO: check for write errors */
static int
write_record(FILE *file, const char *format, ...)
{
  va_list ap;
  const char *p;
  int failure = 0;
  int write_delimiter = 0;

  va_start(ap, format);

  for (p = format; *p && !failure; p++)
  {
    if (write_delimiter)
    {
      failure = putc('|', file) == EOF;
      if (failure)
	break;
    }

    switch (*p) {
    case 's':
      failure = write_string(file, va_arg(ap, const char *));
      break;
    case 'k':
      /* Ideally we should check whether the contents of the string
       * match the definition of "keyword" in the fileformat */
      failure = fputs(va_arg(ap, const char *), file) == EOF;
      break;
    case 'i':
      failure = fprintf(file, "%d", va_arg(ap, int)) < 0;
      break;

    default:
      /* Error because format character is unsupported */
      failure = -1;
    }
    write_delimiter = 1;
  }
  if (!failure)
    failure = putc('\n', file) == EOF;
  va_end(ap);
  return failure;
}


/**
 * @brief Write a plugin to the cache file.
 *
 * The parameter PLUGIN is the plugin to write and FILE is be the stream
 * to be used to write to the cache file.  The plugin is written as a
 * single record as described in the file format description.
 *
 * @return Returns 0 on success and != 0 otherwise.
 */
static int
write_plugin(struct nessus_plugin *plugin, FILE *file)
{
  char * md5sum = plugin->md5sum;
  if (md5sum == NULL)
    md5sum = "";
  return write_record(file, "kssssssssssssss", NVT_KEYWORD,
      plugin->oid, md5sum, plugin->name, plugin->category, plugin->copyright,
      nessus_plugin_get_description(plugin), plugin->summary, plugin->family,
      plugin->version, plugin->cve, plugin->bid, plugin->xrefs, 
      plugin->sign_key_ids, plugin->script_tags);
}


/**
 * @brief Write a dependency to the cache file.
 *
 * The parameter DEP is the dependency to write and FILE is be the stream
 * to be used to write to the cache file.  The dependency is written as a
 * single record as described in the file format description.
 *
 * @return Returns 0 on success and != 0 otherwise.
 */
static int
write_dep(struct arglist *dep, FILE *file)
{
  struct arglist * lst = dep->value;
  while (lst && lst->next) {
    if (write_record(file, "kss", DEP_KEYWORD, dep->name, lst->name))
      return 1;
    lst = lst->next;
  }
  return 0;
}


/**
 * @brief Write all plugins in the linked list PLUGINS.
 *
 * The parameter PLUGINS is the first plugin in the list of plugins to
 * write and FILE is be the stream to be used to write to the cache
 * file.  Each plugin is written with write_plugin.
 *
 * @return Returns 0 on success and != 0 otherwise.
 */
static int
write_plugin_list(struct nessus_plugin *plugins, FILE *file)
{
  while (plugins != NULL)
  {
    if (write_plugin(plugins, file) != 0)
      return -1;
    plugins = plugins->next;
  }

  return 0;
}

/**
 * @brief Write all dependencies in the linked list DEPS.
 *
 * The parameter DEPS is the first dependency in the list of dependencies to
 * write and FILE is be the stream to be used to write to the cache
 * file. Each dependency is written with write_dep.
 *
 * @return Returns 0 on success and != 0 otherwise.
 */
static int
write_deps_list(struct arglist *deps, FILE *file)
{
  while (deps && deps->next)
  {
    if (write_dep(deps, file) != 0)
      return -1;
    deps = deps->next;
  }

  return 0;
}

/**
 * @brief Write the plugins in CONTEXT to a cache.
 *
 * The parameter CONTEXT is the context whose plugins are to be written.
 * The filename for the cache is determined with
 * plugin_cache_get_filename.
 * The parameter SERVER_MD5SUM should be the string with the hex-encoded
 * md5sum reported by the server while connecting.  It can also be the
 * empty string for situations where the checking it will not be needed
 * such as when storing the plugins for a report.
 *
 * If an error occurs when writing the file, the file is removed to
 * avoid incorrect caches lying around.
 * 
 * @return Returns 0 on success and != 0 otherwise.
 */
int
plugin_cache_write(struct context * context, const char * server_md5sum)
{
  gchar *filename;
  FILE *file;
  int failure = 0;

  filename = plugin_cache_get_filename(context);

  file = fopen(filename, "w");
  if (!file)
  {
    show_error(_("Could not open file '%s' for writing plugin cache"), filename);
    g_free (filename);
    return -1;
  }

  failure = (write_record(file, "kis", HEADER_MAGIC, FILE_FORMAT_VERSION,
	  server_md5sum)
      || write_plugin_list(context->scanners, file)
      || write_plugin_list(context->plugins, file)
      || write_deps_list(context->dependencies, file)
      || write_record(file, "k", END_KEYWORD));

  if (fclose(file) != 0)
    failure = -1;

  if (failure)
    unlink(filename);
  g_free (filename);

  return failure;
}


/**
 * @brief Read one line from FILE.
 *
 * The return value is a NUL-terminated string including the trailing
 * newline of the line if any.  The return value is allocated with
 * emalloc and has to be freed by the caller with efree.
 * When the end of the file is reached the function returns an empty string.
 * 
 * @return NULL if an error occured, empty string when end of file is reached
 *         or the (nul-terminated) line of a file, including trailing newline if
 *         any.
 */
static char *
read_line(FILE *file)
{
  char *buf;
  char *end;
  char *cur;
  int c;
  size_t buf_size;
  size_t len;

  buf_size = 1024;
  buf = emalloc(buf_size);
  end = buf + buf_size - 1;
  cur = buf;

  for (;;)
  {
    while ((c = getc(file)) != EOF && (*cur++ = c) != '\n' && cur != end)
      ;
    if (c == '\n')
      break;
    if (c == EOF)
    {
      if (ferror(file))
	goto fail;
      break;
    }
    /* line doesn't fit into the buffer.  Realloc and continue */
    len = cur - buf;
    buf_size += 1024;
    buf = erealloc(buf, buf_size);
    if (!buf)
      return NULL;
    end = buf + buf_size - 1;
    cur = buf + len;
  }

  *cur = '\0';
  return buf;

fail:
  efree(&buf);
  return NULL;
}


/**
 * @brief Split LINE at the vertical bars.
 *
 * The NUL-terminated string LINE is split into fields at the vertical
 * bar characters ('|') by replacing the vertical bars with NUL
 * characters and writing pointers to the start of each field into the
 * array ITEMS.  The ITEMS array must be provided by the caller.  The
 * parameter NITEMS should be the length of ITEMS and it must be at
 * least 1.  No more than NITEMS values will be put into ITEMS by this
 * function.  If LINE ends in a newline, that newline character will
 * also be replaced by NUL.
 *
 * The return value is the number of fields found in the line.  This
 * number may be larger than NITEMS in which case the fields beyond the
 * first NITEMS fields won't be accesssible to the caller.
 * 
 * @return Number of fields found in the line.
 */
static int
split_line(char * line, char **items, int nitems)
{
  int found = 0;
  char *cur = line;

  /* the first item is always at the beginning of the line */
  items[0] = line;
  found = 1;

  while (*cur)
  {
    if (*cur == '|')
    {
      *cur = '\0';
      if (found < nitems)
      {
	items[found] = cur + 1;
      }
      found++;
    }
    cur++;
  }

  /* cur now points at the final NUL character.  If there's a newline
   * just in front of it, remove it */
  if (cur > line && cur[-1] == '\n')
  {
    cur[-1] = '\0';
  }

  return found;
}

/* Macro that determines the numerical value of a hex digit. */
#define HEXDECODE(c) \
  (('0' <= (c) && c <= '9') ? (c) - '0' \
      : ('A' <= (c) && c <= 'F') ? (c) - 'A' + 10\
      : ('a' <= (c) && c <= 'f') ? (c) - 'a' + 10\
      : -1)

/**
 * @brief Percent-unquote a NUL-terminated string in place.
 * 
 * @return Returns 0 on success and != 0 otherwise.
 */
static int
unquote(char * s)
{
  char * from = s;
  char * to = s;

  while ((*to++ = *from++))
  {
    if (from[-1] == '%')
    {
      /* we copied a '%'. decode it. */
      int hi, lo;

      if (   (hi = HEXDECODE(from[0])) >= 0
	  && (lo = HEXDECODE(from[1])) >= 0)
      {
	to[-1] = hi << 4 | lo;
	from += 2;
      }
      else
      {
	/* TODO: error */
	return -1;
      }
    }
  }

  return 0;
}


/**
 * @brief Read a line from the cache file and decode it.
 *
 * The parameter FILE is the stread to the read the cache line from.
 * The parameters ITEMS and NITEMS are passed through to split_line and
 * have the same semantics.
 *
 * This function reads a line from FILE with read_line and splits it
 * using split_line.  Each item is then decoded with unquote.
 *
 * When the function is successful, the return value is the actual
 * number of fields in the line as determined by split_line and thus may
 * be larger than NITEMS.  items[0] is always the same as the pointer
 * returned by read_line and the caller has to release this memory as
 * documented for read_line.
 *
 * When end of file is reached, the return value is 0.  ITEMS will not
 * be modified.
 *
 * When an error occurs, the return value is -1.  The ITEMS array may
 * have been modified already in that case, but the memory the items now
 * point to is invalid.
 * 
 * @return Actual number of fields in a line, 0 on end of file, -1 on error.
 */
static int
read_cache_line(FILE * file, char **items, int nitems)
{
  int real_nitems;
  char *line = read_line(file);
  int i;

  if (line == NULL)
    return -1;
      
  /* if the line is not empty, decode it */
  if (line[0])
  {
    real_nitems = split_line(line, items, nitems);

    /* The keyword is not quoted, so do not unquote it */
    for (i = 1; i < real_nitems; i++)
    {
      if (unquote(items[i]) != 0)
      {
	real_nitems = -1;
	break;
      }
    }
  }
  else
    /* empty string, i.e. EOF */
    real_nitems = 0;

  if (real_nitems <= 0)
    efree(&line);

 
  return real_nitems;
}


/**
 * @brief Read the file header.
 *
 * The file header is the first line of the file with the md5sum for the
 * md5sum originally reported by the server.
 *
 * If the header could not be read successfully, the function returns a
 * value < 0.  Otherwise the return value is 0.
 *
 * The md5sum will be stored in a string newly allocated with emalloc in
 * *server_md5sum.
 * 
 * @return Returns 0 on success, < 0 on error.
 */
static int
check_header(FILE * file, char ** server_md5sum)
{
  char *items[MAX_HEADER_ITEMS];
  int nitems = read_cache_line(file, items, MAX_HEADER_ITEMS);
  int result = 0;

  if (nitems <=0)
  {
    /* error or EOF.  A file without a header is always an error,
     * though. */
    return -1;
  }

  /* the header line always has exactly three fields with the first
   * being the magic string, so anything else is an error */
  if (nitems == 3)
  {
    char *tail;
    long version = strtol(items[1], &tail, 10);
    if (*tail != '\0')
    {
      version = -1;
    }

    if (strcmp(items[0], HEADER_MAGIC) != 0)
    {
      result = -1;
    }
    else if (version != FILE_FORMAT_VERSION)
    {
      result = -1;
    }
    else
    {
      *server_md5sum = estrdup(items[2]);
    }
  }
  else
  {
    result = -1;
  }

  efree(&items[0]);
  return result;
}

/**
 * @brief Read the plugin cache and optionally check the md5sum for the cache.
 *
 * CONTEXT is the context into which the cache should be read.
 * SERVER_MD5SUM should be the string with the hex-encoded md5sum
 * reported by the server while connecting.  This md5sum is compared to
 * the md5sum read from the plugin cache for the context and the cache
 * is considered to be current if they match.  It's OK to pass an empty
 * string as the md5sum if the caller is not interested in the check.
 *
 * As long as no error occurs the plugins are read, even when the cache
 * is not current so that e.g. the caller can update individual plugins
 * withoug having to refetch all of them.
 *
 * If an error occurs the function returns a value < 0.  Otherwise if
 * the cache was current it returns 0 and a value > 0 if the cache was
 * outdated.
 * 
 * @return If the cache is current, returns 0. On outdated caches >0, on errors
 *        <0.
 */
int
plugin_cache_read(struct context * context)
{
  int result = 0;
  int end_found = 0;
  gchar *filename;
  FILE *file;
  char *server_md5sum = NULL;

  filename = plugin_cache_get_filename(context);
  if (!filename)
  {
    /* the only failure cause currently is that the user has specified a
     * different nessusrc file.  In that case no caching takes place.
     */
    return 1;
  }

  file = fopen(filename, "r");
  if (!file)
  {
    /* the file could not be opened for reading.  This is normal if the
     * cache simply doesn't exist yet, so we don't give an error
     * message.
     */
    result = -1;
    goto fail;
  }

  /* check the header and compare md5sums */
  result = check_header(file, &server_md5sum);
  if (result < 0)
    goto fail;

  /* read the rest of the lines */
  while (result == 0)
  {
    char *items[MAX_LINE_ITEMS];
    int nitems = read_cache_line(file, items, MAX_LINE_ITEMS);

    if (nitems < 0)
    {
      /* error */
      result = -1;
      break;
    }

    if (nitems == 0)
    {
      /* EOF.  If the "end" line hasn't been read yet it's an error */
      if (!end_found)
      {
	result = -1;
      }
      break;
    }

    /* If the line is not empty but we've already read the end-line,
     * it's an error */
    if (end_found)
    {
      result = -1;
      efree(&items[0]);
      break;
    }
    /* If the line has 14 items and first one is the nvt- keyword, parse a nvt*/
    if (nitems == 15 && strcmp(items[0], NVT_KEYWORD) == 0)
    {
      struct nessus_plugin *plugin = nessus_plugin_new(items[1] /*oid*/,
	  items[3] /*name*/, items[4] /*category*/, items[5] /*copyright*/,
	  items[6] /*description*/, items[7] /*summary*/,
	  items[8] /*family*/, items[9] /*version*/, items[10] /*cve*/,
	  items[11] /*bid*/, items[12] /*xref*/, items[13] /*sign_key_ids*/,
          items[14] /* script_tags*/ );
      /* add the md5sum */
      nessus_plugin_set_md5sum(plugin, items[2]);

      context_add_plugin(context, plugin);
    }
    /* If there are three items in line && DEP-keyw. found, parse dependencies*/
    else if (nitems == 3 && strcmp(items[0], DEP_KEYWORD) == 0)
    {
      struct arglist *deps = arg_get_value(context->dependencies, items[1]);
      if (!deps) {
        deps = emalloc(sizeof(struct arglist));
        arg_add_value(deps, items[2], ARG_INT,
                      (sizeof(int)), (void *)1);
        if (! context->dependencies)
          context->dependencies = emalloc(sizeof(struct arglist));
        arg_add_value(context->dependencies, items[1],
                      ARG_ARGLIST, -1, deps);
      } else {
        arg_add_value(deps, items[2], ARG_INT,
                      (sizeof(int)), (void *)1);
      }
    }
    else if (nitems == 1 && strcmp(items[0], END_KEYWORD) == 0)
    {
      end_found = 1;
    }
    else
    {
      /* TODO: free plugins in case of error */
      result = -1;
    }

    efree(&items[0]);
  }

  context_set_plugins_md5sum(context, server_md5sum);

 fail:
  g_free (filename);
  efree(&server_md5sum);
  if (file != NULL)
    fclose(file);

  if (result < 0)
    context_reset_plugins(context);

  return result;
}
