Export host statistics in prometheus text format on /metrics.
[darkstat] / http.c
diff --git a/http.c b/http.c
index e39f16b..7ba7e47 100644 (file)
--- a/http.c
+++ b/http.c
@@ -1,5 +1,5 @@
 /* darkstat 3
- * copyright (c) 2001-2012 Emil Mikulic.
+ * copyright (c) 2001-2016 Emil Mikulic.
  *
  * http.c: embedded webserver.
  * This borrows a lot of code from darkhttpd.
 #include <unistd.h>
 #include <zlib.h>
 
+static char *http_base_url = NULL;
+static int http_base_len = 0;
+
 static const char mime_type_xml[] = "text/xml";
 static const char mime_type_html[] = "text/html; charset=us-ascii";
+static const char mime_type_text_prometheus[] = "text/plain; version=0.0.4";
 static const char mime_type_css[] = "text/css";
 static const char mime_type_js[] = "text/javascript";
+static const char mime_type_png[] = "image/png";
 static const char encoding_identity[] = "identity";
 static const char encoding_gzip[] = "gzip";
 
@@ -56,7 +61,7 @@ struct connection {
 
     int socket;
     struct sockaddr_storage client;
-    long last_active_mono;
+    time_t last_active_mono;
     enum {
         RECV_REQUEST,          /* receiving request */
         SEND_HEADER_AND_REPLY, /* try to send header+reply together */
@@ -381,23 +386,28 @@ static void generate_header(struct connection *conn,
     if (conn->encoding == NULL)
         conn->encoding = encoding_identity;
 
-    verbosef("http: %d %s (%s: %zu bytes)", code, text,
-        conn->encoding, conn->reply_length);
+    verbosef("http: %d %s (%s: %zu bytes)",
+             code,
+             text,
+             conn->encoding,
+             conn->reply_length);
     conn->header_length = xasprintf(&(conn->header),
         "HTTP/1.1 %d %s\r\n"
         "Date: %s\r\n"
         "Server: %s\r\n"
         "Vary: Accept-Encoding\r\n"
         "Content-Type: %s\r\n"
-        "Content-Length: %d\r\n"
+        "Content-Length: %qu\r\n"
         "Content-Encoding: %s\r\n"
         "X-Robots-Tag: noindex, noarchive\r\n"
         "%s"
-        "\r\n"
-        ,
+        "\r\n",
         code, text,
-        rfc1123_date(date, now_real()), server,
-        conn->mime_type, conn->reply_length, conn->encoding,
+        rfc1123_date(date, now_real()),
+        server,
+        conn->mime_type,
+        (qu)conn->reply_length,
+        conn->encoding,
         conn->header_extra);
     conn->http_code = code;
 }
@@ -407,6 +417,9 @@ static void generate_header(struct connection *conn,
 /* ---------------------------------------------------------------------------
  * A default reply for any (erroneous) occasion.
  */
+static void default_reply(struct connection *conn,
+    const int errcode, const char *errname, const char *format, ...)
+    _printflike_(4, 5);
 static void default_reply(struct connection *conn,
     const int errcode, const char *errname, const char *format, ...)
 {
@@ -457,8 +470,8 @@ static char *parse_field(const struct connection *conn, const char *field)
 
     /* find end */
     for (bound2 = bound1;
-        conn->request[bound2] != '\r' &&
-        bound2 < conn->request_length; bound2++)
+        bound2 < conn->request_length &&
+        conn->request[bound2] != '\r'; bound2++)
             ;
 
     /* copy to buffer */
@@ -530,7 +543,7 @@ static_style_css(struct connection *conn)
 {
 #include "stylecss.h"
 
-    conn->reply = style_css;
+    conn->reply = (char*)style_css;
     conn->reply_length = style_css_len;
     conn->reply_dont_free = 1;
     conn->mime_type = mime_type_css;
@@ -544,12 +557,26 @@ static_graph_js(struct connection *conn)
 {
 #include "graphjs.h"
 
-    conn->reply = graph_js;
+    conn->reply = (char*)graph_js;
     conn->reply_length = graph_js_len;
     conn->reply_dont_free = 1;
     conn->mime_type = mime_type_js;
 }
 
+/* ---------------------------------------------------------------------------
+ * Web interface: favicon.
+ */
+static void
+static_favicon(struct connection *conn)
+{
+#include "favicon.h"
+
+    conn->reply = (char*)favicon_png;
+    conn->reply_length = sizeof(favicon_png);
+    conn->reply_dont_free = 1;
+    conn->mime_type = mime_type_png;
+}
+
 /* ---------------------------------------------------------------------------
  * gzip a reply, if requested and possible.  Don't bother with a minimum
  * length requirement, I've never seen a page fail to compress.
@@ -571,11 +598,16 @@ process_gzip(struct connection *conn)
     zs.zfree = Z_NULL;
     zs.opaque = Z_NULL;
 
-    if (deflateInit2(&zs, Z_BEST_COMPRESSION, Z_DEFLATED,
-        15+16, /* 15 = biggest window, 16 = add gzip header+trailer */
-        8 /* default */,
-        Z_DEFAULT_STRATEGY) != Z_OK)
-       return;
+    if (deflateInit2(&zs,
+                     Z_BEST_COMPRESSION,
+                     Z_DEFLATED,
+                     15+16, /* 15 = biggest window,
+                               16 = add gzip header+trailer */
+                     8 /* default */,
+                     Z_DEFAULT_STRATEGY) != Z_OK) {
+        free(buf);
+        return;
+    }
 
     zs.avail_in = conn->reply_length;
     zs.next_in = (unsigned char *)conn->reply;
@@ -586,7 +618,7 @@ process_gzip(struct connection *conn)
     if (deflate(&zs, Z_FINISH) != Z_STREAM_END) {
         deflateEnd(&zs);
         free(buf);
-        verbosef("failed to compress %u bytes", (unsigned int)len);
+        verbosef("failed to compress %zu bytes", len);
         return;
     }
 
@@ -605,22 +637,32 @@ process_gzip(struct connection *conn)
  */
 static void process_get(struct connection *conn)
 {
-    char *decoded_url, *safe_url;
+    char *safe_url;
 
     verbosef("http: %s \"%s\" %s", conn->method, conn->uri,
         (conn->query == NULL)?"":conn->query);
 
-    /* work out path of file being requested */
-    decoded_url = urldecode(conn->uri);
-
-    /* make sure it's safe */
-    safe_url = make_safe_uri(decoded_url);
-    free(decoded_url);
-    if (safe_url == NULL)
     {
-        default_reply(conn, 400, "Bad Request",
-                "You requested an invalid URI: %s", conn->uri);
-        return;
+        /* Decode the URL being requested. */
+        char *decoded_url;
+        char *decoded_url_offset;
+
+        decoded_url = urldecode(conn->uri);
+
+        /* Optionally strip the base. */
+        decoded_url_offset = decoded_url;
+        if (str_starts_with(decoded_url, http_base_url)) {
+            decoded_url_offset += http_base_len - 1;
+        }
+
+        /* Make sure it's safe. */
+        safe_url = make_safe_uri(decoded_url_offset);
+        free(decoded_url);
+        if (safe_url == NULL) {
+            default_reply(conn, 400, "Bad Request",
+                    "You requested an invalid URI: %s", conn->uri);
+            return;
+        }
     }
 
     if (strcmp(safe_url, "/") == 0) {
@@ -647,11 +689,19 @@ static void process_get(struct connection *conn)
         /* hack around Opera caching the XML */
         conn->header_extra = "Pragma: no-cache\r\n";
     }
+    else if (str_starts_with(safe_url, "/metrics")) {
+        struct str *buf = text_metrics();
+        str_extract(buf, &(conn->reply_length), &(conn->reply));
+        conn->mime_type = mime_type_text_prometheus;
+    }
     else if (strcmp(safe_url, "/style.css") == 0)
         static_style_css(conn);
     else if (strcmp(safe_url, "/graph.js") == 0)
         static_graph_js(conn);
-    else {
+    else if (strcmp(safe_url, "/favicon.ico") == 0) {
+        /* serves a PNG instead of an ICO, might cause problems for IE6 */
+        static_favicon(conn);
+    } else {
         default_reply(conn, 404, "Not Found",
             "The page you requested could not be found.");
         free(safe_url);
@@ -873,6 +923,40 @@ static void poll_send_reply(struct connection *conn)
     if (conn->reply_sent == conn->reply_length) conn->state = DONE;
 }
 
+
+
+/* --------------------------------------------------------------------------
+ * Initialize the base url.
+ */
+void http_init_base(const char *url) {
+    char *slashed_url, *safe_url;
+    size_t urllen;
+
+    if (url == NULL) {
+        http_base_url = strdup("/");
+    } else {
+        /* Make sure that the url has leading and trailing slashes. */
+        urllen = strlen(url);
+        slashed_url = xmalloc(urllen+3);
+        slashed_url[0] = '/';
+        memcpy(slashed_url+1, url, urllen); /* don't copy NUL */
+        slashed_url[urllen+1] = '/';
+        slashed_url[urllen+2] = '\0';
+
+        /* Clean the url. */
+        safe_url = make_safe_uri(slashed_url);
+        free(slashed_url);
+        if (safe_url == NULL) {
+            verbosef("invalid base \"%s\", ignored", url);
+            http_base_url = strdup("/"); /* set to default */
+        } else {
+            http_base_url = safe_url;
+        }
+    }
+    http_base_len = strlen(http_base_url);
+    verbosef("set base url to \"%s\"", http_base_url);
+}
+
 /* Use getaddrinfo to figure out what type of socket to create and
  * what to bind it to.  "bindaddr" can be NULL.  Remember to freeaddrinfo()
  * the result.
@@ -962,11 +1046,12 @@ static void http_listen_one(struct addrinfo *ai,
     if (listen(sockin, 128) == -1)
         err(1, "listen() failed");
 
-    verbosef("listening on http://%s%s%s:%u/",
+    verbosef("listening on http://%s%s%s:%u%s",
         (ai->ai_family == AF_INET6) ? "[" : "",
         ipaddr,
         (ai->ai_family == AF_INET6) ? "]" : "",
-        bindport);
+        bindport,
+        http_base_url);
 
     /* add to insocks */
     insocks = xrealloc(insocks, sizeof(*insocks) * (insock_num + 1));
@@ -1120,8 +1205,11 @@ void http_poll(fd_set *recv_set, fd_set *send_set)
 
 void http_stop(void) {
     struct connection *conn;
+    struct connection *next;
     unsigned int i;
 
+    free(http_base_url);
+
     /* Close listening sockets. */
     for (i=0; i<insock_num; i++)
         close(insocks[i]);
@@ -1129,7 +1217,7 @@ void http_stop(void) {
     insocks = NULL;
 
     /* Close in-flight connections. */
-    LIST_FOREACH(conn, &connlist, entries) {
+    LIST_FOREACH_SAFE(conn, &connlist, entries, next) {
         LIST_REMOVE(conn, entries);
         free_connection(conn);
         free(conn);