/*
 * Copyright (C) 2000-2002 David Jao <djao@dominia.org>
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use, copy,
 * modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice, this permission notice, and the
 * following disclaimer shall be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

#include "httpd.h"
#include "http_config.h"
#include "http_request.h"
#include "http_protocol.h"
#include "http_core.h"
#include "http_main.h"
#include "http_log.h"
#include "ap_mpm.h"
#include "apr_strings.h"
#include "scoreboard.h"

#define MODULE_NAME "mod_limitipconn"
#define MODULE_VERSION "0.22"

module AP_MODULE_DECLARE_DATA limitipconn_module;

int server_limit, thread_limit;

typedef struct {
    signed int limit;       /* max number of connections per IP */
    apr_array_header_t *no_limit;   /* array of MIME types exempt from limit
				 checking */
    apr_array_header_t *excl_limit; /* array of MIME types to limit check; all
				 other types are exempt */
} limitipconn_dir_config;

static void *limitipconn_create_dir_config(apr_pool_t *p, char *path)
{
    limitipconn_dir_config *cfg = (limitipconn_dir_config *)
	    apr_pcalloc(p, sizeof (*cfg));

    /* default configuration: no limit, and both arrays are empty */
    cfg->limit = 0;
    cfg->no_limit = apr_array_make(p, 0, sizeof(char *));
    cfg->excl_limit = apr_array_make(p, 0, sizeof(char *));

    return (void *) cfg;
}

static int limitipconn_handler(request_rec *r)
{
    /* get configuration information */
    limitipconn_dir_config *cfg = (limitipconn_dir_config *)
	    ap_get_module_config(r->per_dir_config, &limitipconn_module);

    /* convert Apache arrays to normal C arrays */
    char **nolim = (char **) cfg->no_limit->elts;
    char **exlim = (char **) cfg->excl_limit->elts;

    const char *address;

    /* loop index variables */
    int i;
    int j;

    /* running count of number of connections from this address */
    int ip_count = 0;

    /* Content-type of the current request */
    const char *content_type;

    /* scoreboard data structure */
    worker_score *ws_record;

    /* We decline to handle subrequests: otherwise, in the next step we
     * could get into an infinite loop. */
    if (!ap_is_initial_req(r)) {
	return DECLINED;
    }

    /* Look up the Content-type of this request. We need a subrequest
     * here since this module might be called before the URI has been
     * translated into a MIME type. */
    content_type = ap_sub_req_lookup_uri(r->uri, r, NULL)->content_type;

    /* If there's no Content-type, use the default. */
    if (!content_type) {
	content_type = ap_default_type(r);
    }

#ifdef RECORD_FORWARD
	if ((address = apr_table_get(r->headers_in, "X-Forwarded-For")) == NULL)
#endif
    address = r->connection->remote_ip;

    /* A limit value of 0 by convention means no limit. */
    if (cfg->limit == 0) {
	return OK;
    }

    /* Cycle through the exempt list; if our content_type is exempt,
     * return OK */
    for (i = 0; i < cfg->no_limit->nelts; i++) {
	if ((ap_strcasecmp_match(content_type, nolim[i]) == 0)
	    || (strncmp(nolim[i], content_type, strlen(nolim[i])) == 0)) {
	    return OK;
	}
    }

    /* Cycle through the exclusive list, if it exists; if our MIME type
     * is not present, bail out */
    if (cfg->excl_limit->nelts) {
	int excused = 1;
	for (i = 0; i < cfg->excl_limit->nelts; i++) {
	    if ((ap_strcasecmp_match(content_type, exlim[i]) == 0)
		|| (strncmp(exlim[i], content_type, strlen(exlim[i])) == 0)) {
		excused = 0;
	    }
	}
	if (excused) {
	    return OK;
	}
    }

    /* Count up the number of connections we are handling right now from
     * this IP address */
    for (i = 0; i < server_limit; ++i) {
      for (j = 0; j < thread_limit; ++j) {
	ws_record = ap_get_scoreboard_worker(i, j);
	switch (ws_record->status) {
	    case
	SERVER_BUSY_READ:
	    case
	SERVER_BUSY_WRITE:
	    case
	SERVER_BUSY_KEEPALIVE:
	    case
	SERVER_BUSY_DNS:
	    case
	SERVER_GRACEFUL:
        if ((strcmp(address, ws_record->client) == 0)
#ifdef RECORD_FORWARD
            || (strcmp(address, ws_record->fwdclient) == 0)
#endif
            ) {
		ip_count++;
	    }
	    break;
	    case
	SERVER_DEAD:
	    case
	SERVER_READY:
	    case
	SERVER_STARTING:
	    case
	SERVER_BUSY_LOG:
	    case
	SERVER_IDLE_KILL:
	    case
	SERVER_CLOSING:
	    break;
	}
      }
    }

    if ((ip_count > cfg->limit) && (cfg->limit)) {
      ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Rejecting client at %s", 
		   address);
      /* set an environment variable */
      apr_table_setn(r->subprocess_env, "LIMITIP", "1");
      /* return 503 */
      return HTTP_SERVICE_UNAVAILABLE;
    } else {
	return OK;
    }
}

/* Parse the MaxConnPerIP directive */
static const char *limit_config_cmd(cmd_parms *parms, void *mconfig,
				    const char *arg)
{
    limitipconn_dir_config *cfg = (limitipconn_dir_config *) mconfig;

    signed long int limit = strtol(arg, (char **) NULL, 10);

    /* No reasonable person would want more than 2^16. Better would be
       to use LONG_MAX but that causes portability problems on win32 */
    if ((limit > 65535) || (limit < 0)) {
	return "Integer overflow or invalid number";
    }

    cfg->limit = limit;
    return NULL;
}

/* Parse the NoIPLimit directive */
static const char *no_limit_config_cmd(cmd_parms *parms, void *mconfig,
				       const char *arg)
{
    limitipconn_dir_config *cfg = (limitipconn_dir_config *) mconfig;

    *(char **) apr_array_push(cfg->no_limit) = apr_pstrdup(parms->pool, arg);
    return NULL;
}

/* Parse the OnlyIPLimit directive */
static const char *excl_limit_config_cmd(cmd_parms *parms, void *mconfig,
					 const char *arg)
{
    limitipconn_dir_config *cfg = (limitipconn_dir_config *) mconfig;

    *(char **) apr_array_push(cfg->excl_limit) = apr_pstrdup(parms->pool, arg);
    return NULL;
}

/* Array describing structure of configuration directives */
static command_rec limitipconn_cmds[] = {
    AP_INIT_TAKE1("MaxConnPerIP", limit_config_cmd, NULL, OR_LIMIT,
     "maximum simultaneous connections per IP address"),
    AP_INIT_ITERATE("NoIPLimit", no_limit_config_cmd, NULL, OR_LIMIT,
     "MIME types for which limit checking is disabled"),
    AP_INIT_ITERATE("OnlyIPLimit", excl_limit_config_cmd, NULL, OR_LIMIT,
     "restrict limit checking to these MIME types only"),
    {NULL},
};

/* Set up startup-time initialization */
static int limitipconn_init(apr_pool_t *p, apr_pool_t *plog, apr_pool_t *ptemp, server_rec *s)
{
    ap_log_error(APLOG_MARK, APLOG_INFO, 0, s,
                 MODULE_NAME " " MODULE_VERSION " started.");
    ap_mpm_query(AP_MPMQ_HARD_LIMIT_THREADS, &thread_limit);
    ap_mpm_query(AP_MPMQ_HARD_LIMIT_DAEMONS, &server_limit);
    return OK;
}

static void register_hooks(apr_pool_t *p)
{
    ap_hook_access_checker(limitipconn_handler,NULL,NULL,APR_HOOK_MIDDLE);
    ap_hook_post_config(limitipconn_init, NULL, NULL, APR_HOOK_MIDDLE);
}

module AP_MODULE_DECLARE_DATA limitipconn_module = {
    STANDARD20_MODULE_STUFF,
    limitipconn_create_dir_config, /* create per-dir config structures */
    NULL,			/* merge  per-dir    config structures */
    NULL,			/* create per-server config structures */
    NULL,			/* merge  per-server config structures */
    limitipconn_cmds,		/* table of config file commands       */
    register_hooks
};
