/* http_admin.c -- Routines for handling Cyrus admin/info requests in httpd
 *
 * Copyright (c) 1994-2015 Carnegie Mellon University.  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. The name "Carnegie Mellon University" must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission. For permission or any legal
 *    details, please contact
 *      Carnegie Mellon University
 *      Center for Technology Transfer and Enterprise Creation
 *      4615 Forbes Avenue
 *      Suite 302
 *      Pittsburgh, PA  15213
 *      (412) 268-7393, fax: (412) 268-7395
 *      innovation@andrew.cmu.edu
 *
 * 4. Redistributions of any form whatsoever must retain the following
 *    acknowledgment:
 *    "This product includes software developed by Computing Services
 *     at Carnegie Mellon University (http://www.cmu.edu/computing/)."
 *
 * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
 * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
 * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 */

/*
 * TODO:
 */

#include <config.h>

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <ctype.h>
#include <string.h>
#include <syslog.h>
#include <assert.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <sys/types.h>

#include "lib/times.h"
#include "cyr_qsort_r.h"
#include "global.h"
#include "httpd.h"
#include "http_proxy.h"
#include "../master/masterconf.h"
#include "proc.h"
#include "procinfo.h"
#include "proxy.h"
#include "ptrarray.h"
#include "time.h"
#include "tok.h"
#include "util.h"
#include "version.h"
#include "xmalloc.h"
#include "xstrlcat.h"
#include "xstrlcpy.h"

/* config.c stuff */
const char *MASTER_CONFIG_FILENAME = DEFAULT_MASTER_CONFIG_FILENAME;

/* generated headers are not necessarily in current directory */
#include "imap/http_err.h"

static time_t compile_time;
static void admin_init(struct buf *serverinfo);
static int meth_get(struct transaction_t *txn, void *params);
static int action_murder(struct transaction_t *txn);
static int action_menu(struct transaction_t *txn);
static int action_proc(struct transaction_t *txn);
static int action_df(struct transaction_t *txn);
static int action_conf(struct transaction_t *txn);


/* Namespace for admin service */
struct namespace_t namespace_admin = {
    URL_NS_ADMIN, 1, "admin", "/admin", NULL,
    http_allow_noauth_get, /*authschemes*/0,
    /*mbtype*/0,
    ALLOW_READ,
    admin_init, NULL, NULL, NULL, NULL, NULL,
    {
        { NULL,                 NULL },                 /* ACL          */
        { NULL,                 NULL },                 /* BIND         */
        { NULL,                 NULL },                 /* CONNECT      */
        { NULL,                 NULL },                 /* COPY         */
        { NULL,                 NULL },                 /* DELETE       */
        { &meth_get,            NULL },                 /* GET          */
        { &meth_get,            NULL },                 /* HEAD         */
        { NULL,                 NULL },                 /* LOCK         */
        { NULL,                 NULL },                 /* MKCALENDAR   */
        { NULL,                 NULL },                 /* MKCOL        */
        { NULL,                 NULL },                 /* MOVE         */
        { &meth_options,        NULL },                 /* OPTIONS      */
        { NULL,                 NULL },                 /* PATCH        */
        { NULL,                 NULL },                 /* POST         */
        { NULL,                 NULL },                 /* PROPFIND     */
        { NULL,                 NULL },                 /* PROPPATCH    */
        { NULL,                 NULL },                 /* PUT          */
        { NULL,                 NULL },                 /* REPORT       */
        { &meth_trace,          NULL },                 /* TRACE        */
        { NULL,                 NULL },                 /* UNBIND       */
        { NULL,                 NULL }                  /* UNLOCK       */
    }
};


static void admin_init(struct buf *serverinfo __attribute__((unused)))
{
    namespace_admin.enabled = config_httpmodules & IMAP_ENUM_HTTPMODULES_ADMIN;

    if (!namespace_admin.enabled) return;

    compile_time = calc_compile_time(__TIME__, __DATE__);
}

const struct action_t {
    const char *name;
    const char *desc;
    int (*func)(struct transaction_t *txn);
} actions[] = {
    { "",     "Available Admin Functions",  &action_menu },
    { "proc", "Currently Running Services", &action_proc },
    { "df",   "Spool Partition Disk Usage", &action_df   },
    { "conf", "Cyrus Configuration File",   &action_conf },
    { NULL, NULL, NULL }
};


/* Perform a GET/HEAD request */
static int meth_get(struct transaction_t *txn,
                    void *params __attribute__((unused)))
{
    struct request_target_t *tgt = &txn->req_tgt;
    int (*action)(struct transaction_t *txn) = NULL;
    size_t len;
    char *p;
    int i;

    if (!httpd_userid) return HTTP_UNAUTHORIZED;

    /* Admins only */
    if (!(httpd_userisadmin || httpd_userisproxyadmin)) return HTTP_FORBIDDEN;

    /* Make a working copy of target path */
    strlcpy(tgt->path, txn->req_uri->path, sizeof(tgt->path));
    p = tgt->path;

    /* Skip namespace */
    p += strlen(namespace_admin.prefix);
    if (*p == '/') *p++ = '\0';

    /* Check if we're in murder space */
    len = strcspn(p, "/");
    if (config_mupdate_server && len == 6 && !strncmp(p, "murder", len)) {
        p += len;
        if (!*p || !*++p) return action_murder(txn);

        /* Get backend server */
        len = strcspn(p, "/");
        tgt->userid = xstrndup(p, len);
        p += len;
        if (*p == '/') *p++ = '\0';
    }

    /* Get collection (action) */
    tgt->collection = p;
    p += strcspn(p, "/");
    if (*p == '/') *p++ = '\0';

    /* Find the matching action */
    for (i = 0; actions[i].name; i++) {
        if (!strcmp(tgt->collection, actions[i].name) && !*p) {
            action = actions[i].func;
            break;
        }
    }

    if (!action) return HTTP_NOT_FOUND;

    else if (tgt->userid && !config_getstring(IMAPOPT_PROXYSERVERS)) {
        /* Proxy to backend */
        struct backend *be;

        be = proxy_findserver(tgt->userid,
                              &http_protocol, httpd_userid,
                              &backend_cached, NULL, NULL, httpd_in);
        if (!be) return HTTP_UNAVAILABLE;

        return http_pipe_req_resp(be, txn);
    }

    else return action(txn);
}


/* Perform a murder action */
static int action_murder(struct transaction_t *txn)
{
    int precond;
    struct message_guid guid;
    const char *etag, *serverlist = config_getstring(IMAPOPT_SERVERLIST);
    static time_t lastmod = 0;
    unsigned level = 0;
    static struct buf resp = BUF_INITIALIZER;
    struct stat sbuf;
    time_t mtime;

    if (!serverlist) {
        /* Add HTML header */
        buf_reset(&resp);
        buf_printf_markup(&resp, level, HTML_DOCTYPE);
        buf_printf_markup(&resp, level++, "<html>");
        buf_printf_markup(&resp, level++, "<head>");
        buf_printf_markup(&resp, level, "<title>%s</title>",
                          "Available Backend Servers");
        buf_printf_markup(&resp, --level, "</head>");
        buf_printf_markup(&resp, level++, "<body>");
        buf_printf_markup(&resp, level, "<h2>%s</h2>",
                          "Error: Can not generate a list of backend servers "
                          "without <tt>serverlist</tt> option being set "
                          "in <tt>imapd.conf</tt>");

        /* Finish HTML */
        buf_printf_markup(&resp, --level, "</body>");
        buf_printf_markup(&resp, --level, "</html>");

        /* Output the HTML response */
        txn->resp_body.type = "text/html; charset=utf-8";
        write_body(HTTP_UNAVAILABLE, txn, buf_cstring(&resp), buf_len(&resp));

        return 0;
    }

    /* Generate ETag based on compile date/time of this source file,
       and the config file size/mtime */
    assert(!buf_len(&txn->buf));
    stat(config_filename, &sbuf);
    buf_printf(&txn->buf, TIME_T_FMT "-" TIME_T_FMT "-" OFF_T_FMT, compile_time,
               sbuf.st_mtime, sbuf.st_size);

    message_guid_generate(&guid, buf_cstring(&txn->buf), buf_len(&txn->buf));
    etag = message_guid_encode(&guid);
    mtime = MAX(compile_time, sbuf.st_mtime);

    /* Check any preconditions, including range request */
    txn->flags.ranges = 1;
    precond = check_precond(txn, etag, mtime);

    switch (precond) {
    case HTTP_OK:
    case HTTP_PARTIAL:
    case HTTP_NOT_MODIFIED:
        /* Fill in Etag,  Last-Modified, Expires */
        txn->resp_body.etag = etag;
        txn->resp_body.lastmod = mtime;
        txn->resp_body.maxage = 86400;  /* 24 hrs */
        txn->flags.cc |= CC_MAXAGE;

        if (precond != HTTP_NOT_MODIFIED) break;

        GCC_FALLTHROUGH

    default:
        /* We failed a precondition - don't perform the request */
        return precond;
    }

    if (txn->resp_body.lastmod > lastmod) {
        /* Add HTML header */
        const char *sep =
            txn->req_uri->path[strlen(txn->req_uri->path)-1] == '/' ? "" : "/";
        char *server;
        tok_t tok;

        buf_reset(&resp);
        buf_printf_markup(&resp, level, HTML_DOCTYPE);
        buf_printf_markup(&resp, level++, "<html>");
        buf_printf_markup(&resp, level++, "<head>");
        buf_printf_markup(&resp, level, "<title>%s</title>",
                          "Available Backend Servers");
        buf_printf_markup(&resp, --level, "</head>");
        buf_printf_markup(&resp, level++, "<body>");
        buf_printf_markup(&resp, level, "<h2>%s @ %s</h2>",
                          "Available Backend Servers", config_servername);

        /* Add servers */
        tok_init(&tok, serverlist, " \t", TOK_TRIMLEFT|TOK_TRIMRIGHT);
        while ((server = tok_next(&tok))) {
            buf_printf_markup(&resp, level, "<p><a href=\"%s%s%s\">%s</a>",
                              txn->req_uri->path, sep, server, server);
        }
        tok_fini(&tok);

        /* Finish HTML */
        buf_printf_markup(&resp, --level, "</body>");
        buf_printf_markup(&resp, --level, "</html>");

        /* Update lastmod */
        lastmod = txn->resp_body.lastmod;
    }

    /* Output the HTML response */
    txn->resp_body.type = "text/html; charset=utf-8";
    write_body(precond, txn, buf_cstring(&resp), buf_len(&resp));

    return 0;
}


/* Perform a menu action */
static int action_menu(struct transaction_t *txn)
{
    int precond;
    struct message_guid guid;
    const char *etag;
    static time_t lastmod = 0;
    unsigned level = 0, i;
    static struct buf resp = BUF_INITIALIZER;

    /* Generate ETag based on compile date/time of this source file.
     * Extend this to include config file size/mtime if we add run-time options.
     */
    assert(!buf_len(&txn->buf));
    buf_printf(&txn->buf, TIME_T_FMT, compile_time);
    message_guid_generate(&guid, buf_cstring(&txn->buf), buf_len(&txn->buf));
    etag = message_guid_encode(&guid);

    /* Check any preconditions, including range request */
    txn->flags.ranges = 1;
    precond = check_precond(txn, etag, compile_time);

    switch (precond) {
    case HTTP_OK:
    case HTTP_PARTIAL:
    case HTTP_NOT_MODIFIED:
        /* Fill in Etag,  Last-Modified, Expires */
        txn->resp_body.etag = etag;
        txn->resp_body.lastmod = compile_time;
        txn->resp_body.maxage = 86400;  /* 24 hrs */
        txn->flags.cc |= CC_MAXAGE;

        if (precond != HTTP_NOT_MODIFIED) break;

        GCC_FALLTHROUGH

    default:
        /* We failed a precondition - don't perform the request */
        return precond;
    }

    if (txn->resp_body.lastmod > lastmod) {
        /* Add HTML header */
        const char *sep =
            txn->req_uri->path[strlen(txn->req_uri->path)-1] == '/' ? "" : "/";

        buf_reset(&resp);
        buf_printf_markup(&resp, level, HTML_DOCTYPE);
        buf_printf_markup(&resp, level++, "<html>");
        buf_printf_markup(&resp, level++, "<head>");
        buf_printf_markup(&resp, level, "<title>%s</title>", actions[0].desc);
        buf_printf_markup(&resp, --level, "</head>");
        buf_printf_markup(&resp, level++, "<body>");
        buf_printf_markup(&resp, level, "<h2>%s @ %s</h2>",
                          actions[0].desc, config_servername);

        /* Add actions */
        for (i = 1; actions[i].name; i++) {
            buf_printf_markup(&resp, level, "<p><a href=\"%s%s%s\">%s</a>",
                              txn->req_uri->path, sep, actions[i].name,
                              actions[i].desc);
        }

        /* Finish HTML */
        buf_printf_markup(&resp, --level, "</body>");
        buf_printf_markup(&resp, --level, "</html>");

        /* Update lastmod */
        lastmod = txn->resp_body.lastmod;
    }

    /* Output the HTML response */
    txn->resp_body.type = "text/html; charset=utf-8";
    write_body(precond, txn, buf_cstring(&resp), buf_len(&resp));

    return 0;
}


/* Perform a proc action */
static int action_proc(struct transaction_t *txn)
{
    unsigned level = 0, i;
    struct buf *body = &txn->resp_body.payload;
    piarray_t piarray;
    time_t now = time(0);
    struct strlist *param;
    struct tm tnow;
    char key = 0;
    struct proc_columns {
        char key;
        const char *name;
    } columns[] = {
        { 'P', "PID" },
        { 'S', "Service" },
        { 'Q', "State" },
        { 'T', "Start" },
        { 'V', "VmSize" },
        { 'H', "Client" },
        { 'U', "User" },
        { 'R', "Resource" },
        { 'C', "Command" },
        { 0, NULL}
    };

    localtime_r(&now, &tnow);

    /* Setup for chunked response */
    txn->flags.te |= TE_CHUNKED;
    txn->resp_body.type = "text/html; charset=utf-8";

    /* Short-circuit for HEAD request */
    if (txn->meth == METH_HEAD) {
        response_header(HTTP_OK, txn);
        goto done;
    }

    /* Check for a sort key */
    param = hash_lookup("sort", &txn->req_qparams);
    if (param) {
        char Ukey = toupper((int) *param->s);
        for (i = 0; columns[i].key; i++) {
            if (Ukey == columns[i].key) {
                key = *param->s;
                if (isupper((int) key)) columns[i].key = tolower((int) key);
                break;
            }
        }
    }
    if (!key) {
        key = 'P';
        columns[0].key = 'p';
    }

    init_piarray(&piarray);

    /* Get and sort info for running processes */
    proc_foreach(add_procinfo, &piarray);

    cyr_qsort_r(piarray.data, piarray.count, sizeof(struct proc_info *),
                &sort_procinfo, &key);

    /* Send HTML header */
    buf_reset(body);
    buf_printf_markup(body, level, HTML_DOCTYPE);
    buf_printf_markup(body, level++, "<html>");
    buf_printf_markup(body, level++, "<head>");
    buf_printf_markup(body, level, "<meta http-equiv=\"%s\" content=\"%s\">",
                      "Refresh", "1");
    buf_printf_markup(body, level, "<title>%s</title>", actions[1].desc);
    buf_printf_markup(body, --level, "</head>");
    buf_printf_markup(body, level++, "<body>");
    buf_printf_markup(body, level, "<h2>%s @ %s</h2>",
                      actions[1].desc, config_servername);
    buf_printf_markup(body, level++, "<table border cellpadding=5>");
    buf_printf_markup(body, level, "<caption><b>%.*s</b></caption>",
                      24 /* clip LF */, asctime(&tnow));
    buf_printf_markup(body, level++, "<tr>");
    for (i = 0; columns[i].key; i++) {
        buf_printf_markup(body, level, "<th><a href=\"%s?sort=%c\">%s</a></th>",
                          txn->req_uri->path, columns[i].key, columns[i].name);
    }
    buf_printf_markup(body, --level, "</tr>");

    /* Add processes to table */
    for (i = 0; i < piarray.count; i++) {
        struct proc_info *pinfo = piarray.data[i];

        /* Send a chunk every 100 processes */
        if (!(i % 100)) {
            write_body(HTTP_OK, txn, buf_cstring(body), buf_len(body));
            buf_reset(body);
        }

        buf_printf_markup(body, level++, "<tr>");
        buf_printf_markup(body, level, "<td>%d</td>", (int) pinfo->pid);
        buf_printf_markup(body, level, "<td>%s</td>", pinfo->servicename);

        buf_printf_markup(body, level, "<td>%s</td>", pinfo->state);

        if (pinfo->start) {
            struct tm start;

            localtime_r(&pinfo->start, &start);
            if (start.tm_yday != tnow.tm_yday) {
                buf_printf_markup(body, level, "<td>%s %02d</td>",
                                  monthname[start.tm_mon], start.tm_mday);
            }
            else {
                buf_printf_markup(body, level, "<td>%02d:%02d</td>",
                                  start.tm_hour, start.tm_min);
            }
        } else {
            buf_printf_markup(body, level, "<td></td>");
        }

        if (pinfo->vmsize) {
            buf_printf_markup(body, level, "<td>%lu</td>", pinfo->vmsize/1024);
        } else {
            buf_printf_markup(body, level, "<td></td>");
        }

        buf_printf_markup(body, level, "<td>%s</td>", pinfo->host);
        buf_printf_markup(body, level, "<td>%s</td>", pinfo->user);
        buf_printf_markup(body, level, "<td>%s</td>", pinfo->mailbox);
        buf_printf_markup(body, level, "<td>%s</td>", pinfo->cmdname);
        buf_printf_markup(body, --level, "</tr>");

        free(pinfo->servicename);
        free(pinfo->host);
        free(pinfo->user);
        free(pinfo->mailbox);
        free(pinfo->cmdname);
        free(pinfo);
    }

    deinit_piarray(&piarray);

    /* Finish table */
    buf_printf_markup(body, --level, "</table>");

    /* Finish HTML */
    buf_printf_markup(body, --level, "</body>");
    buf_printf_markup(body, --level, "</html>");
    write_body(0, txn, buf_cstring(body), buf_len(body));

    /* End of output */
    write_body(0, txn, NULL, 0);

 done:
    return 0;
}


/* Perform a disk usage action */
/*
 * config_foreachoverflowstring() callback function to find partition-
 * options and print filesystem stats
 */
struct part_rock {
    const char *defpart;
    unsigned meta;
    struct buf *body;
    unsigned *level;
};

static void get_part_stats(const char *key, const char *val, void *rock)
{
    struct part_rock *prock = (struct part_rock *) rock;
    struct buf *body = prock->body;
    unsigned level = *prock->level;
    const char *part, *path;
    struct statvfs s;
    long blocks_used;
    long blocks_percent_used;

    if (prock->meta) {
        if (strncmp("meta", key, 4)) return;
        key += 4;
    }
    if (strncmp("partition-", key, 10)) return;

    part = key+10;
    path = val;

    if (statvfs(path, &s)) return;

    blocks_used = s.f_blocks - s.f_bfree;
    blocks_percent_used = (long)
        (blocks_used * 100.0 / (blocks_used + s.f_bavail) + 0.5);

    buf_printf_markup(body, level++, "<tr>");
    if (prock->defpart && !strcmp(part, prock->defpart))
        buf_printf_markup(body, level, "<td><i>%s</i></td>", part);
    else
        buf_printf_markup(body, level, "<td>%s</td>", part);
    buf_printf_markup(body, level, "<td align=\"right\">%ld</td>",
                      (long) (s.f_blocks * (s.f_frsize / 1024.0)));
    buf_printf_markup(body, level, "<td align=\"right\">%ld</td>",
                      (long) ((s.f_blocks - s.f_bfree) * (s.f_frsize / 1024.0)));
    buf_printf_markup(body, level, "<td align=\"right\">%ld</td>",
                      (long) (s.f_bavail * (s.f_frsize / 1024.0)));
    buf_printf_markup(body, level, "<td align=\"right\">%ld%%</td>",
                      blocks_percent_used);
    buf_printf_markup(body, level, "<td>%s</td>", path);
    buf_printf_markup(body, --level, "</tr>");

    *prock->level = level;
}

static int action_df(struct transaction_t *txn)
{
    int precond;
    struct message_guid guid;
    const char *etag;
    static time_t lastmod = 0;
    unsigned level = 0;
    static struct buf resp = BUF_INITIALIZER;
    struct stat sbuf;
    time_t mtime;

    /* Generate ETag based on compile date/time of this source file,
       and the config file size/mtime */
    assert(!buf_len(&txn->buf));
    stat(config_filename, &sbuf);
    buf_printf(&txn->buf, TIME_T_FMT "-" TIME_T_FMT "-" OFF_T_FMT, compile_time,
               sbuf.st_mtime, sbuf.st_size);

    message_guid_generate(&guid, buf_cstring(&txn->buf), buf_len(&txn->buf));
    etag = message_guid_encode(&guid);
    mtime = MAX(compile_time, sbuf.st_mtime);

    /* Check any preconditions, including range request */
    txn->flags.ranges = 1;
    precond = check_precond(txn, etag, mtime);

    switch (precond) {
    case HTTP_OK:
    case HTTP_PARTIAL:
    case HTTP_NOT_MODIFIED:
        /* Fill in Etag,  Last-Modified, Expires */
        txn->resp_body.etag = etag;
        txn->resp_body.lastmod = mtime;
        txn->resp_body.maxage = 600;  /* 10 min */
        txn->flags.cc |= CC_MAXAGE;

        if (precond != HTTP_NOT_MODIFIED) break;

        GCC_FALLTHROUGH

    default:
        /* We failed a precondition - don't perform the request */
        return precond;
    }

    if (txn->resp_body.lastmod > lastmod) {
        /* Add HTML header */
        struct part_rock prock = { config_defpartition, 0, &resp, &level };

        buf_reset(&resp);
        buf_printf_markup(&resp, level, HTML_DOCTYPE);
        buf_printf_markup(&resp, level++, "<html>");
        buf_printf_markup(&resp, level++, "<head>");
        buf_printf_markup(&resp, level, "<title>%s</title>", actions[2].desc);
        buf_printf_markup(&resp, --level, "</head>");
        buf_printf_markup(&resp, level++, "<body>");
        buf_printf_markup(&resp, level, "<h2>%s @ %s</h2>",
                          actions[2].desc, config_servername);
        buf_printf_markup(&resp, level++, "<table border cellpadding=5>");
        buf_printf_markup(&resp, level++, "<tr>");
        buf_printf_markup(&resp, level, "<th>Partition</th>");
        buf_printf_markup(&resp, level, "<th align=\"right\">1k-blocks</th>");
        buf_printf_markup(&resp, level, "<th align=\"right\">Used</th>");
        buf_printf_markup(&resp, level, "<th align=\"right\">Available</th>");
        buf_printf_markup(&resp, level, "<th align=\"right\">Use%%</th>");
        buf_printf_markup(&resp, level, "<th>Location</th>");
        buf_printf_markup(&resp, --level, "</tr>");

        /* Add partition stats */
        config_foreachoverflowstring(get_part_stats, &prock);

        /* Finish table */
        buf_printf_markup(&resp, --level, "</table>");

        /* Finish HTML */
        buf_printf_markup(&resp, --level, "</body>");
        buf_printf_markup(&resp, --level, "</html>");

        /* Update lastmod */
        lastmod = txn->resp_body.lastmod;
    }

    /* Output the HTML response */
    txn->resp_body.type = "text/html; charset=utf-8";
    write_body(precond, txn, buf_cstring(&resp), buf_len(&resp));

    return 0;
}


struct service_item {
    char *prefix;
    int prefixlen;
    struct service_item *next;
};

static void add_service(const char *name,
                        struct entry *e __attribute__((unused)),
                        void *rock)
{
    struct service_item **ksp = (struct service_item **)rock;
    struct service_item *knew = xmalloc(sizeof(struct service_item));
    knew->prefix = strconcat(name, "_", (char *)NULL);
    knew->prefixlen = strlen(knew->prefix);
    knew->next = *ksp;
    *ksp = knew;
}

enum {
    OVER_UNKNOWN = 0,
    OVER_SERVICE,
    OVER_SASL,
    OVER_PARTITION,
    OVER_LAST
};

static int known_regular(const char *key)
{
    int i;

    for (i = 1; i < IMAPOPT_LAST; i++) {
        if (!strcmp(imapopts[i].optname, key))
            return 1;
    }

    return 0;
}

static unsigned known_overflow(const char *key)
{
    const char *match;

    /* any SASL key is OK */
    if (!strncmp(key, "sasl_", 5)) return OVER_SASL;

    /* any partition is OK */
    if (!strncmp(key, "partition-", 10)) return OVER_PARTITION;

    /* only valid if there's a partition with the same name */
    if (!strncmp(key, "metapartition-", 14) &&
        config_getoverflowstring(key+4, NULL)) return OVER_PARTITION;

    /* only valid if there's a partition with the same name */
    if (!strncmp(key, "archivepartition-", 17) &&
        config_getoverflowstring(key+7, NULL)) return OVER_PARTITION;


    /* only valid if there's a partition with the same name */
    if ((match = strstr(key, "searchpartition-")) &&
        config_getoverflowstring(match+6, NULL)) return OVER_PARTITION;

    return OVER_UNKNOWN;
}

struct option_t {
    const char *key;
    const char *val;
};

struct conf_rock {
    struct service_item *known_services;
    ptrarray_t overflow[OVER_LAST];
};

static void overflow_cb(const char *key, const char *val, void *rock)
{
    struct conf_rock *crock = (struct conf_rock *) rock;
    struct option_t *newopt = xmalloc(sizeof(struct option_t));
    unsigned known = known_overflow(key);
    struct service_item *svc;

    newopt->key = key;
    newopt->val = val;

    if (known) {
        ptrarray_append(&crock->overflow[known], newopt);
        return;
    }

    for (svc = crock->known_services; svc; svc = svc->next) {
        if (!strncmp(key, svc->prefix, svc->prefixlen)) {
            /* check if it's a known key */
            if (known_regular(key+svc->prefixlen) ||
                known_overflow(key+svc->prefixlen)) {
                ptrarray_append(&crock->overflow[OVER_SERVICE], newopt);
                return;
            }
        }
    }

    ptrarray_append(&crock->overflow[OVER_UNKNOWN], newopt);
    return;
}

static int optcmp(struct option_t **a, struct option_t **b)
{
    return strcmp((*a)->key, (*b)->key);
}

static void print_imapopt(struct imapopt_s *imapopt,
                          struct buf *resp, unsigned level)
{
    const union config_value *val =
        imapopt->seen ? &imapopt->val : &imapopt->def;
    int i;
    
    buf_printf_markup(resp, level++, "<tr>");
    buf_printf_markup(resp, level, "<td>%s</td>", imapopt->optname);
    buf_printf_markup(resp, level++, "<td>");
    
    switch (imapopt->t) {
    case OPT_BITFIELD:
        for (i = 0; imapopt->enum_options[i].name; i++) {
            buf_printf_markup(resp, level++,
                              "<input disabled type=checkbox "
                              "name=\"%s\" value=\"%s\" %s>",
                              imapopt->optname,
                              imapopts->enum_options[i].name,
                              (val->x & (1<<i)) ? "checked" : "");
            if (imapopt->def.x & (1<<i)) {
                buf_printf_markup(resp, level--, "<b>%s</b>",
                                  imapopt->enum_options[i].name);
            }
            else {
                buf_printf_markup(resp, level--, "%s",
                                  imapopt->enum_options[i].name);
            }
 
            if (!((i+1) % 6)) buf_printf_markup(resp, level, "<br>");
        }
        break;

    case OPT_ENUM:
        for (i = 0; imapopt->enum_options[i].name; i++) {
            buf_printf_markup(resp, level++,
                              "<input disabled type=radio "
                              "name=\"%s\" value=\"%s\" %s>",
                              imapopt->optname,
                              imapopt->enum_options[i].name,
                              (val->e == imapopt->enum_options[i].val) ?
                              "checked" : "");
            if (imapopt->def.e == imapopt->enum_options[i].val) {
                buf_printf_markup(resp, level--, "<b>%s</b>",
                                  imapopt->enum_options[i].name);
            }
            else {
                buf_printf_markup(resp, level--, "%s",
                                  imapopt->enum_options[i].name);
            }
        }
        break;

    case OPT_INT:
        if (val->i == imapopt->def.i) {
            buf_printf_markup(resp, level, "<b>%ld</b>", val->i);
        }
        else {
            buf_printf_markup(resp, level, "%ld <sub><b>%ld</b></sub>",
                              val->i, imapopt->def.i);
        }
        break;

    case OPT_STRING:
        if (imapopt->def.s && *imapopt->def.s) {
            const char *defval = imapopt->def.s;
            char *freeme = NULL;

            if (!strncasecmp(defval, "{configdirectory}", 17)) {
                freeme = strconcat(config_dir, defval+17, NULL);
                defval = freeme;
            }
            if (!imapopt->seen || !strcasecmp(val->s, defval)) {
                buf_printf_markup(resp, level, "<b>%s</b>", defval);
            }
            else {
                buf_printf_markup(resp, level, "%s <sub><b>%s</b></sub>",
                                  val->s, defval);
            }
            free(freeme);
        }
        else if (val->s) {
            tok_t tok;
            const char *str;

            tok_init(&tok, val->s, " \t", TOK_TRIMLEFT|TOK_TRIMRIGHT);
            while ((str = tok_next(&tok))) {
                buf_printf_markup(resp, level, "%s<br>", str);
            }
            tok_fini(&tok);
        }
        break;

    case OPT_STRINGLIST:
        for (i = 0; imapopt->enum_options[i].name; i++) {
            buf_printf_markup(resp, level++,
                              "<input disabled type=radio "
                              "name=\"%s\" value=\"%s\" %s>",
                              imapopt->optname,
                              imapopt->enum_options[i].name,
                              (val->s &&
                               !strcasecmp(val->s,
                                           imapopt->enum_options[i].name)) ?
                              "checked" : "");
            if (imapopt->def.s &&
                !strcasecmp(imapopt->def.s,
                            imapopt->enum_options[i].name)) {
                buf_printf_markup(resp, level--, "<b>%s</b>",
                                  imapopt->enum_options[i].name);
            }
            else {
                buf_printf_markup(resp, level--, "%s",
                                  imapopt->enum_options[i].name);
            }
        }
        break;

    case OPT_SWITCH:
        buf_printf_markup(resp, level,
                          "<input disabled type=checkbox "
                          "name=\"%s\" value=\"on\" %s> %s",
                          imapopt->optname, val->b ? "checked" : "",
                          imapopt->def.b ? "<b>on</b>" : "on");
        break;

    default:
        break;
    }

    buf_printf_markup(resp, --level, "</td>");
    buf_printf_markup(resp, --level, "</tr>");
}

/* Perform a conf action */
static int action_conf(struct transaction_t *txn)
{
    int precond;
    struct message_guid guid;
    const char *etag;
    static time_t lastmod = 0;
    static struct buf resp = BUF_INITIALIZER;
    struct stat sbuf;
    time_t mtime;

    /* Generate ETag based on compile date/time of this source file,
       and the config file size/mtime */
    assert(!buf_len(&txn->buf));
    stat(config_filename, &sbuf);
    buf_printf(&txn->buf, TIME_T_FMT "-" TIME_T_FMT "-" OFF_T_FMT, compile_time,
               sbuf.st_mtime, sbuf.st_size);

    message_guid_generate(&guid, buf_cstring(&txn->buf), buf_len(&txn->buf));
    etag = message_guid_encode(&guid);
    mtime = MAX(compile_time, sbuf.st_mtime);

    /* Check any preconditions, including range request */
    txn->flags.ranges = 1;
    precond = check_precond(txn, etag, mtime);

    switch (precond) {
    case HTTP_OK:
    case HTTP_PARTIAL:
    case HTTP_NOT_MODIFIED:
        /* Fill in Etag,  Last-Modified, Expires */
        txn->resp_body.etag = etag;
        txn->resp_body.lastmod = mtime;
        txn->resp_body.maxage = 86400;  /* 24 hrs */
        txn->flags.cc |= CC_MAXAGE;

        if (precond != HTTP_NOT_MODIFIED) break;

        GCC_FALLTHROUGH

    default:
        /* We failed a precondition - don't perform the request */
        return precond;
    }

    if (txn->resp_body.lastmod > lastmod) {
        /* Add HTML header */
        ptrarray_t deprecated = PTRARRAY_INITIALIZER;
        ptrarray_t unset = PTRARRAY_INITIALIZER;
        unsigned level = 0;
        struct conf_rock crock;
        struct service_item *ks;
        int i, k;

        buf_reset(&resp);
        buf_printf_markup(&resp, level, HTML_DOCTYPE);
        buf_printf_markup(&resp, level++, "<html>");
        buf_printf_markup(&resp, level++, "<head>");
        buf_printf_markup(&resp, level, "<title>%s</title>", actions[3].desc);
        buf_printf_markup(&resp, --level, "</head>");
        buf_printf_markup(&resp, level++, "<body>");
        buf_printf_markup(&resp, level, "<h2>%s @ %s</h2>",
                          actions[3].desc, config_servername);
        buf_printf_markup(&resp, level++, "<table border cellpadding=5>");
        buf_printf_markup(&resp, level, "<caption>Default values are shown in "
                          "<b>bold</b> and are possibly "
                          "<b><sub>subscripted</sub></b></caption>");
        buf_printf_markup(&resp, level++, "<tr>");
        buf_printf_markup(&resp, level,
                          "<th align=\"left\">Standard Options</th>");
        buf_printf_markup(&resp, level, "<th align=\"left\">Value</th>");
        buf_printf_markup(&resp, --level, "</tr>");

        /* Add config options */
        for (i = 1; i < IMAPOPT_LAST; i++) {
            if (imapopts[i].deprecated_since) {
                if (imapopts[i].seen) {
                    ptrarray_append(&deprecated, &imapopts[i]);
                }
            }
            else if (imapopts[i].seen) {
                print_imapopt(&imapopts[i], &resp, level);
            }
            else {
                ptrarray_append(&unset, &imapopts[i]);
            }
        }

        /* Pull the config from cyrus.conf to get service names */
        memset(&crock, 0, sizeof(struct conf_rock));
        masterconf_getsection("SERVICES", &add_service, &crock.known_services);

        /* Build overflow arrays */
        config_foreachoverflowstring(overflow_cb, &crock);

        /* Clean up service items */
        ks = crock.known_services;
        while (ks) {
            struct service_item *next = ks->next;
            free(ks->prefix);
            free(ks);
            ks = next;
        }

        /* Add the overflow options */
        for (k = OVER_PARTITION; k >= OVER_UNKNOWN; k--) {
            if (crock.overflow[k].count) {
                const char *colname = "UNKNOWN";

                switch (k) {
                case OVER_UNKNOWN:
                    colname = "Unknown/Invalid Options"; break;

                case OVER_SERVICE:
                    colname = "Service-specific Options"; break;

                case OVER_SASL:
                    colname = "SASL Options"; break;

                case OVER_PARTITION:
                    colname = "Partition Options"; break;
                }

                buf_printf_markup(&resp, level,
                                  "<tr><td colspan=2><br></td></tr>");
                buf_printf_markup(&resp, level++, "<tr>");
                buf_printf_markup(&resp, level,
                                  "<th align=\"left\">%s</th>", colname);
                buf_printf_markup(&resp, level,
                                  "<th align=\"left\">Value</th>");
                buf_printf_markup(&resp, --level, "</tr>");

                ptrarray_sort(&crock.overflow[k],
                              (int (*)(const void **, const void **)) &optcmp);
                for (i = 0; i < crock.overflow[k].count; i++) {
                    struct option_t *opt = ptrarray_nth(&crock.overflow[k], i);
                    tok_t tok;
                    const char *val;

                    buf_printf_markup(&resp, level++, "<tr>");
                    buf_printf_markup(&resp, level, "<td>%s</td>", opt->key);

                    buf_printf_markup(&resp, level++, "<td>");
                    tok_init(&tok, opt->val, " \t", TOK_TRIMLEFT|TOK_TRIMRIGHT);
                    while ((val = tok_next(&tok))) {
                        buf_printf_markup(&resp, level, "%s<br>", val);
                    }
                    tok_fini(&tok);
                    buf_printf_markup(&resp, --level, "</td>");

                    buf_printf_markup(&resp, --level, "</tr>");
                    free(opt);
                }
                ptrarray_fini(&crock.overflow[k]);
            }
        }

        if (deprecated.count) {
            /* Add the deprecated options */
            buf_printf_markup(&resp, level,
                              "<tr><td colspan=2><br></td></tr>");
            buf_printf_markup(&resp, level++, "<tr>");
            buf_printf_markup(&resp, level,
                              "<th align=\"left\">Deprecated Options</th>");
            buf_printf_markup(&resp, level,
                              "<th align=\"left\">History</th>");
            buf_printf_markup(&resp, --level, "</tr>");

            for (i = 0; i < deprecated.count; i++) {
                struct imapopt_s *imapopt = ptrarray_nth(&deprecated, i);

                buf_printf_markup(&resp, level++, "<tr>");
                buf_printf_markup(&resp, level, "<td>%s</td>",
                                  imapopt->optname);
                buf_printf_markup(&resp, level++, "<td>");
                buf_printf_markup(&resp, level, "Since %s",
                                  imapopt->deprecated_since);
                if (imapopt->preferred_opt != IMAPOPT_ZERO) {
                    buf_printf_markup(&resp, level, " in favor of <b>%s</b>",
                                      imapopts[imapopt->preferred_opt].optname);
                }

                buf_printf_markup(&resp, --level, "</td>");
                buf_printf_markup(&resp, --level, "</tr>");
            }
            ptrarray_fini(&deprecated);
        }

        if (unset.count) {
            /* Add the unset options */
            buf_printf_markup(&resp, level,
                              "<tr><td colspan=2><br></td></tr>");
            buf_printf_markup(&resp, level++, "<tr>");
            buf_printf_markup(&resp, level,
                              "<th align=\"left\">Unset (Default) Options</th>");
            buf_printf_markup(&resp, level,
                              "<th align=\"left\">Value</th>");
            buf_printf_markup(&resp, --level, "</tr>");

            for (i = 0; i < unset.count; i++) {
                print_imapopt(ptrarray_nth(&unset, i), &resp, level);
            }
            ptrarray_fini(&unset);
        }

        /* Finish table */
        buf_printf_markup(&resp, --level, "</table>");

        /* Finish HTML */
        buf_printf_markup(&resp, --level, "</body>");
        buf_printf_markup(&resp, --level, "</html>");

        /* Update lastmod */
        lastmod = txn->resp_body.lastmod;
    }

    /* Output the HTML response */
    txn->resp_body.type = "text/html; charset=utf-8";
    write_body(precond, txn, buf_cstring(&resp), buf_len(&resp));

    return 0;
}
