/*
 *   Copyright (c) 2004, 2005, 2006 NOVELL (All rights reserved)
 *   Copyright (c) 2014 Canonical, Ltd. (All rights reserved)
 *
 *    The mod_apparmor module is licensed under the terms of the GNU
 *    Lesser General Public License, version 2.1. Please see the file
 *    COPYING.LGPL.
 *
 * mod_apparmor - (apache 2.0.x)
 * Author: Steve Beattie <steve@nxnw.org>
 *
 * This currently only implements change_hat functionality, but could be
 * extended for other stuff we decide to do.
 */

#include "ap_config.h"
#include "httpd.h"
#include "http_config.h"
#include "http_request.h"
#include "http_log.h"
#include "http_main.h"
#include "http_protocol.h"
#include "util_filter.h"
#include "apr.h"
#include "apr_strings.h"
#include "apr_lib.h"

#include <sys/apparmor.h>
#include <unistd.h>

/* #define DEBUG */

#ifndef unused_
#define unused_ __attribute__ ((unused))
#endif

/* should the following be configurable? */
#define DEFAULT_HAT "HANDLING_UNTRUSTED_INPUT"
#define DEFAULT_URI_HAT "DEFAULT_URI"

/* Compatibility with apache 2.2 */
#if AP_SERVER_MAJORVERSION_NUMBER == 2 && AP_SERVER_MINORVERSION_NUMBER < 3
  #define APLOG_TRACE1 APLOG_DEBUG
  server_rec *ap_server_conf = NULL;
#endif

#ifdef APLOG_USE_MODULE
  APLOG_USE_MODULE(apparmor);
#endif
module AP_MODULE_DECLARE_DATA apparmor_module;

static unsigned long magic_token = 0;
static int inside_default_hat = 0;

typedef struct {
        const char *hat_name;
        char *path;
} apparmor_dir_cfg;

typedef struct {
        const char *hat_name;
        int is_initialized;
} apparmor_srv_cfg;

/* aa_init() gets invoked in the post_config stage of apache.
 * Unfortunately, apache reads its config once when it starts up, then
 * it re-reads it when goes into its restart loop, where it starts it's
 * children. This means we cannot call change_hat here, as the modules
 * memory will be wiped out, and the magic_token will be lost, so apache
 * wouldn't be able to change_hat back out. */
static int
aa_init(apr_pool_t *p, unused_ apr_pool_t *plog, unused_ apr_pool_t *ptemp, unused_ server_rec *s)
{
    apr_file_t *file;
    apr_size_t size = sizeof(magic_token);
    int ret;

    ret = apr_file_open (&file, "/dev/urandom", APR_READ, APR_OS_DEFAULT, p);
    if (!ret) {
        apr_file_read(file, (void *) &magic_token, &size);
        apr_file_close(file);
    } else {
        ap_log_error(APLOG_MARK, APLOG_ERR, errno, ap_server_conf,
                     "Failed to open /dev/urandom");
    }
    ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, ap_server_conf,
                 "Opened /dev/urandom successfully");

    return OK;
}

/* As each child starts up, we'll change_hat into a default hat, mostly
 * to protect ourselves from bugs in parsing network input, but before
 * we change_hat to the uri specific hat. */
static void
aa_child_init(unused_ apr_pool_t *p, unused_ server_rec *s)
{
    int ret;

    ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, ap_server_conf,
                 "init: calling change_hat with '%s'", DEFAULT_HAT);
    ret = aa_change_hat(DEFAULT_HAT, magic_token);
    if (ret < 0) {
        ap_log_error(APLOG_MARK, APLOG_ERR, errno, ap_server_conf,
                     "Failed to change_hat to '%s'", DEFAULT_HAT);
    } else {
        inside_default_hat = 1;
    }
}

static void
debug_dump_uri(request_rec *r)
{
    apr_uri_t *uri = &r->parsed_uri;
    if (uri)
        ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "Dumping uri info "
                      "scheme='%s' host='%s' path='%s' query='%s' fragment='%s'",
                      uri->scheme, uri->hostname, uri->path, uri->query,
                      uri->fragment);
    else
        ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "Asked to dump NULL uri");

}

/*
   aa_enter_hat will attempt to change_hat in the following order:
   (1) to a hatname in a location directive
   (2) to the server name or a defined per-server default
   (3) to the server name + "-" + uri
   (4) to the uri
   (5) to DEFAULT_URI
   (6) back to the parent profile
*/
static int
aa_enter_hat(request_rec *r)
{
    int aa_ret = -1;
    apparmor_dir_cfg *dcfg = (apparmor_dir_cfg *)
                    ap_get_module_config(r->per_dir_config, &apparmor_module);
    apparmor_srv_cfg *scfg = (apparmor_srv_cfg *)
                    ap_get_module_config(r->server->module_config, &apparmor_module);
    const char *aa_hat_array[6] = { NULL, NULL, NULL, NULL, NULL, NULL };
    int i = 0;
    char *aa_label, *aa_mode, *aa_hat;
    const char *vhost_uri;

    debug_dump_uri(r);
    ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "aa_enter_hat (%s) n:0x%lx p:0x%lx main:0x%lx",
                  dcfg->path, (unsigned long) r->next, (unsigned long) r->prev,
                  (unsigned long) r->main);

    /* We only call change_hat for the main request, not subrequests */
    if (r->main)
        return OK;

    if (inside_default_hat) {
        aa_change_hat(NULL, magic_token);
        inside_default_hat = 0;
    }

    if (dcfg != NULL && dcfg->hat_name != NULL) {
        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                      "[dcfg] adding hat '%s' to aa_change_hat vector", dcfg->hat_name);
        aa_hat_array[i++] = dcfg->hat_name;
    }

    if (scfg) {
        ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "Dumping scfg info: "
                      "scfg='0x%lx' scfg->hat_name='%s'",
                      (unsigned long) scfg, scfg->hat_name);
    } else {
        ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "scfg is null");
    }
    if (scfg != NULL) {
        if (scfg->hat_name != NULL) {
            ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                          "[scfg] adding hat '%s' to aa_change_hat vector", scfg->hat_name);
            aa_hat_array[i++] = scfg->hat_name;
        } else {
            ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                          "[scfg] adding server_name '%s' to aa_change_hat vector",
                          r->server->server_hostname);
            aa_hat_array[i++] = r->server->server_hostname;
        }

        vhost_uri = apr_pstrcat(r->pool, r->server->server_hostname, "-", r->uri, NULL);
        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                      "[vhost+uri] adding vhost+uri '%s' to aa_change_hat vector", vhost_uri);
        aa_hat_array[i++] = vhost_uri;
    }

    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                  "[uri] adding uri '%s' to aa_change_hat vector", r->uri);
    aa_hat_array[i++] = r->uri;

    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                  "[default] adding '%s' to aa_change_hat vector", DEFAULT_URI_HAT);
    aa_hat_array[i++] = DEFAULT_URI_HAT;

    aa_ret = aa_change_hatv(aa_hat_array, magic_token);
    if (aa_ret < 0) {
        ap_log_rerror(APLOG_MARK, APLOG_WARNING, errno, r, "aa_change_hatv call failed");
    }

    /* Check to see if a defined AAHatName or AADefaultHatName would
     * apply, but wasn't the hat we landed up in; report a warning if
     * that's the case. */
    aa_ret = aa_getcon(&aa_label, &aa_mode);
    if (aa_ret < 0) {
        ap_log_rerror(APLOG_MARK, APLOG_WARNING, errno, r, "aa_getcon call failed");
    } else {
        ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r,
                      "AA checks: aa_getcon result is '%s', mode '%s'", aa_label, aa_mode);
        /* TODO: use libapparmor get hat_name fn here once it is implemented */
        aa_hat = strstr(aa_label, "//");
        if (aa_hat != NULL && strcmp(aa_mode, "enforce") == 0) {
            aa_hat += 2;  /* skip "//" */
            ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r,
                          "AA checks: apache is in hat '%s', mode '%s'", aa_hat, aa_mode);
            if (dcfg != NULL && dcfg->hat_name != NULL) {
                if (strcmp(aa_hat, dcfg->hat_name) != 0)
                    ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                        "AAHatName '%s' applies, but does not appear to be a hat in the apache apparmor policy",
                        dcfg->hat_name);
            } else if (scfg != NULL && scfg->hat_name != NULL) {
                if (strcmp(aa_hat, scfg->hat_name) != 0 &&
                    strcmp(aa_hat, r->uri) != 0)
                    ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                        "AADefaultHatName '%s' applies, but does not appear to be a hat in the apache apparmor policy",
                        scfg->hat_name);
            }
        }
        free(aa_label);
    }

    return OK;
}

static int
aa_exit_hat(request_rec *r)
{
    int aa_ret;
    apparmor_dir_cfg *dcfg = (apparmor_dir_cfg *)
                    ap_get_module_config(r->per_dir_config, &apparmor_module);
    /* apparmor_srv_cfg *scfg = (apparmor_srv_cfg *)
                    ap_get_module_config(r->server->module_config, &apparmor_module); */
    ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "exiting change_hat: dir hat %s dir path %s",
                  dcfg->hat_name, dcfg->path);

    /* can convert the following back to aa_change_hat() when the
     * aa_change_hat() bug addressed in trunk commit 2329 lands in most
     * system libapparmors */
    aa_change_hatv(NULL, magic_token);

    aa_ret = aa_change_hat(DEFAULT_HAT, magic_token);
    if (aa_ret < 0) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, errno, r,
                      "Failed to change_hat to '%s'", DEFAULT_HAT);
    } else {
        inside_default_hat = 1;
    }

    return OK;
}

static const char *
aa_cmd_ch_path(unused_ cmd_parms *cmd, unused_ void *mconfig, const char *parm1)
{
    ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, ap_server_conf, "directory config change hat %s",
                 parm1 ? parm1 : "DEFAULT");
    apparmor_dir_cfg *dcfg = mconfig;
    if (parm1 != NULL) {
        dcfg->hat_name = parm1;
    } else {
        dcfg->hat_name = "DEFAULT";
    }
    return NULL;
}

static int path_warn_once;

static const char *
immunix_cmd_ch_path(cmd_parms *cmd, void *mconfig, const char *parm1)
{
    if (path_warn_once == 0) {
        ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, ap_server_conf, "ImmHatName is "
                     "deprecated, please use AAHatName instead");
        path_warn_once = 1;
    }
    return aa_cmd_ch_path(cmd, mconfig, parm1);
}

static const char *
aa_cmd_ch_srv(cmd_parms *cmd, unused_ void *mconfig, const char *parm1)
{
    ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, ap_server_conf, "server config change hat %s",
                            parm1 ? parm1 : "DEFAULT");
    apparmor_srv_cfg *scfg = (apparmor_srv_cfg *)
            ap_get_module_config(cmd->server->module_config, &apparmor_module);
    if (parm1 != NULL) {
        scfg->hat_name = parm1;
    } else {
        scfg->hat_name = "DEFAULT";
    }
    return NULL;
}

static int srv_warn_once;

static const char *
immunix_cmd_ch_srv(cmd_parms *cmd, void *mconfig, const char *parm1)
{
    if (srv_warn_once == 0) {
        ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, ap_server_conf, "ImmDefaultHatName is "
                     "deprecated, please use AADefaultHatName instead");
        srv_warn_once = 1;
    }
    return aa_cmd_ch_srv(cmd, mconfig, parm1);
}

static void *
aa_create_dir_config(apr_pool_t *p, char *path)
{
    apparmor_dir_cfg *newcfg = (apparmor_dir_cfg *) apr_pcalloc(p, sizeof(*newcfg));

    ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, ap_server_conf,
                 "aa_create_dir_cfg (%s)", path ? path : ":no path:");
    if (newcfg == NULL) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, ap_server_conf,
                     "aa_create_dir_config: couldn't alloc dir config");
        return NULL;
    }
    newcfg->path = apr_pstrdup(p, path ? path : ":no path:");

    return newcfg;
}

/* XXX: Should figure out an appropriate action to take here, if any

static void *
aa_merge_dir_config(apr_pool_t *p, void *parent, void *child)
{
    apparmor_dir_cfg *newcfg = (apparmor_dir_cfg *) apr_pcalloc(p, sizeof(*newcfg));

    ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, ap_server_conf, "in immunix_merge_dir ()");
    if (newcfg == NULL)
            return NULL;

    return newcfg;
}
*/

static void *
aa_create_srv_config(apr_pool_t *p, unused_ server_rec *srv)
{
    apparmor_srv_cfg *newcfg = (apparmor_srv_cfg *) apr_pcalloc(p, sizeof(*newcfg));

    ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, ap_server_conf,
                 "in aa_create_srv_config");
    if (newcfg == NULL) {
        ap_log_error(APLOG_MARK, APLOG_ERR, 0, ap_server_conf,
                     "aa_create_srv_config: couldn't alloc srv config");
        return NULL;
    }

    return newcfg;
}


static const command_rec mod_apparmor_cmds[] = {

    AP_INIT_TAKE1(
        "ImmHatName",
        immunix_cmd_ch_path,
        NULL,
        ACCESS_CONF,
        ""
    ),
    AP_INIT_TAKE1(
        "ImmDefaultHatName",
        immunix_cmd_ch_srv,
        NULL,
        RSRC_CONF,
        ""
    ),
    AP_INIT_TAKE1(
        "AAHatName",
        aa_cmd_ch_path,
        NULL,
        ACCESS_CONF,
        ""
    ),
    AP_INIT_TAKE1(
        "AADefaultHatName",
        aa_cmd_ch_srv,
        NULL,
        RSRC_CONF,
        ""
    ),
    { NULL }
};

static void
register_hooks(unused_ apr_pool_t *p)
{
    ap_hook_post_config(aa_init, NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_child_init(aa_child_init, NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_access_checker(aa_enter_hat, NULL, NULL, APR_HOOK_FIRST);
    /* ap_hook_post_read_request(aa_enter_hat, NULL, NULL, APR_HOOK_FIRST); */
    ap_hook_log_transaction(aa_exit_hat, NULL, NULL, APR_HOOK_LAST);
}

module AP_MODULE_DECLARE_DATA apparmor_module = {
    STANDARD20_MODULE_STUFF,
    aa_create_dir_config,        /* dir config creator */
    NULL,                        /* dir merger --- default is to override */
    /* immunix_merge_dir_config, */        /* dir merger --- default is to override */
    aa_create_srv_config,        /* server config */
    NULL,                        /* merge server config */
    mod_apparmor_cmds,           /* command table */
    register_hooks               /* register hooks */
};
