/*-
 * Copyright (c) 2004 Free (Olivier Beyssac)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <syslog.h>
#include <string.h>
#include <errno.h>
#include "netlist.h"
#include "utils.h"

#define ACL_MAX_LINE       80
#define WHITELIST_MAX_LINE 80


#define trim_buffer(buf) \
  do {                                                       \
    size_t i;                                                \
    for (i = strlen(buf) - 1;                                \
	 i > 0 && (buf[i] == '\n' || buf[i] == '\r'          \
		   || buf[i] == '\t' || buf[i] == ' '); i--) \
      buf[i] = '\0';                                         \
  } while(0)


static unsigned int bitmasks[] = { 0x00000000, 0x80000000,
				   0xc0000000, 0xe0000000,
				   0xf0000000, 0xf8000000,
				   0xfc000000, 0xfe000000,
				   0xff000000, 0xff800000,
				   0xffc00000, 0xffe00000,
				   0xfff00000, 0xfff80000,
				   0xfffc0000, 0xfffe0000,
				   0xffff0000, 0xffff8000,
				   0xffffc000, 0xffffe000,
				   0xfffff000, 0xfffff800,
				   0xfffffc00, 0xfffffe00,
				   0xffffff00, 0xffffff80,
				   0xffffffc0, 0xffffffe0,
				   0xfffffff0, 0xfffffff8,
				   0xfffffffc, 0xfffffffe,
				   0xffffffff };


struct netlist
{
  size_t size;     /* netlist size */
  in_addr_t *net;  /* Networks list */
  in_addr_t *mask; /* Masks list */
  int *mode;       /* Permissions list */
};


/* Return a newly allocated acl */
extern netlist netlist_init(void)
{
  netlist tmp;

  tmp = calloc(sizeof(struct netlist), 1);
  
  return tmp;
}


/* Free a netlist */
extern void netlist_free(const netlist nl)
{
  nl->size = 0;
  free(nl->net);
  free(nl->mask);
  free(nl->mode);
}


/* Search for an IP in the netlist.  Return the mode if found, -1 otherwise.
   If not NULL, parameter index is modified to get position in the list */
static int netlist_search(const netlist nl, const in_addr_t ip, int *index)
{
  int i, j;

  if (nl->size == 0)
    return -1;

  for (i = 0, j = nl->size - 1;
       i < j && nl->net[i] != ip && nl->net[j] != ip;
       i++, j--);

  if (nl->net[i] == ip) {
    if (index != NULL)
      *index = i;
    return nl->mode[i];
  }
  
  if (nl->net[j] == ip) {
    if (index != NULL)
      *index = j;
    return nl->mode[j];
  }
  
  return -1;
}


/* Add or update IP/mask in the netlist */
static int netlist_add(const netlist nl, const in_addr_t ip,
		       const in_addr_t mask, const int mode)
{
  int index;
  
  if (netlist_search(nl, ip, &index) != -1) {
    nl->mode[index] = mode;
    return 1;
  }

  nl->size++;
  nl->net = realloc(nl->net, sizeof(in_addr_t) * nl->size);
  nl->mask = realloc(nl->mask, sizeof(int) * nl->size);
  nl->mode = realloc(nl->mode, sizeof(int) * nl->size);
  nl->net[nl->size - 1] = ip;
  nl->mask[nl->size - 1] = mask;
  nl->mode[nl->size - 1] = mode;
  
  return 1;
}


/* Convert the string-mode to an int-mode (ignoring whitespaces and tabs) */
static int acl_mode_str2int(char *str_mode)
{
  int mode;
  char *p, *q;
  char svc;
  size_t i, len, svl;

  /* Trim the beginning */
  for (p = str_mode; *p && (*p == ' ' || *p == '\t'); p++);

  /* Trim the end */
  q = p;
  len = strlen(q);
  for (i = len - 1; i > 0 && (q[i] == ' ' || q[i] == '\t'); i--);

  /* Remember how to restore the buffer to its initial state */
  savebufpos(svc, svl, q, i+1);

  if (strcmp(p, "submit") == 0)
    mode = ACL_M_SUBMIT;
  else if (strcmp(p, "query") == 0)
    mode = ACL_M_QUERY;
  else if (strcmp(p, "insert") == 0)
    mode = ACL_M_INSERT;
  else if (strcmp(p, "decrement") == 0)
    mode = ACL_M_DECR;
  else if (strcmp(p, "all") == 0)
    mode = ACL_M_ALL;
  else {
    restorebuf(svc, svl, q);
    syslog(LOG_ERR, "invalid mode in ACL file: %s", str_mode);
    return -1;
  }

  restorebuf(svc, svl, q);

  return mode;
}


/* Parse buf and fill IP/Mask.  Return 1 if successful, 0 otherwise */
static int netlist_fill_ip_mask(char *buf, in_addr_t *ip, in_addr_t *mask)
{
  char *b, *p;
  char svc;
  size_t svl;
  struct in_addr inp;
  int tmpmask;
  
  *ip = 0;
  *mask = 0;

  /* Ignore leading whitespaces and tabs */
  for (b = buf; *b != '\0' && (*b == ' ' || *b == '\t'); b++);

  if ((p = strchr(b, '/')) == NULL) {
    *mask = bitmasks[32];
    
    if (inet_aton(b, &inp) == 0) {
      syslog(LOG_ERR, "invalid IP address %s", buf);
      return 0;
    }
    *ip = ntohl(inp.s_addr);
    return 1;
  }

  /* Remember how to unbreak the buffer */
  savebufpos(svc, svl, b, strlen(buf) - strlen(p));

  p++;
  if (!inet_aton(b, &inp)) {
    syslog(LOG_ERR, "invalid IP address %s", buf);
    return 0;
  }
  *ip = ntohl(inp.s_addr);

  if ((tmpmask = xstrtol(p)) == -1) {
    if (!inet_aton(p, &inp)) {
      syslog(LOG_ERR, "invalid netmask %s", b);
      return 0;
    } else {
      *mask = ntohl(inp.s_addr);
    }
  } else
    *mask = bitmasks[tmpmask];

  *ip &= *mask;

  restorebuf(svc, svl, b);
  
  return 1;
}
  

/* Load ACL from file. Return 1 on success, 0 if file was not found, -1
   for all other errors */ 
extern int netlist_acl_getfromfile(const netlist nl, const char *filename)
{
  FILE *f;
  char buf[ACL_MAX_LINE];
  char *str_mode, *cur_mode;
  int mode, tmp_mode;
  in_addr_t ip, mask;
  
  if ((f = fopen(filename, "r")) == NULL) {
    if (errno == ENOENT)
      return 0;
    syslog(LOG_ERR, "fopen %s: %s", filename, strerror(errno));
    return -1;
  }
  
  while (fgets(buf, sizeof(buf), f)) {
    size_t len;
    
    len = strlen(buf);
    if ((len >= 1 && (buf[0] == '#' || buf[0] == '\n'))
	|| (len >= 2 && buf[0] == '\r' && buf[1] == '\n'))
      continue;
    trim_buffer(buf);
    
    if ((str_mode = strchr(buf, ':')) == NULL)
      return -1;
    
    *str_mode = '\0';
    if (!netlist_fill_ip_mask(buf, &ip, &mask)) {
      fclose(f);
      return -1;
    }
    
    str_mode++;
    mode = 0;
    /* Parse mode list */
    while ((cur_mode = strchr(str_mode, ','))) {
      *cur_mode = '\0';
      cur_mode++;
      if ((tmp_mode = acl_mode_str2int(str_mode)) == -1) {
	fclose(f);
	return -1;
      } else 
	mode |= tmp_mode;
      
      str_mode = cur_mode;
    }
    if ((tmp_mode = acl_mode_str2int(str_mode)) == -1) {
      fclose(f);
      return -1;
    } else 
      mode |= tmp_mode;

    netlist_add(nl, ip, mask, mode);
  }
  
  fclose(f);
  
  return 1;
}


/* Load whitelist from file. Return 1 on success, 0 if file was not found, -1
   for all other errors */ 
extern int netlist_whitelist_getfromfile(const netlist nl,
					 const char *filename)
{
  FILE *f;
  char buf[WHITELIST_MAX_LINE];
  in_addr_t ip, mask;
  
  if ((f = fopen(filename, "r")) == NULL) {
    if (errno == ENOENT)
      return 0;
    syslog(LOG_ERR, "fopen %s: %s", filename, strerror(errno));
    return -1;
  }

  while (fgets(buf, sizeof(buf), f)) {
    size_t len;

    len = strlen(buf);
    if ((len >= 1 && (buf[0] == '#' || buf[0] == '\n'))
	|| (len >= 2 && buf[0] == '\r' && buf[1] == '\n'))
      continue;
    trim_buffer(buf);

    if (!netlist_fill_ip_mask(buf, &ip, &mask)) {
      fclose(f);
      return -1;
    }
    
    netlist_add(nl, ip, mask, 1);
  }
  
  fclose(f);
  
  return 1;
}
  

/* Return allowed actions for IP */
extern int netlist_getmode(const netlist nl, const in_addr_t ip)
{
  size_t i;

  if (nl->size == 0)
    return ACL_M_ALL;

  for (i = 0; i < nl->size; i++)
    if ((ip & nl->mask[i]) == nl->net[i])
      return nl->mode[i];
  
  return 0;
}
