diff options
author | Igor Pashev <pashev.igor@gmail.com> | 2014-10-26 12:33:50 +0400 |
---|---|---|
committer | Igor Pashev <pashev.igor@gmail.com> | 2014-10-26 12:33:50 +0400 |
commit | 47e6e7c84f008a53061e661f31ae96629bc694ef (patch) | |
tree | 648a07f3b5b9d67ce19b0fd72e8caa1175c98f1a /src/pmwebapi/pmwebapi.c | |
download | pcp-debian/3.9.10.tar.gz |
Debian 3.9.10debian/3.9.10debian
Diffstat (limited to 'src/pmwebapi/pmwebapi.c')
-rw-r--r-- | src/pmwebapi/pmwebapi.c | 1343 |
1 files changed, 1343 insertions, 0 deletions
diff --git a/src/pmwebapi/pmwebapi.c b/src/pmwebapi/pmwebapi.c new file mode 100644 index 0000000..5134215 --- /dev/null +++ b/src/pmwebapi/pmwebapi.c @@ -0,0 +1,1343 @@ +/* + * JSON web bridge for PMAPI. + * + * Copyright (c) 2011-2014 Red Hat Inc. + * + * 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. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "pmwebapi.h" +#include <stdio.h> +#include <stdarg.h> +#include <inttypes.h> +#include <ctype.h> + + +/* ------------------------------------------------------------------------ */ + +struct webcontext { + /* __pmHashNode key is unique randomized web context key, [1,INT_MAX] */ + /* XXX: optionally bind session to a particular IP address? */ + char *userid; /* NULL, or strdup'd username */ + char *password; /* NULL, or strdup'd password */ + unsigned mypolltimeout; + time_t expires; /* poll timeout, 0 if never expires */ + int context; /* PMAPI context handle, 0 if deleted/free */ +}; + +/* if-threaded: pthread_mutex_t context_lock; */ +static __pmHashCtl contexts; + + + +struct web_gc_iteration { + time_t now; + time_t soonest; /* 0 => no context pending gc */ +}; + + +static __pmHashWalkState +pmwebapi_gc_fn (const __pmHashNode *kv, void *cdata) +{ + const struct webcontext *value = kv->data; + struct web_gc_iteration *t = (struct web_gc_iteration *) cdata; + + if (! value->expires) + return PM_HASH_WALK_NEXT; + + /* Expired. */ + if (value->expires < t->now) + { + int rc; + if (verbosity) + __pmNotifyErr (LOG_INFO, "context (web%d=pm%d) expired.\n", kv->key, value->context); + rc = pmDestroyContext (value->context); + if (rc) + __pmNotifyErr (LOG_ERR, "pmDestroyContext (%d) failed: %d\n", + value->context, rc); + + free (value->userid); + free (value->password); + + return PM_HASH_WALK_DELETE_NEXT; + } + + /* Expiring soon? */ + if (t->soonest == 0) + t->soonest = value->expires; + else if (t->soonest > value->expires) + t->soonest = value->expires; + + return PM_HASH_WALK_NEXT; +} + + +/* Check whether any contexts have been unpolled so long that they + should be considered abandoned. If so, close 'em, free 'em, yak + 'em, smack 'em. Return the number of seconds to the next good time + to check for garbage. */ +unsigned pmwebapi_gc () +{ + struct web_gc_iteration t; + (void) time (& t.now); + t.soonest = 0; + + /* if-multithread: Lock contexts. */ + __pmHashWalkCB (pmwebapi_gc_fn, & t, & contexts); + /* if-multithread: Unlock contexts. */ + + return t.soonest ? (unsigned)(t.soonest - t.now) : maxtimeout; +} + + + +static __pmHashWalkState +pmwebapi_deallocate_all_fn (const __pmHashNode *kv, void *cdata) +{ + struct webcontext *value = kv->data; + int rc; + (void) cdata; + + if (verbosity) + __pmNotifyErr (LOG_INFO, "context (web%d=pm%d) deleted.\n", kv->key, value->context); + rc = pmDestroyContext (value->context); + if (rc) + __pmNotifyErr (LOG_ERR, "pmDestroyContext (%d) failed: %d\n", + value->context, rc); + free (value->userid); + free (value->password); + free (value); + return PM_HASH_WALK_DELETE_NEXT; +} + + +void pmwebapi_deallocate_all () +{ + /* if-multithread: Lock contexts. */ + __pmHashWalkCB (pmwebapi_deallocate_all_fn, NULL, & contexts); + /* if-multithread: Unlock contexts. */ +} + + +/* Allocate an zeroed webcontext structure, and enroll it in the + hash table with the given context#. */ +static int webcontext_allocate (int webapi_ctx, struct webcontext** wc) +{ + if (__pmHashSearch (webapi_ctx, & contexts) != NULL) + return -EEXIST; + + /* Allocate & clear our webapi context. */ + assert (wc); + *wc = calloc(1, sizeof(struct webcontext)); + if (! *wc) + return -ENOMEM; + + int rc = __pmHashAdd(webapi_ctx, *wc, & contexts); + if (rc < 0) { + free (*wc); + return rc; + } + + return 0; +} + + +int pmwebapi_bind_permanent (int webapi_ctx, int pcp_context) +{ + struct webcontext* c; + int rc = webcontext_allocate (webapi_ctx, &c); + + if (rc < 0) + return rc; + + assert (c); + assert (pcp_context >= 0); + c->context = pcp_context; + c->mypolltimeout = ~0; + c->expires = 0; + + return 0; +} + + +/* Print a best-effort message as a plain-text http response; anything + to avoid a dreaded MHD_NO, which results in a 500 return code. + That in turn could be interpreted by a web client as an invitation + to try, try again. */ +static int pmwebapi_notify_error (struct MHD_Connection *connection, int rc) +{ + char error_message [1000]; + char pmmsg [PM_MAXERRMSGLEN]; + struct MHD_Response *resp; + + (void) pmErrStr_r (rc, pmmsg, sizeof(pmmsg)); + (void) snprintf (error_message, sizeof(error_message), + "PMWEBAPI error, code %d: %s", rc, pmmsg); + resp = MHD_create_response_from_buffer (strlen(error_message), error_message, + MHD_RESPMEM_MUST_COPY); + if (resp == NULL) { + pmweb_notify (LOG_ERR, connection, "MHD_create_response_from_buffer failed\n"); + return MHD_NO; + } + + (void) MHD_add_response_header (resp, "Content-Type", "text/plain"); + + rc = MHD_queue_response (connection, MHD_HTTP_BAD_REQUEST, resp); + MHD_destroy_response (resp); + + if (rc != MHD_YES) { + pmweb_notify (LOG_ERR, connection, "MHD_queue_response failed\n"); + return MHD_NO; + } + + return MHD_YES; +} + + +static int pmwebapi_respond_new_context (struct MHD_Connection *connection) +{ + /* Create a context. */ + const char *val; + int rc = 0; + int context = -EINVAL; + char http_response [30]; + char context_description [512] = "<none>"; /* for logging */ + unsigned polltimeout; + struct MHD_Response *resp; + int webapi_ctx; + int iterations = 0; + char *userid = NULL; + char *password = NULL; + + val = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "hostspec"); + if (val == NULL) /* backward compatibility alias */ + val = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "hostname"); + if (val) { + pmHostSpec *hostSpec; + int hostSpecCount; + __pmHashCtl hostAttrs; + char *hostAttrsError; + + __pmHashInit (& hostAttrs); + rc = __pmParseHostAttrsSpec (val, & hostSpec, & hostSpecCount, + & hostAttrs, & hostAttrsError); + if (rc == 0) { + __pmHashNode *node; + node = __pmHashSearch (PCP_ATTR_USERNAME, & hostAttrs); /* XXX: PCP_ATTR_AUTHNAME? */ + if (node) userid = strdup (node->data); + node = __pmHashSearch (PCP_ATTR_PASSWORD, & hostAttrs); + if (node) password = strdup (node->data); + __pmFreeHostAttrsSpec (hostSpec, hostSpecCount, & hostAttrs); + __pmHashClear (& hostAttrs); + } else { + /* Ignore the parse error at this stage; pmNewContext will give it to us. */ + free (hostAttrsError); + } + + context = pmNewContext (PM_CONTEXT_HOST, val); /* XXX: limit access */ + snprintf (context_description, sizeof(context_description), "PM_CONTEXT_HOST %s", val); + } else { + val = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "archivefile"); + if (val) { + char archive_fullpath[PATH_MAX]; + + snprintf(archive_fullpath, sizeof(archive_fullpath), + "%s%c%s", archivesdir, __pmPathSeparator(), val); + + /* Block some basic ways of escaping archive_dir */ + if (NULL != strstr (archive_fullpath, "/../")) { + pmweb_notify (LOG_ERR, connection, "pmwebapi suspicious archive path %s\n", + archive_fullpath); + rc = -EINVAL; + goto out; + } + + context = pmNewContext (PM_CONTEXT_ARCHIVE, archive_fullpath); + snprintf (context_description, sizeof(context_description), "PM_CONTEXT_ARCHIVE %s", val); + } else if (MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "local")) { + /* Note we need to use a dummy parameter to local=FOO, + since the MHD_lookup* API does not differentiate + between an absent argument vs. an argument given + without a parameter value. */ + context = pmNewContext (PM_CONTEXT_LOCAL, NULL); + snprintf (context_description, sizeof(context_description), "PM_CONTEXT_LOCAL"); + } else { + /* context remains -something */ + } + } + + if (context < 0) { + pmweb_notify (LOG_ERR, connection, "new context failed (%s)\n", pmErrStr (context)); + rc = context; + goto out; + } + + /* Process optional ?polltimeout=SECONDS field. If broken/missing, assume maxtimeout. */ + val = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "polltimeout"); + if (val) { + long pt; + char *endptr; + errno = 0; + pt = strtol(val, &endptr, 0); + if (errno != 0 || *endptr != '\0' || pt <= 0 || pt > (long)maxtimeout) { + polltimeout = maxtimeout; + } + else + polltimeout = (unsigned) pt; + } else { + polltimeout = maxtimeout; + } + + + struct webcontext* c = NULL; + /* Create a new context key for the webapi. We just use a random integer within + a reasonable range: 1..INT_MAX */ + while (1) { + /* Preclude infinite looping here, for example due to a badly behaving + random(3) implementation. */ + iterations ++; + if (iterations > 100) + { + pmweb_notify (LOG_ERR, connection, "webapi_ctx allocation failed\n"); + pmDestroyContext (context); + rc = -EMFILE; + goto out; + } + + webapi_ctx = random(); /* we hope RAND_MAX is large enough */ + if (webapi_ctx <= 0) + continue; + + rc = webcontext_allocate (webapi_ctx, & c); + if (rc == 0) + break; + /* This may already exist. We loop in case the key id already exists. */ + } + + assert (c); + c->context = context; + time (& c->expires); + c->mypolltimeout = polltimeout; + c->expires += c->mypolltimeout; + c->userid = userid; /* may be NULL; otherwise, will be freed at context release. */ + c->password = password; /* ditto */ + + /* Errors beyond this point don't require instant cleanup; the + periodic context GC will do it all. */ + + if (verbosity) { + const char* context_hostname = pmGetContextHostName (context); + pmweb_notify (LOG_INFO, connection, "context %s%s%s%s (web%d=pm%d) created%s%s, expires in %us.\n", + context_description, + context_hostname ? " (" : "", + context_hostname ? context_hostname : "", + context_hostname ? ")" : "", + webapi_ctx, context, + userid ? ", user=" : "", + userid ? userid : "", + polltimeout); + } + + rc = snprintf (http_response, sizeof(http_response), "{ \"context\": %d }", webapi_ctx); + assert (rc >= 0 && rc < (int)sizeof(http_response)); + resp = MHD_create_response_from_buffer (strlen(http_response), http_response, + MHD_RESPMEM_MUST_COPY); + if (resp == NULL) { + pmweb_notify (LOG_ERR, connection, "MHD_create_response_from_buffer failed\n"); + rc = -ENOMEM; + goto out; + } + rc = MHD_add_response_header (resp, "Content-Type", "application/json"); + if (rc != MHD_YES) { + pmweb_notify (LOG_ERR, connection, "MHD_add_response_header failed\n"); + rc = -ENOMEM; + goto out1; + } + rc = MHD_add_response_header (resp, "Access-Control-Allow-Origin", "*"); + if (rc != MHD_YES) { + pmweb_notify (LOG_ERR, connection, "MHD_add_response_header ACAO failed\n"); + rc = -ENOMEM; + goto out1; + } + rc = MHD_queue_response (connection, MHD_HTTP_OK, resp); + MHD_destroy_response (resp); + if (rc != MHD_YES) { + pmweb_notify (LOG_ERR, connection, "MHD_queue_response failed\n"); + rc = -ENOMEM; + goto out; + } + + return MHD_YES; + + out1: + MHD_destroy_response (resp); + out: + return pmwebapi_notify_error (connection, rc); +} + + +/* ------------------------------------------------------------------------ */ + +/* Buffer for building a MHD_Response incrementally. libmicrohttpd does not + provide such a facility natively. */ +struct mhdb { + size_t buf_size; + size_t buf_used; + char *buf; /* malloc/realloc()'d. If NULL, mhdbuf is a loser: + Attempt no landings there. */ +}; + + +/* Create a MHD response buffer of given initial size. Upon failure, + leave the buffer unallocated, which will block any mhdb_printfs, + and cause mhdb_fini_response to fail. */ +static void mhdb_init (struct mhdb *md, size_t size) +{ + md->buf_size = size; + md->buf_used = 0; + md->buf = malloc (md->buf_size); /* may be NULL => loser */ +} + + +/* Add a given formatted string to the end of the MHD response buffer. + Extend/realloc the buffer if needed. If unable, free the buffer, + which will block any further mhdb_vsprintfs, and cause the + mhdb_fini_response to fail. */ +static void mhdb_printf(struct mhdb *md, const char *fmt, ...) __attribute__((format(printf, 2, 3))); +static void mhdb_printf(struct mhdb *md, const char *fmt, ...) +{ + va_list vl; + int n; + int buf_remaining = md->buf_size - md->buf_used; + + if (md->buf == NULL) return; /* reject loser */ + + va_start (vl, fmt); + n = vsnprintf (md->buf + md->buf_used, buf_remaining, fmt, vl); + va_end (vl); + + if (n >= buf_remaining) { /* complex case: buffer overflow */ + void *nbuf; + + md->buf_size += n*128; /* pad it to discourage reoffense */ + buf_remaining += n*128; + nbuf = realloc (md->buf, md->buf_size); + + if (nbuf == NULL) { /* ENOMEM */ + free (md->buf); /* realloc left it alone */ + md->buf = NULL; /* tag loser */ + } else { /* success */ + md->buf = nbuf; + /* Try vsnprintf again into the new buffer. */ + va_start (vl, fmt); + n = vsnprintf (md->buf + md->buf_used, buf_remaining, fmt, vl); + va_end (vl); + assert (n >= 0 && n < buf_remaining); /* should not fail */ + md->buf_used += n; + } + } else if (n < 0) { /* simple case: other vsprintf error */ + free (md->buf); + md->buf = NULL; /* tag loser */ + } else { /* normal case: vsprintf success */ + md->buf_used += n; + } + +} + + +/* Print a string with JSON quoting. Replace non-ASCII characters + with \uFFFD "REPLACEMENT CHARACTER". */ +static void mhdb_print_qstring(struct mhdb *md, const char *value) +{ + const char *c; + + mhdb_printf(md, "\""); + for (c = value; *c; c++) { + if (! isascii(*c)) + mhdb_printf(md, "\\uFFFD"); + else if (isalnum(*c)) + mhdb_printf(md, "%c", *c); + else if (ispunct(*c) && !iscntrl(*c) && (*c != '\\' && *c != '\"')) + mhdb_printf(md, "%c", *c); + else if (*c == ' ') + mhdb_printf(md, "%c", *c); + else + mhdb_printf(md, "\\u00%02x", *c); + } + mhdb_printf(md, "\""); +} + + +/* A convenience function to print a vanilla-ascii key and an + unknown ancestry value as a JSON pair. Add given suffix, + which is likely to be a comma or a \n. */ +static void mhdb_print_key_value(struct mhdb *md, const char *key, const char *value, + const char *suffix) +{ + mhdb_printf(md, "\"%s\":", key); + mhdb_print_qstring(md, value); + mhdb_printf(md, "%s", suffix); +} + + +#if 0 /* unused */ +/* Ensure that any recent mhdb_printfs are canceled, and that the + final mhdb_fini_response will fail. */ +static void mhdb_unprintf(struct mhdb *md) +{ + if (md->buf) { + free (md->buf); + md->buf = NULL; + } +} +#endif + + +/* Create a MHD_Response from the mhdb, now that we're done with it. + If the buffer overflowed earlier (was unable to be extended), or if + the MHD_create_response* function fails, return NULL. Ensure that + the buffer will be freed either by us here, or later by a + successful MHD_create_response* function. */ +static struct MHD_Response* mhdb_fini_response(struct mhdb *md) +{ + struct MHD_Response *r; + + if (md->buf == NULL) return NULL; /* reject loser */ + r = MHD_create_response_from_buffer (md->buf_used, md->buf, MHD_RESPMEM_MUST_FREE); + if (r == NULL) { + free (md->buf); /* we need to free it ourselves */ + /* fall through */ + } + return r; +} + + + + +/* ------------------------------------------------------------------------ */ + +struct metric_list_traverse_closure { + struct MHD_Connection *connection; + struct webcontext *c; + struct mhdb mhdb; + unsigned num_metrics; +}; + + + +static void metric_list_traverse (const char* metric, void *closure) +{ + struct metric_list_traverse_closure *mltc = closure; + pmID metric_id; + pmDesc metric_desc; + int rc; + char *metric_text; + char *metrics[1] = { (char*) metric }; + + assert (mltc != NULL); + + rc = pmLookupName (1, metrics, & metric_id); + if (rc != 1) { + /* Quietly skip this metric. */ + return; + } + assert (metric_id != PM_ID_NULL); + + rc = pmLookupDesc (metric_id, & metric_desc); + if (rc != 0) { + /* Quietly skip this metric. */ + return; + } + + if (mltc->num_metrics > 0) + mhdb_printf (& mltc->mhdb, ",\n"); + + mhdb_printf (& mltc->mhdb, "{ "); + mhdb_print_key_value (& mltc->mhdb, "name", metric, ","); + rc = pmLookupText (metric_id, PM_TEXT_ONELINE, & metric_text); + if (rc == 0) { + mhdb_print_key_value (& mltc->mhdb, "text-oneline", metric_text, ","); + free (metric_text); + } + rc = pmLookupText (metric_id, PM_TEXT_HELP, & metric_text); + if (rc == 0) { + mhdb_print_key_value (& mltc->mhdb, "text-help", metric_text, ","); + free (metric_text); + } + mhdb_printf (& mltc->mhdb, "\"pmid\":%lu, ", (unsigned long) metric_id); + if (metric_desc.indom != PM_INDOM_NULL) + mhdb_printf (& mltc->mhdb, "\"indom\":%lu, ", (unsigned long) metric_desc.indom); + mhdb_print_key_value (& mltc->mhdb, "sem", + (metric_desc.sem == PM_SEM_COUNTER ? "counter" : + metric_desc.sem == PM_SEM_INSTANT ? "instant" : + metric_desc.sem == PM_SEM_DISCRETE ? "discrete" : + "unknown"), + ","); + mhdb_print_key_value (& mltc->mhdb, "units", + pmUnitsStr (& metric_desc.units), ","); + mhdb_print_key_value (& mltc->mhdb, "type", + pmTypeStr (metric_desc.type), ""); + mhdb_printf (& mltc->mhdb, "}"); + + mltc->num_metrics ++; +} + + +static int pmwebapi_respond_metric_list (struct MHD_Connection *connection, + struct webcontext *c) +{ + struct metric_list_traverse_closure mltc; + const char* val; + struct MHD_Response* resp; + int rc; + + mltc.connection = connection; + mltc.c = c; + mltc.num_metrics = 0; + + /* We need to construct a copy of the entire JSON metric metadata string, + in one long malloc()'d buffer. We size it generously to avoid having + to realloc the bad boy and cause copies. */ + mhdb_init (& mltc.mhdb, 300000); /* 1000 pmns entries * 300 bytes each */ + mhdb_printf(& mltc.mhdb, "{ \"metrics\":[\n"); + + val = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "prefix"); + if (val == NULL) val = ""; + (void) pmTraversePMNS_r (val, & metric_list_traverse, & mltc); /* cannot fail */ + /* XXX: also handle pmids=... */ + /* XXX: also handle names=... */ + + mhdb_printf(&mltc.mhdb, "] }"); + resp = mhdb_fini_response (& mltc.mhdb); + if (resp == NULL) { + pmweb_notify (LOG_ERR, connection, "mhdb_response failed\n"); + rc = -ENOMEM; + goto out; + } + rc = MHD_add_response_header (resp, "Content-Type", "application/json"); + if (rc != MHD_YES) { + pmweb_notify (LOG_ERR, connection, "MHD_add_response_header failed\n"); + rc = -ENOMEM; + goto out1; + } + rc = MHD_add_response_header (resp, "Access-Control-Allow-Origin", "*"); + if (rc != MHD_YES) { + pmweb_notify (LOG_ERR, connection, "MHD_add_response_header ACAO failed\n"); + rc = -ENOMEM; + goto out1; + } + rc = MHD_queue_response (connection, MHD_HTTP_OK, resp); + if (rc != MHD_YES) { + pmweb_notify (LOG_ERR, connection, "MHD_queue_response failed\n"); + rc = -ENOMEM; + goto out1; + } + MHD_destroy_response (resp); + return MHD_YES; + + out1: + MHD_destroy_response (resp); + out: + return pmwebapi_notify_error (connection, rc); +} + + +/* ------------------------------------------------------------------------ */ + +/* Print "value":<RENDERING>, possibly nested for PM_TYPE_EVENT. + Upon failure to decode, print less and return non-0. */ + +static int pmwebapi_format_value (struct mhdb* output, + pmDesc* desc, + pmValueSet* pvs, + int vsidx) +{ + pmValue* value = & pvs->vlist[vsidx]; + pmAtomValue a; + int rc; + + /* unpack, as per pmevent.c:myeventdump */ + if (desc->type == PM_TYPE_EVENT) { + pmResult **res; + int numres, i; + + numres = pmUnpackEventRecords (pvs, vsidx, &res); + if (numres < 0) + return numres; + + mhdb_printf (output, "\"events\":["); + for (i=0; i<numres; i++) { + int j; + if (i>0) + mhdb_printf (output, ","); + mhdb_printf (output, "{ \"timestamp\": { \"s\":%lu, \"us\":%lu } ", + res[i]->timestamp.tv_sec, + res[i]->timestamp.tv_usec); + + mhdb_printf (output, ", \"fields\":["); + for (j=0; j<res[i]->numpmid; j++) { + pmValueSet *fieldvsp = res[i]->vset[j]; + pmDesc fielddesc; + char *fieldname; + + if (j>0) + mhdb_printf (output, ","); + + mhdb_printf (output, "{"); + + /* recurse */ + rc = pmLookupDesc (fieldvsp->pmid, &fielddesc); + if (rc == 0) + rc = pmwebapi_format_value (output, &fielddesc, fieldvsp, 0); + + if (rc == 0) /* printer value: ... etc. ? */ + mhdb_printf (output, ", "); + /* XXX: handle value: for event.flags / event.missed */ + + rc = pmNameID (fieldvsp->pmid, & fieldname); + if (rc == 0) { + mhdb_printf (output, "\"name\":\"%s\"", fieldname); + free (fieldname); + } else { + mhdb_printf (output, "\"name\":\"%s\"", pmIDStr (fieldvsp->pmid)); + } + + mhdb_printf (output, "}"); + } + mhdb_printf (output, "]"); + + mhdb_printf (output, "}\n"); + } + mhdb_printf (output, "]"); + pmFreeEventResult(res); + return 0; + } + + rc = pmExtractValue (pvs->valfmt, value, desc->type, &a, desc->type); + if (rc != 0) + return rc; + + switch (desc->type) { + case PM_TYPE_32: + mhdb_printf (output, "\"value\":%i", a.l); + break; + case PM_TYPE_U32: + mhdb_printf (output, "\"value\":%u", a.ul); + break; + case PM_TYPE_64: + mhdb_printf (output, "\"value\":%" PRIi64, a.ll); + break; + case PM_TYPE_U64: + mhdb_printf (output, "\"value\":%" PRIu64, a.ull); + break; + case PM_TYPE_FLOAT: + mhdb_printf (output, "\"value\":%g", (double)a.f); + break; + case PM_TYPE_DOUBLE: + mhdb_printf (output, "\"value\":%g", a.d); + break; + case PM_TYPE_STRING: + mhdb_print_key_value (output, "value", a.cp, ""); + free (a.cp); + break; + case PM_TYPE_AGGREGATE: + case PM_TYPE_AGGREGATE_STATIC: + { + /* base64-encode binary data */ + const char* p_bytes = & value->value.pval->vbuf[0]; /* from pmevent.c:mydump() */ + unsigned p_size = value->value.pval->vlen - PM_VAL_HDR_SIZE; + unsigned i; + const char base64_encoding_table[64] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + if (value->value.pval->vlen < PM_VAL_HDR_SIZE) /* less than zero size? */ + return -EINVAL; + + mhdb_printf (output, "\"value\":\""); + for (i=0; i<p_size; /* */) { + unsigned char byte_0 = i < p_size ? p_bytes[i++] : 0; + unsigned char byte_1 = i < p_size ? p_bytes[i++] : 0; + unsigned char byte_2 = i < p_size ? p_bytes[i++] : 0; + unsigned int triple = (byte_0 << 16) | (byte_1 << 8) | byte_2; + mhdb_printf (output, "%c", base64_encoding_table[(triple >> 3*6) & 63]); + mhdb_printf (output, "%c", base64_encoding_table[(triple >> 2*6) & 63]); + mhdb_printf (output, "%c", base64_encoding_table[(triple >> 1*6) & 63]); + mhdb_printf (output, "%c", base64_encoding_table[(triple >> 0*6) & 63]); + } + switch (p_size % 3) { + case 0: /*mhdb_printf (output, "");*/ break; + case 1: mhdb_printf (output, "=="); break; + case 2: mhdb_printf (output, "="); break; + } + mhdb_printf (output, "\""); + + if (desc->type != PM_TYPE_AGGREGATE_STATIC) /* XXX: correct? */ + free (a.vbp); + break; + } + default: + /* ... just a complete unknown ... */ + return -EINVAL; + } + + return 0; +} + + + +static int pmwebapi_respond_metric_fetch (struct MHD_Connection *connection, + struct webcontext *c) +{ + const char* val_pmids; + const char* val_names; + struct MHD_Response* resp; + int rc = 0; + int max_num_metrics; + int num_metrics; + int printed_metrics; /* exclude skipped ones */ + pmID *metrics; + struct mhdb output; + pmResult *results; + int i; + + (void) c; + + val_pmids = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "pmids"); + if (val_pmids == NULL) val_pmids = ""; + val_names = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "names"); + if (val_names == NULL) val_names = ""; + + /* Pessimistically overestimate maximum number of pmID elements + we'll need, to allocate the metrics[] array just once, and not have + to range-check. */ + max_num_metrics = strlen(val_pmids)+strlen(val_names); + /* The real minimum is actually closer to strlen()/2, to account + for commas. */ + + num_metrics = 0; + metrics = calloc ((size_t) max_num_metrics, sizeof(pmID)); + if (metrics == NULL) { + pmweb_notify (LOG_ERR, connection, "calloc pmIDs[%d] oom\n", max_num_metrics); + rc = -ENOMEM; + goto out; + } + + /* Loop over names= names in val_names, collect them in metrics[]. */ + while (*val_names != '\0') { + char *name; + char *name_end = strchr (val_names, ','); + char *names[1]; + pmID found_pmid; + int num; + + /* Ignore plain "," XXX: elsewhere too? */ + if (*val_names == ',') { + val_names ++; + continue; + } + + /* Copy just this name piece. */ + if (name_end) { + name = strndup (val_names, (name_end - val_names)); + val_names = name_end + 1; /* skip past , */ + } else { + name = strdup (val_names); + val_names += strlen (val_names); /* skip onto \0 */ + } + names[0] = name; + num = pmLookupName (1, names, & found_pmid); + free(name); + + if (num == 1) { + assert (num_metrics < max_num_metrics); + metrics[num_metrics++] = found_pmid; + } + } + + + /* Loop over pmids= numbers in val_pmids, append them to metrics[]. */ + while (*val_pmids) { + char *numend; + unsigned long pmid = strtoul (val_pmids, & numend, 10); /* matches pmid printing above */ + if (numend == val_pmids) break; /* invalid contents */ + + assert (num_metrics < max_num_metrics); + metrics[num_metrics++] = pmid; + + if (*numend == '\0') break; /* end of string */ + if (*numend == ',') + val_pmids = numend+1; /* advance to next string */ + } + + /* Time to fetch the metric values. */ + /* num_metrics=0 ==> PM_ERR_TOOSMALL */ + rc = pmFetch (num_metrics, metrics, & results); + free (metrics); /* don't need any more */ + if (rc < 0) { + pmweb_notify (LOG_ERR, connection, "pmFetch failed\n"); + goto out; + } + /* NB: we don't care about the possibility of PMCD_*_AGENT bits + being set, so rc > 0. */ + + /* We need to construct a copy of the entire JSON metric value + string, in one long malloc()'d buffer. We size it generously to + avoid having to realloc the bad boy and cause copies. */ + mhdb_init (& output, num_metrics * 200); /* WAG: per-metric size */ + mhdb_printf(& output, "{ \"timestamp\": { \"s\":%lu, \"us\":%lu },\n", + results->timestamp.tv_sec, + results->timestamp.tv_usec); + + mhdb_printf(& output, "\"values\": [\n"); + + assert (results->numpmid == num_metrics); + printed_metrics = 0; + for (i=0; i<results->numpmid; i++) { + int j; + pmValueSet *pvs = results->vset[i]; + char *metric_name; + pmDesc desc; + + if (pvs->numval <= 0) continue; /* error code; skip metric */ + + rc = pmLookupDesc (pvs->pmid, &desc); /* need to find desc.type only */ + if (rc < 0) continue; /* quietly skip it */ + + if (printed_metrics >= 1) + mhdb_printf (& output, ",\n"); + mhdb_printf (& output, "{ "); + + mhdb_printf (& output, "\"pmid\":%lu, ", (unsigned long) pvs->pmid); + rc = pmNameID (pvs->pmid, &metric_name); + if (rc == 0) { + mhdb_print_key_value (& output, "name", metric_name, ","); + free (metric_name); + } + mhdb_printf (& output, "\"instances\": [\n"); + for (j=0; j<pvs->numval; j++) { + pmValue* val = & pvs->vlist[j]; + int printed_value; + + mhdb_printf (& output, "{"); + + printed_value = ! pmwebapi_format_value (& output, & desc, pvs, j); + + if (desc.indom != PM_INDOM_NULL) + mhdb_printf (& output, "%s \"instance\":%d", + printed_value ? ", " : "", /* comma separation */ + val->inst); + + mhdb_printf (& output, "}"); + if (j+1 < pvs->numval) + mhdb_printf (& output, ","); /* comma separation */ + } + mhdb_printf(& output, "] }"); /* iteration over instances */ + printed_metrics ++; /* comma separation at beginning of loop */ + } + mhdb_printf(& output, "] }"); /* iteration over metrics */ + + pmFreeResult (results); /* not needed any more */ + + resp = mhdb_fini_response (& output); + if (resp == NULL) { + pmweb_notify (LOG_ERR, connection, "mhdb_response failed\n"); + rc = -ENOMEM; + goto out; + } + rc = MHD_add_response_header (resp, "Content-Type", "application/json"); + if (rc != MHD_YES) { + pmweb_notify (LOG_ERR, connection, "MHD_add_response_header failed\n"); + rc = -ENOMEM; + goto out1; + } + rc = MHD_add_response_header (resp, "Access-Control-Allow-Origin", "*"); + if (rc != MHD_YES) { + pmweb_notify (LOG_ERR, connection, "MHD_add_response_header ACAO failed\n"); + rc = -ENOMEM; + goto out1; + } + rc = MHD_queue_response (connection, MHD_HTTP_OK, resp); + if (rc != MHD_YES) { + pmweb_notify (LOG_ERR, connection, "MHD_queue_response failed\n"); + rc = -ENOMEM; + goto out1; + } + MHD_destroy_response (resp); + return MHD_YES; + + out1: + MHD_destroy_response (resp); + out: + return pmwebapi_notify_error (connection, rc); +} + + +/* ------------------------------------------------------------------------ */ + + +static int pmwebapi_respond_instance_list (struct MHD_Connection *connection, + struct webcontext *c) +{ + const char* val_indom; + const char* val_name; + const char* val_instance; + const char* val_iname; + struct MHD_Response* resp; + int rc = 0; + int max_num_instances; + int num_instances; + int printed_instances; + int *instances; + struct mhdb output; + pmID metric_id; + pmDesc metric_desc; + pmInDom inDom; + int i; + + (void) c; + val_indom = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "indom"); + if (val_indom == NULL) val_indom = ""; + val_name = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "name"); + if (val_name == NULL) val_name = ""; + val_instance = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "instance"); + if (val_instance == NULL) val_instance = ""; + val_iname = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "iname"); + if (val_iname == NULL) val_iname = ""; + + /* Obtain the instance domain. */ + if (0 == strcmp(val_indom, "")) { + rc = pmLookupName (1, (char **)& val_name, & metric_id); + if (rc != 1) { + pmweb_notify (LOG_ERR, connection, "failed to lookup metric '%s'\n", val_name); + goto out; + } + assert (metric_id != PM_ID_NULL); + + rc = pmLookupDesc (metric_id, & metric_desc); + if (rc != 0) { + pmweb_notify (LOG_ERR, connection, "failed to lookup metric '%s'\n", val_name); + goto out; + } + + inDom = metric_desc.indom; + } else { + char *numend; + inDom = strtoul (val_indom, & numend, 10); + if (numend == val_indom) { + pmweb_notify (LOG_ERR, connection, "failed to parse indom '%s'\n", val_indom); + rc = -EINVAL; + goto out; + } + } + + /* Pessimistically overestimate maximum number of instance IDs needed. */ + max_num_instances = strlen(val_indom) + strlen(val_name); + num_instances = 0; + instances = calloc ((size_t) max_num_instances, sizeof(int)); + if (instances == NULL) { + pmweb_notify (LOG_ERR, connection, "calloc instances[%d] oom\n", max_num_instances); + rc = -ENOMEM; + goto out; + } + + /* In the case where neither val_instance nor val_iname are + specified, pmGetInDom will allocate different arrays on our + behalf, so we don't have to worry about accounting for how many + instances are returned in that case. */ + + /* Loop over instance= numbers in val_instance, collect them in instances[]. */ + while (*val_instance != '\0') { + char *numend; + int iid = (int) strtoul (val_instance, & numend, 10); + if (numend == val_instance) break; /* invalid contents */ + assert (num_instances < max_num_instances); + instances[num_instances++] = iid; + + if (*numend == '\0') break; /* end of string */ + if (*numend == ',') + val_instance = numend+1; /* advance to next string */ + } + + /* Loop over iname= names in val_iname, collect them in instances[]. */ + while (*val_iname != '\0') { + char *iname; + char *iname_end = strchr (val_iname, ','); + int iid; + + /* Ignore plain "," XXX: elsewhere too? */ + if (*val_iname == ',') { + val_iname ++; + continue; + } + + if (iname_end) { + iname = strndup (val_iname, (iname_end - val_iname)); + val_iname = iname_end + 1; /* skip past , */ + } else { + iname = strdup (val_iname); + val_iname += strlen (val_iname); /* skip onto \0 */ + } + + iid = pmLookupInDom(inDom, iname); + + if (iid > 0) { + assert (num_instances < max_num_instances); + instances[num_instances++] = iid; + } + } + + /* Time to fetch the instance info. */ + int *instlist; + char **namelist = NULL; + if (num_instances == 0) { + free (instances); + + num_instances = pmGetInDom(inDom, & instlist, & namelist); + if (num_instances < 1) { + pmweb_notify (LOG_ERR, connection, "pmGetInDom failed\n"); + rc = num_instances; + goto out; + } + } else { + instlist = instances; + } + + /* Build the response string all in one giant buffer: */ + mhdb_init (& output, num_instances * 200); + mhdb_printf (& output, "{ \"indom\": %lu,\n", (unsigned long) inDom); + mhdb_printf (& output, "\"instances\": [\n"); + + printed_instances = 0; + for (i=0; i<num_instances; i++) { + char *instance_name; + + if (namelist != NULL) { + instance_name = namelist[i]; + } else { + rc = pmNameInDom (inDom, instlist[i], & instance_name); + if (rc != 0) continue; /* skip this instance quietly */ + } + + if (printed_instances >= 1) + mhdb_printf (& output, ",\n"); + mhdb_printf (& output, "{ \"instance\":%d,\n", instlist[i]); + mhdb_print_key_value (& output, "name", instance_name, "\n"); + mhdb_printf (& output, "}"); + + if (namelist == NULL) free (instance_name); + + printed_instances ++; /* comma separation at beginning of loop */ + } + mhdb_printf(& output, "] }"); /* iteration over instances */ + + /* Free no-longer-needed things: */ + free (instlist); + if (namelist != NULL) free (namelist); + + resp = mhdb_fini_response (& output); + if (resp == NULL) { + pmweb_notify (LOG_ERR, connection, "mhdb_response failed\n"); + rc = -ENOMEM; + goto out; + } + rc = MHD_add_response_header (resp, "Content-Type", "application/json"); + if (rc != MHD_YES) { + pmweb_notify (LOG_ERR, connection, "MHD_add_response_header failed\n"); + rc = -ENOMEM; + goto out1; + } + rc = MHD_add_response_header (resp, "Access-Control-Allow-Origin", "*"); + if (rc != MHD_YES) { + pmweb_notify (LOG_ERR, connection, "MHD_add_response_header ACAO failed\n"); + rc = -ENOMEM; + goto out1; + } + rc = MHD_queue_response (connection, MHD_HTTP_OK, resp); + if (rc != MHD_YES) { + pmweb_notify (LOG_ERR, connection, "MHD_queue_response failed\n"); + rc = -ENOMEM; + goto out1; + } + MHD_destroy_response (resp); + return MHD_YES; + + out1: + MHD_destroy_response (resp); + out: + return pmwebapi_notify_error (connection, rc); +} + + +/* ------------------------------------------------------------------------ */ + + +int pmwebapi_respond (void *cls, struct MHD_Connection *connection, + const char* url, + const char* method, const char* upload_data, size_t *upload_data_size) +{ + /* We emit CORS header for all successful json replies, namely: + Access-Control-Access-Origin: * + https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS */ + + /* NB: url is already edited to remove the /pmapi/ prefix. */ + long webapi_ctx; + __pmHashNode *chn; + struct webcontext *c; + char *context_command; + int rc = 0; + + (void) cls; + (void) upload_data; + (void) upload_data_size; + + /* Decode the calls to the web API. */ + + /* -------------------------------------------------------------------- */ + /* context creation */ + /* if-multithreaded: write-lock contexts */ + if (0 == strcmp (url, "context") && + new_contexts_p && /* permitted */ + (0 == strcmp (method, "POST") || 0 == strcmp (method, "GET"))) + return pmwebapi_respond_new_context (connection); + + /* -------------------------------------------------------------------- */ + /* All other calls use $CTX/command, so we parse $CTX + generally and map it to the webcontext* */ + if (! (0 == strcmp (method, "POST") || 0 == strcmp (method, "GET"))) { + pmweb_notify (LOG_WARNING, connection, "unknown method %s\n", method); + rc = -EINVAL; + goto out; + } + errno = 0; + webapi_ctx = strtol (url, & context_command, 10); /* matches %d above */ + if (errno != 0 + || webapi_ctx <= 0 /* range check, plus string-nonemptyness check */ + || webapi_ctx > INT_MAX /* matches random() loop above */ + || *context_command != '/') { /* parsed up to the next slash */ + pmweb_notify (LOG_WARNING, connection, "unrecognized %s url %s \n", method, url); + rc = -EINVAL; + goto out; + } + context_command ++; /* skip the / */ + + /* if-multithreaded: read-lock contexts */ + chn = __pmHashSearch ((int)webapi_ctx, & contexts); + if (chn == NULL) { + pmweb_notify (LOG_WARNING, connection, "unknown web context #%ld\n", webapi_ctx); + rc = PM_ERR_NOCONTEXT; + goto out; + } + c = (struct webcontext *) chn->data; + assert (c != NULL); + + /* Process HTTP Basic userid/password, if supplied. Both returned strings + need to be free(3)'d later. */ + if (c->userid != NULL) { /* Did this context requires userid/password auth? */ + char *userid = NULL; + char *password = NULL; + userid = MHD_basic_auth_get_username_password (connection, &password); + + /* 401 */ + if (userid == NULL || password == NULL) { + static char auth_req_msg[] = "authentication required"; + struct MHD_Response *resp; + char auth_realm[40]; + + free (userid); + free (password); + resp = MHD_create_response_from_buffer (strlen(auth_req_msg), + auth_req_msg, + MHD_RESPMEM_PERSISTENT); + if (! resp) { + rc = -ENOMEM; + goto out; + } + + /* We need the user to resubmit this with http + authentication info, with a custom HTTP authentication + realm for this context. */ + snprintf (auth_realm, sizeof(auth_realm), + "%s/%ld", uriprefix, webapi_ctx); + rc = MHD_queue_basic_auth_fail_response (connection, auth_realm, resp); + MHD_destroy_response (resp); + if (rc != MHD_YES) { + rc = -ENOMEM; + goto out; + } + return MHD_YES; + } + /* 403 */ + if (strcmp (userid, c->userid) || + (c->password && strcmp (password, c->password))) { + static char auth_failed_msg[] = "authentication failed"; + struct MHD_Response *resp; + + free (userid); + free (password); + resp = MHD_create_response_from_buffer (strlen(auth_failed_msg), + auth_failed_msg, + MHD_RESPMEM_PERSISTENT); + if (! resp) { + rc = -ENOMEM; + goto out; + } + /* We need the user to resubmit this with http authentication info. */ + rc = MHD_queue_response (connection, MHD_HTTP_FORBIDDEN, resp); + MHD_destroy_response (resp); + if (rc != MHD_YES) { + rc = -ENOMEM; + goto out; + } + return MHD_YES; + } + /* FALLTHROUGH: authentication required & succeeded. */ + free (userid); + free (password); + } + + /* Update last-use of this connection. */ + if (c->expires != 0) { + time (& c->expires); + c->expires += c->mypolltimeout; + } + + /* Switch to this context for subsequent operations. */ + /* if-multithreaded: watch out. */ + rc = pmUseContext (c->context); + if (rc) + goto out; + + /* -------------------------------------------------------------------- */ + /* metric enumeration: /context/$ID/_metric */ + if (0 == strcmp (context_command, "_metric") && + (0 == strcmp (method, "POST") || 0 == strcmp (method, "GET"))) + return pmwebapi_respond_metric_list (connection, c); + + /* -------------------------------------------------------------------- */ + /* metric instance metadata: /context/$ID/_indom */ + if (0 == strcmp (context_command, "_indom") && + (0 == strcmp (method, "POST") || 0 == strcmp (method, "GET"))) + return pmwebapi_respond_instance_list (connection, c); + + /* -------------------------------------------------------------------- */ + /* metric fetch: /context/$ID/_fetch */ + if (0 == strcmp (context_command, "_fetch") && + (0 == strcmp (method, "POST") || 0 == strcmp (method, "GET"))) + return pmwebapi_respond_metric_fetch (connection, c); + + pmweb_notify (LOG_WARNING, connection, "unrecognized %s context command %s \n", method, context_command); + + out: + return pmwebapi_notify_error (connection, rc); +} |