/* IP address authorization module for HTTP server.
 *
 * IRC Services is copyright (c) 1996-2009 Andrew Church.
 *     E-mail: <achurch@achurch.org>
 * Parts written by Andrew Kempe and others.
 * This program is free but copyrighted software; see the file COPYING for
 * details.
 */

#include "services.h"
#include "modules.h"
#include "conffile.h"
#include "http.h"
#include <netinet/in.h>
#include <netdb.h>

/*************************************************************************/

static Module *module;
static Module *module_httpd;

/* List of hosts to allow/deny */
typedef struct {
    char *path;
    int pathlen;	/* for convenience */
    uint32 ip, mask;
    int allow;		/* 1 = allow, 0 = deny */
} DirInfo;
static DirInfo *protected = NULL;
static int protected_count = 0;

/*************************************************************************/
/************************ Authorization callback *************************/
/*************************************************************************/

static int do_auth(Client *c, int *close_ptr)
{
    int i;

    ARRAY_FOREACH (i, protected) {
	if (strncmp(c->url, protected[i].path, protected[i].pathlen) != 0)
	    continue;
	if ((c->ip & protected[i].mask) != protected[i].ip)
	    continue;
	if (protected[i].allow) {
	    return HTTP_AUTH_UNDECIDED;
	} else {
	    module_log("Denying request for %s from %s", c->url, c->address);
	    return HTTP_AUTH_DENY;
	}
    }
    return HTTP_AUTH_UNDECIDED;
}

/*************************************************************************/
/***************************** Module stuff ******************************/
/*************************************************************************/

const int32 module_version = MODULE_VERSION_CODE;

static int do_prefix(const char *filename, int linenum, char *param);
static int do_AllowHost(const char *filename, int linenum, char *param);
static int do_DenyHost(const char *filename, int linenum, char *param);
static int do_AllowDenyHost(const char *filename, int linenum, char *param,
			    int allow);
ConfigDirective module_config[] = {
    { "AllowHost",        { { CD_FUNC, 0, do_prefix },
                            { CD_FUNC, 0, do_AllowHost } } },
    { "DenyHost",         { { CD_FUNC, 0, do_prefix },
                            { CD_FUNC, 0, do_DenyHost } } },
    { NULL }
};

static char *prefix;

/*************************************************************************/

static int do_prefix(const char *filename, int linenum, char *param)
{
    if (filename) {
	free(prefix);
	prefix = strdup(param);
	if (!prefix) {
	    config_error(filename, linenum, "Out of memory");
	    return 0;
	}
    }
    return 1;
}

static int do_AllowHost(const char *filename, int linenum, char *param)
{
    return do_AllowDenyHost(filename, linenum, param, 1);
}

static int do_DenyHost(const char *filename, int linenum, char *param)
{
    return do_AllowDenyHost(filename, linenum, param, 0);
}

/*************************************************************************/

static int do_AllowDenyHost(const char *filename, int linenum, char *param,
			    int allow)
{
    char *s;
    long mask = 32;
    const uint8 *ip;
    int recursing = 0, i;
    DirInfo di;
    static DirInfo *new_protected = NULL;
    static int new_protected_count = 0;

    if (!filename) {
	/* filename == NULL, special actions */
	switch (linenum) {
	  case CDFUNC_INIT:	/* prepare for reading */
	    free(new_protected);
	    new_protected = NULL;
	    new_protected_count = 0;
	    break;
	  case CDFUNC_SET:	/* store new values in config variables */
	    if (new_protected_count >= 0) {
		protected = new_protected;
		protected_count = new_protected_count;
		new_protected = NULL;
		new_protected_count = -1;  /* flag to say "don't copy again" */
	    }
	    break;
	  case CDFUNC_DECONFIG:	/* clear out config variables */
	    free(protected);
	    protected = NULL;
	    protected_count = 0;
	    break;
	} /* switch (linenum) */
	return 1;
    } /* if (!filename) */

    /* filename != NULL, process directive */

    if (linenum < 0) {
	recursing = 1;
	linenum = -linenum;
    }
    di.path = prefix;
    di.pathlen = strlen(prefix);
    prefix = NULL;

    s = strchr(param, '/');
    if (s) {
	char *t;
	*s++ = 0;
	mask = strtol(s, &t, 10);
	if (*t || mask < 1 || mask > 31) {
	    config_error(filename, linenum, "Invalid mask length `%s'", s);
	    free(di.path);
	    return 0;
	}
    }

    if (strcmp(param, "*") == 0) {
	/* All-hosts wildcard -> equivalent to 0.0.0.0/0 */
	ip = (const uint8 *)"\0\0\0\0";
	mask = 0;
    } else if ((ip = pack_ip(param)) != NULL) {
	/* IP address -> okay as is */
    } else {
	/* hostname -> check for double recursion, then look up and
	 *             recursively add addresses */
#ifdef HAVE_GETHOSTBYNAME
	struct hostent *hp;
#endif
	if (recursing) {
	    config_error(filename, linenum, "BUG: double recursion (param=%s)",
			 param);
	    free(di.path);
	    return 0;
	}
#ifdef HAVE_GETHOSTBYNAME
	if ((hp = gethostbyname(param)) != NULL) {
	    if (hp->h_addrtype == AF_INET) {
		for (i = 0; hp->h_addr_list[i]; i++) {
		    char ipbuf[16];
		    ip = (const uint8 *)hp->h_addr_list[i];
		    snprintf(ipbuf, sizeof(ipbuf), "%u.%u.%u.%u",
			     ip[0], ip[1], ip[2], ip[3]);
		    if (strlen(ipbuf) > 15) {
			config_error(filename, linenum,
				     "BUG: strlen(ipbuf) > 15 [%s]", ipbuf);
			free(di.path);
			return 0;
		    }
		    prefix = strdup(di.path);
		    if (!prefix) {
			config_error(filename, linenum, "Out of memory");
			free(di.path);
			return 0;
		    }
		    if (!do_AllowDenyHost(filename, -linenum, ipbuf, allow)) {
			free(di.path);
			return 0;
		    }
		}
		free(di.path);
		return 1;  /* Success */
	    } else {		    
		config_error(filename, linenum, "%s: no IPv4 addresses found",
			     param);
	    }
	} else {
	    config_error(filename, linenum, "%s: %s", param,
			 hstrerror(h_errno));
	}
#else
	config_error(filename, linenum,
		     "gethostbyname() not available, hostnames may not be"
		     " used");
#endif
	free(di.path);
	return 0;
    }

    di.ip = *((uint32 *)ip);
    di.mask = mask ? htonl(0xFFFFFFFFUL << (32-mask)) : 0;
    di.ip &= di.mask;
    di.allow = allow;
    ARRAY_EXTEND(new_protected);
    new_protected[new_protected_count-1] = di;
    return 1;
}

/*************************************************************************/
/*************************************************************************/

int init_module(Module *module_)
{
    module = module_;
    module_httpd = NULL;

    module_httpd = find_module("httpd/main");
    if (!module_httpd) {
	module_log("Main httpd module not loaded");
	exit_module(0);
	return 0;
    }
    use_module(module_httpd);

    if (!add_callback(module_httpd, "auth", do_auth)) {
	module_log("Unable to add callback");
	exit_module(0);
	return 0;
    }

    return 1;
}

/*************************************************************************/

int exit_module(int shutdown_unused)
{
    int i;

#ifdef CLEAN_COMPILE
    shutdown_unused = shutdown_unused;
#endif

    if (module_httpd) {
	remove_callback(module_httpd, "auth", do_auth);
	unuse_module(module_httpd);
	module_httpd = NULL;
    }

    for (i = 0; i < protected_count; i++)
	free(protected[i].path);
    free(protected);
    protected = NULL;
    protected_count = 0;

    return 1;
}

/*************************************************************************/
