/*
 * JSON web bridge for PMAPI.
 *
 * Copyright (c) 2011-2014 Red Hat.
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 */

#include "pmwebapi.h"

const char uriprefix[] = "/pmapi";
char *resourcedir;	     /* set by -R option */
char *archivesdir = ".";       /* set by -A option */
unsigned verbosity;	    /* set by -v option */
unsigned maxtimeout = 300;     /* set by -t option */
unsigned perm_context = 1;     /* set by -c option, changed by -h/-a/-L */
unsigned new_contexts_p = 1;   /* set by -N option */
unsigned exit_p;	       /* counted by SIG* handler */
static char *logfile = "pmwebd.log";
static char *fatalfile = "/dev/tty"; /* fatal messages at startup go here */
static char *username;
static __pmServerPresence *presence;

static int
mhd_log_args(void *connection, enum MHD_ValueKind kind, 
		const char *key, const char *value)
{
    (void)kind;
    pmweb_notify(LOG_DEBUG, connection, "%s%s%s",
		 key, value ? "=" : "", value ? value : "");
    return MHD_YES;
}

/*
 * Respond to a new incoming HTTP request.  It may be
 * one of three general categories:
 * (a) creation of a new PMAPI context: do it
 * (b) operation on an existing context: do it
 * (c) access to some non-API URI: serve it from $resourcedir/ if configured.
 */
static int
mhd_respond(void *cls, struct MHD_Connection *connection,
	   const char *url, const char *method, const char *version,
	   const char *upload_data, size_t *upload_data_size, void **con_cls)
{
   /* "error_page" could also be an actual error page... */
    static const char	error_page[] = "PMWEBD error";
    static int		dummy;

    struct MHD_Response	*resp = NULL;
    int			sts;

    /*
     * MHD calls us at least twice per request.  Skip the first one,
     * since it only gives us headers, and not any POST content.
     */
    if (& dummy != *con_cls) {
	*con_cls = &dummy;
	return MHD_YES;
    }
    *con_cls = NULL;

    if (verbosity > 1)
	pmweb_notify(LOG_INFO, connection, "%s %s %s", version, method, url);
    if (verbosity > 2) /* Print arguments too. */
	(void) MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND,
					 &mhd_log_args, connection);

    /* pmwebapi? */
    if (0 == strncmp(url, uriprefix, strlen(uriprefix)))
	return pmwebapi_respond(cls, connection,
				&url[strlen(uriprefix)+1], /* strip prefix */
				method, upload_data, upload_data_size);
    /* pmresapi? */
    else if (0 == strcmp(method, "GET") && resourcedir != NULL)
	return pmwebres_respond(cls, connection, url);

    /* junk? */
    MHD_add_response_header(resp, "Content-Type", "text/plain");
    resp = MHD_create_response_from_buffer(strlen(error_page),
					   (char*)error_page, 
					   MHD_RESPMEM_PERSISTENT);
    if (resp == NULL) {
	pmweb_notify(LOG_ERR, connection,
		     "MHD_create_response_from_callback failed\n");
	return MHD_NO;
    }

    sts = MHD_queue_response(connection, MHD_HTTP_BAD_REQUEST, resp);
    MHD_destroy_response(resp);
    if (sts != MHD_YES) {
	pmweb_notify(LOG_ERR, connection, "MHD_queue_response failed\n");
	return MHD_NO;
    }
    return MHD_YES;
}

static void
handle_signals(int sig)
{
    (void)sig;
    exit_p++;
}

static void
pmweb_dont_start(void)
{
    FILE	*tty;
    FILE	*log;

    __pmNotifyErr(LOG_ERR, "pmwebd not started due to errors!\n");

    if ((tty = fopen(fatalfile, "w")) != NULL) {
	fflush(stderr);
	fprintf(tty, "NOTE: pmwebd not started due to errors!  ");
	if ((log = fopen(logfile, "r")) != NULL) {
	    int	 c;

	    fprintf(tty, "Log file \"%s\" contains ...\n", logfile);
	    while ((c = fgetc(log)) != EOF)
		fputc(c, tty);
	    fclose(log);
	}
	else
	    fprintf(tty, "Log file \"%s\" has vanished!\n", logfile);
	fclose(tty);
    }
    exit(1);
}

/*
 * Local variant of __pmServerDumpRequestPorts - remove this
 * when an NSS-based pmweb daemon is built.
 */
static void
server_dump_request_ports(FILE *f, int ipv4, int ipv6, int port)
{
    if (ipv4)
	fprintf(f, "Started daemon on IPv4 TCP port %d\n", port);
    if (ipv6)
	fprintf(f, "Started daemon on IPv6 TCP port %d\n", port);
}

static void
server_dump_configuration(FILE *f)
{
    char	*cwd;
    char	path[MAXPATHLEN];
    char	cwdpath[MAXPATHLEN];
    int		sep = __pmPathSeparator();
    int		len;

    cwd = getcwd(cwdpath, sizeof(cwdpath));
    if (resourcedir) {
	len = (__pmAbsolutePath(resourcedir) || !cwd) ?
	    snprintf(path, sizeof(path), "%s", resourcedir) :
	    snprintf(path, sizeof(path), "%s%c%s", cwd, sep, resourcedir);
	while (len-- > 1) {
	    if (path[len] != '.' && path[len] != sep) break;
	    path[len] = '\0';
	}
	fprintf (f, "Serving non-pmwebapi URLs under directory %s\n", path);
    }
    if (new_contexts_p) {
	len = (__pmAbsolutePath(archivesdir) || !cwd) ?
	    snprintf(path, sizeof(path), "%s", archivesdir) :
	    snprintf(path, sizeof(path), "%s%c%s", cwd, sep, archivesdir);
	while (len-- > 1) {
	    if (path[len] != '.' && path[len] != sep) break;
	    path[len] = '\0';
	}
	/* XXX: network outbound ACL */
	fprintf(f, "Serving PCP archives under directory %s\n", path);
    } else {
	fprintf(f, "Remote context creation requests disabled\n");
    }
}

static void
pmweb_init_random_seed(void)
{
    struct timeval tv;

    gettimeofday(&tv, NULL);
    srandom((unsigned int) getpid() ^
	    (unsigned int) tv.tv_sec ^
	    (unsigned int) tv.tv_usec);
}

static void
pmweb_shutdown(struct MHD_Daemon *d4, struct MHD_Daemon *d6)
{
    /* Shut down cleanly, out of a misplaced sense of propriety. */
    if (d4)
	MHD_stop_daemon(d4);
    if (d6)
	MHD_stop_daemon(d6);

    /* No longer advertise pmwebd presence on the network. */
    __pmServerUnadvertisePresence(presence);

    /*
     * Let's politely clean up all the active contexts.
     * The OS will do all that for us anyway, but let's make valgrind happy.
     */
    pmwebapi_deallocate_all();

    __pmNotifyErr(LOG_INFO, "pmwebd Shutdown\n");
    fflush(stderr);
}

static int
option_overrides(int opt, pmOptions *opts)
{
    (void)opts;

    switch (opt) {
    case 'A': case 'a': case 'h': case 'L': case 'N': case 'p': case 't':
	return 1;
    }
    return 0;
}

static pmLongOptions longopts[] = {
    PMAPI_OPTIONS_HEADER("Network options"),
    { "port", 1, 'p', "NUM", "listen on given TCP port [default 44323]" },
    { "ipv4", 0, '4', 0, "listen on IPv4 only" },
    { "ipv6", 0, '6', 0, "listen on IPv6 only" },
    { "timeout", 1, 't', "SEC", "max time (seconds) for PMAPI polling [default 300]" },
    { "resources", 1, 'R', "DIR", "serve non-API files from given directory" },
    PMAPI_OPTIONS_HEADER("Context options"),
    { "context", 1, 'c', "NUM", "set next permanent-binding context number" },
    { "host", 1, 'h', "HOST", "permanent-bind next context to PMCD on host" },
    { "archive", 1, 'a', "FILE", "permanent-bind next context to archive" },
    { "local-PMDA", 0, 'L', 0, "permanent-bind next context to local PMDAs" },
    PMOPT_SPECLOCAL,
    PMOPT_LOCALPMDA,
    { "", 0, 'N', 0, "disable remote new-context requests" },
    { "", 1, 'A', "DIR", "permit remote new-archive-context under dir [default CWD]" },
    PMAPI_OPTIONS_HEADER("Other"),
    PMOPT_DEBUG,
    { "foreground", 0, 'f', 0, "run in the foreground" },
    { "log", 1, 'l', "FILE", "redirect diagnostics and trace output" },
    { "verbose", 0, 'v', 0, "increase verbosity" },
    { "username", 1, 'U', "USER", "assume identity of username [default pcp]" },
    { "", 1, 'x', "PATH", "fatal messages at startup sent to file [default /dev/tty]" },
    PMOPT_HELP,
    PMAPI_OPTIONS_END
};

static pmOptions opts = {
    .short_options = "A:a:c:D:fh:K:Ll:Np:R:t:U:vx:46?",
    .long_options = longopts,
    .override = option_overrides,
};

int
main(int argc, char *argv[])
{
    int		c;
    int		sts;
    int		ctx;
    int		mhd_ipv4 = 1;
    int		mhd_ipv6 = 1;
    int		run_daemon = 1;
    int		port = PMWEBD_PORT;
    char	*endptr;
    struct MHD_Daemon *d4 = NULL;
    struct MHD_Daemon *d6 = NULL;

    umask(022);
    __pmGetUsername(&username);

    while ((c = pmGetOptions(argc, argv, &opts)) != EOF) {
	switch (c) {
	case 'p':
	    port = (int)strtol(opts.optarg, &endptr, 0);
	    if (*endptr != '\0' || port < 0 || port > 65535) {
		pmprintf("%s: invalid port number %s\n",
			 pmProgname, opts.optarg);
		opts.errors++;
	    }
	    break;

	case 't':
	    maxtimeout = strtoul(opts.optarg, &endptr, 0);
	    if (*endptr != '\0') {
		pmprintf("%s: invalid timeout %s\n", pmProgname, opts.optarg);
		opts.errors++;
	    }
	    break;

	case 'R':
	    resourcedir = opts.optarg;
	    break;

	case 'A':
	    archivesdir = opts.optarg;
	    break;

	case '6':
	    mhd_ipv6 = 1;
	    mhd_ipv4 = 0;
	    break;

	case '4':
	    mhd_ipv4 = 1;
	    mhd_ipv6 = 0;
	    break;

	case 'v':
	    verbosity++;
	    break;

	case 'c':
	    perm_context = strtoul(opts.optarg, &endptr, 0);
	    if (*endptr != '\0' || perm_context >= INT_MAX) {
		pmprintf("%s: invalid context number %s\n",
			pmProgname, opts.optarg);
		opts.errors++;
	    }
	    break;

	case 'h':
	    if ((ctx = pmNewContext(PM_CONTEXT_HOST, opts.optarg)) < 0) {
		__pmNotifyErr(LOG_ERR, "new context failed\n");
		exit(EXIT_FAILURE);
	    }
	    if ((sts = pmwebapi_bind_permanent(perm_context++, ctx)) < 0) {
		__pmNotifyErr(LOG_ERR, "permanent bind failed\n");
		exit(EXIT_FAILURE);
	    }
	    __pmNotifyErr(LOG_INFO,
			"context (web%d=pm%d) created, host %s, permanent\n", 
			perm_context - 1, ctx, opts.optarg);
	    break;

	case 'a':
	    if ((ctx = pmNewContext(PM_CONTEXT_ARCHIVE, opts.optarg)) < 0) {
		__pmNotifyErr(LOG_ERR, "new context failed\n");
		exit(EXIT_FAILURE);
	    }
	    if ((sts = pmwebapi_bind_permanent(perm_context++, ctx)) < 0) {
		__pmNotifyErr(LOG_ERR, "permanent bind failed\n");
		exit(EXIT_FAILURE);
	    }
	    __pmNotifyErr(LOG_INFO,
			"context (web%d=pm%d) created, archive %s, permanent\n",
			perm_context - 1, ctx, opts.optarg);
	    break;

	case 'L':
	    if ((ctx = pmNewContext(PM_CONTEXT_LOCAL, NULL)) < 0) {
		__pmNotifyErr(LOG_ERR, "new context failed\n");
		exit(EXIT_FAILURE);
	    }
	    if ((sts = pmwebapi_bind_permanent(perm_context++, ctx)) < 0) {
		__pmNotifyErr(LOG_ERR, "permanent bind failed\n");
		exit(EXIT_FAILURE);
	    }
	    __pmNotifyErr(LOG_INFO,
			"context (web%d=pm%d) created, local, permanent\n", 
			perm_context - 1, ctx);
	    break;

	case 'N':
	    new_contexts_p = 0;
	    break;

	case 'f':
	    /* foreground, i.e. do _not_ run as a daemon */
	    run_daemon = 0;
	    break;

	case 'l':
	    /* log file name */
	    logfile = opts.optarg;
	    break;

	case 'U':
	    /* run as user username */
	    username = opts.optarg;
	    break;

	case 'x':
	    fatalfile = opts.optarg;
	    break;
	}
    }

    if (opts.errors) {
	pmUsageMessage(&opts);
	exit(EXIT_FAILURE);
    }

    if (run_daemon) {
	fflush(stderr);
	pmweb_start_daemon(argc, argv);
    }

    /*
     * Start microhttp daemon.  Use the application-driven threading
     * model, so we don't complicate things with threads.  In the
     * future, if this daemon becomes safe to run multithreaded,
     * we could make use of MHD_USE_THREAD_PER_CONNECTION; we'd need
     * to add ample locking over pmwebd context structures etc.
     */
    if (mhd_ipv4)
	d4 = MHD_start_daemon(0,
			      port,
			      NULL, NULL, /* default accept policy */
			      &mhd_respond, NULL, /* handler callback */
			      MHD_OPTION_CONNECTION_TIMEOUT, maxtimeout,
			      MHD_OPTION_END);
    if (mhd_ipv6)
	d6 = MHD_start_daemon(MHD_USE_IPv6,
			      port,
			      NULL, NULL, /* default accept policy */
			      &mhd_respond, NULL, /* handler callback */
			      MHD_OPTION_CONNECTION_TIMEOUT, maxtimeout,
			      MHD_OPTION_END);
    if (d4 == NULL && d6 == NULL) {
	__pmNotifyErr(LOG_ERR, "error starting microhttpd daemons\n");
	pmweb_dont_start();
    }

    /* tell the world we have arrived */
    __pmServerCreatePIDFile(PM_SERVER_WEBD_SPEC, 0);
    presence = __pmServerAdvertisePresence(PM_SERVER_WEBD_SPEC, port);

    __pmOpenLog(pmProgname, logfile, stderr, &sts);
    /* close old stdout, and force stdout into same stream as stderr */
    fflush(stdout);
    close(fileno(stdout));
    if (dup(fileno(stderr)) == -1)
	fprintf(stderr, "Warning: dup() failed: %s\n", pmErrStr(-oserror()));
    fprintf(stderr, "%s: PID = %" FMT_PID ", PMAPI URL = %s\n",
	    pmProgname, getpid(), uriprefix);
    server_dump_request_ports(stderr, d4 != NULL, d6 != NULL, port);
    server_dump_configuration(stderr);
    fflush(stderr);

    /* Set up signal handlers. */
    __pmSetSignalHandler(SIGHUP, SIG_IGN);
    __pmSetSignalHandler(SIGINT, handle_signals);
    __pmSetSignalHandler(SIGTERM, handle_signals);
    __pmSetSignalHandler(SIGQUIT, handle_signals);
    /* Not this one; might get it from pmcd momentary disconnection. */
    /* __pmSetSignalHandler(SIGPIPE, handle_signals); */

    /* lose root privileges if we have them */
    __pmSetProcessIdentity(username);

    /* Setup randomness for calls to random() */
    pmweb_init_random_seed();

    /* Block indefinitely. */
    while (! exit_p) {
	struct timeval tv;
	fd_set rs;
	fd_set ws;
	fd_set es;
	int max = 0;

	/* Based upon MHD fileserver_example_external_select.c */
	FD_ZERO(&rs);
	FD_ZERO(&ws);
	FD_ZERO(&es);
	if (d4 && MHD_YES != MHD_get_fdset(d4, &rs, &ws, &es, &max))
	    break; /* fatal internal error */
	if (d6 && MHD_YES != MHD_get_fdset(d6, &rs, &ws, &es, &max))
	    break; /* fatal internal error */

	/*
	 * Find the next expiry.  We don't need to bound it by
	 * MHD_get_timeout, since we don't use a small
	 * MHD_OPTION_CONNECTION_TIMEOUT.
	 */
	tv.tv_sec = pmwebapi_gc();
	tv.tv_usec = 0;

	select(max+1, &rs, &ws, &es, &tv);

	if (d4)
	    MHD_run(d4);
	if (d6)
	    MHD_run(d6);
    }

    pmweb_shutdown(d4, d6);
    return 0;
}

/*
 * Generate a __pmNotifyErr with the given arguments,
 * but also adding some per-connection metadata info.
 */
void
pmweb_notify(int priority, struct MHD_Connection *conn, const char *fmt, ...)
{
    struct sockaddr *so;
    va_list	arg;
    char	message_buf[2048]; /* size similar to __pmNotifyErr */
    char	*message_tail;
    size_t	message_len;
    char	hostname[128];
    char	servname[128];
    int		sts = -1;

    /* Look up client address data. */
    so = (struct sockaddr *) MHD_get_connection_info(conn,
		MHD_CONNECTION_INFO_CLIENT_ADDRESS)->client_addr;

    if (so && so->sa_family == AF_INET)
	sts = getnameinfo(so, sizeof(struct sockaddr_in),
			  hostname, sizeof(hostname),
			  servname, sizeof(servname),
			  NI_NUMERICHOST|NI_NUMERICSERV);
    else if (so && so->sa_family == AF_INET6)
	sts = getnameinfo(so, sizeof(struct sockaddr_in6),
			  hostname, sizeof(hostname),
			  servname, sizeof(servname),
			  NI_NUMERICHOST|NI_NUMERICSERV);
    if (sts != 0)
	hostname[0] = servname[0] = '\0';

    /* Add the [hostname:port] as a prefix */
    sts = snprintf(message_buf, sizeof(message_buf), "[%s:%s] ",
		   hostname, servname);

    if (sts > 0 && sts < (int)sizeof(message_buf)) {
	message_tail = message_buf + sts; /* Keep it only if successful. */
	message_len = sizeof(message_buf) - sts;
    } else {
	message_tail = message_buf;
	message_len = sizeof(message_buf);
    }

    /* Add the remaining incoming text. */
    va_start(arg, fmt);
    sts = vsnprintf(message_tail, message_len, fmt, arg);
    va_end(arg);

    /*
     * Delegate, but avoid format-string vulnerabilities.  Drop the
     * trailing \n, if there is one, since __pmNotifyErr will add one
     * for us (since it is missing from the %s format string).
     */
    if (sts >= 0 && sts < (int)message_len)
	if (message_tail[sts-1] == '\n')
	    message_tail[sts-1] = '\0';
    __pmNotifyErr(priority, "%s", message_buf);
}
