/*
 *   klines.c - Kline interface and storage
 *   Copyright (C) 2005 Trevor Talbot and
 *                      the DALnet coding team
 *
 *   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 1, 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/* $Id: klines.c,v 1.1 2005/09/04 05:25:00 sheik Exp $ */

/*
 * This is a simple K-Line journal implementation.  When a K-Line with a
 * duration greater than KLINE_MIN_STORE_TIME is added, it is written to the
 * journal file (.klines):
 *   + expireTS usermask hostmask reason
 * 
 * When a K-Line is manually removed, it also results in a journal entry:
 *   - usermask hostmask
 * 
 * This allows K-Lines to be saved across restarts and rehashes.
 *
 * To keep the journal from getting larger than it needs to be, it is
 * periodically compacted: all active K-Lines are dumped into a new file, which
 * then replaces the active journal.  This is done on startup as well as every
 * KLINE_STORE_COMPACT_THRESH journal entries.
 */
 

#include "struct.h"

#include "common.h"

#include "sys.h"

#include "h.h"


#include <fcntl.h>


#include "userban.h"

#include "numeric.h"


static int journal = -1;
static char journalfilename[512];
static int journalcount;

void klinestore_add(struct userBan *);
void klinestore_remove(struct userBan *);

/* ircd.c */
extern int forked;

/* s_misc.c */
extern char *smalldate(time_t);


/*
 * m_kline
 * Add a local user@host ban.
 *
 *    parv[0] = sender
 *    parv[1] = duration (optional)
 *    parv[2] = nick or user@host mask
 *    parv[3] = reason (optional)
 */
int
m_kline(aClient *cptr, aClient *sptr, int parc, char *parv[])
{
    char rbuf[512];
    char *target;
    char *user;
    char *host;
    char *reason = "<no reason>";
    int tkminutes = DEFAULT_KLINE_TIME;
    int tkseconds;
    long lval;
    struct userBan *ban;
    struct userBan *existing;

    if (!OPCanKline(sptr))
    {
        sendto_one(sptr, err_str(ERR_NOPRIVILEGES), me.name, parv[0]);
        return 0;
    }

    if (parc < 2)
    {
        sendto_one(sptr, err_str(ERR_NEEDMOREPARAMS), me.name, parv[0],
                   "KLINE");
        return 0;
    }

    lval = strtol(parv[1], &target, 10);
    if (*target != 0)
    {
        target = parv[1];
        if (parc > 2)
            reason = parv[2];
    }
    else
    {
        /* valid expiration time */
        tkminutes = lval;

        if (parc < 3)
        {
            sendto_one(sptr, err_str(ERR_NEEDMOREPARAMS), me.name, parv[0],
                       "KLINE");
            return 0;
        }

        target = parv[2];

        if (parc > 3)
            reason = parv[3];
    }

    /* negative times, or times greater than a year, are permanent */
    if (tkminutes < 0 || tkminutes > (365 * 24 * 60))
        tkminutes = 0;
    tkseconds = tkminutes * 60;

    if ((host = strchr(target, '@')))
    {
        *host++ = 0;
        user = target;
    }
    else
    {
        user = "*";
        host = target;
    }

    if (!match(user, "akjhfkahfasfjd") &&
        !match(host, "ldksjfl.kss...kdjfd.jfklsjf"))
    {
        sendto_one(sptr, ":%s NOTICE %s :KLINE: %s@%s mask is too wide",
                   me.name, parv[0], user, host);
        return 0;
    }

    /*
     * XXX: nick target support to be re-added
     */

    if (!(ban = make_hostbased_ban(user, host)))
    {
        sendto_one(sptr, ":%s NOTICE %s :KLINE: invalid ban mask %s@%s",
                   me.name, parv[0], user, host);
        return 0;
    }

    ban->flags |= UBAN_LOCAL;

    /* only looks for duplicate klines, not akills */
    if ((existing = find_userban_exact(ban, UBAN_LOCAL)))
    {
        sendto_one(sptr, ":%s NOTICE %s :KLINE: %s@%s is already %s: %s",
                   me.name, parv[0], user, host, LOCAL_BANNED_NAME,
                   existing->reason ? existing->reason : "<no reason>");
        userban_free(ban);
        return 0;
    }

    if (user_match_ban(sptr, ban))
    {
        sendto_one(sptr, ":%s NOTICE %s :KLINE: %s@%s matches you, rejected",
                   me.name, parv[0], user, host);
        userban_free(ban);
        return 0;
    }

    ircsnprintf(rbuf, sizeof(rbuf), "%s (%s)", reason, smalldate(0));
    ban->reason = MyMalloc(strlen(rbuf) + 1);
    strcpy(ban->reason, rbuf);

    if (tkseconds)
    {
        ban->flags |= UBAN_TEMPORARY;
        ban->timeset = NOW;
        ban->duration = tkseconds;
    }

    add_hostbased_userban(ban);

    if (!tkminutes || tkminutes >= KLINE_MIN_STORE_TIME)
        klinestore_add(ban);

    userban_sweep(ban);

    host = get_userban_host(ban, rbuf, sizeof(rbuf));

    if (tkminutes)
        sendto_realops("%s added temporary %d min. "LOCAL_BAN_NAME" for"
                       " [%s@%s] [%s]", parv[0], tkminutes, user, host,
                       reason);
    else
        sendto_realops("%s added "LOCAL_BAN_NAME" for [%s@%s] [%s]", parv[0],
                       user, host, reason);

    return 0;
}

/*
 * m_unkline
 * Remove a local user@host ban.
 *
 *     parv[0] = sender
 *     parv[1] = user@host mask
 */
int m_unkline(aClient *cptr, aClient *sptr, int parc, char *parv[])
{
    char hbuf[512];
    char *user;
    char *host;
    struct userBan *ban;
    struct userBan *existing;

    if (!OPCanUnKline(sptr))
    {
        sendto_one(sptr, err_str(ERR_NOPRIVILEGES), me.name, parv[0]);
        return 0;
    }

    if (parc < 2)
    {
        sendto_one(sptr, err_str(ERR_NEEDMOREPARAMS), me.name, parv[0],
                   "UNKLINE");
        return 0;
    }

    if ((host = strchr(parv[1], '@')))
    {
        *host++ = 0;
        user = parv[1];
    }
    else
    {
        user = "*";
        host = parv[1];
    }

    if (!(ban = make_hostbased_ban(user, host)))
    {
        sendto_one(sptr, ":%s NOTICE %s :UNKLINE: No such ban %s@%s", me.name,
                   parv[0], user, host);
        return 0;
    }

    ban->flags |= UBAN_LOCAL;
    existing = find_userban_exact(ban, UBAN_LOCAL);
    host = get_userban_host(ban, hbuf, sizeof(hbuf));
    userban_free(ban);

    if (!existing)
    {
        sendto_one(sptr, ":%s NOTICE %s :UNKINE: No such ban %s@%s", me.name,
                   parv[0], user, host);
        return 0;
    }

    if (existing->flags & UBAN_CONF)
    {
        sendto_one(sptr, ":%s NOTICE %s :UNKLINE: %s@%s is specified in the"
                   " configuration file and cannot be removed online", me.name,
                   parv[0], user, host);
        return 0;
    }

    remove_userban(existing);
    klinestore_remove(existing);
    userban_free(existing);

    sendto_ops("%s has removed the K-Line for: [%s@%s]", parv[0], user, host);

    return 0;
}


static void
ks_error(char *msg)
{
    if (!forked)
        puts(msg);
    else
        sendto_ops("%s", msg);
}

/*
 * Writes a K-Line to the appropriate file.
 */
void
ks_write(int f, char type, struct userBan *ub)
{
    char outbuf[1024];
    char cidr[4] = "";
    time_t expiretime = 0;
    char *user = "*";
    char *reason = "";
    char *host = ub->h;
    int len;

    /* userban.c */
    unsigned int netmask_to_cidr(unsigned int);

    if (ub->flags & UBAN_TEMPORARY)
        expiretime = ub->timeset + ub->duration;

    if (ub->u)
        user = ub->u;

    if (ub->reason)
        reason = ub->reason;

    if (ub->flags & (UBAN_CIDR4|UBAN_CIDR4BIG))
    {
        host = inetntoa((char *)&ub->cidr4ip);
        ircsprintf(cidr, "/%d", netmask_to_cidr(ntohl(ub->cidr4mask)));
    }

    if (type == '+')
        len = ircsprintf(outbuf, "%c %d %s %s%s %s\n", type, (int)expiretime,
                         user, host, cidr, reason);
    else
        len = ircsprintf(outbuf, "%c %s %s%s\n", type, user, host, cidr);

    write(f, outbuf, len);
}

/*
 * Parses a K-Line entry from a storage journal line.
 * Returns 0 on invalid input, 1 otherwise.
 */
static int
ks_read(char *s)
{
    char type;
    time_t duration = 0;
    char *user;
    char *host;
    char *reason = "";
    struct userBan *ban;
    struct userBan *existing;

    type = *s++;

    /* bad type */
    if (type != '+' && type != '-')
        return 0;

    /* malformed */
    if (*s++ != ' ')
        return 0;

    if (type == '+')
    {
        duration = strtol(s, &s, 0);
        if (duration)
        {
            /* already expired */
            if (NOW >= duration)
                return 1;

            duration -= NOW;
        }

        /* malformed */
        if (*s++ != ' ')
            return 0;
    }

    /* usermask */
    user = s;
    while (*s && *s != ' ')
        s++;

    /* malformed */
    if (*s != ' ')
        return 0;

    /* mark end of user mask */
    *s++ = 0;

    /* hostmask */
    host = s;
    while (*s && *s != ' ')
        s++;

    if (type == '+')
    {
        /* malformed */
        if (*s != ' ')
            return 0;

        /* mark end of host mask */
        *s++ = 0;

        /* reason is the only thing left */
        reason = s;
    }

    ban = make_hostbased_ban(user, host);
    if (!ban)
        return 0;

    ban->flags |= UBAN_LOCAL;

    if (type == '+')
    {
        if (duration)
        {
            ban->flags |= UBAN_TEMPORARY;
            ban->timeset = NOW;
            ban->duration = duration;
        }

        if (*reason)
            DupString(ban->reason, reason);

        add_hostbased_userban(ban);
    }
    else
    {
        existing = find_userban_exact(ban, UBAN_LOCAL|UBAN_CONF);
        userban_free(ban);

        /* add may have been skipped due to being expired, so not an error */
        if (!existing)
            return 1;

        remove_userban(existing);
        userban_free(existing);
    }

    return 1;
}

/*
 * Compact K-Line store: dump active klines to a new file and remove the
 * current journal.
 * Returns 1 on success, 0 on failure.
 */
int
klinestore_compact(void)
{
    char buf1[512];
    int newfile;

    /* userban.c */
    extern void ks_dumpklines(int);

    if (forked)
        sendto_ops_lev(DEBUG_LEV, "Compacting K-Line store...");
    journalcount = 0;

    /* open a compaction file to dump all active klines to */
    ircsnprintf(buf1, sizeof(buf1), "%s/.klines_c", dpath);
    newfile = open(buf1, O_WRONLY|O_CREAT|O_TRUNC, 0700);
    if (newfile < 0)
    {
        ircsnprintf(buf1, sizeof(buf1), "ERROR: Unable to create K-Line"
                    " compaction file .klines_c: %s",
                    strerror(errno));
        ks_error(buf1);
        return 0;
    }

    /* do the dump */
    ks_dumpklines(newfile);
    close(newfile);

    /* close active storage file, rename compaction file, and reopen */
    if (journal >= 0)
    {
        close(journal);
        journal = -1;
    }
    if (rename(buf1, journalfilename) < 0)
    {
        ircsnprintf(buf1, sizeof(buf1), "ERROR: Unable to rename K-Line"
                    " compaction file .klines_c to .klines: %s",
                    strerror(errno));
        ks_error(buf1);
        return 0;
    }
    journal = open(journalfilename, O_WRONLY|O_APPEND, 0700);
    if (journal < 0)
    {
        ircsnprintf(buf1, sizeof(buf1), "ERROR: Unable to reopen K-Line"
                    " storage file .klines: %s", strerror(errno));
        ks_error(buf1);
        return 0;
    }

    return 1;
}

/*
 * Add a K-Line to the active store.
 */
void
klinestore_add(struct userBan *ban)
{
    if (journal >= 0)
        ks_write(journal, '+', ban);

    if (++journalcount > KLINE_STORE_COMPACT_THRESH)
        klinestore_compact();
}

/*
 * Remove a K-Line from the active store.
 */
void
klinestore_remove(struct userBan *ban)
{
    if (journal >= 0)
        ks_write(journal, '-', ban);

    if (++journalcount > KLINE_STORE_COMPACT_THRESH)
        klinestore_compact();
}

/*
 * Initialize K-Line storage.  Pass 1 when klines don't need to be reloaded.
 * Returns 0 on failure, 1 otherwise.
 */
int
klinestore_init(int noreload)
{
    char buf1[1024];
    FILE *jf;

    ircsnprintf(journalfilename, sizeof(journalfilename), "%s/.klines", dpath);

    if (journal >= 0)
    {
        if (noreload)
            return 1;

        close(journal);
        journal = -1;
    }

    /* "a+" to create if it doesn't exist */
    jf = fopen(journalfilename, "a+");
    if (!jf)
    {
        ircsnprintf(buf1, sizeof(buf1), "ERROR: Unable to open K-Line storage"
                    " file .klines: %s", strerror(errno));
        ks_error(buf1);
        return 0;
    }
    rewind(jf);

    /* replay journal */
    while (fgets(buf1, sizeof(buf1), jf))
    {
        char *s = strchr(buf1, '\n');

        /* no newline, consider it malformed and stop here */
        if (!s)
            break;

        *s = 0;

        if (!ks_read(buf1))
            break;
    }

    fclose(jf);

    /* this will reopen the journal for appending */
    return klinestore_compact();
}



syntax highlighted by Code2HTML, v. 0.9.1