#include <glib.h>

#include <syslog.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <ctype.h>
#include <unistd.h>

#include "netconsole.h"
#include "netdumpclient.h"
#include "configuration.h"
#include "server.h"

#define USE_SAME_DIR_TIMEOUT 60

typedef struct {
  time_t time;
  char *path;
} CrashDir;

GHashTable *clients; /* Maps ip -> NetdumpClient */
GHashTable *dirs; /* Maps ip -> time + directory */
int master_socket;

#define BUFLEN 10000
#define IP(a,b,c,d) ((d) | ((c) << 8) | ((b) << 16) | ((a) << 24))  

void
remove_client (NetdumpClient *client)
{
  g_hash_table_remove (clients, client);
}

static const char *
get_dir_from_ip (guint32 ip, gboolean create_new)
{
  CrashDir *dir;
  time_t now;
  int res;

  now = time (NULL);

  dir = g_hash_table_lookup (dirs, GUINT_TO_POINTER (ip));

  if (dir && (now - dir->time) > USE_SAME_DIR_TIMEOUT && create_new)
    {
      g_hash_table_remove (dirs, GUINT_TO_POINTER(ip));
      g_free (dir->path);
      g_free (dir);
      dir = NULL;
    }

  if (dir == NULL)
    {
      struct tm *tm;
      char date[128];

      tm = localtime (&now);
      strftime (date, sizeof (date), "%Y-%m-%d-%H:%M", tm);
      dir = g_new (CrashDir, 1);
      dir->time = now;
      dir->path = g_strdup_printf ("%s/%d.%d.%d.%d-%s",
				   config.dumpdirprefix,
				   (ip >> 24) & 0xff,
				   (ip >> 16) & 0xff,
				   (ip >> 8) & 0xff,
				   (ip) & 0xff,
				   date);

      res = mkdir(dir->path, S_IRWXU);
      if (res)
	{
	  char *msg = g_strdup_printf ("Error making crashdump directory %s", dir->path);
	  syslog (LOG_ERR, msg);
	  g_free (msg);
	  g_free (dir->path);
	  g_free (dir);
	  return NULL;
	}
      
      g_hash_table_insert (dirs, GUINT_TO_POINTER(ip), dir);
    }

  return dir->path;
}

int
execute_script (const char *script,
		guint32     ip,
		const char *dir)
{
  struct stat sbuf;
  char *filename;
  char *cmdline;
  int res = -1;

  filename = g_strconcat (config.dumpdirprefix, "/scripts/",
			  script,
			  NULL);
  
  if (stat (filename, &sbuf) == 0)
    {
      cmdline = g_strdup_printf ("%s %d.%d.%d.%d '%s'",
				 filename,
				 (ip >> 24) & 0xff, (ip >> 16) & 0xff,
				 (ip >> 8) & 0xff, (ip >> 0) & 0xff,
				 (dir)?dir:"");
      res = system (cmdline);
      g_free (cmdline);
    }

  g_free (filename);
  return res;
}


guint
hex_value (gchar c)
{
  if (c >= 'A' && c <= 'F')
    return c - 'A' + 10;
  if (c >= 'a' && c <= 'f')
    return c - 'a' + 10;
  return c - '0';
}


static gboolean
load_magic (guint32 ip, guchar magic[MAGIC_SIZE])
{
  char *path;
  FILE *file;
  char buffer[1024];
  size_t size;
  int i;
  
  if (!config.secure)
    {
      memset(magic, 1, MAGIC_SIZE);
      return TRUE;
    }

  path = g_strdup_printf ("%s/magic/%d.%d.%d.%d",
			 config.dumpdirprefix,
			 (ip >> 24) & 0xff,
			 (ip >> 16) & 0xff,
			 (ip >> 8) & 0xff,
			 (ip) & 0xff);

  file = fopen (path, "r");

  g_free(path);

  if (file == NULL)
    return FALSE;

  size = fread(buffer, 1, sizeof(buffer), file);
  
  fclose (file);

  if (size < MAGIC_SIZE*2)
    return FALSE;
  
  for (i=0;i<MAGIC_SIZE;i++)
    {
      if (!isxdigit(buffer[2*i]) ||
	  !isxdigit(buffer[2*i+1]))
	return FALSE;
      magic[i] = (hex_value (buffer[2*i]) << 4) + hex_value(buffer[2*i+1]);
    }
  
  return TRUE;
}


static void
handle_new_client (guint32 ip, guchar *magic, reply_t *reply, char *msg)
{
  NetdumpClient *client;
  const char *dir;
  gboolean just_reboot;
  int res;

  dir = get_dir_from_ip (ip, 0);
  if (dir)
    {
      res = execute_script ("netdump-crash", ip, dir);

      if (res > 0)
	just_reboot = TRUE;
      else
	just_reboot = FALSE;
      
      if (!just_reboot &&
	  (g_hash_table_size (clients) > config.max_concurrent_dumps))
	{
	  char *msg = g_strdup_printf ("Too many concurrent netdumps, ignoring dump request from %d.%d.%d.%d\n",
				       (ip >> 24) & 0xff, (ip >> 16) & 0xff,
				       (ip >> 8) & 0xff, (ip >> 0) & 0xff);
	  syslog (LOG_WARNING, msg);
	  g_free (msg);
	  just_reboot = TRUE;
	}

      client = netdump_client_new (ip, dir, magic, just_reboot, reply, msg);
      g_hash_table_insert (clients, client, client);
    }
}

/*
 *  Here we use offset to tell if this is a fresh system boot.  The client
 *  will always start at 0 and increment from there.  If 0, we create a new
 *  directory for this log (and potential crash).  If non-zero, we use the
 *  existing directory in /var/crash if it exists.
 */
static void
write_log (guint32 ip, char *logline, int len, unsigned int offset)
{
  const char *dir;

  dir = get_dir_from_ip (ip, !offset);

  if (dir)
    {
      char *file;
      int fd;
      int res;
      
      file = g_strconcat (dir, "/log", NULL);
      fd = open (file, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR| S_IWUSR);
      g_free (file);
      if (fd != -1)
	{
	  res = write (fd, logline, len);
	  close (fd);
	}
    }
}

static gboolean
master_socket_packet (GIOChannel  *channel,
		      GIOCondition cond,
		      gpointer     callback_data)
{
  NetdumpClient *client, key;
  unsigned char buf[BUFLEN];
  int len;
  struct sockaddr_in addr;
  int addr_len = sizeof(addr);
  
  int fd = g_io_channel_unix_get_fd (channel);

  len = recvfrom (fd, buf, BUFLEN, 0, (struct sockaddr *)&addr, &addr_len);

  if (len < 0)
    {
      syslog (LOG_ERR, "Error receiving from master socket: recvfrom returned: %d", len);
      return TRUE;
    }
  
  key.ip = ntohl (addr.sin_addr.s_addr);
#if 0
  g_print ("got packet from %d.%d.%d.%d\n",
	   (key.ip >> 24) & 0xff,
	   (key.ip >> 16) & 0xff,
	   (key.ip >> 8) & 0xff,
	   (key.ip >> 0) & 0xff);
#endif
  
  client = g_hash_table_lookup (clients, &key);

  if (client)
    {
      if (client->process_packet)
	(client->process_packet) (client, &addr,
				  buf, len);
    }
  else
    {
      reply_t reply;
      guchar magic[MAGIC_SIZE];

      if (!load_magic(key.ip, magic))
        {
	  /* don't log anything, as an attacker could fill up the logs */
	  return TRUE;
        }

      if (len < HEADER_LEN || !VERSION_VERIFIED(buf[0]))
        {
	  syslog (LOG_WARNING, "Got strange packet from ip %d.%d.%d.%d\n",
		(key.ip >> 24) & 0xff,
           	(key.ip >> 16) & 0xff,
           	(key.ip >> 8) & 0xff,
           	(key.ip >> 0) & 0xff);
	  return TRUE;
	}

      /* first byte is the netconsole version number */
      memcpy ((char *)&reply, buf + 1, sizeof(reply_t));

      if (ntohl (reply.code) == REPLY_START_NETDUMP)
	{
	  handle_new_client (key.ip, magic, &reply, &buf[HEADER_LEN]);
	}
      else if (ntohl (reply.code) == REPLY_LOG)
	{
	  char *startup_message = "[...network console startup...]\n";

	  if ((len - HEADER_LEN) == strlen(startup_message) &&
	      strncmp (startup_message, buf + HEADER_LEN, strlen(startup_message)) == 0)
            execute_script ("netdump-start", key.ip, NULL);

	  write_log(key.ip, buf + HEADER_LEN, len - HEADER_LEN, reply.info);
	}
      else
	{
	  syslog (LOG_WARNING, "Got unexpected packet type %d from ip %d.%d.%d.%d\n", ntohl (reply.code),
		(key.ip >> 24) & 0xff,
                (key.ip >> 16) & 0xff,
                (key.ip >> 8) & 0xff,
                (key.ip >> 0) & 0xff); 
	  return TRUE;
	}
    }

  return TRUE;
}


static void
setup_pidfile(void) {
	if (config.pidfile) {
		char pid[32]; /* more than enough for an int... */
		int pidfd;

		sprintf(pid, "%d\n", getpid());
		pidfd = open(config.pidfile, O_RDWR|O_CREAT|O_TRUNC, 0644);
		write(pidfd, pid, strlen(pid));
		close(pidfd);
	}
}

static void
delete_pidfile(void) {
	if (config.pidfile) {
		unlink(config.pidfile);
	}
}

static void
cleanup_and_exit(int signal) {
	delete_pidfile();
	exit(0);
}

int
main (int argc, char *argv[])
{
  struct sockaddr_in saddr;
  char *str;
  GIOChannel *channel;
  GMainLoop *loop;
  struct sigaction sact;/* used to initialize the signal handler */

  config_init (argc, argv);

  openlog("netdump", LOG_PID, LOG_DAEMON);

  if (chdir (config.dumpdirprefix) == -1)
    {
      syslog (LOG_ERR, "can't cd to %s", config.dumpdirprefix);
      fprintf (stderr, "can't cd to %s\n", config.dumpdirprefix);
      exit (1);
    }
  
  if (config.daemon) {
    daemon(1, 0);
  }
  
  setup_pidfile(); /* Do after daemon() call */
  
  /* set the signal handler to quit cleanly. */
  sact.sa_handler = cleanup_and_exit;
  sigemptyset (&sact.sa_mask);
  sact.sa_flags = 0;
  
  sigaction(SIGINT, &sact, NULL);
  sigaction(SIGTERM, &sact, NULL);

  clients = g_hash_table_new (netdump_client_hash,
			      netdump_client_compare);
  
  dirs = g_hash_table_new (g_direct_hash,
			   g_direct_equal);

  
  master_socket = socket (PF_INET, SOCK_DGRAM,  IPPROTO_IP);
  if (master_socket < 0)
    {
      syslog (LOG_ERR, "Couldn't allocate master socket");
      exit (1);
    }

  memset (&saddr, 0, sizeof (saddr));
  saddr.sin_family = PF_INET;
  saddr.sin_port = htons (config.port);
  saddr.sin_addr.s_addr = INADDR_ANY;
  
  if (bind (master_socket,
	    (struct sockaddr *) &saddr,
	    sizeof (saddr)) < 0)
    {
      str = g_strdup_printf ("Couldn't bind master socket to port %d", config.port);
      syslog (LOG_ERR, str);
      g_free (str);
      exit (1);
    }

  channel = g_io_channel_unix_new (master_socket);
  g_io_add_watch (channel, G_IO_IN | G_IO_HUP, master_socket_packet, NULL);
  g_io_channel_unref (channel);

  loop = g_main_new (TRUE);
  g_main_run (loop);

  if (config.pidfile != NULL)
    free(config.pidfile);
  if (config.dumpdirprefix != NULL)
    free(config.dumpdirprefix);

  return 0;
}

/*
 *  Verify that a new client's netdump_magic number matches 
 *  its associated /var/crash/magic/<ip> value.
 */
gboolean 
verify_magic (NetdumpClient *client)
{
	int i;
	u64 value;
	unsigned char check_magic[MAGIC_SIZE];

  	if (!config.secure)
		return TRUE;

	value = client->netdump_magic;

  	for (i = MAGIC_SIZE-1; i >= 0; i--) {
        	check_magic[i] = value & 0xffULL;
        	value >>= 8;
  	}

  	for (i = 0; i < MAGIC_SIZE; i++) {
        	if (check_magic[i] != client->magic_value[i]) {
      			syslog (LOG_ERR, "Got invalid magic number (%llx) from client %s", 
				(long long)client->netdump_magic, client->ip_addr);
                	return FALSE; 
		}
	}

	return TRUE;
}

/*
 *  Allow a back-door to avoid the filesystem space check.
 */
gboolean
perform_space_check(void)
{
	return config.space_check;	
}


syntax highlighted by Code2HTML, v. 0.9.1