/* ====================================================================
 * Copyright (c) 2000 The Apache Group.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer. 
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. All advertising materials mentioning features or use of this
 *    software must display the following acknowledgment:
 *    "This product includes software developed by the Apache Group
 *    for use in the Apache HTTP server project (http://www.apache.org/)."
 *
 * 4. The names "Apache Server" and "Apache Group" must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission. For written permission, please contact
 *    apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache"
 *    nor may "Apache" appear in their names without prior written
 *    permission of the Apache Group.
 *
 * 6. Redistributions of any form whatsoever must retain the following
 *    acknowledgment:
 *    "This product includes software developed by the Apache Group
 *    for use in the Apache HTTP server project (http://www.apache.org/)."
 *
 * THIS SOFTWARE IS PROVIDED BY THE APACHE GROUP ``AS IS'' AND ANY
 * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE APACHE GROUP OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Group and was originally based
 * on public domain software written at the National Center for
 * Supercomputing Applications, University of Illinois, Urbana-Champaign.
 * For more information on the Apache Group and the Apache HTTP server
 * project, please see <http://www.apache.org/>.
 *
 */

/*
 * Module derived from mod_access.c in Apache HTTP Server Version 1.3
 *
 * Original author:   Roberto Arturo Tena Sánchez
 *                    <arturo__at__users.sourceforge.net>
 * Original Web site: http://accessreferer.sourceforge.net/
 */



/* XX patch AllowOverride directive on order to accept mod_access_referer
   directives ? */

/* XX There can be absolute or relative URIs: absolute URIs always have ':'
   ("Uniform Resource Identifiers (URI): Generic Syntax and Semantics,"
   RFC 2396) */



#include "httpd.h"
#include "http_core.h"
#include "http_config.h"
#include "http_log.h"
#include "http_request.h"


enum allowdeny_type {
	/* T_ENV,       XX Not enviroment variable check
			implementation needed */
	T_ALL,
	T_IP,
	T_HOST,
	T_FAIL
};
typedef struct {
	int limited;
	union {
		char *from;
		struct {
			unsigned long net;
			unsigned long mask;
		} ip;
	} x;
	enum allowdeny_type type;
} allowdeny;


typedef struct {
	int		order[METHODS];
	int		default_access_referer[METHODS];
	array_header *	allows;
	array_header *	denys;
	int		allows_empty;
	int		denys_empty;
} access_referer_dir_conf;
/* values in order */
#define OMITTED_ORDER   0
#define DENY_THEN_ALLOW 1
#define ALLOW_THEN_DENY 2
#define MUTUAL_FAILURE  3
/* values in default_access_referer */
#define OMITTED_DEFAULT       0
#define DEFAULT_DENY_REFERER  1
#define DEFAULT_ALLOW_REFERER 2



module MODULE_VAR_EXPORT access_referer_module;



static int is_ip (const char *host);
static int in_domain (const char *domain, const char *what);
static int find_allowdeny (request_rec *r, array_header *a, int method);
static const char * order_referer_cmd (cmd_parms *cmd, void *dv, char *arg);
static const char * default_referer_cmd (cmd_parms *cmd, void *dv, char *arg);
static const char * allow_referer_cmd (cmd_parms *cmd, void *dv, char *from,
				       char *where);
static void * create_access_referer_dir_config_mod (pool *p, char *dummy);
static int check_dir_access_referer_mod (request_rec *r);
static void *merge_access_referer_dir_config_mod (pool *p, void *basev,
						  void *addv);



static void *
create_access_referer_dir_config_mod (pool *p, char *dummy)
{
	int i;
	access_referer_dir_conf *conf;

	conf = (access_referer_dir_conf *)
		ap_pcalloc (p, sizeof (access_referer_dir_conf));

	for (i = 0; i < METHODS; ++i) {
		conf->order[i] = OMITTED_ORDER;
		conf->default_access_referer[i] = OMITTED_DEFAULT;
	}
	conf->allows = ap_make_array (p, 1, sizeof (allowdeny));
	conf->denys  = ap_make_array (p, 1, sizeof (allowdeny));
	conf->allows_empty = 1;
	conf->denys_empty  = 1;

	return conf;
}


static void *
merge_access_referer_dir_config_mod (pool *p, void *basev, void *addv)
{
	int                        i;
	access_referer_dir_conf *  base;
	access_referer_dir_conf *  add;
	access_referer_dir_conf *  new;

	base = (access_referer_dir_conf *) basev;
	add  = (access_referer_dir_conf *) addv;
	new  = (access_referer_dir_conf *)
		ap_pcalloc (p, sizeof (access_referer_dir_conf));

	for (i = 0; i < METHODS; ++i) {
		if (add->order[i] == OMITTED_ORDER) {
			new->order[i] = base->order[i];
		} else {
			new->order[i] = add->order[i];
		}
		if (add->default_access_referer[i] == OMITTED_ORDER) {
			new->default_access_referer[i] =
				base->default_access_referer[i];
		} else {
			new->default_access_referer[i] =
				add->default_access_referer[i];
		}
	}

	/* XX wouldn't it be better to check add->allows->nelts ? */
	if (add->allows_empty) {
		new->allows = base->allows;
	} else {
		new->allows = add->allows;
	}
	if (add->denys_empty) {
		new->denys  = base->denys;
	} else {
		new->denys  = add->denys;
	}

	return new;
}



static const char *
order_referer_cmd (cmd_parms *cmd, void *dv, char *arg)
{
	int i;
	int o;
	access_referer_dir_conf *d;

	d = (access_referer_dir_conf *) dv;

	if (!strcasecmp (arg, "allow_referer,deny_referer"))
		o = ALLOW_THEN_DENY;
	else if (!strcasecmp (arg, "deny_referer,allow_referer"))
		o = DENY_THEN_ALLOW;
	else if (!strcasecmp (arg, "mutual-failure"))
		o = MUTUAL_FAILURE;
	else
		return "unknown order_referer";

	for (i = 0; i < METHODS; ++i)
		if (cmd->limited & (1 << i))
			d->order[i] = o;

	return NULL;
}


static const char *
default_referer_cmd (cmd_parms *cmd, void *dv, char *arg)
{
	int i;
	int o;
	access_referer_dir_conf *d;

	d = (access_referer_dir_conf *) dv;

	if (!strcasecmp (arg, "allow_referer"))
		o = DEFAULT_ALLOW_REFERER;
	else if (!strcasecmp (arg, "deny_referer"))
		o = DEFAULT_DENY_REFERER;
	else
		return "unknown default_referer";

	for (i = 0; i < METHODS; ++i)
		if (cmd->limited & (1 << i))
			d->default_access_referer[i] = o;

	return NULL;
}


static const char *
allow_referer_cmd (cmd_parms *cmd, void *dv, char *from, char *where)
{
	char *s;
	allowdeny *a;
	access_referer_dir_conf *d;

	d = (access_referer_dir_conf *) dv;

	if (strcasecmp (from, "from"))
		return "allow_request and deny_request must be followed by 'from'";

	if (cmd->info != NULL) {
		a = (allowdeny *) ap_push_array (d->allows);
		d->allows_empty = 0;
	} else {
		a = (allowdeny *) ap_push_array (d->denys);
		d->denys_empty = 0;
	}
	a->x.from  = where;
	a->limited = cmd->limited;

	/* XX Not enviroment variable check implementation needed
	if (!strncasecmp (where, "env=", 4)) {
		a->type = T_ENV;
		a->x.from += 4;
	} else */
	if (!strcasecmp (where, "all")) {
		a->type = T_ALL;
	} else if ((s = strchr (where, '/'))) {
		/* network plus netmask */
		unsigned long mask;

		a->type = T_IP;

		/* split netmask from network */
		*s = '\0';
		s++;

		/* get network */
		if (!is_ip (where)
		    || (a->x.ip.net = ap_inet_addr (where)) == INADDR_NONE) {
			a->type = T_FAIL;
			return "syntax error in network portion of network/netmask";
		}

		/* get netmask */
		if (!is_ip (s)) {
			a->type = T_FAIL;
			return "syntax error in mask portion of network/netmask";
		}
		if (strchr(s, '.')) {
			/* netmask is in /a.b.c.d form */
			mask = ap_inet_addr (s);
			if (mask == INADDR_NONE) {
				a->type = T_FAIL;
				return "syntax error in mask portion of network/netmask";
			}
		} else {
			/* assume netmask is in /nnn form */
			mask = atoi (s);
			if (mask > 32 || mask <= 0) {
				a->type = T_FAIL;
				return "invalid mask in network/netmask";
			}
			mask = 0xFFFFFFFFUL << (32 - mask);
			mask = htonl (mask);
		}
		a->x.ip.mask = mask;

		/* recalculate network from netmask */
		a->x.ip.net  = (a->x.ip.net & mask);
	} else if (ap_isdigit (*where) && is_ip (where)) {
		/* legacy syntax for ip addrs: a.b.c. ==> a.b.c.0/24
		   for example */
		char *t;
		int shift;
		int octet;

		a->type = T_IP;

		/* parse components */
		/* XX check */
		s = where;
		a->x.ip.net  = 0;
		a->x.ip.mask = 0;
		shift = 24;
		while (*s) {
			t = s;
			if (!ap_isdigit (*t)) {
				a->type = T_FAIL;
				return "invalid ip address";
			}
			while (ap_isdigit (*t)) {
				++t;
			}
			if (*t == '.') {
				*t++ = 0;
			} else if (*t) {
				a->type = T_FAIL;
				return "invalid ip address";
			}
			if (shift < 0) {
				return "invalid ip address, only 4 octets allowed";
			}
			octet = atoi(s);
			if (octet < 0 || octet > 255) {
				a->type = T_FAIL;
				return "each octet must be between 0 and 255 inclusive";
			}
				a->x.ip.net  |= octet  << shift;
				a->x.ip.mask |= 0xFFUL << shift;
				s = t;
				shift -= 8;
		}
		a->x.ip.net  = ntohl (a->x.ip.net);
		a->x.ip.mask = ntohl (a->x.ip.mask);
	} else {
		a->type = T_HOST;
	}

	return NULL;
}



static int
check_dir_access_referer_mod (request_rec *r)
{
	int ret;
	int method;
	access_referer_dir_conf *a;


	ret = OK;
	method = r->method_number;
	a = (access_referer_dir_conf *)
	    ap_get_module_config (r->per_dir_config, &access_referer_module);

	if (ap_table_get (r->headers_in, "Referer") == NULL) {
		/* The "Referer" HTTP header was not sent */
		if (a->default_access_referer[method] == DEFAULT_ALLOW_REFERER
		    || a->default_access_referer[method] == OMITTED_DEFAULT)
			ret = OK;
		else
			ret = FORBIDDEN;
	} else if (a->order[method] == ALLOW_THEN_DENY) {
		ret = FORBIDDEN;
		if (find_allowdeny (r, a->allows, method))
			ret = OK;
		if (find_allowdeny (r, a->denys, method))
			ret = FORBIDDEN;
	} else if (a->order[method] == DENY_THEN_ALLOW
		   || a->order[method] == OMITTED_ORDER) {
		if (find_allowdeny (r, a->denys, method))
			ret = FORBIDDEN;
		if (find_allowdeny (r, a->allows, method))
			ret = OK;
	} else /* a->order[method] == MUTUAL_FAILURE */ {
		if (find_allowdeny (r, a->allows, method)
		    && !find_allowdeny (r, a->denys, method))
			ret = OK;
		else
			ret = FORBIDDEN;
	}

	if (ret == FORBIDDEN && (ap_satisfies (r) != SATISFY_ANY
				 || !ap_some_auth_required (r))) {
		ap_log_rerror (APLOG_MARK, APLOG_NOERRNO|APLOG_ERR, r,
			       /* XX print IP and/or hostname, and path */
			       "client denied by server configuration: %s",
			       r->filename);
	}

	return ret;
}



static int
find_allowdeny (request_rec *r, array_header *a, int method)
{
	int		i;
	int		mmask;
	allowdeny *	ap;
	int		got_refererhost_name;
	int		got_refererhost_ip;
	const char *	refererhost_name;
	unsigned long	refererhost_ip;
	uri_components	uptr;

	mmask = (1 << method);
	ap = (allowdeny *) a->elts;
	got_refererhost_name = 0;
	got_refererhost_ip = 0;
	refererhost_name = NULL;
	refererhost_ip = (unsigned long) -1; /* XX is INADDR_NONE better? */

	/* XX allow/deny referer by the following fields from Referer header:
		uptr.scheme
		uptr.hostinfo
		uptr.path
		uptr.query
		uptr.fragment
		uptr.user
		uptr.password
		uptr.port_str
		uptr.port */

	for (i = 0; i < a->nelts; i++) {
		/* enter here only if there are items in deny_referer
		  or allow_referer */

		if (!(mmask & ap[i].limited))
			continue;

		switch (ap[i].type) {
		/* XX Not enviroment variable check implementation needed
		case T_ENV:
			if (ap_table_get (r->subprocess_env, ap[i].x.from)) {
				return 1;
			}
			break;
		*/

		case T_ALL:
			return 1;

		case T_IP:
			if (!got_refererhost_ip) {
				ap_parse_uri_components (r->pool,
							 ap_table_get
							 (r->headers_in,
							  "Referer"),
							 &uptr);
				if (!is_ip (uptr.hostname)) {
					/* XX resolv the domain name */
					got_refererhost_ip = 1;
				} else {
					refererhost_ip = ap_inet_addr
						(uptr.hostname);
					if (refererhost_ip == INADDR_NONE) {
						got_refererhost_ip = 1;
					} else {
						got_refererhost_ip = 2;
					}
				}

			}

			if ((got_refererhost_ip == 2)
			    && (ap[i].x.ip.net != INADDR_NONE)
			    && (refererhost_ip & ap[i].x.ip.mask)
			       == ap[i].x.ip.net)
				return 1;
			break;

		case T_HOST:
			if (!got_refererhost_name) {
				ap_parse_uri_components (r->pool,
							 ap_table_get
							 (r->headers_in,
							  "Referer"),
							 &uptr);
				refererhost_name = uptr.hostname;
				if (refererhost_name == NULL)
					got_refererhost_name = 1;
				else if (is_ip (refererhost_name))
					/* XX reverse the domain name */
					got_refererhost_name = 1;
				else
					got_refererhost_name = 2;
			}

			if ((got_refererhost_name == 2)
			    && in_domain (ap[i].x.from, refererhost_name))
				return 1;
			break;

		case T_FAIL:
			/* XX do nothing? */
			break;
		}
	}

	return 0;
}


static int
is_ip (const char *host)
{
	/* this just tests if it matches [\d.]* */
	/* XX is a better test needed? */
	while ((*host == '.') || ap_isdigit (*host))
		host++;

	return (*host == '\0');
}


static int
in_domain (const char *domain, const char *what)
{
	int dl;
	int wl;

	dl = strlen (domain);
	wl = strlen (what);

	if ((wl - dl) >= 0) {
		if (strcasecmp (domain, &what[wl - dl]))
			return 0;

		/* Make sure we matched an *entire* subdomain --- if the user
		   said 'allow from good.com', we don't want people from
		   nogood.com to be able to get in. */
		if (wl == dl)
			return 1; /* matched whole thing */
		else
			return (domain[0] == '.' || what[wl - dl - 1] == '.');
	} else
		return 0;
}


static char it_is_an_allow;
static const command_rec access_referer_cmds_mod[] = {
	{ "allow_referer",
		allow_referer_cmd,
		&it_is_an_allow,
		OR_LIMIT,
		ITERATE2,
		"'from' followed by hostnames or IP-address wildcards" },
	{ "deny_referer",
		allow_referer_cmd,
		NULL, /* it is not an allow */
		OR_LIMIT,
		ITERATE2,
		"'from' followed by hostnames or IP-address wildcards" },
	{ "order_referer",
		order_referer_cmd,
		NULL,
		OR_LIMIT,
		TAKE1,
		"'allow_referer,deny_referer', 'deny_referer,allow_referer', or 'mutual-failure'" },
	{ "default_referer",
		default_referer_cmd,
		NULL,
		OR_LIMIT,
		TAKE1,
		"'allow_referer' or 'deny_referer'" },
	{ NULL }
};


module MODULE_VAR_EXPORT access_referer_module =
{
	STANDARD_MODULE_STUFF,
	NULL,					/* initializer */
	create_access_referer_dir_config_mod,	/* dir config creater */
	merge_access_referer_dir_config_mod,	/* dir merger
						   (default is to override) */
	NULL,					/* server config */
	NULL,					/* merge server config */
	access_referer_cmds_mod,		/* directives */
	NULL,					/* handlers */
	NULL,					/* filename translation */
	NULL,					/* check_user_id */
	NULL,					/* check auth */
	check_dir_access_referer_mod,		/* check access by referer */
	NULL,					/* type_checker */
	NULL,					/* fixups */
	NULL,					/* logger */
	NULL,					/* header parser */
	NULL,					/* child_init */
	NULL,					/* child_exit */
	NULL					/* post read-request */
};

