Blob Blame History Raw
5af58c8af pmdastatsd: fix minor sizeof issues found by Coverity scan
b3f78dc82 pmlogconf: fix resource leak found by coverity scan
8a3ed1b26 pmdastatsd: initialize stack variable to keep Coverity happy
6902959e5 pmdastatsd: fix Coverity LOCK issues on error paths
548cad8c5 libpcp_web: ensure context is freed only after timer is fully closed
01e8bb436 services: pmlogger and pmie services want pmcd on boot
20959e794 Fix of 1845241 - Intermittent pmlogconf core dumps
32d6febf4 pcp-atop: resolve other paths of potential null task pointer dereference
cda567efe pmproxy: improve diagnostics, particularly relating to http requests
e0bb9e66c pmproxy: cleanup, remove unused flags and dead code in http encoding
9da331eb8 pmproxy: support the OPTIONS protocol in HTTP 1.1
1d84081af libpcp_web: add resilience to descriptor lookup paths

--- a/src/pmdas/statsd/src/aggregator-metric-duration-exact.c	2019-08-21 11:33:26.000000000 +1000
+++ b/src/pmdas/statsd/src/aggregator-metric-duration-exact.c	2020-06-11 13:10:57.393576397 +1000
@@ -45,7 +45,7 @@
     double** new_values = realloc(collection->values, sizeof(double*) * new_length);
     ALLOC_CHECK("Unable to allocate memory for collection value.");
     collection->values = new_values;
-    collection->values[collection->length] = (double*) malloc(sizeof(double*));
+    collection->values[collection->length] = (double*) malloc(sizeof(double));
     ALLOC_CHECK("Unable to allocate memory for duration collection value.");
     *(collection->values[collection->length]) = value;
     collection->length = new_length;
--- a/src/pmdas/statsd/src/aggregator-metric-labels.c	2020-02-18 16:32:40.000000000 +1100
+++ b/src/pmdas/statsd/src/aggregator-metric-labels.c	2020-06-11 13:10:57.393576397 +1000
@@ -140,7 +140,7 @@
 
 static char*
 create_instance_label_segment_str(char* tags) {
-    char buffer[JSON_BUFFER_SIZE];
+    char buffer[JSON_BUFFER_SIZE] = {'\0'};
     size_t tags_length = strlen(tags) + 1;
     if (tags_length > JSON_BUFFER_SIZE) {
         return NULL;
@@ -197,7 +197,7 @@
     ALLOC_CHECK("Unable to allocate memory for labels string in metric label record.");
     memcpy((*out)->labels, datagram->tags, labels_length);
     struct metric_label_metadata* meta = 
-        (struct metric_label_metadata*) malloc(sizeof(struct metric_label_metadata*));
+        (struct metric_label_metadata*) malloc(sizeof(struct metric_label_metadata));
     ALLOC_CHECK("Unable to allocate memory for metric label metadata.");
     (*out)->meta = meta;
     (*out)->type = METRIC_TYPE_NONE;
--- a/src/pmdas/statsd/src/network-listener.c	2019-08-27 11:09:16.000000000 +1000
+++ b/src/pmdas/statsd/src/network-listener.c	2020-06-11 13:10:57.393576397 +1000
@@ -68,7 +68,7 @@
     struct timeval tv;
     freeaddrinfo(res);
     int max_udp_packet_size = config->max_udp_packet_size;
-    char *buffer = (char *) malloc(max_udp_packet_size * sizeof(char*));
+    char *buffer = (char *) malloc(max_udp_packet_size * sizeof(char));
     struct sockaddr_storage src_addr;
     socklen_t src_addr_len = sizeof(src_addr);
     int rv;
--- a/src/pmlogconf/pmlogconf.c	2020-05-23 13:33:27.000000000 +1000
+++ b/src/pmlogconf/pmlogconf.c	2020-06-11 13:10:57.394576411 +1000
@@ -735,7 +735,7 @@
 static int
 evaluate_number_values(group_t *group, int type, numeric_cmp_t compare)
 {
-    unsigned int	i, found;
+    int			i, found;
     pmValueSet		*vsp;
     pmValue		*vp;
     pmAtomValue		atom;
@@ -769,7 +769,7 @@
 static int
 evaluate_string_values(group_t *group, string_cmp_t compare)
 {
-    unsigned int	i, found;
+    int			i, found;
     pmValueSet		*vsp;
     pmValue		*vp;
     pmAtomValue		atom;
@@ -828,7 +828,7 @@
 static int
 evaluate_string_regexp(group_t *group, regex_cmp_t compare)
 {
-    unsigned int	i, found;
+    int			i, found;
     pmValueSet		*vsp;
     pmValue		*vp;
     pmAtomValue		atom;
@@ -1478,6 +1478,10 @@
 	} else if (strncmp("#+ groupdir ", bytes, 12) == 0) {
 	    group_dircheck(bytes + 12);
 	} else if (strncmp("#+ ", bytes, 3) == 0) {
+	    if (group) {
+		/* reported by COVERITY RESOURCE LEAK */
+	    	group_free(group);
+	    }
 	    group = group_create(bytes + 3, line);
 	    head = 0;
 	} else if (group) {
--- a/src/pmdas/statsd/src/aggregator-metrics.c	2020-02-18 16:32:40.000000000 +1100
+++ b/src/pmdas/statsd/src/aggregator-metrics.c	2020-06-11 13:10:57.394576411 +1000
@@ -212,7 +212,10 @@
     VERBOSE_LOG(0, "Writing metrics to file...");
     pthread_mutex_lock(&container->mutex);
     metrics* m = container->metrics;
-    if (strlen(config->debug_output_filename) == 0) return; 
+    if (strlen(config->debug_output_filename) == 0) {
+        pthread_mutex_unlock(&container->mutex);
+        return; 
+    }
     int sep = pmPathSeparator();
     char debug_output[MAXPATHLEN];
     pmsprintf(
--- a/src/pmdas/statsd/src/aggregator-stats.c	2020-02-18 16:32:40.000000000 +1100
+++ b/src/pmdas/statsd/src/aggregator-stats.c	2020-06-11 13:10:57.394576411 +1000
@@ -141,7 +141,10 @@
 write_stats_to_file(struct agent_config* config, struct pmda_stats_container* stats) {
     VERBOSE_LOG(0, "Writing stats to file...");
     pthread_mutex_lock(&stats->mutex);
-    if (strlen(config->debug_output_filename) == 0) return; 
+    if (strlen(config->debug_output_filename) == 0) {
+        pthread_mutex_unlock(&stats->mutex);
+        return; 
+    }
     int sep = pmPathSeparator();
     char debug_output[MAXPATHLEN];
     pmsprintf(
--- a/src/libpcp_web/src/webgroup.c	2020-05-22 11:29:27.000000000 +1000
+++ b/src/libpcp_web/src/webgroup.c	2020-06-11 13:10:57.394576411 +1000
@@ -56,17 +56,28 @@
 }
 
 static void
+webgroup_release_context(uv_handle_t *handle)
+{
+    struct context	*context = (struct context *)handle->data;
+
+    if (pmDebugOptions.http)
+	fprintf(stderr, "releasing context %p\n", context);
+
+    pmwebapi_free_context(context);
+}
+
+static void
 webgroup_destroy_context(struct context *context, struct webgroups *groups)
 {
     context->garbage = 1;
 
     if (pmDebugOptions.http)
-	fprintf(stderr, "freeing context %p\n", context);
+	fprintf(stderr, "destroying context %p\n", context);
 
     uv_timer_stop(&context->timer);
     if (groups)
 	dictUnlink(groups->contexts, &context->randomid);
-    pmwebapi_free_context(context);
+    uv_close((uv_handle_t *)&context->timer, webgroup_release_context);
 }
 
 static void
--- a/src/pmie/pmie.service.in	2020-05-27 13:36:47.000000000 +1000
+++ b/src/pmie/pmie.service.in	2020-06-11 13:10:57.394576411 +1000
@@ -4,6 +4,7 @@
 After=network-online.target pmcd.service
 After=pmie_check.timer pmie_check.path pmie_daily.timer
 BindsTo=pmie_check.timer pmie_check.path pmie_daily.timer
+Wants=pmcd.service
 
 [Service]
 Type=notify
--- a/src/pmlogger/pmlogger.service.in	2020-05-22 16:48:32.000000000 +1000
+++ b/src/pmlogger/pmlogger.service.in	2020-06-11 13:10:57.394576411 +1000
@@ -4,6 +4,7 @@
 After=network-online.target pmcd.service
 After=pmlogger_check.timer pmlogger_check.path pmlogger_daily.timer pmlogger_daily-poll.timer
 BindsTo=pmlogger_check.timer pmlogger_check.path pmlogger_daily.timer pmlogger_daily-poll.timer
+Wants=pmcd.service
 
 [Service]
 Type=notify
--- a/src/pcp/atop/showgeneric.c	2020-03-30 12:13:55.000000000 +1100
+++ b/src/pcp/atop/showgeneric.c	2020-06-11 13:10:57.395576426 +1000
@@ -2024,6 +2024,9 @@
 	*/
 	for (numusers=i=0; i < numprocs; i++, curprocs++)
 	{
+	        if (*curprocs == NULL)
+		        continue;
+		
 		if (procsuppress(*curprocs, &procsel))
 			continue;
 
@@ -2069,6 +2072,9 @@
 	*/
 	for (numprogs=i=0; i < numprocs; i++, curprocs++)
 	{
+	        if (*curprocs == NULL)
+		        continue;
+		
 		if (procsuppress(*curprocs, &procsel))
 			continue;
 
@@ -2112,6 +2118,9 @@
 	*/
 	for (numconts=i=0; i < numprocs; i++, curprocs++)
 	{
+	        if (*curprocs == NULL)
+		        continue;
+		
 		if (procsuppress(*curprocs, &procsel))
 			continue;
 
--- a/src/libpcp_web/src/exports	2020-05-22 15:38:47.000000000 +1000
+++ b/src/libpcp_web/src/exports	2020-06-11 13:10:57.397576455 +1000
@@ -189,3 +189,14 @@
     pmWebGroupDestroy;
     sdsKeyDictCallBacks;
 } PCP_WEB_1.12;
+
+PCP_WEB_1.14 {
+  global:
+    dictFetchValue;
+    http_method_str;
+    http_body_is_final;
+    http_parser_version;
+    http_parser_url_init;
+    http_parser_parse_url;
+    http_parser_settings_init;
+} PCP_WEB_1.13;
--- a/src/pmproxy/src/http.c	2020-03-23 09:47:47.000000000 +1100
+++ b/src/pmproxy/src/http.c	2020-06-11 13:10:57.398576470 +1000
@@ -21,6 +21,18 @@
 static int chunked_transfer_size; /* pmproxy.chunksize, pagesize by default */
 static int smallest_buffer_size = 128;
 
+#define MAX_PARAMS_SIZE 4096
+#define MAX_HEADERS_SIZE 128
+
+static sds HEADER_ACCESS_CONTROL_REQUEST_HEADERS,
+	   HEADER_ACCESS_CONTROL_REQUEST_METHOD,
+	   HEADER_ACCESS_CONTROL_ALLOW_METHODS,
+	   HEADER_ACCESS_CONTROL_ALLOW_HEADERS,
+	   HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
+	   HEADER_ACCESS_CONTROL_ALLOWED_HEADERS,
+	   HEADER_CONNECTION, HEADER_CONTENT_LENGTH,
+	   HEADER_ORIGIN, HEADER_WWW_AUTHENTICATE;
+
 /*
  * Simple helpers to manage the cumulative addition of JSON
  * (arrays and/or objects) to a buffer.
@@ -121,45 +133,9 @@
 	return "text/html";
     if (flags & HTTP_FLAG_TEXT)
 	return "text/plain";
-    if (flags & HTTP_FLAG_JS)
-	return "text/javascript";
-    if (flags & HTTP_FLAG_CSS)
-	return "text/css";
-    if (flags & HTTP_FLAG_ICO)
-	return "image/x-icon";
-    if (flags & HTTP_FLAG_JPG)
-	return "image/jpeg";
-    if (flags & HTTP_FLAG_PNG)
-	return "image/png";
-    if (flags & HTTP_FLAG_GIF)
-	return "image/gif";
     return "application/octet-stream";
 }
 
-http_flags
-http_suffix_type(const char *suffix)
-{
-    if (strcmp(suffix, "js") == 0)
-	return HTTP_FLAG_JS;
-    if (strcmp(suffix, "ico") == 0)
-	return HTTP_FLAG_ICO;
-    if (strcmp(suffix, "css") == 0)
-	return HTTP_FLAG_CSS;
-    if (strcmp(suffix, "png") == 0)
-	return HTTP_FLAG_PNG;
-    if (strcmp(suffix, "gif") == 0)
-	return HTTP_FLAG_GIF;
-    if (strcmp(suffix, "jpg") == 0)
-	return HTTP_FLAG_JPG;
-    if (strcmp(suffix, "jpeg") == 0)
-	return HTTP_FLAG_JPG;
-    if (strcmp(suffix, "html") == 0)
-	return HTTP_FLAG_HTML;
-    if (strcmp(suffix, "txt") == 0)
-	return HTTP_FLAG_TEXT;
-    return 0;
-}
-
 static const char * const
 http_content_encoding(http_flags flags)
 {
@@ -259,26 +235,28 @@
 
     header = sdscatfmt(sdsempty(),
 		"HTTP/%u.%u %u %s\r\n"
-		"Connection: Keep-Alive\r\n"
-		"Access-Control-Allow-Origin: *\r\n"
-		"Access-Control-Allow-Headers: Accept, Accept-Language, Content-Language, Content-Type\r\n",
+		"%S: Keep-Alive\r\n",
 		parser->http_major, parser->http_minor,
-		sts, http_status_mapping(sts));
+		sts, http_status_mapping(sts), HEADER_CONNECTION);
+    header = sdscatfmt(header,
+		"%S: *\r\n"
+		"%S: %S\r\n",
+		HEADER_ACCESS_CONTROL_ALLOW_ORIGIN,
+		HEADER_ACCESS_CONTROL_ALLOW_HEADERS,
+		HEADER_ACCESS_CONTROL_ALLOWED_HEADERS);
 
     if (sts == HTTP_STATUS_UNAUTHORIZED && client->u.http.realm)
-	header = sdscatfmt(header, "WWW-Authenticate: Basic realm=\"%S\"\r\n",
-				client->u.http.realm);
+	header = sdscatfmt(header, "%S: Basic realm=\"%S\"\r\n",
+				HEADER_WWW_AUTHENTICATE, client->u.http.realm);
 
-    if ((flags & HTTP_FLAG_STREAMING))
-	header = sdscatfmt(header, "Transfer-encoding: %s\r\n", "chunked");
-
-    if (!(flags & HTTP_FLAG_STREAMING))
-	header = sdscatfmt(header, "Content-Length: %u\r\n", length);
+    if ((flags & (HTTP_FLAG_STREAMING | HTTP_FLAG_NO_BODY)))
+	header = sdscatfmt(header, "Transfer-encoding: chunked\r\n");
+    else
+	header = sdscatfmt(header, "%S: %u\r\n", HEADER_CONTENT_LENGTH, length);
 
-    header = sdscatfmt(header,
-		"Content-Type: %s%s\r\n"
-		"Date: %s\r\n\r\n",
-		http_content_type(flags), http_content_encoding(flags),
+    header = sdscatfmt(header, "Content-Type: %s%s\r\n",
+		http_content_type(flags), http_content_encoding(flags));
+    header = sdscatfmt(header, "Date: %s\r\n\r\n",
 		http_date_string(time(NULL), date, sizeof(date)));
 
     if (pmDebugOptions.http && pmDebugOptions.desperate) {
@@ -288,8 +266,130 @@
     return header;
 }
 
+static sds
+http_header_value(struct client *client, sds header)
+{
+    if (client->u.http.headers == NULL)
+	return NULL;
+    return (sds)dictFetchValue(client->u.http.headers, header);
+}
+
+static sds
+http_headers_allowed(sds headers)
+{
+    (void)headers;
+    return sdsdup(HEADER_ACCESS_CONTROL_ALLOWED_HEADERS);
+}
+
+/* check whether the (preflight) method being proposed is acceptable */
+static int
+http_method_allowed(sds value, http_options options)
+{
+    if (strcmp(value, "GET") == 0 && (options & HTTP_OPT_GET))
+	return 1;
+    if (strcmp(value, "PUT") == 0 && (options & HTTP_OPT_PUT))
+	return 1;
+    if (strcmp(value, "POST") == 0 && (options & HTTP_OPT_POST))
+	return 1;
+    if (strcmp(value, "HEAD") == 0 && (options & HTTP_OPT_HEAD))
+	return 1;
+    if (strcmp(value, "TRACE") == 0 && (options & HTTP_OPT_TRACE))
+	return 1;
+    return 0;
+}
+
+static char *
+http_methods_string(char *buffer, size_t length, http_options options)
+{
+    char		*p = buffer;
+
+    /* ensure room for all options, spaces and comma separation */
+    if (!options || length < 48)
+	return NULL;
+
+    memset(buffer, 0, length);
+    if (options & HTTP_OPT_GET)
+	strcat(p, ", GET");
+    if (options & HTTP_OPT_PUT)
+	strcat(p, ", PUT");
+    if (options & HTTP_OPT_HEAD)
+	strcat(p, ", HEAD");
+    if (options & HTTP_OPT_POST)
+	strcat(p, ", POST");
+    if (options & HTTP_OPT_TRACE)
+	strcat(p, ", TRACE");
+    if (options & HTTP_OPT_OPTIONS)
+	strcat(p, ", OPTIONS");
+    return p + 2; /* skip leading comma+space */
+}
+
+static sds
+http_response_trace(struct client *client)
+{
+    dictIterator	*iterator;
+    dictEntry		*entry;
+    sds			result = sdsempty();
+
+    iterator = dictGetSafeIterator(client->u.http.headers);
+    while ((entry = dictNext(iterator)) != NULL)
+	result = sdscatfmt("%S: %S\r\n", dictGetKey(entry), dictGetVal(entry));
+    dictReleaseIterator(iterator);
+    return result;
+}
+
+static sds
+http_response_access(struct client *client, http_code sts, http_options options)
+{
+    struct http_parser	*parser = &client->u.http.parser;
+    char		buffer[64];
+    sds			header, value, result;
+
+    value = http_header_value(client, HEADER_ACCESS_CONTROL_REQUEST_METHOD);
+    if (value && http_method_allowed(value, options) == 0)
+	sts = HTTP_STATUS_METHOD_NOT_ALLOWED;
+
+    parser->http_major = parser->http_minor = 1;
+
+    header = sdscatfmt(sdsempty(),
+		"HTTP/%u.%u %u %s\r\n"
+		"%S: Keep-Alive\r\n",
+		parser->http_major, parser->http_minor,
+		sts, http_status_mapping(sts), HEADER_CONNECTION);
+    header = sdscatfmt(header, "%S: %u\r\n", HEADER_CONTENT_LENGTH, 0);
+
+    if (sts >= HTTP_STATUS_OK && sts < HTTP_STATUS_BAD_REQUEST) {
+	if ((value = http_header_value(client, HEADER_ORIGIN)))
+	    header = sdscatfmt(header, "%S: %S\r\n",
+			        HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, value);
+
+	header = sdscatfmt(header, "%S: %s\r\n",
+			    HEADER_ACCESS_CONTROL_ALLOW_METHODS,
+			    http_methods_string(buffer, sizeof(buffer), options));
+
+	value = http_header_value(client, HEADER_ACCESS_CONTROL_REQUEST_HEADERS);
+	if (value && (result = http_headers_allowed(value)) != NULL) {
+	    header = sdscatfmt(header, "%S: %S\r\n",
+				HEADER_ACCESS_CONTROL_ALLOW_HEADERS, result);
+	    sdsfree(result);
+	}
+    }
+    if (sts == HTTP_STATUS_UNAUTHORIZED && client->u.http.realm)
+	header = sdscatfmt(header, "%S: Basic realm=\"%S\"\r\n",
+			    HEADER_WWW_AUTHENTICATE, client->u.http.realm);
+
+    header = sdscatfmt(header, "Date: %s\r\n\r\n",
+		http_date_string(time(NULL), buffer, sizeof(buffer)));
+
+    if (pmDebugOptions.http && pmDebugOptions.desperate) {
+	fprintf(stderr, "access response to client %p\n", client);
+	fputs(header, stderr);
+    }
+    return header;
+}
+
 void
-http_reply(struct client *client, sds message, http_code sts, http_flags type)
+http_reply(struct client *client, sds message,
+		http_code sts, http_flags type, http_options options)
 {
     http_flags		flags = client->u.http.flags;
     char		length[32]; /* hex length */
@@ -313,6 +413,15 @@
 
 	suffix = sdsnewlen("0\r\n\r\n", 5);		/* chunked suffix */
 	client->u.http.flags &= ~HTTP_FLAG_STREAMING;	/* end of stream! */
+
+    } else if (flags & HTTP_FLAG_NO_BODY) {
+	if (client->u.http.parser.method == HTTP_OPTIONS)
+	    buffer = http_response_access(client, sts, options);
+	else if (client->u.http.parser.method == HTTP_TRACE)
+	    buffer = http_response_trace(client);
+	else	/* HTTP_HEAD */
+	    buffer = http_response_header(client, 0, sts, type);
+	suffix = NULL;
     } else {	/* regular non-chunked response - headers + response body */
 	if (client->buffer == NULL) {
 	    suffix = message;
@@ -326,10 +435,11 @@
 	buffer = http_response_header(client, sdslen(suffix), sts, type);
     }
 
-    if (pmDebugOptions.http) {
-	fprintf(stderr, "HTTP response (client=%p)\n%s%s",
-			client, buffer, suffix);
-    }
+    if (pmDebugOptions.http)
+	fprintf(stderr, "HTTP %s response (client=%p)\n%s%s",
+			http_method_str(client->u.http.parser.method),
+			client, buffer, suffix ? suffix : "");
+
     client_write(client, buffer, suffix);
 }
 
@@ -363,7 +473,7 @@
 	if (pmDebugOptions.desperate)
 	    fputs(message, stderr);
     }
-    http_reply(client, message, status, HTTP_FLAG_HTML);
+    http_reply(client, message, status, HTTP_FLAG_HTML, 0);
 }
 
 void
@@ -371,6 +481,7 @@
 {
     struct http_parser	*parser = &client->u.http.parser;
     http_flags		flags = client->u.http.flags;
+    const char		*method;
     sds			buffer, suffix;
 
     /* If the client buffer length is now beyond a set maximum size,
@@ -390,16 +501,18 @@
 		buffer = sdsempty();
 	    }
 	    /* prepend a chunked transfer encoding message length (hex) */
-	    buffer = sdscatprintf(buffer, "%lX\r\n", (unsigned long)sdslen(client->buffer));
+	    buffer = sdscatprintf(buffer, "%lX\r\n",
+				 (unsigned long)sdslen(client->buffer));
 	    suffix = sdscatfmt(client->buffer, "\r\n");
 	    /* reset for next call - original released on I/O completion */
 	    client->buffer = NULL;	/* safe, as now held in 'suffix' */
 
 	    if (pmDebugOptions.http) {
-		fprintf(stderr, "HTTP chunked buffer (client %p, len=%lu)\n%s"
-				"HTTP chunked suffix (client %p, len=%lu)\n%s",
-				client, (unsigned long)sdslen(buffer), buffer,
-				client, (unsigned long)sdslen(suffix), suffix);
+		method = http_method_str(client->u.http.parser.method);
+		fprintf(stderr, "HTTP %s chunk buffer (client %p, len=%lu)\n%s"
+				"HTTP %s chunk suffix (client %p, len=%lu)\n%s",
+			method, client, (unsigned long)sdslen(buffer), buffer,
+			method, client, (unsigned long)sdslen(suffix), suffix);
 	    }
 	    client_write(client, buffer, suffix);
 
@@ -527,6 +640,8 @@
 
     if (length == 0)
 	return NULL;
+    if (length > MAX_PARAMS_SIZE)
+	return NULL;
     for (p = url; p < end; p++) {
 	if (*p == '\0')
 	    break;
@@ -558,6 +673,11 @@
     struct servlet	*servlet;
     sds			url;
 
+    if (pmDebugOptions.http || pmDebugOptions.appl0)
+	fprintf(stderr, "HTTP %s %.*s\n",
+			http_method_str(client->u.http.parser.method),
+			(int)length, offset);
+
     if (!(url = http_url_decode(offset, length, &client->u.http.parameters)))
 	return NULL;
     for (servlet = proxy->servlets; servlet != NULL; servlet = servlet->next) {
@@ -576,13 +696,24 @@
 {
     struct client	*client = (struct client *)request->data;
     struct servlet	*servlet;
+    sds			buffer;
     int			sts;
 
     http_client_release(client);	/* new URL, clean slate */
-
-    if ((servlet = servlet_lookup(client, offset, length)) != NULL) {
+    /* server options - https://tools.ietf.org/html/rfc7231#section-4.3.7 */
+    if (length == 1 && *offset == '*' &&
+	client->u.http.parser.method == HTTP_OPTIONS) {
+	buffer = http_response_access(client, HTTP_STATUS_OK, HTTP_SERVER_OPTIONS);
+	client_write(client, buffer, NULL);
+    } else if ((servlet = servlet_lookup(client, offset, length)) != NULL) {
 	client->u.http.servlet = servlet;
 	if ((sts = client->u.http.parser.status_code) == 0) {
+	    if (client->u.http.parser.method == HTTP_OPTIONS ||
+		client->u.http.parser.method == HTTP_TRACE ||
+		client->u.http.parser.method == HTTP_HEAD)
+		client->u.http.flags |= HTTP_FLAG_NO_BODY;
+	    else
+		client->u.http.flags &= ~HTTP_FLAG_NO_BODY;
 	    client->u.http.headers = dictCreate(&sdsOwnDictCallBacks, NULL);
 	    return 0;
 	}
@@ -616,6 +747,11 @@
 
     if (client->u.http.parser.status_code || !client->u.http.headers)
 	return 0;	/* already in process of failing connection */
+    if (dictSize(client->u.http.headers) >= MAX_HEADERS_SIZE) {
+	client->u.http.parser.status_code =
+		HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE;
+	return 0;
+    }
 
     field = sdsnewlen(offset, length);
     if (pmDebugOptions.http)
@@ -826,6 +962,17 @@
     if (chunked_transfer_size < smallest_buffer_size)
 	chunked_transfer_size = smallest_buffer_size;
 
+    HEADER_ACCESS_CONTROL_REQUEST_HEADERS = sdsnew("Access-Control-Request-Headers");
+    HEADER_ACCESS_CONTROL_REQUEST_METHOD = sdsnew("Access-Control-Request-Method");
+    HEADER_ACCESS_CONTROL_ALLOW_METHODS = sdsnew("Access-Control-Allow-Methods");
+    HEADER_ACCESS_CONTROL_ALLOW_HEADERS = sdsnew("Access-Control-Allow-Headers");
+    HEADER_ACCESS_CONTROL_ALLOW_ORIGIN = sdsnew("Access-Control-Allow-Origin");
+    HEADER_ACCESS_CONTROL_ALLOWED_HEADERS = sdsnew("Accept, Accept-Language, Content-Language, Content-Type");
+    HEADER_CONNECTION = sdsnew("Connection");
+    HEADER_CONTENT_LENGTH = sdsnew("Content-Length");
+    HEADER_ORIGIN = sdsnew("Origin");
+    HEADER_WWW_AUTHENTICATE = sdsnew("WWW-Authenticate");
+
     register_servlet(proxy, &pmseries_servlet);
     register_servlet(proxy, &pmwebapi_servlet);
 }
@@ -839,4 +986,15 @@
 	servlet->close(proxy);
 
     proxymetrics_close(proxy, METRICS_HTTP);
+
+    sdsfree(HEADER_ACCESS_CONTROL_REQUEST_HEADERS);
+    sdsfree(HEADER_ACCESS_CONTROL_REQUEST_METHOD);
+    sdsfree(HEADER_ACCESS_CONTROL_ALLOW_METHODS);
+    sdsfree(HEADER_ACCESS_CONTROL_ALLOW_HEADERS);
+    sdsfree(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN);
+    sdsfree(HEADER_ACCESS_CONTROL_ALLOWED_HEADERS);
+    sdsfree(HEADER_CONNECTION);
+    sdsfree(HEADER_CONTENT_LENGTH);
+    sdsfree(HEADER_ORIGIN);
+    sdsfree(HEADER_WWW_AUTHENTICATE);
 }
--- a/src/pmproxy/src/series.c	2020-02-25 17:47:56.000000000 +1100
+++ b/src/pmproxy/src/series.c	2020-06-11 13:10:57.398576470 +1000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019 Red Hat.
+ * Copyright (c) 2019-2020 Red Hat.
  *
  * This program is free software; you can redistribute it and/or modify it
  * under the terms of the GNU Lesser General Public License as published
@@ -15,8 +15,7 @@
 #include <assert.h>
 
 typedef enum pmSeriesRestKey {
-    RESTKEY_NONE	= 0,
-    RESTKEY_SOURCE,
+    RESTKEY_SOURCE	= 1,
     RESTKEY_DESC,
     RESTKEY_INSTS,
     RESTKEY_LABELS,
@@ -29,7 +28,8 @@
 
 typedef struct pmSeriesRestCommand {
     const char		*name;
-    unsigned int	size;
+    unsigned int	namelen : 16;
+    unsigned int	options : 16;
     pmSeriesRestKey	key;
 } pmSeriesRestCommand;
 
@@ -39,7 +39,8 @@
     pmSeriesFlags	flags;
     pmSeriesTimeWindow	window;
     uv_work_t		loading;
-    unsigned int	working;
+    unsigned int	working : 1;
+    unsigned int	options : 16;
     int			nsids;
     pmSID		*sids;
     pmSID		sid;
@@ -55,16 +56,25 @@
 } pmSeriesBaton;
 
 static pmSeriesRestCommand commands[] = {
-    { .key = RESTKEY_QUERY, .name = "query", .size = sizeof("query")-1 },
-    { .key = RESTKEY_DESC,  .name = "descs",  .size = sizeof("descs")-1 },
-    { .key = RESTKEY_INSTS, .name = "instances", .size = sizeof("instances")-1 },
-    { .key = RESTKEY_LABELS, .name = "labels", .size = sizeof("labels")-1 },
-    { .key = RESTKEY_METRIC, .name = "metrics", .size = sizeof("metrics")-1 },
-    { .key = RESTKEY_SOURCE, .name = "sources", .size = sizeof("sources")-1 },
-    { .key = RESTKEY_VALUES, .name = "values", .size = sizeof("values")-1 },
-    { .key = RESTKEY_LOAD, .name = "load", .size = sizeof("load")-1 },
-    { .key = RESTKEY_PING, .name = "ping", .size = sizeof("ping")-1 },
-    { .key = RESTKEY_NONE }
+    { .key = RESTKEY_QUERY, .options = HTTP_OPTIONS_GET | HTTP_OPTIONS_POST,
+	    .name = "query", .namelen = sizeof("query")-1 },
+    { .key = RESTKEY_DESC, .options = HTTP_OPTIONS_GET | HTTP_OPTIONS_POST,
+	    .name = "descs", .namelen = sizeof("descs")-1 },
+    { .key = RESTKEY_INSTS, .options = HTTP_OPTIONS_GET | HTTP_OPTIONS_POST,
+	    .name = "instances", .namelen = sizeof("instances")-1 },
+    { .key = RESTKEY_LABELS, .options = HTTP_OPTIONS_GET | HTTP_OPTIONS_POST,
+	    .name = "labels", .namelen = sizeof("labels")-1 },
+    { .key = RESTKEY_METRIC, .options = HTTP_OPTIONS_GET | HTTP_OPTIONS_POST,
+	    .name = "metrics", .namelen = sizeof("metrics")-1 },
+    { .key = RESTKEY_SOURCE, .options = HTTP_OPTIONS_GET | HTTP_OPTIONS_POST,
+	    .name = "sources", .namelen = sizeof("sources")-1 },
+    { .key = RESTKEY_VALUES, .options = HTTP_OPTIONS_GET | HTTP_OPTIONS_POST,
+	    .name = "values", .namelen = sizeof("values")-1 },
+    { .key = RESTKEY_LOAD, .options = HTTP_OPTIONS_GET | HTTP_OPTIONS_POST,
+	    .name = "load", .namelen = sizeof("load")-1 },
+    { .key = RESTKEY_PING, .options = HTTP_OPTIONS_GET,
+	    .name = "ping", .namelen = sizeof("ping")-1 },
+    { .name = NULL }	/* sentinel */
 };
 
 /* constant string keys (initialized during servlet setup) */
@@ -78,8 +88,8 @@
 static const char pmseries_success[] = "{\"success\":true}\r\n";
 static const char pmseries_failure[] = "{\"success\":false}\r\n";
 
-static pmSeriesRestKey
-pmseries_lookup_restkey(sds url)
+static pmSeriesRestCommand *
+pmseries_lookup_rest_command(sds url)
 {
     pmSeriesRestCommand	*cp;
     const char		*name;
@@ -88,11 +98,11 @@
 	strncmp(url, "/series/", sizeof("/series/") - 1) == 0) {
 	name = (const char *)url + sizeof("/series/") - 1;
 	for (cp = &commands[0]; cp->name; cp++) {
-	    if (strncmp(cp->name, name, cp->size) == 0)
-		return cp->key;
+	    if (strncmp(cp->name, name, cp->namelen) == 0)
+		return cp;
 	}
     }
-    return RESTKEY_NONE;
+    return NULL;
 }
 
 static void
@@ -518,6 +528,7 @@
 {
     pmSeriesBaton	*baton = (pmSeriesBaton *)arg;
     struct client	*client = baton->client;
+    http_options	options = baton->options;
     http_flags		flags = client->u.http.flags;
     http_code		code;
     sds			msg;
@@ -545,7 +556,7 @@
 	    msg = sdsnewlen(pmseries_failure, sizeof(pmseries_failure) - 1);
 	flags |= HTTP_FLAG_JSON;
     }
-    http_reply(client, msg, code, flags);
+    http_reply(client, msg, code, flags, options);
 }
 
 static void
@@ -555,6 +566,14 @@
 	fprintf(stderr, "series module setup (arg=%p)\n", arg);
 }
 
+static void
+pmseries_log(pmLogLevel level, sds message, void *arg)
+{
+    pmSeriesBaton	*baton = (pmSeriesBaton *)arg;
+
+    proxylog(level, message, baton->client->proxy);
+}
+
 static pmSeriesSettings pmseries_settings = {
     .callbacks.on_match		= on_pmseries_match,
     .callbacks.on_desc		= on_pmseries_desc,
@@ -567,7 +586,7 @@
     .callbacks.on_label		= on_pmseries_label,
     .callbacks.on_done		= on_pmseries_done,
     .module.on_setup		= pmseries_setup,
-    .module.on_info		= proxylog,
+    .module.on_info		= pmseries_log,
 };
 
 static void
@@ -686,7 +705,6 @@
     case RESTKEY_PING:
 	break;
 
-    case RESTKEY_NONE:
     default:
 	client->u.http.parser.status_code = HTTP_STATUS_BAD_REQUEST;
 	break;
@@ -702,15 +720,16 @@
 pmseries_request_url(struct client *client, sds url, dict *parameters)
 {
     pmSeriesBaton	*baton;
-    pmSeriesRestKey	key;
+    pmSeriesRestCommand	*command;
 
-    if ((key = pmseries_lookup_restkey(url)) == RESTKEY_NONE)
+    if ((command = pmseries_lookup_rest_command(url)) == NULL)
 	return 0;
 
     if ((baton = calloc(1, sizeof(*baton))) != NULL) {
 	client->u.http.data = baton;
 	baton->client = client;
-	baton->restkey = key;
+	baton->restkey = command->key;
+	baton->options = command->options;
 	pmseries_setup_request_parameters(client, baton, parameters);
     } else {
 	client->u.http.parser.status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR;
@@ -794,10 +813,12 @@
 
     if (baton->query == NULL) {
 	message = sdsnewlen(failed, sizeof(failed) - 1);
-	http_reply(client, message, HTTP_STATUS_BAD_REQUEST, HTTP_FLAG_JSON);
+	http_reply(client, message, HTTP_STATUS_BAD_REQUEST,
+			HTTP_FLAG_JSON, baton->options);
     } else if (baton->working) {
 	message = sdsnewlen(loading, sizeof(loading) - 1);
-	http_reply(client, message, HTTP_STATUS_CONFLICT, HTTP_FLAG_JSON);
+	http_reply(client, message, HTTP_STATUS_CONFLICT,
+			HTTP_FLAG_JSON, baton->options);
     } else {
 	uv_queue_work(client->proxy->events, &baton->loading,
 			pmseries_load_work, pmseries_load_done);
@@ -810,8 +831,17 @@
     pmSeriesBaton	*baton = (pmSeriesBaton *)client->u.http.data;
     int			sts;
 
-    if (client->u.http.parser.status_code)
+    if (client->u.http.parser.status_code) {
+	on_pmseries_done(-EINVAL, baton);
+	return 1;
+    }
+
+    if (client->u.http.parser.method == HTTP_OPTIONS ||
+	client->u.http.parser.method == HTTP_TRACE ||
+	client->u.http.parser.method == HTTP_HEAD) {
+	on_pmseries_done(0, baton);
 	return 0;
+    }
 
     switch (baton->restkey) {
     case RESTKEY_QUERY:
--- a/src/pmproxy/src/webapi.c	2020-04-17 15:39:17.000000000 +1000
+++ b/src/pmproxy/src/webapi.c	2020-06-11 13:10:57.399576484 +1000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019 Red Hat.
+ * Copyright (c) 2019-2020 Red Hat.
  *
  * This program is free software; you can redistribute it and/or modify it
  * under the terms of the GNU Lesser General Public License as published
@@ -18,8 +18,7 @@
 #include "util.h"
 
 typedef enum pmWebRestKey {
-    RESTKEY_NONE	= 0,
-    RESTKEY_CONTEXT,
+    RESTKEY_CONTEXT	= 1,
     RESTKEY_METRIC,
     RESTKEY_FETCH,
     RESTKEY_INDOM,
@@ -32,7 +31,8 @@
 
 typedef struct pmWebRestCommand {
     const char		*name;
-    unsigned int	size;
+    unsigned int	namelen : 16;
+    unsigned int	options : 16;
     pmWebRestKey	key;
 } pmWebRestCommand;
 
@@ -47,6 +47,7 @@
     sds			password;	/* from basic auth header */
     unsigned int	times : 1;
     unsigned int	compat : 1;
+    unsigned int	options : 16;
     unsigned int	numpmids;
     unsigned int	numvsets;
     unsigned int	numinsts;
@@ -56,21 +57,31 @@
 } pmWebGroupBaton;
 
 static pmWebRestCommand commands[] = {
-    { .key = RESTKEY_CONTEXT, .name = "context", .size = sizeof("context")-1 },
-    { .key = RESTKEY_PROFILE, .name = "profile", .size = sizeof("profile")-1 },
-    { .key = RESTKEY_SCRAPE, .name = "metrics", .size = sizeof("metrics")-1 },
-    { .key = RESTKEY_METRIC, .name = "metric", .size = sizeof("metric")-1 },
-    { .key = RESTKEY_DERIVE, .name = "derive", .size = sizeof("derive")-1 },
-    { .key = RESTKEY_FETCH, .name = "fetch", .size = sizeof("fetch")-1 },
-    { .key = RESTKEY_INDOM, .name = "indom", .size = sizeof("indom")-1 },
-    { .key = RESTKEY_STORE, .name = "store", .size = sizeof("store")-1 },
-    { .key = RESTKEY_CHILD, .name = "children", .size = sizeof("children")-1 },
-    { .key = RESTKEY_NONE }
+    { .key = RESTKEY_CONTEXT, .options = HTTP_OPTIONS_GET,
+	    .name = "context", .namelen = sizeof("context")-1 },
+    { .key = RESTKEY_PROFILE, .options = HTTP_OPTIONS_GET,
+	    .name = "profile", .namelen = sizeof("profile")-1 },
+    { .key = RESTKEY_SCRAPE, .options = HTTP_OPTIONS_GET,
+	    .name = "metrics", .namelen = sizeof("metrics")-1 },
+    { .key = RESTKEY_METRIC, .options = HTTP_OPTIONS_GET,
+	    .name = "metric", .namelen = sizeof("metric")-1 },
+    { .key = RESTKEY_DERIVE, .options = HTTP_OPTIONS_GET | HTTP_OPTIONS_POST,
+	    .name = "derive", .namelen = sizeof("derive")-1 },
+    { .key = RESTKEY_FETCH, .options = HTTP_OPTIONS_GET,
+	    .name = "fetch", .namelen = sizeof("fetch")-1 },
+    { .key = RESTKEY_INDOM, .options = HTTP_OPTIONS_GET,
+	    .name = "indom", .namelen = sizeof("indom")-1 },
+    { .key = RESTKEY_STORE, .options = HTTP_OPTIONS_GET,
+	    .name = "store", .namelen = sizeof("store")-1 },
+    { .key = RESTKEY_CHILD, .options = HTTP_OPTIONS_GET,
+	    .name = "children", .namelen = sizeof("children")-1 },
+    { .name = NULL }	/* sentinel */
 };
 
 static pmWebRestCommand openmetrics[] = {
-    { .key = RESTKEY_SCRAPE, .name = "/metrics", .size = sizeof("/metrics")-1 },
-    { .key = RESTKEY_NONE }
+    { .key = RESTKEY_SCRAPE, .options = HTTP_OPTIONS_GET,
+	    .name = "/metrics", .namelen = sizeof("/metrics")-1 },
+    { .name = NULL }	/* sentinel */
 };
 
 static sds PARAM_NAMES, PARAM_NAME, PARAM_PMIDS, PARAM_PMID,
@@ -78,8 +89,8 @@
 	   PARAM_CONTEXT, PARAM_CLIENT;
 
 
-static pmWebRestKey
-pmwebapi_lookup_restkey(sds url, unsigned int *compat, sds *context)
+static pmWebRestCommand *
+pmwebapi_lookup_rest_command(sds url, unsigned int *compat, sds *context)
 {
     pmWebRestCommand	*cp;
     const char		*name, *ctxid = NULL;
@@ -94,7 +105,7 @@
 		name++;
 	    } while (isdigit((int)(*name)));
 	    if (*name++ != '/')
-		return RESTKEY_NONE;
+		return NULL;
 	    *context = sdsnewlen(ctxid, name - ctxid - 1);
 	}
 	if (*name == '_') {
@@ -102,13 +113,13 @@
 	    *compat = 1;	/* backward-compatibility mode */
 	}
 	for (cp = &commands[0]; cp->name; cp++)
-	    if (strncmp(cp->name, name, cp->size) == 0)
-		return cp->key;
+	    if (strncmp(cp->name, name, cp->namelen) == 0)
+		return cp;
     }
     for (cp = &openmetrics[0]; cp->name; cp++)
-	if (strncmp(cp->name, url, cp->size) == 0)
-	    return cp->key;
-    return RESTKEY_NONE;
+	if (strncmp(cp->name, url, cp->namelen) == 0)
+	    return cp;
+    return NULL;
 }
 
 static void
@@ -584,9 +595,10 @@
 {
     pmWebGroupBaton	*baton = (pmWebGroupBaton *)arg;
     struct client	*client = (struct client *)baton->client;
-    sds			quoted, msg;
+    http_options	options = baton->options;
     http_flags		flags = client->u.http.flags;
     http_code		code;
+    sds			quoted, msg;
 
     if (pmDebugOptions.series)
 	fprintf(stderr, "%s: client=%p (sts=%d,msg=%s)\n", "on_pmwebapi_done",
@@ -596,7 +608,9 @@
 	code = HTTP_STATUS_OK;
 	/* complete current response with JSON suffix if needed */
 	if ((msg = baton->suffix) == NULL) {	/* empty OK response */
-	    if (flags & HTTP_FLAG_JSON) {
+	    if (flags & HTTP_FLAG_NO_BODY) {
+		msg = sdsempty();
+	    } else if (flags & HTTP_FLAG_JSON) {
 		msg = sdsnewlen("{", 1);
 		if (context)
 		    msg = sdscatfmt(msg, "\"context\":%S,", context);
@@ -628,10 +642,18 @@
 	sdsfree(quoted);
     }
 
-    http_reply(client, msg, code, flags);
+    http_reply(client, msg, code, flags, options);
     client_put(client);
 }
 
+static void
+on_pmwebapi_info(pmLogLevel level, sds message, void *arg)
+{
+    pmWebGroupBaton	*baton = (pmWebGroupBaton *)arg;
+
+    proxylog(level, message, baton->client->proxy);
+}
+
 static pmWebGroupSettings pmwebapi_settings = {
     .callbacks.on_context	= on_pmwebapi_context,
     .callbacks.on_metric	= on_pmwebapi_metric,
@@ -645,7 +667,7 @@
     .callbacks.on_scrape_labels	= on_pmwebapi_scrape_labels,
     .callbacks.on_check		= on_pmwebapi_check,
     .callbacks.on_done		= on_pmwebapi_done,
-    .module.on_info		= proxylog,
+    .module.on_info		= on_pmwebapi_info,
 };
 
 /*
@@ -734,7 +756,6 @@
 	client->u.http.flags |= HTTP_FLAG_JSON;
 	break;
 
-    case RESTKEY_NONE:
     default:
 	client->u.http.parser.status_code = HTTP_STATUS_BAD_REQUEST;
 	break;
@@ -750,11 +771,11 @@
 pmwebapi_request_url(struct client *client, sds url, dict *parameters)
 {
     pmWebGroupBaton	*baton;
-    pmWebRestKey	key;
+    pmWebRestCommand	*command;
     unsigned int	compat = 0;
     sds			context = NULL;
 
-    if ((key = pmwebapi_lookup_restkey(url, &compat, &context)) == RESTKEY_NONE) {
+    if (!(command = pmwebapi_lookup_rest_command(url, &compat, &context))) {
 	sdsfree(context);
 	return 0;
     }
@@ -762,7 +783,8 @@
     if ((baton = calloc(1, sizeof(*baton))) != NULL) {
 	client->u.http.data = baton;
 	baton->client = client;
-	baton->restkey = key;
+	baton->restkey = command->key;
+	baton->options = command->options;
 	baton->compat = compat;
 	baton->context = context;
 	pmwebapi_setup_request_parameters(client, baton, parameters);
@@ -885,17 +907,27 @@
     uv_loop_t		*loop = client->proxy->events;
     uv_work_t		*work;
 
-    /* fail early if something has already gone wrong */
-    if (client->u.http.parser.status_code != 0)
+    /* take a reference on the client to prevent freeing races on close */
+    client_get(client);
+
+    if (client->u.http.parser.status_code) {
+	on_pmwebapi_done(NULL, -EINVAL, NULL, baton);
 	return 1;
+    }
+
+    if (client->u.http.parser.method == HTTP_OPTIONS ||
+	client->u.http.parser.method == HTTP_TRACE ||
+	client->u.http.parser.method == HTTP_HEAD) {
+	on_pmwebapi_done(NULL, 0, NULL, baton);
+	return 0;
+    }
 
-    if ((work = (uv_work_t *)calloc(1, sizeof(uv_work_t))) == NULL)
+    if ((work = (uv_work_t *)calloc(1, sizeof(uv_work_t))) == NULL) {
+	client_put(client);
 	return 1;
+    }
     work->data = baton;
 
-    /* take a reference on the client to prevent freeing races on close */
-    client_get(client);
-
     /* submit command request to worker thread */
     switch (baton->restkey) {
     case RESTKEY_CONTEXT:
@@ -925,11 +957,10 @@
     case RESTKEY_SCRAPE:
 	uv_queue_work(loop, work, pmwebapi_scrape, pmwebapi_work_done);
 	break;
-    case RESTKEY_NONE:
     default:
+	pmwebapi_work_done(work, -EINVAL);
 	client->u.http.parser.status_code = HTTP_STATUS_BAD_REQUEST;
-	client_put(client);
-	free(work);
+	on_pmwebapi_done(NULL, -EINVAL, NULL, baton);
 	return 1;
     }
     return 0;
--- a/src/pmproxy/src/http.h	2019-12-02 16:43:20.000000000 +1100
+++ b/src/pmproxy/src/http.h	2020-06-11 13:10:57.398576470 +1000
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019 Red Hat.
+ * Copyright (c) 2019-2020 Red Hat.
  * 
  * This program is free software; you can redistribute it and/or modify it
  * under the terms of the GNU Lesser General Public License as published
@@ -34,29 +34,39 @@
     HTTP_FLAG_JSON	= (1<<0),
     HTTP_FLAG_TEXT	= (1<<1),
     HTTP_FLAG_HTML	= (1<<2),
-    HTTP_FLAG_JS	= (1<<3),
-    HTTP_FLAG_CSS	= (1<<4),
-    HTTP_FLAG_ICO	= (1<<5),
-    HTTP_FLAG_JPG	= (1<<6),
-    HTTP_FLAG_PNG	= (1<<7),
-    HTTP_FLAG_GIF	= (1<<8),
     HTTP_FLAG_UTF8	= (1<<10),
     HTTP_FLAG_UTF16	= (1<<11),
+    HTTP_FLAG_NO_BODY	= (1<<13),
     HTTP_FLAG_COMPRESS	= (1<<14),
     HTTP_FLAG_STREAMING	= (1<<15),
     /* maximum 16 for server.h */
 } http_flags;
 
+typedef enum http_options {
+    HTTP_OPT_GET	= (1 << HTTP_GET),
+    HTTP_OPT_PUT	= (1 << HTTP_PUT),
+    HTTP_OPT_HEAD	= (1 << HTTP_HEAD),
+    HTTP_OPT_POST	= (1 << HTTP_POST),
+    HTTP_OPT_TRACE	= (1 << HTTP_TRACE),
+    HTTP_OPT_OPTIONS	= (1 << HTTP_OPTIONS),
+    /* maximum 16 in command opts fields */
+} http_options;
+
+#define HTTP_COMMON_OPTIONS (HTTP_OPT_HEAD | HTTP_OPT_TRACE | HTTP_OPT_OPTIONS)
+#define HTTP_OPTIONS_GET    (HTTP_COMMON_OPTIONS | HTTP_OPT_GET)
+#define HTTP_OPTIONS_PUT    (HTTP_COMMON_OPTIONS | HTTP_OPT_PUT)
+#define HTTP_OPTIONS_POST   (HTTP_COMMON_OPTIONS | HTTP_OPT_POST)
+#define HTTP_SERVER_OPTIONS (HTTP_OPTIONS_GET | HTTP_OPT_PUT | HTTP_OPT_POST)
+
 typedef unsigned int http_code;
 
 extern void http_transfer(struct client *);
-extern void http_reply(struct client *, sds, http_code, http_flags);
+extern void http_reply(struct client *, sds, http_code, http_flags, http_options);
 extern void http_error(struct client *, http_code, const char *);
 
 extern int http_decode(const char *, size_t, sds);
 extern const char *http_status_mapping(http_code);
 extern const char *http_content_type(http_flags);
-extern http_flags http_suffix_type(const char *);
 
 extern sds http_get_buffer(struct client *);
 extern void http_set_buffer(struct client *, sds, http_flags);
--- a/qa/1837	1970-01-01 10:00:00.000000000 +1000
+++ b/qa/1837	2020-06-11 13:10:57.396576440 +1000
@@ -0,0 +1,55 @@
+#!/bin/sh
+# PCP QA Test No. 1837
+# Exercise PMWEBAPI handling server OPTIONS.
+#
+# Copyright (c) 2020 Red Hat.  All Rights Reserved.
+#
+
+seq=`basename $0`
+echo "QA output created by $seq"
+
+# get standard environment, filters and checks
+. ./common.product
+. ./common.filter
+. ./common.check
+
+_check_series
+which curl >/dev/null 2>&1 || _notrun "No curl binary installed"
+curl --request-targets 2>&1 | grep -q 'requires parameter' && \
+	_notrun "Test requires curl --request-targets option"
+
+status=1	# failure is the default!
+$sudo rm -rf $tmp.* $seq.full
+trap "cd $here; _cleanup; exit \$status" 0 1 2 3 15
+
+pmproxy_was_running=false
+[ -f $PCP_RUN_DIR/pmproxy.pid ] && pmproxy_was_running=true
+echo "pmproxy_was_running=$pmproxy_was_running" >>$here/$seq.full
+
+_cleanup()
+{
+    if $pmproxy_was_running
+    then
+	echo "Restart pmproxy ..." >>$here/$seq.full
+	_service pmproxy restart >>$here/$seq.full 2>&1
+	_wait_for_pmproxy
+    else
+	echo "Stopping pmproxy ..." >>$here/$seq.full
+	_service pmproxy stop >>$here/$seq.full 2>&1
+    fi
+    $sudo rm -f $tmp.*
+}
+
+# real QA test starts here
+_service pmproxy restart >/dev/null 2>&1
+
+curl -isS --request-target "*" -X OPTIONS http://localhost:44322 \
+	2>&1 | tee -a $here/$seq.full | _webapi_header_filter
+
+echo >>$here/$seq.full
+echo "=== pmproxy log ===" >>$here/$seq.full
+cat $PCP_LOG_DIR/pmproxy/pmproxy.log >>$here/$seq.full
+
+# success, all done
+status=0
+exit
--- a/qa/1837.out	1970-01-01 10:00:00.000000000 +1000
+++ b/qa/1837.out	2020-06-11 13:10:57.397576455 +1000
@@ -0,0 +1,6 @@
+QA output created by 1837
+
+Access-Control-Allow-Methods: GET, PUT, HEAD, POST, TRACE, OPTIONS
+Content-Length: 0
+Date: DATE
+HTTP/1.1 200 OK
--- a/qa/780	2020-04-14 14:41:41.000000000 +1000
+++ b/qa/780	2020-06-11 13:10:57.397576455 +1000
@@ -1,8 +1,8 @@
 #!/bin/sh
 # PCP QA Test No. 780
-# Exercise PMWEBAPI Access-Control-Allow-Origin HTTP header.
+# Exercise PMWEBAPI CORS headers.
 #
-# Copyright (c) 2014,2019 Red Hat.
+# Copyright (c) 2014,2019-2020 Red Hat.
 #
 
 seq=`basename $0`
@@ -16,7 +16,6 @@
 _check_series
 which curl >/dev/null 2>&1 || _notrun "No curl binary installed"
 
-signal=$PCP_BINADM_DIR/pmsignal
 status=1	# failure is the default!
 $sudo rm -rf $tmp.* $seq.full
 trap "cd $here; _cleanup; exit \$status" 0 1 2 3 15
@@ -39,13 +38,21 @@
     $sudo rm -f $tmp.*
 }
 
-unset http_proxy
-unset HTTP_PROXY
-
 # real QA test starts here
 _service pmproxy restart >/dev/null 2>&1
 
-curl -s -S "http://localhost:44323/pmapi/context" -I | _webapi_header_filter
+echo "=== Basic" | tee -a $here/$seq.full
+curl -IsS "http://localhost:44323/pmapi/context" | _webapi_header_filter
+
+echo "=== Preflight" | tee -a $here/$seq.full
+curl -isS -X OPTIONS "http://localhost:44323/series/query?expr=hinv*" | _webapi_header_filter
+
+echo "=== OK Request Method" | tee -a $here/$seq.full
+curl -isS -X OPTIONS -H "Origin: http://example.com" -H "Access-Control-Request-Method: GET" "http://localhost:44323/pmapi/context" | _webapi_header_filter
+
+echo "=== Bad Request Method" | tee -a $here/$seq.full
+curl -isS -X OPTIONS -H "Origin: http://example.com" -H "Access-Control-Request-Method: BAD" "http://localhost:44323/pmapi/context" | _webapi_header_filter
+
 echo >>$here/$seq.full
 echo "=== pmproxy log ===" >>$here/$seq.full
 cat $PCP_LOG_DIR/pmproxy/pmproxy.log >>$here/$seq.full
--- a/qa/780.out	2020-03-23 09:47:47.000000000 +1100
+++ b/qa/780.out	2020-06-11 13:10:57.397576455 +1000
@@ -1,8 +1,27 @@
 QA output created by 780
+=== Basic
 
 Access-Control-Allow-Headers: Accept, Accept-Language, Content-Language, Content-Type
 Access-Control-Allow-Origin: *
-Content-Length: SIZE
 Content-Type: application/json
 Date: DATE
 HTTP/1.1 200 OK
+Transfer-encoding: chunked
+=== Preflight
+
+Access-Control-Allow-Methods: GET, HEAD, POST, TRACE, OPTIONS
+Content-Length: 0
+Date: DATE
+HTTP/1.1 200 OK
+=== OK Request Method
+
+Access-Control-Allow-Methods: GET, HEAD, TRACE, OPTIONS
+Access-Control-Allow-Origin: http://example.com
+Content-Length: 0
+Date: DATE
+HTTP/1.1 200 OK
+=== Bad Request Method
+
+Content-Length: 0
+Date: DATE
+HTTP/1.1 405 Method Not Allowed
--- a/qa/common.check	2020-05-20 10:51:37.000000000 +1000
+++ b/qa/common.check	2020-06-11 13:10:57.397576455 +1000
@@ -2696,7 +2696,7 @@
     tee -a $here/$seq.full \
     | col -b \
     | sed \
-	-e 's/^\(Content-Length:\) [0-9][0-9]*/\1 SIZE/g' \
+	-e 's/^\(Content-Length:\) [1-9][0-9]*/\1 SIZE/g' \
 	-e 's/^\(Date:\).*/\1 DATE/g' \
 	-e 's/\(\"context\":\) [0-9][0-9]*/\1 CTXID/g' \
 	-e '/^Connection: Keep-Alive/d' \
--- a/qa/group	2020-05-28 09:15:22.000000000 +1000
+++ b/qa/group	2020-06-11 13:10:57.397576455 +1000
@@ -1757,6 +1757,7 @@
 1724 pmda.bpftrace local python
 1768 pmfind local
 1793 pmrep pcp2xxx python local
+1837 pmproxy local
 1855 pmda.rabbitmq local
 1896 pmlogger logutil pmlc local
 4751 libpcp threads valgrind local pcp
--- a/qa/1211.out	2020-01-20 16:53:42.000000000 +1100
+++ b/qa/1211.out	2020-06-11 13:10:57.399576484 +1000
@@ -507,9 +507,11 @@
 Perform simple source-based query ...
 
 Error handling - descriptor for bad series identifier
-pmseries: [Error] no descriptor for series identifier no.such.identifier
 
 no.such.identifier
+    PMID: PM_ID_NULL
+    Data Type: ???  InDom: unknown 0xffffffff
+    Semantics: unknown  Units: unknown
 
 Error handling - metric name for bad series identifier
 
--- a/src/libpcp_web/src/query.c	2020-01-20 15:43:31.000000000 +1100
+++ b/src/libpcp_web/src/query.c	2020-06-11 13:10:57.399576484 +1000
@@ -1938,11 +1938,15 @@
 	return -EPROTO;
     }
 
-    /* sanity check - were we given an invalid series identifier? */
+    /* were we given a non-metric series identifier? (e.g. an instance) */
     if (elements[0]->type == REDIS_REPLY_NIL) {
-	infofmt(msg, "no descriptor for series identifier %s", series);
-	batoninfo(baton, PMLOG_ERROR, msg);
-	return -EINVAL;
+	desc->indom = sdscpylen(desc->indom, "unknown", 7);
+	desc->pmid = sdscpylen(desc->pmid, "PM_ID_NULL", 10);
+	desc->semantics = sdscpylen(desc->semantics, "unknown", 7);
+	desc->source = sdscpylen(desc->source, "unknown", 7);
+	desc->type = sdscpylen(desc->type, "unknown", 7);
+	desc->units = sdscpylen(desc->units, "unknown", 7);
+	return 0;
     }
 
     if (extract_string(baton, series, elements[0], &desc->indom, "indom") < 0)