You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by zw...@apache.org on 2020/06/02 22:33:29 UTC

[trafficserver] 01/02: Rework stats over http so that it supports both a config file and the original path parameter (#6542)

This is an automated email from the ASF dual-hosted git repository.

zwoop pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git

commit c12e01e4f265c18c61444dccac0e9b9ca0c85d4a
Author: Evan Zelkowitz <ez...@apache.org>
AuthorDate: Fri May 1 09:47:10 2020 -0700

    Rework stats over http so that it supports both a config file and the original path parameter (#6542)
    
    * Rework stats over http so that it supports both a config file and the original path parameter. With the addition of a config file you can now set within the config the stats path, an ipv4/6 whitelist for the stats path, and which record types you would like to see in the output.  The majority of this work was done on the Apache Traffic Control "astats" plugin which has been used in that project for years
    
    (cherry picked from commit 32042355a9f0f8937c64117bb7b94fae7148ffdc)
---
 doc/admin-guide/plugins/stats_over_http.en.rst |  25 ++
 plugins/stats_over_http/stats_over_http.c      | 439 ++++++++++++++++++++++++-
 2 files changed, 456 insertions(+), 8 deletions(-)

diff --git a/doc/admin-guide/plugins/stats_over_http.en.rst b/doc/admin-guide/plugins/stats_over_http.en.rst
index 34326cd..5bdca0e 100644
--- a/doc/admin-guide/plugins/stats_over_http.en.rst
+++ b/doc/admin-guide/plugins/stats_over_http.en.rst
@@ -67,3 +67,28 @@ and the URL would then be e.g.::
 
 This is weak security at best, since the secret could possibly leak if you are
 careless and send it over clear text.
+
+Config File Usage
+=================
+
+stats_over_http.so also accepts a configuration file taken as a parameter
+
+The plugin first checks if the parameter that was passed in is a file that exists, if so
+it uses that as a config file, otherwise if a parameter exists it assumes that it is meant
+to be used a path value (as if you were not using a config file)
+
+You can add comments to the config file, starting with a `#` value
+
+Other options you can specify:
+
+.. option:: path=
+
+This sets the path value for stats
+
+.. option:: allow_ip=
+
+A comma separated white list of ipv4 addresses allowed to accesss the endpoint
+
+.. option:: allow_ip6=
+
+A comma separated white list of ipv6 addresses allowed to access the endpoint
diff --git a/plugins/stats_over_http/stats_over_http.c b/plugins/stats_over_http/stats_over_http.c
index 3f44b68..cdfc8f9 100644
--- a/plugins/stats_over_http/stats_over_http.c
+++ b/plugins/stats_over_http/stats_over_http.c
@@ -33,18 +33,57 @@
 #include <string.h>
 #include <inttypes.h>
 #include <getopt.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <unistd.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
 
 #include "tscore/ink_defs.h"
 
 #define PLUGIN_NAME "stats_over_http"
+#define FREE_TMOUT 300000
+#define STR_BUFFER_SIZE 1024
+
+#define SYSTEM_RECORD_TYPE (0x100)
+#define DEFAULT_RECORD_TYPES (SYSTEM_RECORD_TYPE | TS_RECORDTYPE_PROCESS | TS_RECORDTYPE_PLUGIN)
+
+#define DEFAULT_IP "0.0.0.0"
+#define DEFAULT_IP6 "::"
 
 /* global holding the path used for access to this JSON data */
-static const char *url_path = "_stats";
-static int url_path_len;
+#define DEFAULT_URL_PATH "_stats"
 
 static bool integer_counters = false;
 static bool wrap_counters    = false;
 
+typedef struct {
+  unsigned int recordTypes;
+  char *stats_path;
+  int stats_path_len;
+  char *allowIps;
+  int ipCount;
+  char *allowIps6;
+  int ip6Count;
+} config_t;
+typedef struct {
+  char *config_path;
+  volatile time_t last_load;
+  config_t *config;
+} config_holder_t;
+
+int configReloadRequests = 0;
+int configReloads        = 0;
+time_t lastReloadRequest = 0;
+time_t lastReload        = 0;
+time_t astatsLoad        = 0;
+
+static int free_handler(TSCont cont, TSEvent event, void *edata);
+static int config_handler(TSCont cont, TSEvent event, void *edata);
+static config_t *get_config(TSCont cont);
+static config_holder_t *new_config_holder(const char *path);
+static bool is_ip_allowed(const config_t *config, const struct sockaddr *addr);
+
 typedef struct stats_state_t {
   TSVConn net_vc;
   TSVIO read_vio;
@@ -58,6 +97,14 @@ typedef struct stats_state_t {
   int body_written;
 } stats_state;
 
+static char *
+nstr(const char *s)
+{
+  char *mys = (char *)TSmalloc(strlen(s) + 1);
+  strcpy(mys, s);
+  return mys;
+}
+
 static void
 stats_cleanup(TSCont contp, stats_state *my_state)
 {
@@ -236,12 +283,14 @@ stats_origin(TSCont contp ATS_UNUSED, TSEvent event ATS_UNUSED, void *edata)
 {
   TSCont icontp;
   stats_state *my_state;
+  config_t *config;
   TSHttpTxn txnp = (TSHttpTxn)edata;
   TSMBuffer reqp;
   TSMLoc hdr_loc = NULL, url_loc = NULL;
   TSEvent reenable = TS_EVENT_HTTP_CONTINUE;
 
   TSDebug(PLUGIN_NAME, "in the read stuff");
+  config = get_config(contp);
 
   if (TSHttpTxnClientReqGet(txnp, &reqp, &hdr_loc) != TS_SUCCESS) {
     goto cleanup;
@@ -255,7 +304,13 @@ stats_origin(TSCont contp ATS_UNUSED, TSEvent event ATS_UNUSED, void *edata)
   const char *path = TSUrlPathGet(reqp, url_loc, &path_len);
   TSDebug(PLUGIN_NAME, "Path: %.*s", path_len, path);
 
-  if (!(path_len != 0 && path_len == url_path_len && !memcmp(path, url_path, url_path_len))) {
+  if (!(path_len != 0 && path_len == config->stats_path_len && !memcmp(path, config->stats_path, config->stats_path_len))) {
+    goto notforme;
+  }
+
+  const struct sockaddr *addr = TSHttpTxnClientAddrGet(txnp);
+  if (!is_ip_allowed(config, addr)) {
+    TSDebug(PLUGIN_NAME, "not right ip");
     goto notforme;
   }
 
@@ -294,6 +349,8 @@ TSPluginInit(int argc, const char *argv[])
   static const struct option longopts[] = {{(char *)("integer-counters"), no_argument, NULL, 'i'},
                                            {(char *)("wrap-counters"), no_argument, NULL, 'w'},
                                            {NULL, 0, NULL, 0}};
+  TSCont main_cont, config_cont;
+  config_holder_t *config_holder;
 
   info.plugin_name   = PLUGIN_NAME;
   info.vendor_name   = "Apache Software Foundation";
@@ -301,6 +358,7 @@ TSPluginInit(int argc, const char *argv[])
 
   if (TSPluginRegister(&info) != TS_SUCCESS) {
     TSError("[%s] registration failed", PLUGIN_NAME);
+    goto done;
   }
 
   for (;;) {
@@ -322,13 +380,378 @@ init:
   argc -= optind;
   argv += optind;
 
-  if (argc > 0) {
-    url_path = TSstrdup(argv[0] + ('/' == argv[0][0] ? 1 : 0)); /* Skip leading / */
+  config_holder = new_config_holder(argc > 0 ? argv[0] : NULL);
+
+  /* Path was not set during load, so the param was not a config file, we also
+    have an argument so it must be the path, set it here.  Otherwise if no argument
+    then use the default _stats path */
+  if ((config_holder->config != NULL) && (config_holder->config->stats_path == 0) && (argc > 0) &&
+      (config_holder->config_path == NULL)) {
+    config_holder->config->stats_path     = TSstrdup(argv[0] + ('/' == argv[0][0] ? 1 : 0));
+    config_holder->config->stats_path_len = strlen(config_holder->config->stats_path);
+  } else if ((config_holder->config != NULL) && (config_holder->config->stats_path == 0)) {
+    config_holder->config->stats_path     = nstr(DEFAULT_URL_PATH);
+    config_holder->config->stats_path_len = strlen(config_holder->config->stats_path);
   }
-  url_path_len = strlen(url_path);
 
   /* Create a continuation with a mutex as there is a shared global structure
      containing the headers to add */
-  TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, TSContCreate(stats_origin, TSMutexCreate()));
-  TSDebug(PLUGIN_NAME, "stats module registered");
+  main_cont = TSContCreate(stats_origin, NULL);
+  TSContDataSet(main_cont, (void *)config_holder);
+  TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, main_cont);
+
+  /* Create continuation for management updates to re-read config file */
+  config_cont = TSContCreate(config_handler, TSMutexCreate());
+  TSContDataSet(config_cont, (void *)config_holder);
+  TSMgmtUpdateRegister(config_cont, PLUGIN_NAME);
+  TSDebug(PLUGIN_NAME, "stats module registered with path %s", config_holder->config->stats_path);
+
+done:
+  return;
+}
+
+static bool
+is_ip_match(const char *ip, char *ipmask, char mask)
+{
+  unsigned int j, i, k;
+  char cm;
+  // to be able to set mask to 128
+  unsigned int umask = 0xff & mask;
+
+  for (j = 0, i = 0; ((i + 1) * 8) <= umask; i++) {
+    if (ip[i] != ipmask[i]) {
+      return false;
+    }
+    j += 8;
+  }
+  cm = 0;
+  for (k = 0; j < umask; j++, k++) {
+    cm |= 1 << (7 - k);
+  }
+
+  if ((ip[i] & cm) != (ipmask[i] & cm)) {
+    return false;
+  }
+  return true;
+}
+
+static bool
+is_ip_allowed(const config_t *config, const struct sockaddr *addr)
+{
+  char ip_port_text_buffer[INET6_ADDRSTRLEN];
+  int i;
+  char *ipmask;
+  if (!addr) {
+    return true;
+  }
+
+  if (addr->sa_family == AF_INET && config->allowIps) {
+    const struct sockaddr_in *addr_in = (struct sockaddr_in *)addr;
+    const char *ip                    = (char *)&addr_in->sin_addr;
+
+    for (i = 0; i < config->ipCount; i++) {
+      ipmask = config->allowIps + (i * (sizeof(struct in_addr) + 1));
+      if (is_ip_match(ip, ipmask, ipmask[4])) {
+        TSDebug(PLUGIN_NAME, "clientip is %s--> ALLOW", inet_ntop(AF_INET, ip, ip_port_text_buffer, INET6_ADDRSTRLEN));
+        return true;
+      }
+    }
+    TSDebug(PLUGIN_NAME, "clientip is %s--> DENY", inet_ntop(AF_INET, ip, ip_port_text_buffer, INET6_ADDRSTRLEN));
+    return false;
+
+  } else if (addr->sa_family == AF_INET6 && config->allowIps6) {
+    const struct sockaddr_in6 *addr_in6 = (struct sockaddr_in6 *)addr;
+    const char *ip                      = (char *)&addr_in6->sin6_addr;
+
+    for (i = 0; i < config->ip6Count; i++) {
+      ipmask = config->allowIps6 + (i * (sizeof(struct in6_addr) + 1));
+      if (is_ip_match(ip, ipmask, ipmask[sizeof(struct in6_addr)])) {
+        TSDebug(PLUGIN_NAME, "clientip6 is %s--> ALLOW", inet_ntop(AF_INET6, ip, ip_port_text_buffer, INET6_ADDRSTRLEN));
+        return true;
+      }
+    }
+    TSDebug(PLUGIN_NAME, "clientip6 is %s--> DENY", inet_ntop(AF_INET6, ip, ip_port_text_buffer, INET6_ADDRSTRLEN));
+    return false;
+  }
+  return true;
+}
+
+static void
+parseIps(config_t *config, char *ipStr)
+{
+  char buffer[STR_BUFFER_SIZE];
+  char *p, *tok1, *tok2, *ip;
+  int i, mask;
+  char ip_port_text_buffer[INET_ADDRSTRLEN];
+
+  if (!ipStr) {
+    config->ipCount = 1;
+    ip = config->allowIps = TSmalloc(sizeof(struct in_addr) + 1);
+    inet_pton(AF_INET, DEFAULT_IP, ip);
+    ip[4] = 0;
+    return;
+  }
+
+  strcpy(buffer, ipStr);
+  p = buffer;
+  while (strtok_r(p, ", \n", &p)) {
+    config->ipCount++;
+  }
+  if (!config->ipCount) {
+    return;
+  }
+  config->allowIps = TSmalloc(5 * config->ipCount); // 4 bytes for ip + 1 for bit mask
+  strcpy(buffer, ipStr);
+  p = buffer;
+  i = 0;
+  while ((tok1 = strtok_r(p, ", \n", &p))) {
+    TSDebug(PLUGIN_NAME, "%d) parsing: %s", i + 1, tok1);
+    tok2 = strtok_r(tok1, "/", &tok1);
+    ip   = config->allowIps + ((sizeof(struct in_addr) + 1) * i);
+    if (!inet_pton(AF_INET, tok2, ip)) {
+      TSDebug(PLUGIN_NAME, "%d) skipping: %s", i + 1, tok1);
+      continue;
+    }
+
+    if (tok1 != NULL) {
+      tok2 = strtok_r(tok1, "/", &tok1);
+    }
+    if (!tok2) {
+      mask = 32;
+    } else {
+      mask = atoi(tok2);
+    }
+    ip[4] = mask;
+    TSDebug(PLUGIN_NAME, "%d) adding netmask: %s/%d", i + 1, inet_ntop(AF_INET, ip, ip_port_text_buffer, INET_ADDRSTRLEN), ip[4]);
+    i++;
+  }
+}
+static void
+parseIps6(config_t *config, char *ipStr)
+{
+  char buffer[STR_BUFFER_SIZE];
+  char *p, *tok1, *tok2, *ip;
+  int i, mask;
+  char ip_port_text_buffer[INET6_ADDRSTRLEN];
+
+  if (!ipStr) {
+    config->ip6Count = 1;
+    ip = config->allowIps6 = TSmalloc(sizeof(struct in6_addr) + 1);
+    inet_pton(AF_INET6, DEFAULT_IP6, ip);
+    ip[sizeof(struct in6_addr)] = 0;
+    return;
+  }
+
+  strcpy(buffer, ipStr);
+  p = buffer;
+  while (strtok_r(p, ", \n", &p)) {
+    config->ip6Count++;
+  }
+  if (!config->ip6Count) {
+    return;
+  }
+
+  config->allowIps6 = TSmalloc((sizeof(struct in6_addr) + 1) * config->ip6Count); // 16 bytes for ip + 1 for bit mask
+  strcpy(buffer, ipStr);
+  p = buffer;
+  i = 0;
+  while ((tok1 = strtok_r(p, ", \n", &p))) {
+    TSDebug(PLUGIN_NAME, "%d) parsing: %s", i + 1, tok1);
+    tok2 = strtok_r(tok1, "/", &tok1);
+    ip   = config->allowIps6 + ((sizeof(struct in6_addr) + 1) * i);
+    if (!inet_pton(AF_INET6, tok2, ip)) {
+      TSDebug(PLUGIN_NAME, "%d) skipping: %s", i + 1, tok1);
+      continue;
+    }
+
+    if (tok1 != NULL) {
+      tok2 = strtok_r(tok1, "/", &tok1);
+    }
+
+    if (!tok2) {
+      mask = 128;
+    } else {
+      mask = atoi(tok2);
+    }
+    ip[sizeof(struct in6_addr)] = mask;
+    TSDebug(PLUGIN_NAME, "%d) adding netmask: %s/%d", i + 1, inet_ntop(AF_INET6, ip, ip_port_text_buffer, INET6_ADDRSTRLEN),
+            ip[sizeof(struct in6_addr)]);
+    i++;
+  }
+}
+
+static config_t *
+new_config(TSFile fh)
+{
+  char buffer[STR_BUFFER_SIZE];
+  config_t *config       = NULL;
+  config                 = (config_t *)TSmalloc(sizeof(config_t));
+  config->stats_path     = 0;
+  config->stats_path_len = 0;
+  config->allowIps       = 0;
+  config->ipCount        = 0;
+  config->allowIps6      = 0;
+  config->ip6Count       = 0;
+  config->recordTypes    = DEFAULT_RECORD_TYPES;
+
+  if (!fh) {
+    TSDebug(PLUGIN_NAME, "No config file, using defaults");
+    return config;
+  }
+
+  while (TSfgets(fh, buffer, STR_BUFFER_SIZE - 1)) {
+    if (*buffer == '#') {
+      continue; /* # Comments, only at line beginning */
+    }
+    char *p = 0;
+    if ((p = strstr(buffer, "path="))) {
+      p += strlen("path=");
+      if (p[0] == '/') {
+        p++;
+      }
+      config->stats_path     = nstr(strtok_r(p, " \n", &p));
+      config->stats_path_len = strlen(config->stats_path);
+    } else if ((p = strstr(buffer, "record_types="))) {
+      p += strlen("record_types=");
+      config->recordTypes = strtol(strtok_r(p, " \n", &p), NULL, 16);
+    } else if ((p = strstr(buffer, "allow_ip="))) {
+      p += strlen("allow_ip=");
+      parseIps(config, p);
+    } else if ((p = strstr(buffer, "allow_ip6="))) {
+      p += strlen("allow_ip6=");
+      parseIps6(config, p);
+    }
+  }
+  if (!config->ipCount) {
+    parseIps(config, NULL);
+  }
+  if (!config->ip6Count) {
+    parseIps6(config, NULL);
+  }
+  TSDebug(PLUGIN_NAME, "config path=%s", config->stats_path);
+
+  return config;
+}
+
+static void
+delete_config(config_t *config)
+{
+  TSDebug(PLUGIN_NAME, "Freeing config");
+  TSfree(config->allowIps);
+  TSfree(config->allowIps6);
+  TSfree(config->stats_path);
+  TSfree(config);
+}
+
+// standard api below...
+static config_t *
+get_config(TSCont cont)
+{
+  config_holder_t *configh = (config_holder_t *)TSContDataGet(cont);
+  if (!configh) {
+    return 0;
+  }
+  return configh->config;
+}
+
+static void
+load_config_file(config_holder_t *config_holder)
+{
+  TSFile fh = NULL;
+  struct stat s;
+
+  config_t *newconfig, *oldconfig;
+  TSCont free_cont;
+
+  configReloadRequests++;
+  lastReloadRequest = time(NULL);
+
+  // check date
+  if ((config_holder->config_path == NULL) || (stat(config_holder->config_path, &s) < 0)) {
+    TSDebug(PLUGIN_NAME, "Could not stat %s", config_holder->config_path);
+    config_holder->config_path = NULL;
+    if (config_holder->config) {
+      return;
+    }
+  } else {
+    TSDebug(PLUGIN_NAME, "s.st_mtime=%lu, last_load=%lu", s.st_mtime, config_holder->last_load);
+    if (s.st_mtime < config_holder->last_load) {
+      return;
+    }
+  }
+
+  if (config_holder->config_path != NULL) {
+    TSDebug(PLUGIN_NAME, "Opening config file: %s", config_holder->config_path);
+    fh = TSfopen(config_holder->config_path, "r");
+  }
+
+  if (!fh) {
+    TSError("[%s] Unable to open config: %s. Will use the param as the path, or %s if null\n", PLUGIN_NAME,
+            config_holder->config_path, DEFAULT_URL_PATH);
+    if (config_holder->config) {
+      return;
+    }
+  }
+
+  newconfig = 0;
+  newconfig = new_config(fh);
+  if (newconfig) {
+    configReloads++;
+    lastReload               = lastReloadRequest;
+    config_holder->last_load = lastReloadRequest;
+    config_t **confp         = &(config_holder->config);
+    oldconfig                = __sync_lock_test_and_set(confp, newconfig);
+    if (oldconfig) {
+      TSDebug(PLUGIN_NAME, "scheduling free: %p (%p)", oldconfig, newconfig);
+      free_cont = TSContCreate(free_handler, TSMutexCreate());
+      TSContDataSet(free_cont, (void *)oldconfig);
+      TSContScheduleOnPool(free_cont, FREE_TMOUT, TS_THREAD_POOL_TASK);
+    }
+  }
+  if (fh)
+    TSfclose(fh);
+  return;
+}
+
+static config_holder_t *
+new_config_holder(const char *path)
+{
+  config_holder_t *config_holder = TSmalloc(sizeof(config_holder_t));
+  config_holder->config_path     = 0;
+  config_holder->config          = 0;
+  config_holder->last_load       = 0;
+
+  if (path) {
+    config_holder->config_path = nstr(path);
+  } else {
+    config_holder->config_path = NULL;
+  }
+  load_config_file(config_holder);
+  return config_holder;
+}
+
+static int
+free_handler(TSCont cont, TSEvent event, void *edata)
+{
+  config_t *config;
+  config = (config_t *)TSContDataGet(cont);
+  delete_config(config);
+  TSContDestroy(cont);
+  return 0;
+}
+
+static int
+config_handler(TSCont cont, TSEvent event, void *edata)
+{
+  config_holder_t *config_holder;
+  config_holder = (config_holder_t *)TSContDataGet(cont);
+  load_config_file(config_holder);
+
+  /* We received a reload, check if the path value was removed since it was not set after load.
+     If unset, then we'll use the default */
+  if (config_holder->config->stats_path == 0) {
+    config_holder->config->stats_path     = nstr(DEFAULT_URL_PATH);
+    config_holder->config->stats_path_len = strlen(config_holder->config->stats_path);
+  }
+  return 0;
 }