/* Copyright (C) 2000-1 drscholl@users.sourceforge.net
 * This is free software distributed under the terms of the
 * GNU Public License.  See the file COPYING for details.
 *
 * $Id: class.c,v 1.6 2002/08/19 04:04:39 khaytsus Exp $
 *
 * Based on bans.c in part.
 * oracle ip database idea taken from ircd
 * is_address() taken from hybrid ircd and modified to return a complete
 * ip.
 * All this mess put together by Colten Edwards (q)
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>
#include <sys/types.h>
#ifndef WIN32
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#endif
#include "opennap.h"
#include "hashlist.h"
#include "debug.h"
#ifdef WIN32
#include <io.h>
#endif

#define F_SYNC (1<<0)

typedef struct
{
    char   *name;
    LIST   *list;
    unsigned int flags;
    unsigned int oracle[256];
}
acl_t;

static acl_t ilines;
static acl_t dlines;
static acl_t elines;
static acl_t limits;

#define check_oracle(acl,ip)	(((acl)->oracle[(ip) & 0xff] & (ip)) == (ip))

int
check_class (CONNECTION * con, ip_info_t * info)
{
    LIST   *list;
    CLASS  *class;
    int     count, boot_em = 0;

    count = info->users;

    if (!count)
	return count;

    if (Max_Clones > 0 && count >= Max_Clones)
	boot_em = count;

    if (check_oracle (&limits, con->ip))
    {
	for (list = limits.list; list; list = list->next)
	{
	    class = list->data;
	    if ((con->ip & class->mask) == (class->target & class->mask))
	    {
		if (count >= class->limit) {
            log_message_level (LOG_LEVEL_LOGIN, "ACL connection limit (%d) exceeded by %s: %d",
                            class->limit, my_ntoa (BSWAP32 (info->ip)), count);
		    return count;
        } else
		    return 0;
	    }
	}
    }
    return boot_em;
}

/* determine whether a connection from this ip address is allowed by the acls
 * defined.
 */
int
acl_connection_allowed (unsigned int ip)
{
    if (ilines.list)
    {
	/* i:lines exist, don't allow any connections unless the ip
	 * matches
	 */
	if (!check_oracle (&ilines, ip))
	    return 0;
    }

    /* check for a d:line */
    if (check_oracle (&dlines, ip))
    {
	/* check for an e:line */
	if (check_oracle (&elines, ip))
	    return 1;

	return 0;
    }

    return 1;
}

static int
generic_acl_save (acl_t * acl)
{
    int     fd;
    LIST   *list;
    access_t *b;
    char    path[_POSIX_PATH_MAX];
    char    maskstr[sizeof ("/xxx.xxx.xxx.xxx")];
    char    outbuf[1024];
    snprintf (path, sizeof (path), "%s/%s", Config_Dir, acl->name);

    if (acl->list == 0)
    {
	unlink (path);
	return 0;
    }

#ifdef WIN32
#define LE "\r\n"
#else
#define LE "\n"
#endif

    if ((fd = open (path, O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR)) == -1)
	return -1;
    snprintf (outbuf, sizeof (outbuf),
	     "# DO NOT EDIT THIS FILE! - automatically generated by opennap%s",
	     LE);
    fake_fputs (outbuf,fd);
    snprintf (outbuf, sizeof (outbuf), "# Hi Mom, We got %s!%s", acl->name, LE);
    fake_fputs(outbuf, fd);
    for (list = acl->list; list; list = list->next)
    {
	struct in_addr in;

	b = list->data;
	maskstr[0] = 0;
	if (b->mask != 0xffffffff)
	{
	    in.s_addr = BSWAP32 (b->mask);
	    snprintf (maskstr, sizeof (maskstr), "/%s", inet_ntoa (in));
	}
	in.s_addr = BSWAP32 (b->ip);
	snprintf (outbuf, sizeof (outbuf), "%s%s %3d%s", inet_ntoa (in), maskstr, b->count, LE);
  fake_fputs (outbuf,fd);
    }
    if (close (fd))
    {
	logerr ("generic_acl_save", "close");
	return -1;
    }
    return 0;
}

void
acl_save (void)
{
    generic_acl_save (&limits);
    generic_acl_save (&ilines);
    generic_acl_save (&dlines);
    generic_acl_save (&elines);
}

static int
compare_ipmask (unsigned int a, unsigned int amask, unsigned int b,
		unsigned int bmask)
{
    int     r = -2;

    /* always use the smallest mask to compare */
    unsigned int usemask = (amask > bmask) ? bmask : amask;

    /*
       struct in_addr in;
       in.s_addr = ntohl(a);
       printf("comparing %s(%u) and", inet_ntoa(in), a);
       in.s_addr = ntohl(b);
       printf(" %s(%u) = ",inet_ntoa(in), b);
       in.s_addr = ntohl(usemask);
       printf(" using mask=%s  ", inet_ntoa(in));
     */

    if ((a & usemask) == (b & usemask))
    {
	/*printf("masks are equal  "); */
	/* the larger mask (more specific) should sort first */
	if (bmask != amask)
	    r = (amask > bmask) ? 1 : -1;
    }

    /* otherwise sort by ip */
    if (r == -2)
    {
	if (b == a)
	    r = 0;
	else
	    r = (b > a) ? 1 : -1;
    }

    /*
       printf("%d\n", r);
     */

    return r;
}

/** acl_insert
 * @param acl	access list to modify
 * @param ipstr	ip/mask string
 * @param nstr	limit to place on class
 * @returns -1 on error, 0 if new acl was added, 1 if existing acl was changed
 */
static int
acl_insert (acl_t * acl, char *ipstr, int count)
{
    unsigned int ip, mask;
    LIST  **access_list = &acl->list;
    access_t *acc;
    LIST   *list;
    int     r;

    if (!is_address (ipstr, &ip, &mask))
	return -1;

    for (; *access_list; access_list = &(*access_list)->next)
    {
	acc = (*access_list)->data;
	r = compare_ipmask (htonl (BSWAP32 (ip)), htonl (BSWAP32 (mask)),
			    htonl (BSWAP32 (acc->ip)),
			    htonl (BSWAP32 (acc->mask)));
	if (r == 0)
	{
	    acc->count = count;
	    return 1;
	}
	else if (r > 0)
	    break;
    }
    acc = CALLOC (1, sizeof (access_t));
    if (!acc)
    {
	OUTOFMEMORY ("acl_insert");
	return -1;
    }
    acc->ip = ip;
    acc->mask = mask;
    acc->count = count;
    list = CALLOC (1, sizeof (LIST));
    if (!list)
    {
	FREE (acc);
	OUTOFMEMORY ("acl_insert");
	return -1;
    }
    list->next = *access_list;
    *access_list = list;
    list->data = acc;
    acl->oracle[ip & 0xff] |= (ip | ~mask);
    return 0;
}

static int
generic_acl_load (acl_t * acl)
{
    int     fd;
    int     ac;
    char   *av[2], *ptr, path[_POSIX_PATH_MAX];
    int     line = 0;

    snprintf (path, sizeof (path), "%s/%s", Config_Dir, acl->name);

    if ((fd = open (path, O_RDONLY))!=-1)
    {
	if (errno != ENOENT)
	    logerr ("generic_acl_load", path);
	return -1;
    }
    while (fake_fgets (Buf, sizeof (Buf) - 1, fd))
    {
	line++;
	ptr = Buf;
	while (ISSPACE (*ptr))
	    ptr++;
	if (*ptr == '#' || *ptr == 0)
	    continue;
	ac = split_line (av, FIELDS (av), Buf);
	if (ac < 1)
	    continue;

	if (acl_insert (acl, av[0], (ac > 1) ? atoi (av[1]) : 0) == -1)
	{
	    log_message_level (LOG_LEVEL_ERROR, "generic_acl_load:%s:%d:error parsing line:%s %s",
		 path, line, av[0], (ac > 1) ? av[1] : "");
	}
    }
    close (fd);
    return 0;
}

static acl_t *
get_acl (int tag)
{
    if (tag == MSG_CLIENT_DLINE_ADD || tag == MSG_CLIENT_DLINE_DEL
	|| tag == MSG_CLIENT_DLINE_LIST)
	return &dlines;
    if (tag == MSG_CLIENT_ELINE_ADD || tag == MSG_CLIENT_ELINE_DEL
	|| tag == MSG_CLIENT_ELINE_LIST)
	return &elines;
    if (tag == MSG_CLIENT_ILINE_ADD || tag == MSG_CLIENT_ILINE_DEL
	|| tag == MSG_CLIENT_ILINE_LIST)
	return &ilines;
    if (tag == MSG_CLIENT_CLASS_ADD || tag == MSG_CLIENT_CLASS_DEL
	|| tag == MSG_CLIENT_CLASS_LIST)
	return &limits;
    return 0;
}

static void
generic_acl_init (acl_t * acl, const char *name, int flags)
{
    memset (acl, 0, sizeof (acl));
    acl->name = STRDUP (name);
    acl->flags = flags;
}

void
acl_init (void)
{
    generic_acl_init (&limits, "limit", F_SYNC);
    generic_acl_load (&limits);
    generic_acl_init (&ilines, "iline", 0);
    generic_acl_load (&ilines);
    generic_acl_init (&dlines, "dline", 0);
    generic_acl_load (&dlines);
    generic_acl_init (&elines, "eline", 0);
    generic_acl_load (&elines);
}

/* ??? [:sender] <host>[/<mask>] [arg]
 */
HANDLER (generic_acl_add)
{
    int     ac;
    char   *av[2];
    char   *sender_name;
    USER   *sender;
    acl_t  *acl;
    int     count, modified = 0;

    (void) len;
    if (pop_user_server (con, tag, &pkt, &sender_name, &sender))
	return;
    if (sender && sender->level < LEVEL_MODERATOR)
	return;

    ac = split_line (av, FIELDS (av), pkt);
    if (ac < 1)
	return;

    acl = get_acl (tag);
    count = (ac > 1) ? atoi (av[1]) : 0;
    modified = acl_insert (acl, av[0], count);
    if (modified == -1)
    {
	if (ISUSER (con))
	    send_cmd (con, MSG_SERVER_NOSUCH, "unable to parse ip/mask");
	return;
    }
    notify_mods (CHANGELOG_MODE, "%s%s %s %s on %s (%d)",
		 sender ? "" : "Server ",
		 sender_name,
		 modified ? "modified" : "added", acl->name, av[0], count);

    if (acl->flags & F_SYNC)
	pass_message_args (con, tag, ":%s %s %d", sender_name, av[0], count);

    generic_acl_save (acl);
}

/* ??? [:sender] <host>[/<mask>] */
HANDLER (generic_acl_del)
{
    char   *host;
    unsigned int ip, mask;
    LIST  **access_list;
    LIST   *tmp;
    char   *sender_name;
    USER   *sender;
    access_t *acc;
    acl_t  *acl;

    (void) len;
    if (pop_user_server (con, tag, &pkt, &sender_name, &sender))
	return;
    if (sender && sender->level < LEVEL_MODERATOR)
	return;
    host = next_arg (&pkt);
    if (!host)
	return;
    if (!is_address (host, &ip, &mask))
	return;
    acl = get_acl (tag);
    access_list = &acl->list;

    if (!check_oracle (acl, ip))
    {
	if (ISUSER (con))
	    send_cmd (con, MSG_SERVER_NOSUCH, "no matching ip addresses");
	return;
    }

    for (; *access_list; access_list = &(*access_list)->next)
    {
	acc = (*access_list)->data;
	if (mask == acc->mask && (ip & mask) == (acc->ip & acc->mask))
	{
	    tmp = *access_list;
	    *access_list = (*access_list)->next;
	    FREE (tmp);
	    break;
	}
    }

    /* rebuild the oracle */
    memset (acl->oracle, 0, sizeof (int) * 256);

    for (tmp = acl->list; tmp; tmp = tmp->next)
    {
	acc = tmp->data;
	acl->oracle[acc->ip & 0xff] |= (acc->ip | ~acc->mask);
    }

    notify_mods (CHANGELOG_MODE, "%s%s removed %s on %s",
		 sender ? "" : "Server ", sender_name, acl->name, host);

    if (acl->flags & F_SYNC)
	pass_message_args (con, tag, ":%s %s", sender_name, host);

    generic_acl_save (acl);
}

/* ??? */
HANDLER (generic_acl_list)
{
    access_t *acc;
    acl_t  *acl;
    char    maskstr[sizeof ("/xxx.xxx.xxx.xxx")];
    struct in_addr in;
    LIST   *list;

    (void) len;
    (void) pkt;
    CHECK_USER_CLASS ("generic_acl_list");
    if (con->user->level < LEVEL_MODERATOR)
	return;
    acl = get_acl (tag);
    for (list = acl->list; list; list = list->next)
    {
	acc = list->data;

	maskstr[0] = 0;
	if (acc->mask != 0xffffffff)
	{
	    in.s_addr = BSWAP32 (acc->mask);
	    snprintf (maskstr, sizeof (maskstr), "/%s", inet_ntoa (in));
	}

	in.s_addr = BSWAP32 (acc->ip);
	send_cmd (con, tag, "%s%s %d", inet_ntoa (in), maskstr, acc->count);
    }
    send_cmd (con, tag, "");
}

static void
generic_acl_sync (CONNECTION * con, int tag, acl_t * acl)
{
    LIST   *list;
    access_t *c;
    struct in_addr in;
    char    maskstr[sizeof ("/xxx.xxx.xxx.xxx")];

    ASSERT (validate_connection (con));
    for (list = acl->list; list; list = list->next)
    {
	c = list->data;
	maskstr[0] = 0;
	if (c->mask != 0xffffffff)
	{
	    in.s_addr = BSWAP32 (c->mask);
	    snprintf (maskstr, sizeof (maskstr), "/%s", inet_ntoa (in));
	}
	in.s_addr = BSWAP32 (c->ip);
	send_cmd (con, tag, ":%s %s%s %d", Server_Name,
		  inet_ntoa (in), maskstr, c->count);
    }
}

void
acl_sync (CONNECTION * con)
{
    generic_acl_sync (con, MSG_CLIENT_CLASS_ADD, &limits);
}

static void
acl_free (acl_t * acl)
{
    list_free (acl->list, free_pointer);
    FREE (acl->name);
}

void
acl_destroy (void)
{
    acl_free (&limits);
    acl_free (&dlines);
    acl_free (&ilines);
    acl_free (&elines);
}
