/* 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 "global.h"
#include "httpd.h"
#include "http_proxy.h"
#include "../master/masterconf.h"
#include "proc.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", 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 },                 /* 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, "%ld-%ld-%ld", (long) 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, "%ld", (long) 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;
}


struct proc_info {
    pid_t pid;
    char *servicename;
    char *user;
    char *host;
    char *mailbox;
    char *cmdname;
    char state;
    time_t start;
    unsigned long vmsize;
};

typedef struct {
    unsigned count;
    unsigned alloc;
    struct proc_info **data;
} piarray_t;

static int add_procinfo(pid_t pid,
                        const char *servicename, const char *host,
                        const char *user, const char *mailbox,
                        const char *cmdname,
                        void *rock)
{
    piarray_t *piarray = (piarray_t *) rock;
    struct proc_info *pinfo;
    char procpath[100];
    struct stat sbuf;
    FILE *f;

    snprintf(procpath, sizeof(procpath), "/proc/%d", pid);
    if (stat(procpath, &sbuf)) return 0;

    if (piarray->count >= piarray->alloc) {
        piarray->alloc += 100;
        piarray->data = xrealloc(piarray->data,
                                 piarray->alloc * sizeof(struct proc_info *));
    }

    pinfo = piarray->data[piarray->count++] =
        (struct proc_info *) xzmalloc(sizeof(struct proc_info));
    pinfo->pid = pid;
    pinfo->servicename = xstrdupsafe(servicename);
    pinfo->host = xstrdupsafe(host);
    pinfo->user = xstrdupsafe(user);
    pinfo->mailbox = xstrdupsafe(mailbox);
    pinfo->cmdname = xstrdupsafe(cmdname);

    strlcat(procpath, "/stat", sizeof(procpath));
    f = fopen(procpath, "r");
    if (f) {
        int d;
        long ld;
        unsigned u;
        unsigned long vmsize = 0, lu;
        unsigned long long starttime = 0;
        char state = 0, *s = NULL;

        fscanf(f, "%d %ms %c " /* 1-3 */
               "%d %d %d %d %d %u " /* 4-9 */
               "%lu %lu %lu %lu %lu %lu " /* 10-15 */
               "%ld %ld %ld %ld %ld %ld " /* 16-21 */
               "%llu %lu %ld", /* 22-24 */
               &d, &s, &state,
               &d, &d, &d, &d, &d, &u,
               &lu, &lu, &lu, &lu, &lu, &lu,
               &ld, &ld, &ld, &ld, &ld, &ld,
               &starttime, &vmsize, &ld);

        free(s);
        fclose(f);

        pinfo->state = state;
        pinfo->vmsize = vmsize;
        pinfo->start = starttime/sysconf(_SC_CLK_TCK);
    }

    return 0;
}

#if defined(_GNU_SOURCE) && defined (__GLIBC__) && \
	((__GLIBC__ > 2) || ((__GLIBC__ == 2) && (__GLIBC_MINOR__ >=0)))
#define HAVE_GLIBC_QSORT_R
#endif

#if defined(__NEWLIB__) && \
	((__NEWLIB__ > 2) || ((__NEWLIB__ == 2) && (__NEWLIB_MINOR__ >= 2)))
#if defined(_GNU_SOURCE)
#define HAVE_GLIBC_QSORT_R
#else
#define HAVE_BSD_QSORT_R
#endif
#endif

#if !defined(HAVE_GLIBC_QSORT_R) && \
	(defined(__FreeBSD__) || defined(__DragonFly__) || defined(__APPLE__))
#define HAVE_BSD_QSORT_R
#endif

#ifdef HAVE_BSD_QSORT_R
#define QSORT_R_COMPAR_ARGS(a,b,c) (c,a,b)
#define cyr_qsort_r(base, nmemb, size, compar, thunk) qsort_r(base, nmemb, size, thunk, compar)
#else
#define QSORT_R_COMPAR_ARGS(a,b,c) (a,b,c)
#  if defined(HAVE_GLIBC_QSORT_R)
#define cyr_qsort_r(base, nmemb, size, compar, thunk) qsort_r(base, nmemb, size, compar, thunk)
#  elif defined(__GNUC__)
static void cyr_qsort_r(void *base, size_t nmemb, size_t size,
                        int (*compar)(const void *, const void *, void *),
                        void *thunk)
{
    int compar_func(const void *a, const void *b)
    {
        return compar(a, b, thunk);
    }
    qsort(base, nmemb, size, compar_func);
}
#  else
#    error No qsort_r support
#  endif
#endif

static int sort_procinfo QSORT_R_COMPAR_ARGS(
                         const void *pa, const void *pb,
                         void *k)
{
    int r;
    const struct proc_info **a = (const struct proc_info**)pa;
    const struct proc_info **b = (const struct proc_info**)pb;
    char *key = (char*)k;
    int rev = islower((int) *key);

    switch (toupper((int) *key)) {
    default:
    case 'P':
        r = (*a)->pid - (*b)->pid;
        break;

    case 'S':
        r = strcmp((*a)->servicename, (*b)->servicename);
        break;

    case 'Q':
        r = (*a)->state - (*b)->state;
        break;

    case 'T':
        r = (*a)->start - (*b)->start;
        break;

    case 'V':
        r = (*a)->vmsize - (*b)->vmsize;
        break;

    case 'H':
        r = strcmp((*a)->host, (*b)->host);
        break;

    case 'U':
        r = strcmp((*a)->user, (*b)->user);
        break;

    case 'R':
        r = strcmp((*a)->mailbox, (*b)->mailbox);
        break;

    case 'C':
        r = strcmp((*a)->cmdname, (*b)->cmdname);
        break;
    }

    return (rev ? -r : r);
}

/* 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 = { 0, 0, NULL };
    time_t now = time(0), boot_time = 0;
    struct strlist *param;
    struct tm tnow;
    char key = 0;
    FILE *f;
    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';
    }

    /* Find boot time in /proc/stat (needed for calculating process start) */
    f = fopen("/proc/stat", "r");
    if (f) {
        char buf[1024];

        while (fgets(buf, sizeof(buf), f)) {
            if (sscanf(buf, "btime %ld\n", &boot_time) == 1) break;
            while (buf[strlen(buf)-1] != '\n' && fgets(buf, sizeof(buf), f)) {
            }
        }
        fclose(f);
    }

    /* 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);

        if (pinfo->vmsize) {
            const char *proc_states[] = {
                /* A */ "", /* B */ "", /* C */ "",
                /* D */ " (waiting)",
                /* E */ "", /* F */ "", /* G */ "", /* H */ "", /* I */ "",
                /* J */ "", /* K */ "", /* L */ "", /* M */ "", /* N */ "",
                /* O */ "", /* P */ "", /* Q */ "",
                /* R */ " (running)",
                /* S */ " (sleeping)",
                /* T */ " (stopped)",
                /* U */ "", /* V */ "",
                /* W */ " (paging)",
                /* X */ "", /* Y */ "",
                /* Z */ " (zombie)"
            };
            const char *monthname[] = {
                "Jan", "Feb", "Mar", "Apr", "May", "Jun",
                "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
            };

            buf_printf_markup(body, level, "<td>%c%s</td>", pinfo->state,
                              isupper((int) pinfo->state) ?
                              proc_states[pinfo->state - 'A'] : "");

            if (boot_time) {
                struct tm start;

                pinfo->start += boot_time;
                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>");
                              
            buf_printf_markup(body, level, "<td>%lu</td>", pinfo->vmsize/1024);
        }
        else buf_printf_markup(body, level, "<td></td><td></td><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);
    }
    free(piarray.data);

    /* 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, "%ld-%ld-%ld", (long) 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, "%ld-%ld-%ld", (long) 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;

                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;
}
