/* Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * mod_setenvdnsbl.c
 * Set environment variables based on DNSBL result of target host
 *
 * Takanori Ito <necottie@nesitive.net> 12 Jan 2007
 * Based on mod_setenvif by Paul Sutton <paul@ukweb.com>
 */

#include "apr.h"
#include "apr_strings.h"
#include "apr_strmatch.h"

#define APR_WANT_STRFUNC
#include "apr_want.h"

#include "ap_config.h"
#include "httpd.h"
#include "http_config.h"
#include "http_core.h"
#include "http_log.h"
#include "http_protocol.h"


enum special {
    SPECIAL_NOT,
    SPECIAL_REMOTE_ADDR,
    SPECIAL_SERVER_ADDR
};
typedef struct {
    char *name;                 /* header name */
    const char* dnsbl;
    apr_table_t *features;      /* env vars to set (or unset) */
    enum special special_type;  /* is it a "special" header ? */
} sednsbl_entry;

typedef struct {
    apr_array_header_t *conditionals;
} sednsbl_cfg_rec;

module AP_MODULE_DECLARE_DATA setenvdnsbl_module;

/*
 * These routines, the create- and merge-config functions, are called
 * for both the server-wide and the per-directory contexts.  This is
 * because the different definitions are used at different times; the
 * server-wide ones are used in the post-read-request phase, and the
 * per-directory ones are used during the header-parse phase (after
 * the URI has been mapped to a file and we have anything from the
 * .htaccess file and <Directory> and <Files> containers).
 */
static void *create_setenvdnsbl_config(apr_pool_t *p)
{
    sednsbl_cfg_rec *new = (sednsbl_cfg_rec *) apr_palloc(p, sizeof(sednsbl_cfg_rec));

    new->conditionals = apr_array_make(p, 20, sizeof(sednsbl_entry));
    return (void *) new;
}

static void *create_setenvdnsbl_config_svr(apr_pool_t *p, server_rec *dummy)
{
    return create_setenvdnsbl_config(p);
}

static void *create_setenvdnsbl_config_dir(apr_pool_t *p, char *dummy)
{
    return create_setenvdnsbl_config(p);
}

static void *merge_setenvdnsbl_config(apr_pool_t *p, void *basev, void *overridesv)
{
    sednsbl_cfg_rec *a = apr_pcalloc(p, sizeof(sednsbl_cfg_rec));
    sednsbl_cfg_rec *base = basev, *overrides = overridesv;

    a->conditionals = apr_array_append(p, base->conditionals,
                                       overrides->conditionals);
    return a;
}

/*
 * any non-NULL magic constant will do... used to indicate if AP_REG_ICASE should
 * be used
 */
#define SEDNSBL_MAGIC_HEIRLOOM "setenvdnsbl-phase-flag"

static int is_header_regex(apr_pool_t *p, const char* name)
{
    /* If a Header name contains characters other than:
     *    -,_,[A-Z\, [a-z] and [0-9].
     * assume the header name is a regular expression.
     */
    const apr_strmatch_pattern *preg = apr_strmatch_precompile(p, "^[-A-Za-z0-9_]*$", 0);
    ap_assert(preg != NULL);

    if ((preg->compare)(preg, name, strlen(name))) {
        return 1;
    }

    return 0;
}

/* If the input string does not take advantage of regular
 * expression metacharacters, return a pointer to an equivalent
 * string that can be searched using apr_strmatch().  (The
 * returned string will often be the input string.  But if
 * the input string contains escaped characters, the returned
 * string will be a copy with the escapes removed.)
 */
static const char *non_regex_pattern(apr_pool_t *p, const char *s)
{
    const char *src = s;
    int escapes_found = 0;
    int in_escape = 0;

    while (*src) {
        switch (*src) {
        case '^':
        case '.':
        case '$':
        case '|':
        case '(':
        case ')':
        case '[':
        case ']':
        case '*':
        case '+':
        case '?':
        case '{':
        case '}':
            if (!in_escape) {
                return NULL;
            }
            in_escape = 0;
            break;
        case '\\':
            if (!in_escape) {
                in_escape = 1;
                escapes_found = 1;
            }
            else {
                in_escape = 0;
            }
            break;
        default:
            if (in_escape) {
                return NULL;
            }
            break;
        }
        src++;
    }
    if (!escapes_found) {
        return s;
    }
    else {
        char *unescaped = (char *)apr_palloc(p, src - s + 1);
        char *dst = unescaped;
        src = s;
        do {
            if (*src == '\\') {
                src++;
            }
        } while ((*dst++ = *src++));
        return unescaped;
    }
}

static const char *add_setenvdnsbl_core(cmd_parms *cmd, void *mconfig,
                                     char *fname, const char *args)
{
    char *regex;
    const char *simple_pattern;
    const char *feature;
    sednsbl_cfg_rec *sconf;
    sednsbl_entry *new;
    sednsbl_entry *entries;
    char *var;
    int i;
    int beenhere = 0;
    char *dnsbl;
    apr_status_t rv;
    char msgbuf[255];

    /*
     * Determine from our context into which record to put the entry.
     * cmd->path == NULL means we're in server-wide context; otherwise,
     * we're dealing with a per-directory setting.
     */
    sconf = (cmd->path != NULL)
      ? (sednsbl_cfg_rec *) mconfig
      : (sednsbl_cfg_rec *) ap_get_module_config(cmd->server->module_config,
                                               &setenvdnsbl_module);
    entries = (sednsbl_entry *) sconf->conditionals->elts;
    /* get regex */
    dnsbl = ap_getword_conf(cmd->pool, &args);
    if (!*dnsbl) {
        return apr_pstrcat(cmd->pool, "Missing domain for ",
                           cmd->cmd->name, NULL);
    }

    
    /*
     * If we've already got a sednsbl_entry with the same name we want to
     * just copy the name pointer... so that later on we can compare
     * two header names just by comparing the pointers.
     */
    for (i = 0; i < sconf->conditionals->nelts; ++i) {
        new = &entries[i];
        if (!strcasecmp(new->name, fname)) {
            fname = new->name;
            break;
        }
    }

    /* if the last entry has an identical headername and regex then
     * merge with it
     */
    i = sconf->conditionals->nelts - 1;
    if (1/*i < 0
        || entries[i].name != fname*/) {

        /* no match, create a new entry */
        new = apr_array_push(sconf->conditionals);
        new->name = fname;
        new->dnsbl = dnsbl;
        new->features = apr_table_make(cmd->pool, 2);

        if (!strcasecmp(fname, "remote_addr")) {
            new->special_type = SPECIAL_REMOTE_ADDR;
        }
        else if (!strcasecmp(fname, "server_addr")) {
            new->special_type = SPECIAL_SERVER_ADDR;
        }
        else {
            new->special_type = SPECIAL_NOT;
        }
    }
    else {
        new = &entries[i];
    }

    for ( ; ; ) {
        feature = ap_getword_conf(cmd->pool, &args);
        if (!*feature) {
            break;
        }
        beenhere++;

        var = ap_getword(cmd->pool, &feature, '=');
        if (*feature) {
            apr_table_setn(new->features, var, feature);
        }
        else if (*var == '!') {
            apr_table_setn(new->features, var + 1, "!");
        }
        else if (*var == '*') {
            apr_table_setn(new->features, var + 1, "*");
        }
        else {
            apr_table_setn(new->features, var, "1");
        }
    }

    if (!beenhere) {
        return apr_pstrcat(cmd->pool, "Missing envariable expression for ",
                           cmd->cmd->name, NULL);
    }

    return NULL;
}

static const char *add_setenvdnsbl(cmd_parms *cmd, void *mconfig,
                                const char *args)
{
    char *fname;

    /* get header name */
    fname = ap_getword_conf(cmd->pool, &args);
    if (!*fname) {
        return apr_pstrcat(cmd->pool, "Missing header-field name for ",
                           cmd->cmd->name, NULL);
    }
    return add_setenvdnsbl_core(cmd, mconfig, fname, args);
}

static const command_rec setenvdnsbl_module_cmds[] =
{
    AP_INIT_RAW_ARGS("SetEnvIfDNSBL", add_setenvdnsbl, NULL, OR_FILEINFO,
                     "A header-name, ip address and net mask."),
    { NULL },
};

/*
 * This routine gets called at two different points in request processing:
 * once before the URI has been translated (during the post-read-request
 * phase) and once after (during the header-parse phase).  We use different
 * config records for the two different calls to reduce overhead (by not
 * re-doing the server-wide settings during directory processing), and
 * signal which call it is by having the earlier one pass a flag to the
 * later one.
 */
static int match_headers(request_rec *r)
{
    sednsbl_cfg_rec *sconf;
    sednsbl_entry *entries;
    const apr_table_entry_t *elts;
    apr_sockaddr_t *val;
    const char *ip_str = NULL;
    int i, j;
    char *last_name;
    apr_status_t rv;

	if (r->method_number != M_POST) {
		return DECLINED;
	}

    if (!ap_get_module_config(r->request_config, &setenvdnsbl_module)) {
        ap_set_module_config(r->request_config, &setenvdnsbl_module,
                             SEDNSBL_MAGIC_HEIRLOOM);
        sconf  = (sednsbl_cfg_rec *) ap_get_module_config(r->server->module_config,
                                                      &setenvdnsbl_module);
    }
    else {
        sconf = (sednsbl_cfg_rec *) ap_get_module_config(r->per_dir_config,
                                                     &setenvdnsbl_module);
    }

    entries = (sednsbl_entry *) sconf->conditionals->elts;
    last_name = NULL;
    val = NULL;
    for (i = 0; i < sconf->conditionals->nelts; ++i) {
        sednsbl_entry *b = &entries[i];

        /* Optimize the case where a bunch of directives in a row use the
         * same header.  Remember we don't need to strcmp the two header
         * names because we made sure the pointers were equal during
         * configuration.
         */
        if (b->name != last_name) {
            last_name = b->name;
            switch (b->special_type) {
            case SPECIAL_REMOTE_ADDR:
                val = r->connection->remote_addr;
                ip_str = r->connection->remote_ip;
                break;
            case SPECIAL_SERVER_ADDR:
                val = r->connection->local_addr;
                ip_str = r->connection->local_ip;
                break;
            case SPECIAL_NOT:
                /* Not matching against a regex */
                ip_str = apr_table_get(r->headers_in, b->name);
                if (ip_str == NULL) {
                    ip_str = apr_table_get(r->subprocess_env, b->name);
                }
                rv = apr_sockaddr_info_get(&val, ip_str, APR_INET, 0, 0, r->pool);
                if(rv == APR_EINVAL) {
                    ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "ip address excepted in \'%s\'", ip_str);
                    val = NULL;
                }
                if(rv != APR_SUCCESS) {
                    char buf[255];
                    apr_strerror(rv, buf, sizeof buf);
                    ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, buf);
                    val = NULL;
                }
            }
        }

        if (val!=NULL && val->family==APR_INET) {
            const apr_byte_t *sin = (apr_byte_t*)val->ipaddr_ptr;
            int dot = (b->dnsbl[strlen(b->dnsbl)-1]=='.');
            char* dnsbl_request = apr_psprintf(r->pool, "%d.%d.%d.%d.%s%c", sin[3], sin[2], sin[1], sin[0], b->dnsbl, !dot?'.':'\0');
            apr_sockaddr_t *addr;
            apr_status_t rv;
//            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "Requesting '%s' for '%s'", dnsbl_request, last_name);
            rv = apr_sockaddr_info_get(&addr, dnsbl_request, APR_INET, 0, 0, r->pool);
            
            if(rv == APR_SUCCESS) {
                const apr_byte_t *in = (apr_byte_t*)addr->ipaddr_ptr;
                ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "mod_setenvdnsbl: result of request '%s' for '%s' is '%d.%d.%d.%d'", dnsbl_request, last_name, in[0], in[1], in[2], in[3]);
            }
            
            if(rv == APR_SUCCESS) {
                int is_blocked = ((ntohl(*(apr_uint32_t*)addr->ipaddr_ptr) & 0xff000000u) == 0x7f000000u);
                const apr_array_header_t *arr = apr_table_elts(b->features);
                sin = (apr_byte_t*)addr->ipaddr_ptr;
                ip_str = apr_psprintf(r->pool, "%d.%d.%d.%d", sin[0], sin[1], sin[2], sin[3]);
                ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "DNSBL result is '%s'", ip_str);
                elts = (const apr_table_entry_t *) arr->elts;
                
                for (j = 0; j < arr->nelts; ++j) {
                    if (*(elts[j].val) == '*') {
                        apr_table_set(r->subprocess_env, elts[j].key, ip_str);
                    } else if (is_blocked) {
                        if (*(elts[j].val) == '!') {
                            apr_table_unset(r->subprocess_env, elts[j].key);
                        }
                        else {
                            apr_table_setn(r->subprocess_env, elts[j].key,
                                               elts[j].val);
                        }
                    }
                }
            }
        }
    }

    return DECLINED;
}

static void register_hooks(apr_pool_t *p)
{
    ap_hook_header_parser(match_headers, NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_post_read_request(match_headers, NULL, NULL, APR_HOOK_MIDDLE);
}

module AP_MODULE_DECLARE_DATA setenvdnsbl_module =
{
    STANDARD20_MODULE_STUFF,
    create_setenvdnsbl_config_dir, /* dir config creater */
    merge_setenvdnsbl_config,      /* dir merger --- default is to override */
    create_setenvdnsbl_config_svr, /* server config */
    merge_setenvdnsbl_config,      /* merge server configs */
    setenvdnsbl_module_cmds,       /* command apr_table_t */
    register_hooks              /* register hooks */
};

