diff options
Diffstat (limited to 'src/pmwebapi/main.c')
-rw-r--r-- | src/pmwebapi/main.c | 561 |
1 files changed, 561 insertions, 0 deletions
diff --git a/src/pmwebapi/main.c b/src/pmwebapi/main.c new file mode 100644 index 0000000..a8a7920 --- /dev/null +++ b/src/pmwebapi/main.c @@ -0,0 +1,561 @@ +/* + * 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); +} |