/* 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.18 2001/02/15 08:39:45 drscholl 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)
 */

/* Modified 30/05/01 : Added include "extrasocket.h" with inlines for a few
      for Amiga port : bsdsocket.library functions not emulated by ixemul

                       Added define for htonl(), which is not defined in the
                       Amiga GCC 2.7.0 includes
*/

#define htonl(x) (x)

#include <ctype.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "opennap.h"
#include "hashlist.h"
#include "debug.h"

#include "extrasocket.h"

#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)
                    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)
{
    FILE   *fp;
    LIST   *list;
    access_t *b;
    char    path[_POSIX_PATH_MAX];
    char    maskstr[sizeof ("/xxx.xxx.xxx.xxx")];

    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 ((fp = fopen (path, "w")) == 0)
        return -1;
    fprintf (fp,
             "# DO NOT EDIT THIS FILE! - automatically generated by opennap%s",
             LE);
    fprintf (fp, "# Hi Mom, We got %s!%s", acl->name, LE);
    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);
        fprintf (fp, "%s%s %3d%s", inet_ntoa (in), maskstr, b->count, LE);
    }
    if (fclose (fp))
    {
        logerr ("generic_acl_save", "fclose");
        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)
{
    FILE   *fp;
    int     ac;
    char   *av[2], *ptr, path[_POSIX_PATH_MAX];
    int     line = 0;

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

    if (!(fp = fopen (path, "r")))
    {
        if (errno != ENOENT)
            logerr ("generic_acl_load", path);
        return -1;
    }
    while (fgets (Buf, sizeof (Buf) - 1, fp))
    {
        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 ("generic_acl_load:%s:%d:error parsing line:%s %s",
                 path, line, av[0], (ac > 1) ? av[1] : "");
        }
    }
    fclose (fp);
    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);
}
