/* nm-openvpn-service-openvpn-helper - helper called after OpenVPN established
 * a connection, uses DBUS to send information back to nm-openvpn-service
 *
 * Tim Niemueller [www.niemueller.de]
 * Based on work by Dan Williams <dcbw@redhat.com>
 *
 * 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 2 of the License, or
 * (at your option) any later version.
 *
 * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 *
 * (C) Copyright 2005 Red Hat, Inc.
 * (C) Copyright 2005 Tim Niemueller
 *
 * $Id: nm-openvpn-service-openvpn-helper.c 2119 2006-11-25 04:45:31Z dcbw $
 * 
 */

#include <glib.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <regex.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <dbus/dbus.h>
#include <dbus/dbus-glib-lowlevel.h>
#include <dbus/dbus-glib.h>
#include <NetworkManager.h>
#include <syslog.h>

#include "nm-openvpn-service.h"
#include "nm-utils.h"

static void
nm_log_handler (const gchar *    log_domain,
                GLogLevelFlags   log_level,
                const gchar *    message,
                gpointer         ignored)
{
  int syslog_priority;	

  switch (log_level)
  {
    case G_LOG_LEVEL_ERROR:
      syslog_priority = LOG_CRIT;
      break;

    case G_LOG_LEVEL_CRITICAL:
      syslog_priority = LOG_ERR;
      break;

    case G_LOG_LEVEL_WARNING:
      syslog_priority = LOG_WARNING;
      break;

    case G_LOG_LEVEL_MESSAGE:
      syslog_priority = LOG_NOTICE;
      break;

    case G_LOG_LEVEL_DEBUG:
      syslog_priority = LOG_DEBUG;
      break;

    case G_LOG_LEVEL_INFO:
    default:
      syslog_priority = LOG_INFO;
      break;
  }

  syslog (syslog_priority, "%s", message);
}

void
nm_logging_setup ()
{
  openlog (G_LOG_DOMAIN, LOG_CONS, LOG_DAEMON);
  g_log_set_handler (G_LOG_DOMAIN, 
                     G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION,
                     nm_log_handler,
                     NULL);
}

void
nm_logging_shutdown (void)
{
  closelog ();
}

void 
cleanup_and_exit(int n)
{
  nm_logging_shutdown();
  exit(n);
}
/*
 * send_config_error
 *
 * Notify nm-openvpn-service of a config error from 'openvpn'.
 *
 */
static void send_config_error (DBusConnection *con, const char *item)
{
  DBusMessage		*message;

  g_return_if_fail (con != NULL);
  g_return_if_fail (item != NULL);

  if (!(message = dbus_message_new_method_call (NM_DBUS_SERVICE_OPENVPN, NM_DBUS_PATH_OPENVPN, NM_DBUS_INTERFACE_OPENVPN, "signalConfigError")))
    {
      nm_warning ("send_config_error(): Couldn't allocate the dbus message");
      return;
    }

  dbus_message_append_args (message, DBUS_TYPE_STRING, &item, DBUS_TYPE_INVALID);
  if (!dbus_connection_send (con, message, NULL))
    nm_warning ("send_config_error(): could not send dbus message");
  
  dbus_message_unref (message);
}


 /*
 * gpa_to_uint32arr
 *
 * Convert GPtrArray of uint32 to a uint32* array
 *
 */
static void
gpa_to_uint32arr (const GPtrArray *gpa,
		  guint32 **uia,
		  guint32  *uia_len)
{
  
  guint32		num_valid = 0, i = 0;
  struct in_addr	temp_addr;

  *uia = NULL;

  if ( gpa->len > 0 ) {
    /* Pass over the array first to determine how many valid entries there are */
    num_valid = 0;
    for (i = 0; i < gpa->len; ++i) {
      if (inet_aton ((char *)gpa->pdata[i], &temp_addr)) {
	num_valid++;
      }
    }
    
    /* Do the actual string->int conversion and assign to the array. */
    if (num_valid > 0) {
      *uia = g_new0 (guint32, num_valid);
      for (i = 0; i < gpa->len; ++i) {
	if (inet_aton ((char *)gpa->pdata[i], &temp_addr)) {
	  (*uia)[i] = temp_addr.s_addr;
	}
      }
    }
      
    *uia_len = num_valid;
  }
  if (*uia == NULL) {
    *uia     = g_malloc0 (sizeof (guint32));
    *uia_len = 1;
  }
}

static gboolean
ipstr_to_uint32 (const char *ip_str, guint32 *ip)
{
  struct in_addr	temp_addr;

  /* Convert IPv4 address arguments from strings into numbers */
  if (!inet_aton (ip_str, &temp_addr))
    return FALSE;
  *ip = temp_addr.s_addr;
  return TRUE;
}


/*
 * send_config_info
 *
 * Send IP config info to nm-openvpn-service
 *
 */
static gboolean
send_config_info (DBusConnection *con,
		  const char *str_vpn_gateway,
		  const char *str_tundev,
		  const char *str_ip4_address,
		  const char *str_ip4_ptpaddr,
		  const char *str_ip4_netmask,
		  const GPtrArray *gpa_ip4_dns,
		  const GPtrArray *gpa_ip4_nbns
		  )
{
  DBusMessage *	message;
  struct in_addr	temp_addr;
  guint32		uint_vpn_gateway = 0;
  guint32		uint_ip4_address = 0;
  guint32		uint_ip4_ptpaddr = 0;
  guint32		uint_ip4_netmask = 0xFFFFFFFF; /* Default mask of 255.255.255.255 */
  guint32 *	        uint_ip4_dns = NULL;
  guint32		uint_ip4_dns_len = 0;
  guint32 *	        uint_ip4_nbns = NULL;
  guint32		uint_ip4_nbns_len = 0;
  gboolean        success = FALSE;

  g_return_val_if_fail (con != NULL, FALSE);

  if (!(message = dbus_message_new_method_call (NM_DBUS_SERVICE_OPENVPN, NM_DBUS_PATH_OPENVPN, NM_DBUS_INTERFACE_OPENVPN, "signalIP4Config")))
    {
      nm_warning ("send_config_error(): Couldn't allocate the dbus message");
      return FALSE;
    }

  if (! ipstr_to_uint32 (str_vpn_gateway, &uint_vpn_gateway) ) {
    nm_warning ("nm-openvpn-service-openvpn-helper didn't receive a valid VPN Gateway from openvpn.");
    send_config_error (con, "VPN Gateway");
    goto out;
  }

  if (! ipstr_to_uint32 (str_ip4_address, &uint_ip4_address) ) {
    nm_warning ("nm-openvpn-service-openvpn-helper didn't receive a valid Internal IP4 Address from openvpn.");
    send_config_error (con, "IP4 Address");
    goto out;
  }

  if (str_ip4_ptpaddr && ! ipstr_to_uint32 (str_ip4_ptpaddr, &uint_ip4_ptpaddr) ) {
    nm_warning ("nm-openvpn-service-openvpn-helper didn't receive a valid PtP IP4 Address from openvpn.");
    send_config_error (con, "IP4 PtP Address");
    goto out;
  }

  if (strlen (str_ip4_netmask) > 0) {
    ipstr_to_uint32 (str_ip4_netmask, &uint_ip4_netmask);
  }

  gpa_to_uint32arr (gpa_ip4_dns, &uint_ip4_dns, &uint_ip4_dns_len);
  gpa_to_uint32arr (gpa_ip4_nbns, &uint_ip4_nbns, &uint_ip4_nbns_len);

  dbus_message_append_args (message, DBUS_TYPE_UINT32, &uint_vpn_gateway,
			    DBUS_TYPE_STRING, &str_tundev,
			    DBUS_TYPE_UINT32, &uint_ip4_address,
			    DBUS_TYPE_UINT32, &uint_ip4_ptpaddr,
			    DBUS_TYPE_UINT32, &uint_ip4_netmask,
			    DBUS_TYPE_ARRAY, DBUS_TYPE_UINT32, &uint_ip4_dns, uint_ip4_dns_len,
			    DBUS_TYPE_ARRAY, DBUS_TYPE_UINT32, &uint_ip4_nbns, uint_ip4_nbns_len,
			    DBUS_TYPE_INVALID);
  if (dbus_connection_send (con, message, NULL))
    success = TRUE;
  else
    nm_warning ("send_config_error(): could not send dbus message");
  
  dbus_message_unref (message);
  
  g_free (uint_ip4_dns);
  g_free (uint_ip4_nbns);

 out:
  return success;
}


/*
 * See the OpenVPN man page for available environment variables.
 *
 *
 */

#if 0	/* FIXME: Nothing uses this and it is static */
/** Prints all environment variables to /tmp/environ
 */
static void
print_env()
{
  FILE *f = fopen("/tmp/environ", "w");
  int env = 0;
  while ( __environ[env] != NULL ) {
    fprintf(f, "%s\n", __environ[env++]);
  }
  fclose(f);
}
#endif


/*
 * main
 *
 */
int main( int argc, char *argv[] )
{
  DBusConnection  *con;
  DBusError        error;
  char            *vpn_gateway = NULL;
  char            *tundev = NULL;
  char            *ip4_address = NULL;
  char            *ip4_ptp = NULL;
  char            *ip4_netmask = NULL;
  GPtrArray       *ip4_dns = NULL;
  GPtrArray       *ip4_nbns = NULL;
  
  char           **split = NULL;
  char           **item;

  char            *tmp;
  // max(length(envname)) = length("foreign_option_") + length(to_string(MAX_INT)) + 1;
  //                               = 15                     = 10 for 4 byte int
  //                                                    (which should be enough for quite some time)
  char             envname[26];
  int              i = 1;
  int              exit_code = 0;

  g_type_init ();
  if (!g_thread_supported ())
    g_thread_init (NULL);

  nm_logging_setup(); 	
 
  dbus_error_init (&error);
  con = dbus_bus_get (DBUS_BUS_SYSTEM, &error);
  if ((con == NULL) || dbus_error_is_set (&error))
    {
      nm_warning ("Could not get the system bus.  Make sure the message bus daemon is running?");
      cleanup_and_exit (1);
    }
  dbus_connection_set_exit_on_disconnect (con, FALSE);

  // print_env();

  vpn_gateway = getenv( "trusted_ip" );
  tundev      = getenv ("dev");
  ip4_ptp     = getenv("ifconfig_remote");
  ip4_address = getenv("ifconfig_local");
  ip4_netmask = getenv("route_netmask_1");
  
  ip4_dns     = g_ptr_array_new();
  ip4_nbns    = g_ptr_array_new();
  
  while (1) {
    sprintf(envname, "foreign_option_%i", i++);
    tmp = getenv( envname );
    
    if ( (tmp == NULL) || (strlen(tmp) == 0) ) {
      break;
    } else {
      
      if ((split = g_strsplit( tmp, " ", -1))) {
	int size = 0;
	for( item = split; *item; item++) {
		++size;
	}
	if ( size != 3 ) continue;
	
	if (strcmp( split[0], "dhcp-option") == 0) {
	  // Interesting, now check if DNS or NBNS/WINS
	  if (strcmp( split[1], "DNS") == 0) {
	    // DNS, push it!
	    g_ptr_array_add( ip4_dns, (gpointer) split[2] );
	  } else if (strcmp( split[1], "WINS") == 0) {
	    // WINS, push it!
	    g_ptr_array_add( ip4_nbns, (gpointer) split[2] );		  
	  }
	}
      }
    }      
  }

#if 0
	{
		FILE *file = fopen ("/tmp/vpnstuff", "w");
		fprintf (file, "VPNGATEWAY: '%s'\n", vpn_gateway);
		fprintf (file, "TUNDEV: '%s'\n", tundev);
		fprintf (file, "IP4_ADDRESS: '%s'\n", ip4_address);
		fprintf (file, "IP4_NETMASK: '%s'\n", ip4_netmask);
		fclose (file);
	}
#endif
  
  if (!vpn_gateway) {
    nm_warning ("nm-openvpn-service-openvpn-helper didn't receive a VPN Gateway from openvpn.");
    send_config_error (con, "VPN Gateway");
    exit (1);
  }
  if (!tundev || !g_utf8_validate (tundev, -1, NULL)) {
    nm_warning ("nm-openvpn-service-openvpn-helper didn't receive a Tunnel Device from openvpn, or the tunnel device was not valid UTF-8.");
    send_config_error (con, "Tunnel Device");
    cleanup_and_exit (1);
  }
  if (!ip4_address) {
    nm_warning ("nm-openvpn-service-openvpn-helper didn't receive an Internal IP4 Address from openvpn.");
    send_config_error (con, "IP4 Address");
    cleanup_and_exit (1);
  }

  if (!ip4_netmask) {
    ip4_netmask = g_strdup ("");
  }

  if (!send_config_info (con, vpn_gateway, tundev,
			 ip4_address, ip4_ptp, ip4_netmask,
			 ip4_dns, ip4_nbns)) {
    exit_code = 1;
  }
  
  g_strfreev( split );
  g_ptr_array_free( ip4_dns, TRUE );
  g_ptr_array_free( ip4_nbns, TRUE );
  
  cleanup_and_exit (exit_code);

  // Dummy return; cleanup_and_exit() takes care of exit()
  return 0;
}

