/*
 *  OpenVPN -- An application to securely tunnel IP networks
 *             over a single TCP/UDP port, with support for SSL/TLS-based
 *             session authentication and key exchange,
 *             packet encryption, packet authentication, and
 *             packet compression.
 *
 *  Copyright (C) 2002-2010 OpenVPN Technologies, Inc. <sales@openvpn.net>
 *
 *  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 (see the file COPYING included with this
 *  distribution); if not, write to the Free Software Foundation, Inc.,
 *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include "syshead.h"

#ifdef ENABLE_PLUGIN

#include "buffer.h"
#include "error.h"
#include "misc.h"
#include "plugin.h"

#include "memdbg.h"

#define PLUGIN_SYMBOL_REQUIRED (1<<0)

/* used only for program aborts */
static struct plugin_common *static_plugin_common = NULL; /* GLOBAL */

static void
plugin_show_string_array (int msglevel, const char *name, const char *array[])
{
  int i;
  for (i = 0; array[i]; ++i)
    {
      if (env_safe_to_print (array[i]))
	msg (msglevel, "%s[%d] = '%s'", name, i, array[i]);
    }
}

static void
plugin_show_args_env (int msglevel, const char *argv[], const char *envp[])
{
  if (check_debug_level (msglevel))
    {
      plugin_show_string_array (msglevel, "ARGV", argv);
      plugin_show_string_array (msglevel, "ENVP", envp);
    }
}

static const char *
plugin_type_name (const int type)
{
  switch (type)
    {
    case OPENVPN_PLUGIN_UP:
      return "PLUGIN_UP";
    case OPENVPN_PLUGIN_DOWN:
      return "PLUGIN_DOWN";
    case OPENVPN_PLUGIN_ROUTE_UP:
      return "PLUGIN_ROUTE_UP";
    case OPENVPN_PLUGIN_IPCHANGE:
      return "PLUGIN_IPCHANGE";
    case OPENVPN_PLUGIN_TLS_VERIFY:
      return "PLUGIN_TLS_VERIFY";
    case OPENVPN_PLUGIN_AUTH_USER_PASS_VERIFY:
      return "PLUGIN_AUTH_USER_PASS_VERIFY";
    case OPENVPN_PLUGIN_CLIENT_CONNECT:
      return "PLUGIN_CLIENT_CONNECT";
    case OPENVPN_PLUGIN_CLIENT_CONNECT_V2:
      return "PLUGIN_CLIENT_CONNECT";
    case OPENVPN_PLUGIN_CLIENT_DISCONNECT:
      return "PLUGIN_CLIENT_DISCONNECT";
    case OPENVPN_PLUGIN_LEARN_ADDRESS:
      return "PLUGIN_LEARN_ADDRESS";
    case OPENVPN_PLUGIN_TLS_FINAL:
      return "PLUGIN_TLS_FINAL";
    case OPENVPN_PLUGIN_ENABLE_PF:
      return "OPENVPN_PLUGIN_ENABLE_PF";
    default:
      return "PLUGIN_???";
    }
}

static const char *
plugin_mask_string (const unsigned int type_mask, struct gc_arena *gc)
{
  struct buffer out = alloc_buf_gc (256, gc);
  bool first = true;
  int i;

  for (i = 0; i < OPENVPN_PLUGIN_N; ++i)
    {
      if (OPENVPN_PLUGIN_MASK (i) & type_mask)
	{
	  if (!first)
	    buf_printf (&out, "|");
	  buf_printf (&out, "%s", plugin_type_name (i));
	  first = false;
	}
    }
  return BSTR (&out);
}

static inline unsigned int
plugin_supported_types (void)
{
  return ((1<<OPENVPN_PLUGIN_N)-1);
}

struct plugin_option_list *
plugin_option_list_new (struct gc_arena *gc)
{
  struct plugin_option_list *ret;
  ALLOC_OBJ_CLEAR_GC (ret, struct plugin_option_list, gc);
  return ret;
}

bool
plugin_option_list_add (struct plugin_option_list *list, char **p, struct gc_arena *gc)
{
  if (list->n < MAX_PLUGINS)
    {
      struct plugin_option *o = &list->plugins[list->n++];
      o->argv = make_extended_arg_array (p, gc);
      if (o->argv[0])
	o->so_pathname = o->argv[0];
      return true;
    }
  else
    return false;
}

#ifdef ENABLE_DEBUG
void
plugin_option_list_print (const struct plugin_option_list *list, int msglevel)
{
  int i;
  struct gc_arena gc = gc_new ();

  for (i = 0; i < list->n; ++i)
    {
      const struct plugin_option *o = &list->plugins[i];
      msg (msglevel, "  plugin[%d] %s '%s'", i, o->so_pathname, print_argv (o->argv, &gc, PA_BRACKET));
    }

  gc_free (&gc);
}
#endif

#if defined(USE_LIBDL)

static void
libdl_resolve_symbol (void *handle, void **dest, const char *symbol, const char *plugin_name, const unsigned int flags)
{
  *dest = dlsym (handle, symbol);
  if ((flags & PLUGIN_SYMBOL_REQUIRED) && !*dest)
    msg (M_FATAL, "PLUGIN: could not find required symbol '%s' in plugin shared object %s: %s", symbol, plugin_name, dlerror());
}

#elif defined(USE_LOAD_LIBRARY)

static void
dll_resolve_symbol (HMODULE module, void **dest, const char *symbol, const char *plugin_name, const unsigned int flags)
{
  *dest = GetProcAddress (module, symbol);
  if ((flags & PLUGIN_SYMBOL_REQUIRED) && !*dest)
    msg (M_FATAL, "PLUGIN: could not find required symbol '%s' in plugin DLL %s", symbol, plugin_name);
}

#endif

static void
plugin_init_item (struct plugin *p, const struct plugin_option *o)
{
  struct gc_arena gc = gc_new ();
  bool rel = false;

  p->so_pathname = o->so_pathname;
  p->plugin_type_mask = plugin_supported_types ();

#if defined(USE_LIBDL)

  p->handle = NULL;
#if defined(PLUGIN_LIBDIR)
  if (!absolute_pathname (p->so_pathname))
    {
      char full[PATH_MAX];

      openvpn_snprintf (full, sizeof(full), "%s/%s", PLUGIN_LIBDIR, p->so_pathname);
      p->handle = dlopen (full, RTLD_NOW);
#if defined(ENABLE_PLUGIN_SEARCH)
      if (!p->handle)
	{
	  rel = true;
	  p->handle = dlopen (p->so_pathname, RTLD_NOW);
	}
#endif
    }
  else
#endif
    {
      rel = !absolute_pathname (p->so_pathname);
      p->handle = dlopen (p->so_pathname, RTLD_NOW);
    }
  if (!p->handle)
    msg (M_ERR, "PLUGIN_INIT: could not load plugin shared object %s: %s", p->so_pathname, dlerror());

# define PLUGIN_SYM(var, name, flags) libdl_resolve_symbol (p->handle, (void*)&p->var, name, p->so_pathname, flags)

#elif defined(USE_LOAD_LIBRARY)

  rel = !absolute_pathname (p->so_pathname);
  p->module = LoadLibrary (p->so_pathname);
  if (!p->module)
    msg (M_ERR, "PLUGIN_INIT: could not load plugin DLL: %s", p->so_pathname);

# define PLUGIN_SYM(var, name, flags) dll_resolve_symbol (p->module, (void*)&p->var, name, p->so_pathname, flags)

#endif

  PLUGIN_SYM (open1, "openvpn_plugin_open_v1", 0);
  PLUGIN_SYM (open2, "openvpn_plugin_open_v2", 0);
  PLUGIN_SYM (func1, "openvpn_plugin_func_v1", 0);
  PLUGIN_SYM (func2, "openvpn_plugin_func_v2", 0);
  PLUGIN_SYM (close, "openvpn_plugin_close_v1", PLUGIN_SYMBOL_REQUIRED);
  PLUGIN_SYM (abort, "openvpn_plugin_abort_v1", 0);
  PLUGIN_SYM (client_constructor, "openvpn_plugin_client_constructor_v1", 0);
  PLUGIN_SYM (client_destructor, "openvpn_plugin_client_destructor_v1", 0);
  PLUGIN_SYM (min_version_required, "openvpn_plugin_min_version_required_v1", 0);
  PLUGIN_SYM (initialization_point, "openvpn_plugin_select_initialization_point_v1", 0);

  if (!p->open1 && !p->open2)
    msg (M_FATAL, "PLUGIN: symbol openvpn_plugin_open_vX is undefined in plugin: %s", p->so_pathname);

  if (!p->func1 && !p->func2)
    msg (M_FATAL, "PLUGIN: symbol openvpn_plugin_func_vX is undefined in plugin: %s", p->so_pathname);

  /*
   * Verify that we are sufficiently up-to-date to handle the plugin
   */
  if (p->min_version_required)
    {
      const int plugin_needs_version = (*p->min_version_required)();
      if (plugin_needs_version > OPENVPN_PLUGIN_VERSION)
	msg (M_FATAL, "PLUGIN_INIT: plugin needs interface version %d, but this version of OpenVPN only supports version %d: %s",
	     plugin_needs_version,
	     OPENVPN_PLUGIN_VERSION,
	     p->so_pathname);
    }

  if (p->initialization_point)
    p->requested_initialization_point = (*p->initialization_point)();
  else
    p->requested_initialization_point = OPENVPN_PLUGIN_INIT_PRE_DAEMON;

  if (rel)
    msg (M_WARN, "WARNING: plugin '%s' specified by a relative pathname -- using an absolute pathname would be more secure", p->so_pathname);

  p->initialized = true;

  gc_free (&gc);
}

static void
plugin_open_item (struct plugin *p,
		  const struct plugin_option *o,
		  struct openvpn_plugin_string_list **retlist,
		  const char **envp,
		  const int init_point)
{
  ASSERT (p->initialized);

  /* clear return list */
  if (retlist)
    *retlist = NULL;

  if (!p->plugin_handle && init_point == p->requested_initialization_point)
    {
      struct gc_arena gc = gc_new ();

      dmsg (D_PLUGIN_DEBUG, "PLUGIN_INIT: PRE");
      plugin_show_args_env (D_PLUGIN_DEBUG, o->argv, envp);

      /*
       * Call the plugin initialization
       */
      if (p->open2)
	p->plugin_handle = (*p->open2)(&p->plugin_type_mask, o->argv, envp, retlist);
      else if (p->open1)
	p->plugin_handle = (*p->open1)(&p->plugin_type_mask, o->argv, envp);
      else
	ASSERT (0);

      msg (D_PLUGIN, "PLUGIN_INIT: POST %s '%s' intercepted=%s %s",
	   p->so_pathname,
	   print_argv (o->argv, &gc, PA_BRACKET),
	   plugin_mask_string (p->plugin_type_mask, &gc),
	   (retlist && *retlist) ? "[RETLIST]" : "");
      
      if ((p->plugin_type_mask | plugin_supported_types()) != plugin_supported_types())
	msg (M_FATAL, "PLUGIN_INIT: plugin %s expressed interest in unsupported plugin types: [want=0x%08x, have=0x%08x]",
	     p->so_pathname,
	     p->plugin_type_mask,
	     plugin_supported_types());

      if (p->plugin_handle == NULL)
	msg (M_FATAL, "PLUGIN_INIT: plugin initialization function failed: %s",
	     p->so_pathname);

      gc_free (&gc);
    }
}

static int
plugin_call_item (const struct plugin *p,
		  void *per_client_context,
		  const int type,
		  const struct argv *av,
		  struct openvpn_plugin_string_list **retlist,
		  const char **envp)
{
  int status = OPENVPN_PLUGIN_FUNC_SUCCESS;

  /* clear return list */
  if (retlist)
    *retlist = NULL;

  if (p->plugin_handle && (p->plugin_type_mask & OPENVPN_PLUGIN_MASK (type)))
    {
      struct gc_arena gc = gc_new ();
      struct argv a = argv_insert_head (av, p->so_pathname);

      dmsg (D_PLUGIN_DEBUG, "PLUGIN_CALL: PRE type=%s", plugin_type_name (type));
      plugin_show_args_env (D_PLUGIN_DEBUG, (const char **)a.argv, envp);

      /*
       * Call the plugin work function
       */
      if (p->func2)
	status = (*p->func2)(p->plugin_handle, type, (const char **)a.argv, envp, per_client_context, retlist);
      else if (p->func1)
	status = (*p->func1)(p->plugin_handle, type, (const char **)a.argv, envp);
      else
	ASSERT (0);

      msg (D_PLUGIN, "PLUGIN_CALL: POST %s/%s status=%d",
	   p->so_pathname,
	   plugin_type_name (type),
	   status);

      if (status == OPENVPN_PLUGIN_FUNC_ERROR)
	msg (M_WARN, "PLUGIN_CALL: plugin function %s failed with status %d: %s",
	     plugin_type_name (type),
	     status,
	     p->so_pathname);

      argv_reset (&a);
      gc_free (&gc);
    }
  return status;
}

static void
plugin_close_item (struct plugin *p)
{
  if (p->initialized)
    {
      msg (D_PLUGIN, "PLUGIN_CLOSE: %s", p->so_pathname);

      /*
       * Call the plugin close function
       */
      if (p->plugin_handle)
	(*p->close)(p->plugin_handle);

#if defined(USE_LIBDL)
      if (dlclose (p->handle))
	msg (M_WARN, "PLUGIN_CLOSE: dlclose() failed on plugin: %s", p->so_pathname);
#elif defined(USE_LOAD_LIBRARY)
      if (!FreeLibrary (p->module))
	msg (M_WARN, "PLUGIN_CLOSE: FreeLibrary() failed on plugin: %s", p->so_pathname);
#endif

      p->initialized = false;
    }
}

static void
plugin_abort_item (const struct plugin *p)
{
  /*
   * Call the plugin abort function
   */
  if (p->abort)
    (*p->abort)(p->plugin_handle);
}

static void
plugin_per_client_init (const struct plugin_common *pc,
			struct plugin_per_client *cli,
			const int init_point)
{
  const int n = pc->n;
  int i;

  for (i = 0; i < n; ++i)
    {
      const struct plugin *p = &pc->plugins[i];
      if (p->plugin_handle
	  && (init_point < 0 || init_point == p->requested_initialization_point)
	  && p->client_constructor)
	cli->per_client_context[i] = (*p->client_constructor)(p->plugin_handle);
    }
}

static void
plugin_per_client_destroy (const struct plugin_common *pc, struct plugin_per_client *cli)
{
  const int n = pc->n;
  int i;

  for (i = 0; i < n; ++i)
    {
      const struct plugin *p = &pc->plugins[i];
      void *cc = cli->per_client_context[i];

      if (p->client_destructor && cc)
	(*p->client_destructor)(p->plugin_handle, cc);
    }
  CLEAR (*cli);
}

struct plugin_list *
plugin_list_inherit (const struct plugin_list *src)
{
  struct plugin_list *pl;
  ALLOC_OBJ_CLEAR (pl, struct plugin_list);
  pl->common = src->common;
  ASSERT (pl->common);
  plugin_per_client_init (pl->common, &pl->per_client, -1);
  return pl;
}

static struct plugin_common *
plugin_common_init (const struct plugin_option_list *list)
{
  int i;
  struct plugin_common *pc;

  ALLOC_OBJ_CLEAR (pc, struct plugin_common);

  for (i = 0; i < list->n; ++i)
    {
      plugin_init_item (&pc->plugins[i],
			&list->plugins[i]);
      pc->n = i + 1;
    }

  static_plugin_common = pc;
  return pc;
}

static void
plugin_common_open (struct plugin_common *pc,
		    const struct plugin_option_list *list,
		    struct plugin_return *pr,
		    const struct env_set *es,
		    const int init_point)
{
  struct gc_arena gc = gc_new ();
  int i;
  const char **envp;

  envp = make_env_array (es, false, &gc);

  if (pr)
    plugin_return_init (pr);

  for (i = 0; i < pc->n; ++i)
    {
      plugin_open_item (&pc->plugins[i],
			&list->plugins[i],
			pr ? &pr->list[i] : NULL,
			envp,
			init_point);
    }

  if (pr)
    pr->n = i;

  gc_free (&gc);
}

static void
plugin_common_close (struct plugin_common *pc)
{
  static_plugin_common = NULL;
  if (pc)
    {
      int i;

      for (i = 0; i < pc->n; ++i)
	plugin_close_item (&pc->plugins[i]);
      free (pc);
    }
}

struct plugin_list *
plugin_list_init (const struct plugin_option_list *list)
{
  struct plugin_list *pl;
  ALLOC_OBJ_CLEAR (pl, struct plugin_list);
  pl->common = plugin_common_init (list);
  pl->common_owned = true;
  return pl;
}

void
plugin_list_open (struct plugin_list *pl,
		  const struct plugin_option_list *list,
		  struct plugin_return *pr,
		  const struct env_set *es,
		  const int init_point)
{
  plugin_common_open (pl->common, list, pr, es, init_point);
  plugin_per_client_init (pl->common, &pl->per_client, init_point);
}

int
plugin_call (const struct plugin_list *pl,
	     const int type,
	     const struct argv *av,
	     struct plugin_return *pr,
	     struct env_set *es)
{
  if (pr)
    plugin_return_init (pr);

  if (plugin_defined (pl, type))
    {
      struct gc_arena gc = gc_new ();
      int i;
      const char **envp;
      const int n = plugin_n (pl);
      bool success = false;
      bool error = false;
      bool deferred = false;
      
      mutex_lock_static (L_PLUGIN);

      setenv_del (es, "script_type");
      envp = make_env_array (es, false, &gc);

      for (i = 0; i < n; ++i)
	{
	  const int status = plugin_call_item (&pl->common->plugins[i],
					       pl->per_client.per_client_context[i],
					       type,
					       av,
					       pr ? &pr->list[i] : NULL,
					       envp);
	  switch (status)
	    {
	    case OPENVPN_PLUGIN_FUNC_SUCCESS:
	      success = true;
	      break;
	    case OPENVPN_PLUGIN_FUNC_DEFERRED:
	      deferred = true;
	      break;
	    default:
	      error = true;
	      break;
	    }
	}

      if (pr)
	pr->n = i;

      mutex_unlock_static (L_PLUGIN);

      gc_free (&gc);

      if (type == OPENVPN_PLUGIN_ENABLE_PF && success)
	return OPENVPN_PLUGIN_FUNC_SUCCESS;
      else if (error)
	return OPENVPN_PLUGIN_FUNC_ERROR;
      else if (deferred)
	return OPENVPN_PLUGIN_FUNC_DEFERRED;
    }

  return OPENVPN_PLUGIN_FUNC_SUCCESS;
}

void
plugin_list_close (struct plugin_list *pl)
{
  if (pl)
    {
      if (pl->common)
	{
	  plugin_per_client_destroy (pl->common, &pl->per_client);

	  if (pl->common_owned)
	    plugin_common_close (pl->common);
	}

      free (pl);
    }
}

void
plugin_abort (void)
{
  struct plugin_common *pc = static_plugin_common;
  static_plugin_common = NULL;
  if (pc)
    {
      int i;

      for (i = 0; i < pc->n; ++i)
	plugin_abort_item (&pc->plugins[i]);
    }
}

bool
plugin_defined (const struct plugin_list *pl, const int type)
{
  bool ret = false;

  if (pl)
    {
      const struct plugin_common *pc = pl->common;

      if (pc)
	{
	  int i;
	  const unsigned int mask = OPENVPN_PLUGIN_MASK (type);
	  for (i = 0; i < pc->n; ++i)
	    {
	      if (pc->plugins[i].plugin_type_mask & mask)
		{
		  ret = true;
		  break;
		}
	    }
	}
    }
  return ret;
}

/*
 * Plugin return functions
 */

static void
openvpn_plugin_string_list_item_free (struct openvpn_plugin_string_list *l)
{
  if (l)
    {
      free (l->name);
      string_clear (l->value);
      free (l->value);
      free (l);
    }
}

static void
openvpn_plugin_string_list_free (struct openvpn_plugin_string_list *l)
{
  struct openvpn_plugin_string_list *next;
  while (l)
    {
      next = l->next;
      openvpn_plugin_string_list_item_free (l);
      l = next;
    }
}

static struct openvpn_plugin_string_list *
openvpn_plugin_string_list_find (struct openvpn_plugin_string_list *l, const char *name)
{
  while (l)
    {
      if (!strcmp (l->name, name))
	return l;
      l = l->next;
    }
  return NULL;
}

void
plugin_return_get_column (const struct plugin_return *src,
			  struct plugin_return *dest,
			  const char *colname)
{
  int i;

  dest->n = 0;
  for (i = 0; i < src->n; ++i)
    dest->list[i] = openvpn_plugin_string_list_find (src->list[i], colname);
  dest->n = i;
}

void
plugin_return_free (struct plugin_return *pr)
{
  int i;
  for (i = 0; i < pr->n; ++i)
    openvpn_plugin_string_list_free (pr->list[i]);
  pr->n = 0;
}

#ifdef ENABLE_DEBUG
void
plugin_return_print (const int msglevel, const char *prefix, const struct plugin_return *pr)
{
  int i;
  msg (msglevel, "PLUGIN_RETURN_PRINT %s", prefix);
  for (i = 0; i < pr->n; ++i)
    {
      struct openvpn_plugin_string_list *l = pr->list[i];
      int count = 0;

      msg (msglevel, "PLUGIN #%d (%s)", i, prefix);
      while (l)
	{
	  msg (msglevel, "[%d] '%s' -> '%s'\n",
	       ++count,
	       l->name,
	       l->value);
	  l = l->next;
	}
    }
}
#endif

#else
static void dummy(void) {}
#endif /* ENABLE_PLUGIN */