/* mod-sockcheck.c - insecure proxy checking
 * Copyright 2000-2004 srvx Development Team
 *
 * This file is part of srvx.
 *
 * srvx 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 2 of the License, 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 srvx; if not, write to the Free Software Foundation,
 * Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA.
 */

#include "conf.h"
#include "gline.h"
#include "ioset.h"
#include "modcmd.h"
#include "timeq.h"

#ifdef HAVE_SYS_SOCKET_H
#include <sys/socket.h>
#endif
#ifdef HAVE_NETINET_IN_H
#include <netinet/in.h>
#endif
#ifdef HAVE_ARPA_INET_H
#include <arpa/inet.h>
#endif

/* TODO, 1.3 or later: allow rules like "27374:" "reject:Subseven detected";
 * (For convenience; right now it assumes that there will be a state
 * rather than an immediate response upon connection.)
 */

#if !defined(SOCKCHECK_DEBUG)
#define SOCKCHECK_DEBUG 0
#endif
#define SOCKCHECK_TEST_DB "sockcheck.conf"

enum sockcheck_decision {
    CHECKING,
    ACCEPT,
    REJECT
};

typedef struct {
    enum sockcheck_decision decision;
    time_t last_touched;
    const char *reason;
    struct in_addr addr;
    char hostname[16];
} *sockcheck_cache_info;

DECLARE_LIST(sci_list, sockcheck_cache_info);
DEFINE_LIST(sci_list, sockcheck_cache_info)

/* Here's the list of hosts that need to be started on.
 */
static struct sci_list pending_sci_list;

/* Map of previously checked IPs state (if we've accepted the address yet).
 * The data for each entry is a pointer to a sockcheck_cache_info.
 */
static dict_t checked_ip_dict;

/* Each sockcheck template is formed as a Mealy state machine (that is,
 * the output on a state transition is a function of both the current
 * state and the input).  Mealy state machines require fewer states to
 * match the same input than Moore machines (where the output is only
 * a function of the current state).
 * 
 * A state is characterized by sending some data (possibly nothing),
 * waiting a certain amount of time to receive one of zero or more
 * responses, and a decision (accept, reject, continue to another
 * state) based on the received response.
 */

struct sockcheck_response {
    const char *template;
    struct sockcheck_state *next;
};

DECLARE_LIST(response_list, struct sockcheck_response *);
DEFINE_LIST(response_list, struct sockcheck_response *)
static unsigned int max_responses;

struct sockcheck_state {
    unsigned short port;
    unsigned short timeout;
    unsigned short reps;
    enum sockcheck_decision type;
    const char *template;
    struct response_list responses;
};

struct sockcheck_list {
    unsigned int size, used, refs;
    struct sockcheck_state **list;
};

/*
 * List of tests.
 */
static struct sockcheck_list *tests;

/* Stuff to track client state, one instance per open connection. */
struct sockcheck_client {
    struct io_fd *fd;
    struct sockcheck_list *tests;
    sockcheck_cache_info addr;
    unsigned int client_index;
    unsigned int test_index;
    unsigned short test_rep;
    struct sockcheck_state *state;
    unsigned int read_size, read_used, read_pos;
    char *read;
    const char **resp_state;
};

static struct {
    unsigned int max_clients;
    unsigned int max_read;
    unsigned int gline_duration;
    unsigned int max_cache_age;
    struct sockaddr_in *local_addr;
    int local_addr_len;
} sockcheck_conf;

static unsigned int sockcheck_num_clients;
static struct sockcheck_client **client_list;
static unsigned int proxies_detected, checked_ip_count;
static struct module *sockcheck_module;
static struct log_type *PC_LOG;
const char *sockcheck_module_deps[] = { NULL };

static const struct message_entry msgtab[] = {
    { "PCMSG_PROXY_DEFINITION_FAILED", "Proxy definition failed: %s" },
    { "PCMSG_PROXY_DEFINITION_SUCCEEDED", "New proxy type defined." },
    { "PCMSG_UNSCANNABLE_IP", "%s has a spoofed, hidden or localnet IP." },
    { "PCMSG_ADDRESS_QUEUED", "$b%s$b is now queued to be proxy-checked." },
    { "PCMSG_ADDRESS_UNRESOLVED", "Unable to resolve $b%s$b to an IP address." },
    { "PCMSG_CHECKING_ADDRESS", "$b%s$b is currently being checked; unable to clear it." },
    { "PCMSG_NOT_REMOVED_FROM_CACHE", "$b%s$b was not cached and therefore was not cleared." },
    { "PCMSG_REMOVED_FROM_CACHE", "$b%s$b was cleared from the cached hosts list." },
    { "PCMSG_DISABLED", "Proxy scanning is $bdisabled$b." },
    { "PCMSG_NOT_CACHED", "No proxycheck records exist for IP %s." },
    { "PCMSG_STATUS_CHECKING", "IP %s proxycheck state: last touched %s ago, still checking" },
    { "PCMSG_STATUS_ACCEPTED", "IP %s proxycheck state: last touched %s ago, acepted" },
    { "PCMSG_STATUS_REJECTED", "IP %s proxycheck state: last touched %s ago, rejected: %s" },
    { "PCMSG_STATUS_UNKNOWN", "IP %s proxycheck state: last touched %s ago, invalid status" },
    { "PCMSG_STATISTICS", "Since booting, I have checked %d clients for illicit proxies, and detected %d proxy hosts.\nI am currently checking %d clients (out of %d max) and have a backlog of %d more to start on.\nI currently have %d hosts cached.\nI know how to detect %d kinds of proxies." },
    { NULL, NULL }
};

static struct sockcheck_list *
sockcheck_list_alloc(unsigned int size)
{
    struct sockcheck_list *list = malloc(sizeof(*list));
    list->used = 0;
    list->refs = 1;
    list->size = size;
    list->list = malloc(list->size*sizeof(list->list[0]));
    return list;
}

static void
sockcheck_list_append(struct sockcheck_list *list, struct sockcheck_state *new_item)
{
    if (list->used == list->size) {
	list->size <<= 1;
	list->list = realloc(list->list, list->size*sizeof(list->list[0]));
    }
    list->list[list->used++] = new_item;
}

static struct sockcheck_list *
sockcheck_list_clone(struct sockcheck_list *old_list)
{
    struct sockcheck_list *new_list = malloc(sizeof(*new_list));
    new_list->used = old_list->used;
    new_list->refs = 1;
    new_list->size = old_list->size;
    new_list->list = malloc(new_list->size*sizeof(new_list->list[0]));
    memcpy(new_list->list, old_list->list, new_list->used*sizeof(new_list->list[0]));
    return new_list;
}

static void
sockcheck_list_unref(struct sockcheck_list *list)
{
    if (!list || --list->refs > 0) return;
    free(list->list);
    free(list);
}

static void
sockcheck_issue_gline(sockcheck_cache_info sci)
{
    char *target = alloca(3+strlen(sci->hostname));
    strcpy(target, "*@");
    strcpy(target+2, sci->hostname);
    log_module(PC_LOG, LOG_INFO, "Issuing gline for client at IP %s hostname %s: %s", inet_ntoa(sci->addr), sci->hostname, sci->reason);
    gline_add("ProxyCheck", target, sockcheck_conf.gline_duration, sci->reason, now, 1);
}

static struct sockcheck_client *
sockcheck_alloc_client(sockcheck_cache_info sci)
{
    struct sockcheck_client *client;
    client = calloc(1, sizeof(*client));
    client->tests = tests;
    client->tests->refs++;
    client->addr = sci;
    client->read_size = sockcheck_conf.max_read;
    client->read = malloc(client->read_size);
    client->resp_state = malloc(max_responses * sizeof(client->resp_state[0]));
    return client;
}

static void
sockcheck_free_client(struct sockcheck_client *client)
{
    if (SOCKCHECK_DEBUG) {
        log_module(PC_LOG, LOG_INFO, "Goodbye %s (%p)!  I set you free!", client->addr->hostname, client);
    }
    verify(client);
    if (client->fd)
        ioset_close(client->fd->fd, 1);
    sockcheck_list_unref(client->tests);
    free(client->read);
    free(client->resp_state);
    free(client);
}

static void sockcheck_start_client(unsigned int idx);
static void sockcheck_begin_test(struct sockcheck_client *client);
static void sockcheck_advance(struct sockcheck_client *client, unsigned int next_state);

static void
sockcheck_timeout_client(void *data)
{
    struct sockcheck_client *client = data;
    if (SOCKCHECK_DEBUG) {
        log_module(PC_LOG, LOG_INFO, "Client %s timed out.", client->addr->hostname);
    }
    verify(client);
    sockcheck_advance(client, client->state->responses.used-1);
}

static void
sockcheck_print_client(const struct sockcheck_client *client)
{
    static const char *decs[] = {"CHECKING", "ACCEPT", "REJECT"};
    log_module(PC_LOG, LOG_INFO, "client %p: { addr = %p { decision = %s; last_touched = "FMT_TIME_T"; reason = %s; hostname = \"%s\" }; "
        "test_index = %d; state = %p { port = %d; type = %s; template = \"%s\"; ... }; "
        "fd = %p(%d); read = %p; read_size = %d; read_used = %d; read_pos = %d; }",
        client, client->addr, decs[client->addr->decision], client->addr->last_touched, client->addr->reason, client->addr->hostname,
        client->test_index, client->state,
        (client->state ? client->state->port : 0),
        (client->state ? decs[client->state->type] : "N/A"),
        (client->state ? client->state->template : "N/A"),
        client->fd, (client->fd ? client->fd->fd : 0),
        client->read, client->read_size, client->read_used, client->read_pos);
}

static char hexvals[256] = {
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0 */
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 16 */
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 32 */
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, /* 48 */
    0,10,11,12,13,14,15, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 64 */
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 80 */
    0,10,11,12,13,14,15, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 96 */
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0  /* 112 */
};

static void
expand_var(const struct sockcheck_client *client, char var, char **p_expansion, unsigned int *p_exp_length)
{
    extern struct cManagerNode cManager;
    const char *expansion;
    unsigned int exp_length;
#ifndef __SOLARIS__
    u_int32_t exp4;
    u_int16_t exp2;
#else
    uint32_t exp4;
    uint16_t exp2;
#endif
    /* expand variable */
    switch (var) {
    case 'c':
        expansion = client->addr->hostname;
        exp_length = strlen(expansion);
        break;
    case 'i':
	exp4 = client->addr->addr.s_addr;
	exp_length = sizeof(exp4);
	expansion = (char*)&exp4;
	break;
    case 'p':
	exp2 = htons(client->state->port);
	exp_length = sizeof(exp2);
	expansion = (char*)&exp2;
	break;
    case 'u':
	expansion = cManager.uplink->host;
	exp_length = strlen(expansion);
	break;
    default:
        log_module(PC_LOG, LOG_WARNING, "Request to expand unknown sockcheck variable $%c, using empty expansion.", var);
	expansion = "";
	exp_length = 0;
    }
    if (p_expansion) {
	*p_expansion = malloc(exp_length);
	memcpy(*p_expansion, expansion, exp_length);
    }
    if (p_exp_length) {
	*p_exp_length = exp_length;
    }
}

static int
sockcheck_check_template(const char *template, int is_input)
{
    unsigned int nn;
    if (is_input && !strcmp(template, "other")) return 1;
    for (nn=0; template[nn]; nn += 2) {
        switch (template[nn]) {
        case '=':
            if (!template[nn+1]) {
                log_module(MAIN_LOG, LOG_ERROR, "ProxyCheck template %s had = at end of template; needs a second character.", template);
                return 0;
            }
            break;
        case '$':
            switch (template[nn+1]) {
            case 'c': case 'i': case 'p': case 'u': break;
            default:
                log_module(MAIN_LOG, LOG_ERROR, "ProxyCheck template %s refers to unknown variable %c (pos %d).", template, template[nn+1], nn);
                return 0;
            }
            break;
        case '.':
            if (!is_input) {
                log_module(MAIN_LOG, LOG_ERROR, "ProxyCheck template %s: . is only valid in input templates.", template);
                return 0;
            }
            if (template[nn+1] != '.') {
                log_module(MAIN_LOG, LOG_ERROR, "ProxyCheck template %s expects .. to come in twos (pos %d).", template, nn);
                return 0;
            }
            break;
        case '0': case '1': case '2': case '3': case '4':
        case '5': case '6': case '7': case '8': case '9':
        case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
        case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
            if (!hexvals[(unsigned char)template[nn+1]] && (template[nn+1] != '0')) {
                log_module(MAIN_LOG, LOG_ERROR, "ProxyCheck template %s expects hex characters to come in twos (pos %d).", template, nn);
                return 0;
            }
            break;
        default:
            log_module(MAIN_LOG, LOG_ERROR, "ProxyCheck template %s: unrecognized character '%c' (pos %d).", template, template[nn], nn);
            return 0;
        }
    }
    return 1;
}

static void
sockcheck_elaborate_state(struct sockcheck_client *client)
{
    const char *template;
    unsigned int nn;

    for (template = client->state->template, nn = 0; template[nn]; nn += 2) {
        switch (template[nn]) {
        case '=': ioset_write(client->fd, template+nn+1, 1); break;
        case '0': case '1': case '2': case '3': case '4':
        case '5': case '6': case '7': case '8': case '9':
        case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
        case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': {
            char ch = hexvals[(unsigned char)template[nn]] << 4
                | hexvals[(unsigned char)template[nn+1]];
            ioset_write(client->fd, &ch, 1);
            break;
        }
        case '$': {
            char *expansion;
            unsigned int exp_length;
            expand_var(client, template[nn+1], &expansion, &exp_length);
            ioset_write(client->fd, expansion, exp_length);
            free(expansion);
            break;
        }
        }
    }
    for (nn=0; nn<client->state->responses.used; nn++) {
        /* Set their resp_state to the start of the response. */
	client->resp_state[nn] = client->state->responses.list[nn]->template;
        /* If it doesn't require reading, take it now. */
        if (client->resp_state[nn] && !*client->resp_state[nn]) {
            if (SOCKCHECK_DEBUG) {
                log_module(PC_LOG, LOG_INFO, "Skipping straight to easy option %d for %p.", nn, client);
            }
            sockcheck_advance(client, nn);
            return;
        }
    }
    timeq_add(now + client->state->timeout, sockcheck_timeout_client, client);
    if (SOCKCHECK_DEBUG) {
        log_module(PC_LOG, LOG_INFO, "Elaborated state for %s:", client->addr->hostname);
        sockcheck_print_client(client);
    }
}

static void
sockcheck_decide(struct sockcheck_client *client, enum sockcheck_decision decision)
{
    unsigned int n;

    checked_ip_count++;
    client->addr->decision = decision;
    client->addr->last_touched = now;
    switch (decision) {
    case ACCEPT:
	/* do nothing */
        if (SOCKCHECK_DEBUG) {
            log_module(PC_LOG, LOG_INFO, "Proxy check passed for client at IP %s hostname %s.", inet_ntoa(client->addr->addr), client->addr->hostname);
        }
        break;
    case REJECT:
	client->addr->reason = client->state->template;
	proxies_detected++;
	sockcheck_issue_gline(client->addr);
        if (SOCKCHECK_DEBUG) {
            log_module(PC_LOG, LOG_INFO, "Proxy check rejects client at IP %s hostname %s (%s)", inet_ntoa(client->addr->addr), client->addr->hostname, client->addr->reason);
        }
	/* Don't compare test_index != 0 directly, because somebody
	 * else may have reordered the tests already. */
	if (client->tests->list[client->test_index] != tests->list[0]) {
	    struct sockcheck_list *new_tests = sockcheck_list_clone(tests);
	    struct sockcheck_state *new_first = client->tests->list[client->test_index];
            for (n=0; (n<tests->used) && (tests->list[n] != new_first); n++) ;
            for (; n>0; n--) new_tests->list[n] = new_tests->list[n-1];
	    new_tests->list[0] = new_first;
	    sockcheck_list_unref(tests);
	    tests = new_tests;
	}
        break;
    default:
	log_module(PC_LOG, LOG_ERROR, "BUG: sockcheck_decide(\"%s\", %d): unrecognized decision.", client->addr->hostname, decision);
    }
    n = client->client_index;
    sockcheck_free_client(client);
    if ((--sockcheck_num_clients < sockcheck_conf.max_clients)
        && (pending_sci_list.used > 0)) {
        sockcheck_start_client(n);
    } else {
        client_list[n] = 0;
    }
}

static void
sockcheck_advance(struct sockcheck_client *client, unsigned int next_state)
{
    struct sockcheck_state *ns;

    verify(client);
    timeq_del(0, sockcheck_timeout_client, client, TIMEQ_IGNORE_WHEN);
    if (SOCKCHECK_DEBUG) {
        unsigned int n, m;
        char buffer[201];
        static const char *hexmap = "0123456789ABCDEF";
	log_module(PC_LOG, LOG_INFO, "sockcheck_advance(%s) following response %d (type %d) of %d.", client->addr->hostname, next_state, client->state->responses.list[next_state]->next->type, client->state->responses.used);
	for (n=0; n<client->read_used; n++) {
	    for (m=0; (m<(sizeof(buffer)-1)>>1) && ((n+m) < client->read_used); m++) {
		buffer[m << 1] = hexmap[client->read[n+m] >> 4];
		buffer[m << 1 | 1] = hexmap[client->read[n+m] & 15];
	    }
	    buffer[m<<1] = 0;
	    log_module(PC_LOG, LOG_INFO, " .. read data: %s", buffer);
	    n += m;
	}
        sockcheck_print_client(client);
    }

    ns = client->state = client->state->responses.list[next_state]->next;
    switch (ns->type) {
    case CHECKING:
        sockcheck_elaborate_state(client);
        break;
    case REJECT:
        sockcheck_decide(client, REJECT);
        break;
    case ACCEPT:
        if (++client->test_rep < client->tests->list[client->test_index]->reps) {
            sockcheck_begin_test(client);
        } else if (++client->test_index < client->tests->used) {
            client->test_rep = 0;
            sockcheck_begin_test(client);
        } else {
            sockcheck_decide(client, ACCEPT);
        }
        break;
    default:
        log_module(PC_LOG, LOG_ERROR, "BUG: unknown next-state type %d (after %p).", ns->type, client->state);
        break;
    }
}

static void
sockcheck_readable(struct io_fd *fd)
{
    /* read what we can from the fd */
    struct sockcheck_client *client = fd->data;
    unsigned int nn;
    int res;

    verify(client);
    res = read(fd->fd, client->read + client->read_used, client->read_size - client->read_used);
    if (res < 0) {
        switch (res = errno) {
        default:
	    log_module(PC_LOG, LOG_ERROR, "BUG: sockcheck_readable(%d/%s): read() returned errno %d (%s)", fd->fd, client->addr->hostname, errno, strerror(errno));
        case EAGAIN:
            return;
        case ECONNRESET:
            sockcheck_advance(client, client->state->responses.used - 1);
            return;
	}
    } else if (res == 0) {
        sockcheck_advance(client, client->state->responses.used - 1);
        return;
    } else {
	client->read_used += res;
    }
    if (SOCKCHECK_DEBUG) {
        unsigned int n, m;
        char buffer[201];
        static const char *hexmap = "0123456789ABCDEF";
	for (n=0; n<client->read_used; n++) {
	    for (m=0; (m<(sizeof(buffer)-1)>>1) && ((n+m) < client->read_used); m++) {
		buffer[m << 1] = hexmap[client->read[n+m] >> 4];
		buffer[m << 1 | 1] = hexmap[client->read[n+m] & 15];
	    }
	    buffer[m<<1] = 0;
	    log_module(PC_LOG, LOG_INFO, "read %d bytes data: %s", client->read_used, buffer);
	    n += m;
	}
    }

    /* See if what's been read matches any of the expected responses */
    while (client->read_pos < client->read_used) {
        unsigned int last_pos = client->read_pos;
	char bleh;
	const char *resp_state;

	for (nn=0; nn<(client->state->responses.used-1); nn++) {
            char *expected;
            unsigned int exp_length = 1, free_exp = 0;
	    /* compare against possible target */
	    resp_state = client->resp_state[nn];
	    if (resp_state == NULL) continue;
	    switch (*resp_state) {
	    case '=': 
                bleh = resp_state[1];
                expected = &bleh;
                break;
	    case '.':
                /* any character passes */
                client->read_pos++;
                exp_length = 0;
                break;
	    case '0': case '1': case '2': case '3': case '4':
	    case '5': case '6': case '7': case '8': case '9':
	    case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
	    case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
		bleh = hexvals[(unsigned char)resp_state[0]] << 4
                    | hexvals[(unsigned char)resp_state[1]];
                expected = &bleh;
		break;
	    case '$':
		expand_var(client, resp_state[1], &expected, &exp_length);
                free_exp = 1;
                break;
            }
            if (client->read_pos+exp_length <= client->read_used) {
                if (exp_length && memcmp(client->read+client->read_pos, expected, exp_length)) {
                    resp_state = NULL;
                } else {
                    client->read_pos += exp_length;
                }
            } else {
                /* can't check the variable yet, so come back later */
                resp_state -= 2;
            }
            if (free_exp) free(expected);
	    if (resp_state) {
		client->resp_state[nn] = resp_state = resp_state + 2;
		if (!*resp_state) {
                    sockcheck_advance(client, nn);
		    return;
		}
	    } else {
		client->resp_state[nn] = NULL;
	    }
	}
        if (last_pos == client->read_pos) break;
    }

    /* nothing seemed to match.  what now? */
    if (client->read_used >= client->read_size) {
	/* we got more data than we expected to get .. don't read any more */
        if (SOCKCHECK_DEBUG) {
            log_module(PC_LOG, LOG_INFO, "Buffer filled (unmatched) for client %s", client->addr->hostname);
        }
        sockcheck_advance(client, client->state->responses.used-1);
	return;
    }
}

static void
sockcheck_connected(struct io_fd *fd, int rc)
{
    struct sockcheck_client *client = fd->data;
    verify(client);
    client->fd = fd;
    switch (rc) {
    default:
        log_module(PC_LOG, LOG_ERROR, "BUG: connect() got error %d (%s) for client at %s.", rc, strerror(rc), client->addr->hostname);
    case EHOSTUNREACH:
    case ECONNREFUSED:
    case ETIMEDOUT:
        if (SOCKCHECK_DEBUG) {
            log_module(PC_LOG, LOG_INFO, "Client %s gave us errno %d (%s)", client->addr->hostname, rc, strerror(rc));
        }
        sockcheck_advance(client, client->state->responses.used-1);
        return;
    case 0: break;
    }
    fd->wants_reads = 1;
    if (SOCKCHECK_DEBUG) {
        log_module(PC_LOG, LOG_INFO, "Connected: to %s port %d.", client->addr->hostname, client->state->port);
    }
    sockcheck_elaborate_state(client);
}

static void
sockcheck_begin_test(struct sockcheck_client *client)
{
    struct io_fd *io_fd;

    verify(client);
    if (client->fd) {
        ioset_close(client->fd->fd, 1);
        client->fd = NULL;
    }
    do {
        client->state = client->tests->list[client->test_index];
        client->read_pos = 0;
        client->read_used = 0;
        client->fd = io_fd = ioset_connect((struct sockaddr*)sockcheck_conf.local_addr, sizeof(struct sockaddr), client->addr->hostname, client->state->port, 0, client, sockcheck_connected);
        if (!io_fd) {
            client->test_index++;
            continue;
        }
        io_fd->readable_cb = sockcheck_readable;
        timeq_add(now + client->state->timeout, sockcheck_timeout_client, client);
        if (SOCKCHECK_DEBUG) {
            log_module(PC_LOG, LOG_INFO, "Starting proxy check on %s:%d (test %d) with fd %d (%p).", client->addr->hostname, client->state->port, client->test_index, io_fd->fd, io_fd);
        }
        return;
    } while (client->test_index < client->tests->used);
    /* Ran out of tests to run; accept this client. */
    sockcheck_decide(client, ACCEPT);
}

static void
sockcheck_start_client(unsigned int idx)
{
    sockcheck_cache_info sci;
    struct sockcheck_client *client;

    if (pending_sci_list.used == 0) return;
    if (!(sci = pending_sci_list.list[0])) {
        log_module(PC_LOG, LOG_ERROR, "BUG: sockcheck_start_client(%d) found null pointer in pending_sci_list.", idx);
        return;
    }
    memmove(pending_sci_list.list, pending_sci_list.list+1,
	    (--pending_sci_list.used)*sizeof(pending_sci_list.list[0]));
    sockcheck_num_clients++;
    if (!tests) return;
    client = client_list[idx] = sockcheck_alloc_client(sci);
    log_module(PC_LOG, LOG_INFO, "Proxy-checking client at %s (%s) as client %d (%p) of %d.", inet_ntoa(sci->addr), sci->hostname, idx, client, sockcheck_num_clients);
    client->test_rep = 0;
    client->client_index = idx;
    sockcheck_begin_test(client);
}

void
sockcheck_queue_address(struct in_addr addr)
{
    sockcheck_cache_info sci;
    char *ipstr=inet_ntoa(addr);

    sci = dict_find(checked_ip_dict, ipstr, NULL);
    if (sci) {
        verify(sci);
        switch (sci->decision) {
        case CHECKING:
            /* We are already checking this host. */
            return;
        case ACCEPT:
            if ((sci->last_touched + sockcheck_conf.max_cache_age) >= (unsigned)now) return;
            break;
        case REJECT:
            if ((sci->last_touched + sockcheck_conf.gline_duration) >= (unsigned)now) {
                sockcheck_issue_gline(sci);
                return;
            }
            break;
        }
        dict_remove(checked_ip_dict, sci->hostname);
    }
    sci = calloc(1, sizeof(*sci));
    sci->decision = CHECKING;
    sci->last_touched = now;
    sci->reason = NULL;
    sci->addr = addr;
    strncpy(sci->hostname, ipstr, sizeof(sci->hostname));
    dict_insert(checked_ip_dict, sci->hostname, sci);
    sci_list_append(&pending_sci_list, sci);
    if (sockcheck_num_clients < sockcheck_conf.max_clients)
        sockcheck_start_client(sockcheck_num_clients);
}

int
sockcheck_uncache_host(const char *name)
{
    sockcheck_cache_info sci;
    if ((sci = dict_find(checked_ip_dict, name, NULL))
        && (sci->decision == CHECKING)) {
        return -1;
    }
    return dict_remove(checked_ip_dict, name);
}

static int
sockcheck_create_response(const char *key, void *data, void *extra)
{
    const char *str, *end;
    struct record_data *rd = data;
    struct sockcheck_state *parent = extra;
    struct sockcheck_response *resp;
    dict_t resps;
    char *templ;

    /* allocate memory and tack it onto parent->responses */
    resp = malloc(sizeof(*resp));
    for (end = key; *end != ':' && *end != 0; end += 2 && end) ;
    templ = malloc(end - key + 1);
    memcpy(templ, key, end - key);
    templ[end - key] = 0;
    resp->template = templ;
    if (!sockcheck_check_template(resp->template, 1)) _exit(1);
    resp->next = malloc(sizeof(*resp->next));
    resp->next->port = parent->port;
    response_list_append(&parent->responses, resp);
    /* now figure out how to create resp->next */
    if ((str = GET_RECORD_QSTRING(rd))) {
	if (!ircncasecmp(str, "reject", 6)) {
	    resp->next->type = REJECT;
	} else if (!ircncasecmp(str, "accept", 6)) {
	    resp->next->type = ACCEPT;
	} else {
	    log_module(PC_LOG, LOG_ERROR, "Error: unknown sockcheck decision `%s', defaulting to accept.", str);
	    resp->next->type = ACCEPT;
	}
	if (str[6]) {
	    resp->next->template = strdup(str+7);
	} else {
	    resp->next->template = strdup("No explanation given");
	}
    } else if ((resps = GET_RECORD_OBJECT(rd))) {
	resp->next->type = CHECKING;
	response_list_init(&resp->next->responses);
	if (*end == ':') {
	    resp->next->template = strdup(end+1);
            if (!sockcheck_check_template(resp->next->template, 0)) _exit(1);
	} else {
	    resp->next->template = strdup("");
	}
	dict_foreach(resps, sockcheck_create_response, resp->next);
    }
    return 0;
}

/* key: PORT:send-pattern, as in keys of sockcheck.conf.example
 * data: recdb record_data containing response
 * extra: struct sockcheck_list* to append test to
 */
static int
sockcheck_create_test(const char *key, void *data, void *extra)
{
    char *end;
    struct record_data *rd;
    dict_t object;
    struct sockcheck_state *new_test;
    unsigned int n;

    rd = data;
    new_test = malloc(sizeof(*new_test));
    new_test->template = NULL;
    new_test->reps = 1;
    new_test->port = strtoul(key, &end, 0);
    new_test->timeout = 5;
    new_test->type = CHECKING;
    response_list_init(&new_test->responses);
    if (!(object = GET_RECORD_OBJECT(rd))) {
	log_module(PC_LOG, LOG_ERROR, "Error: misformed sockcheck test `%s', skipping it.", key);
        free(new_test);
        return 1;
    }
    while (*end) {
        switch (*end) {
        case '@': new_test->timeout = strtoul(end+1, &end, 0); break;
        case '*': new_test->reps = strtoul(end+1, &end, 0); break;
        case ':':
            new_test->template = strdup(end+1);
            end += strlen(end);
            if (!sockcheck_check_template(new_test->template, 0)) _exit(1);
            break;
        default:
            log_module(PC_LOG, LOG_ERROR, "Error: misformed sockcheck test `%s', skipping it.", key);
            free(new_test);
            return 1;
        }
    }
    if (!new_test->template) {
	log_module(PC_LOG, LOG_ERROR, "Error: misformed sockcheck test `%s', skipping it.", key);
	free(new_test);
	return 1;
    }
    dict_foreach(object, sockcheck_create_response, new_test);
    /* If none of the responses have template "other", create a
     * default response that goes to accept. */
    for (n=0; n<new_test->responses.used; n++) {
	if (!strcmp(new_test->responses.list[n]->template, "other")) break;
    }
    if (n == new_test->responses.used) {
	rd = alloc_record_data_qstring("accept");
	sockcheck_create_response("other", rd, new_test);
	free_record_data(rd);
    } else if (n != (new_test->responses.used - 1)) {
	struct sockcheck_response *tmp;
	/* switch the response for "other" to the end */
	tmp = new_test->responses.list[new_test->responses.used - 1];
	new_test->responses.list[new_test->responses.used - 1] = new_test->responses.list[n];
	new_test->responses.list[n] = tmp;
    }
    if (new_test->responses.used > max_responses) {
	max_responses = new_test->responses.used;
    }
    sockcheck_list_append(extra, new_test);
    return 0;
}

static void
sockcheck_read_tests(void)
{
    dict_t test_db;
    struct sockcheck_list *new_tests;
    test_db = parse_database(SOCKCHECK_TEST_DB);
    if (!test_db)
	return;
    if (dict_size(test_db) > 0) {
	new_tests = sockcheck_list_alloc(dict_size(test_db));
	dict_foreach(test_db, sockcheck_create_test, new_tests);
	if (tests) sockcheck_list_unref(tests);
	tests = new_tests;
    } else {
	log_module(PC_LOG, LOG_ERROR, "%s was empty - disabling sockcheck.", SOCKCHECK_TEST_DB);
    }
    free_database(test_db);
}

void
sockcheck_free_state(struct sockcheck_state *state)
{
    unsigned int n;
    if (state->type == CHECKING) {
	for (n=0; n<state->responses.used; n++) {
	    free((char*)state->responses.list[n]->template);
	    sockcheck_free_state(state->responses.list[n]->next);
	    free(state->responses.list[n]);
	}
	response_list_clean(&state->responses);
    }
    free((char*)state->template);
    free(state);
}

const char *
sockcheck_add_test(const char *desc)
{
    struct sockcheck_list *new_tests;
    const char *reason;
    char *name;
    struct record_data *rd;

    if ((reason = parse_record(desc, &name, &rd)))
        return reason;
    new_tests = sockcheck_list_clone(tests);
    if (sockcheck_create_test(name, rd, new_tests)) {
	sockcheck_list_unref(new_tests);
	return "Sockcheck test parse error";
    }
    sockcheck_list_unref(tests);
    tests = new_tests;
    return 0;
}

static void
sockcheck_shutdown(void)
{
    unsigned int n;

    if (client_list) {
        for (n=0; n<sockcheck_conf.max_clients; n++) {
            if (client_list[n])
                sockcheck_free_client(client_list[n]);
        }
        free(client_list);
    }
    sockcheck_num_clients = 0;
    dict_delete(checked_ip_dict);
    sci_list_clean(&pending_sci_list);
    if (tests)
	for (n=0; n<tests->used; n++)
	    sockcheck_free_state(tests->list[n]);
    sockcheck_list_unref(tests);
    if (sockcheck_conf.local_addr) {
	free(sockcheck_conf.local_addr);
	sockcheck_conf.local_addr_len = 0;
    }
}

static void
sockcheck_clean_cache(UNUSED_ARG(void *data))
{
    dict_t curr_clients;
    dict_iterator_t it, next;
    sockcheck_cache_info sci;
    unsigned int nn;
    int max_age;

    if (SOCKCHECK_DEBUG) {
        struct string_buffer sb;
        string_buffer_init(&sb);
        /* Remember which clients we're still checking; we're not allowed to remove them. */
        for (curr_clients = dict_new(), nn=0; nn < sockcheck_conf.max_clients; nn++) {
            if (!client_list[nn])
                continue;
            dict_insert(curr_clients, client_list[nn]->addr->hostname, client_list[nn]);
            string_buffer_append(&sb, ' ');
            string_buffer_append_string(&sb, client_list[nn]->addr->hostname);
        }
        string_buffer_append(&sb, '\0');
        log_module(PC_LOG, LOG_INFO, "Cleaning sockcheck cache at "FMT_TIME_T"; current clients: %s.", now, sb.list);
        string_buffer_clean(&sb);
    } else {
        for (curr_clients = dict_new(), nn=0; nn < sockcheck_conf.max_clients; nn++) {
            if (!client_list[nn])
                continue;
            dict_insert(curr_clients, client_list[nn]->addr->hostname, client_list[nn]);
        }
    }

    for (it=dict_first(checked_ip_dict); it; it=next) {
        next = iter_next(it);
        sci = iter_data(it);
        max_age = (sci->decision == REJECT) ? sockcheck_conf.gline_duration : sockcheck_conf.max_cache_age;
        if (((sci->last_touched + max_age) < now)
            && !dict_find(curr_clients, sci->hostname, NULL)) {
            if (SOCKCHECK_DEBUG) {
                log_module(PC_LOG, LOG_INFO, " .. nuking %s (last touched "FMT_TIME_T").", sci->hostname, sci->last_touched);
            }
            dict_remove(checked_ip_dict, sci->hostname);
        }
    }
    dict_delete(curr_clients);
    timeq_add(now+sockcheck_conf.max_cache_age, sockcheck_clean_cache, 0);
}

static MODCMD_FUNC(cmd_defproxy)
{
    const char *reason;

    if ((reason = sockcheck_add_test(unsplit_string(argv+1, argc-1, NULL)))) {
	reply("PCMSG_PROXY_DEFINITION_FAILED", reason);
	return 0;
    }
    reply("PCMSG_PROXY_DEFINITION_SUCCEEDED");
    return 1;
}

static MODCMD_FUNC(cmd_hostscan)
{
    unsigned int n;
    unsigned long addr;
    struct in_addr ipaddr;
    char hnamebuf[64];

    for (n=1; n<argc; n++) {
	struct userNode *un = GetUserH(argv[n]);

        if (un) {
            if ((un->ip.s_addr == 0) || (ntohl(un->ip.s_addr) == INADDR_LOOPBACK)) {
                reply("PCMSG_UNSCANNABLE_IP", un->nick);
            } else {
                strcpy(hnamebuf, inet_ntoa(un->ip));
                sockcheck_queue_address(un->ip);
                reply("PCMSG_ADDRESS_QUEUED", hnamebuf);
            }
        } else {
            char *scanhost = argv[n];
            if (getipbyname(scanhost, &addr)) {
                ipaddr.s_addr = htonl(addr);
                sockcheck_queue_address(ipaddr);
                reply("PCMSG_ADDRESS_QUEUED", scanhost);
            } else {
                reply("PCMSG_ADDRESS_UNRESOLVED", scanhost);
            }
        }
    }
    return 1;
}

static MODCMD_FUNC(cmd_clearhost)
{
    unsigned int n;
    char hnamebuf[64];

    for (n=1; n<argc; n++) {
        struct userNode *un = GetUserH(argv[n]);
        const char *scanhost;

        if (un) {
            strcpy(hnamebuf, inet_ntoa(un->ip));
            scanhost = hnamebuf;
        } else {
            scanhost = argv[n];
        }
        switch (sockcheck_uncache_host(scanhost)) {
        case -1:
	    reply("PCMSG_CHECKING_ADDRESS", scanhost);
            break;
        case 0:
	    reply("PCMSG_NOT_REMOVED_FROM_CACHE", scanhost);
            break;
        default:
            reply("PCMSG_REMOVED_FROM_CACHE", scanhost);
            break;
        }
    }
    return 1;
}

static MODCMD_FUNC(cmd_stats_proxycheck)
{
    if (argc > 1) {
        const char *hostname = argv[1];
        char elapse_buf[INTERVALLEN];
        const char *msg;

        sockcheck_cache_info sci = dict_find(checked_ip_dict, hostname, NULL);
        if (!sci) {
            reply("PCMSG_NOT_CACHED", hostname);
            return 0;
        }
        intervalString(elapse_buf, now - sci->last_touched, user->handle_info);
        switch (sci->decision) {
        case CHECKING: msg = "PCMSG_STATUS_CHECKING"; break;
        case ACCEPT: msg = "PCMSG_STATUS_ACCEPTED"; break;
        case REJECT: msg = "PCMSG_STATUS_REJECTED"; break;
        default: msg = "PCMSG_STATUS_UNKNOWN"; break;
        }
        reply(msg, sci->hostname, elapse_buf, sci->reason);
        return 1;
    } else {
        reply("PCMSG_STATISTICS", checked_ip_count, proxies_detected, sockcheck_num_clients, sockcheck_conf.max_clients, pending_sci_list.used, dict_size(checked_ip_dict), (tests ? tests->used : 0));
        return 1;
    }
}

static int
sockcheck_new_user(struct userNode *user) {
    /* If they have a bum IP, or are bursting in, don't proxy-check or G-line them. */
    if (user->ip.s_addr
        && (ntohl(user->ip.s_addr) != INADDR_LOOPBACK)
        && !user->uplink->burst)
        sockcheck_queue_address(user->ip);
    return 0;
}

static void
_sockcheck_init(void)
{
    checked_ip_dict = dict_new();
    dict_set_free_data(checked_ip_dict, free);
    sci_list_init(&pending_sci_list);
    sockcheck_num_clients = 0;
    sockcheck_read_tests();
    timeq_del(0, sockcheck_clean_cache, 0, TIMEQ_IGNORE_WHEN|TIMEQ_IGNORE_DATA);
    client_list = calloc(sockcheck_conf.max_clients, sizeof(client_list[0]));
    timeq_add(now+sockcheck_conf.max_cache_age, sockcheck_clean_cache, 0);
}

static void
sockcheck_read_conf(void)
{
    dict_t my_node;
    const char *str;

    /* set the defaults here in case the entire record is missing */
    sockcheck_conf.max_clients = 32;
    sockcheck_conf.max_read = 1024;
    sockcheck_conf.gline_duration = 3600;
    sockcheck_conf.max_cache_age = 60;
    if (sockcheck_conf.local_addr) {
        free(sockcheck_conf.local_addr);
        sockcheck_conf.local_addr = NULL;
    }
    /* now try to read from the conf database */
    if ((my_node = conf_get_data("modules/sockcheck", RECDB_OBJECT))) {
	str = database_get_data(my_node, "max_sockets", RECDB_QSTRING);
	if (str) sockcheck_conf.max_clients = strtoul(str, NULL, 0);
	str = database_get_data(my_node, "max_clients", RECDB_QSTRING);
	if (str) sockcheck_conf.max_clients = strtoul(str, NULL, 0);
	str = database_get_data(my_node, "max_read", RECDB_QSTRING);
	if (str) sockcheck_conf.max_read = strtoul(str, NULL, 0);
	str = database_get_data(my_node, "max_cache_age", RECDB_QSTRING);
	if (str) sockcheck_conf.max_cache_age = ParseInterval(str);
        str = database_get_data(my_node, "gline_duration", RECDB_QSTRING);
        if (str) sockcheck_conf.gline_duration = ParseInterval(str);
	str = database_get_data(my_node, "address", RECDB_QSTRING);
	if (str) {
	    struct sockaddr_in *sin;
	    unsigned long addr;

	    sockcheck_conf.local_addr_len = sizeof(*sin);
	    if (getipbyname(str, &addr)) {
		sin = malloc(sockcheck_conf.local_addr_len);
		sin->sin_family = AF_INET;
		sin->sin_port = 0;
		sin->sin_addr.s_addr = addr;
#ifdef HAVE_SIN_LEN
	        sin->sin_len = 0;
#endif
        	memset(sin->sin_zero, 0, sizeof(sin->sin_zero));
	        sockcheck_conf.local_addr = sin;
	    } else {
		log_module(PC_LOG, LOG_ERROR, "Error: Unable to get host named `%s', not checking a specific address.", str);
		sockcheck_conf.local_addr = NULL;
	    }
	}
    }
}

int
sockcheck_init(void)
{
    PC_LOG = log_register_type("ProxyCheck", "file:proxycheck.log");
    conf_register_reload(sockcheck_read_conf);
    reg_exit_func(sockcheck_shutdown);
    _sockcheck_init();
    message_register_table(msgtab);

    sockcheck_module = module_register("ProxyCheck", PC_LOG, "mod-sockcheck.help", NULL);
    modcmd_register(sockcheck_module, "defproxy", cmd_defproxy, 2, 0, "level", "999", NULL);
    modcmd_register(sockcheck_module, "hostscan", cmd_hostscan, 2, 0, "level", "650", NULL);
    modcmd_register(sockcheck_module, "clearhost", cmd_clearhost, 2, 0, "level", "650", NULL);
    modcmd_register(sockcheck_module, "stats proxycheck", cmd_stats_proxycheck, 0, 0, NULL);
    reg_new_user_func(sockcheck_new_user);
    return 1;
}

int
sockcheck_finalize(void)
{
    return 1;
}