Export host statistics in prometheus text format on /metrics.
[darkstat] / http.c
diff --git a/http.c b/http.c
index b9d8c58..7ba7e47 100644 (file)
--- a/http.c
+++ b/http.c
@@ -1,5 +1,5 @@
 /* darkstat 3
- * copyright (c) 2001-2008 Emil Mikulic.
+ * copyright (c) 2001-2016 Emil Mikulic.
  *
  * http.c: embedded webserver.
  * This borrows a lot of code from darkhttpd.
@@ -8,14 +8,16 @@
  * GNU General Public License version 2. (see COPYING.GPL)
  */
 
-#include "darkstat.h"
-#include "http.h"
+#include "cdefs.h"
+#include "config.h"
 #include "conv.h"
-#include "hosts_db.h"
-#include "graph_db.h"
 #include "err.h"
-#include "queue.h"
+#include "graph_db.h"
+#include "hosts_db.h"
+#include "http.h"
 #include "now.h"
+#include "queue.h"
+#include "str.h"
 
 #include <sys/uio.h>
 #include <sys/socket.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <time.h>
 #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 encoding_gzip[] =
-    "Vary: Accept-Encoding\r\n"
-    "Content-Encoding: gzip\r\n";
+static const char mime_type_png[] = "image/png";
+static const char encoding_identity[] = "identity";
+static const char encoding_gzip[] = "gzip";
 
 static const char server[] = PACKAGE_NAME "/" PACKAGE_VERSION;
 static int idletime = 60;
-static int sockin = -1;             /* socket to accept connections from */
 #define MAX_REQUEST_LENGTH 4000
 
-#ifndef min
-#define min(a,b) (((a) < (b)) ? (a) : (b))
-#endif
+static int *insocks = NULL;
+static unsigned int insock_num = 0;
 
 struct connection {
     LIST_ENTRY(connection) entries;
 
     int socket;
     struct sockaddr_storage client;
-    time_t last_active;
+    time_t last_active_mono;
     enum {
         RECV_REQUEST,          /* receiving request */
         SEND_HEADER_AND_REPLY, /* try to send header+reply together */
@@ -88,6 +93,13 @@ struct connection {
 static LIST_HEAD(conn_list_head, connection) connlist =
     LIST_HEAD_INITIALIZER(conn_list_head);
 
+struct bindaddr_entry {
+    STAILQ_ENTRY(bindaddr_entry) entries;
+    const char *s;
+};
+static STAILQ_HEAD(bindaddrs_head, bindaddr_entry) bindaddrs =
+    STAILQ_HEAD_INITIALIZER(bindaddrs);
+
 /* ---------------------------------------------------------------------------
  * Decode URL by converting %XX (where XX are hexadecimal digits) to the
  * character it represents.  Don't forget to free the return value.
@@ -259,8 +271,8 @@ static struct connection *new_connection(void)
     struct connection *conn = xmalloc(sizeof(*conn));
 
     conn->socket = -1;
-    memset(&conn->client, '\0', sizeof(conn->client));
-    conn->last_active = now;
+    memset(&conn->client, 0, sizeof(conn->client));
+    conn->last_active_mono = now_mono();
     conn->request = NULL;
     conn->request_length = 0;
     conn->accept_gzip = 0;
@@ -269,7 +281,7 @@ static struct connection *new_connection(void)
     conn->query = NULL;
     conn->header = NULL;
     conn->mime_type = NULL;
-    conn->encoding = "";
+    conn->encoding = NULL;
     conn->header_extra = "";
     conn->header_length = 0;
     conn->header_sent = 0;
@@ -295,7 +307,7 @@ static struct connection *new_connection(void)
 /* ---------------------------------------------------------------------------
  * Accept a connection from sockin and add it to the connection queue.
  */
-static void accept_connection(void)
+static void accept_connection(const int sockin)
 {
     struct sockaddr_storage addrin;
     socklen_t sin_size;
@@ -324,10 +336,10 @@ static void accept_connection(void)
     memcpy(&conn->client, &addrin, sizeof(conn->client));
     LIST_INSERT_HEAD(&connlist, conn, entries);
 
-    getnameinfo((struct sockaddr *) &addrin, sizeof(addrin),
+    getnameinfo((struct sockaddr *) &addrin, sin_size,
             ipaddr, sizeof(ipaddr), portstr, sizeof(portstr),
             NI_NUMERICHOST | NI_NUMERICSERV);
-    verbosef("accepted connection from %s:%u", ipaddr, portstr);
+    verbosef("accepted connection from %s:%s", ipaddr, portstr);
 }
 
 
@@ -338,13 +350,16 @@ static void accept_connection(void)
 static void free_connection(struct connection *conn)
 {
     dverbosef("free_connection(%d)", conn->socket);
-    if (conn->socket != -1) close(conn->socket);
-    if (conn->request != NULL) free(conn->request);
-    if (conn->method != NULL) free(conn->method);
-    if (conn->uri != NULL) free(conn->uri);
-    if (conn->query != NULL) free(conn->query);
-    if (conn->header != NULL && !conn->header_dont_free) free(conn->header);
-    if (conn->reply != NULL && !conn->reply_dont_free) free(conn->reply);
+    if (conn->socket != -1)
+        close(conn->socket);
+    free(conn->request);
+    free(conn->method);
+    free(conn->uri);
+    free(conn->query);
+    if (!conn->header_dont_free)
+        free(conn->header);
+    if (!conn->reply_dont_free)
+        free(conn->reply);
 }
 
 
@@ -354,13 +369,47 @@ static void free_connection(struct connection *conn)
  * buffer is returned for convenience.
  */
 #define DATE_LEN 30 /* strlen("Fri, 28 Feb 2003 00:02:08 GMT")+1 */
-static char *rfc1123_date(char *dest, const time_t when)
-{
-    time_t tmp = when;
+static char *rfc1123_date(char *dest, time_t when) {
     if (strftime(dest, DATE_LEN,
-        "%a, %d %b %Y %H:%M:%S %Z", gmtime(&tmp) ) == 0)
+        "%a, %d %b %Y %H:%M:%S %Z", gmtime(&when) ) == 0)
             errx(1, "strftime() failed [%s]", dest);
-    return (dest);
+    return dest;
+}
+
+static void generate_header(struct connection *conn,
+    const int code, const char *text)
+{
+    char date[DATE_LEN];
+
+    assert(conn->header == NULL);
+    assert(conn->mime_type != NULL);
+    if (conn->encoding == NULL)
+        conn->encoding = encoding_identity;
+
+    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: %qu\r\n"
+        "Content-Encoding: %s\r\n"
+        "X-Robots-Tag: noindex, noarchive\r\n"
+        "%s"
+        "\r\n",
+        code, text,
+        rfc1123_date(date, now_real()),
+        server,
+        conn->mime_type,
+        (qu)conn->reply_length,
+        conn->encoding,
+        conn->header_extra);
+    conn->http_code = code;
 }
 
 
@@ -368,39 +417,34 @@ static char *rfc1123_date(char *dest, const time_t when)
 /* ---------------------------------------------------------------------------
  * 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, ...)
 {
-    char *reason, date[DATE_LEN];
+    char *reason;
     va_list va;
 
     va_start(va, format);
     xvasprintf(&reason, format, va);
     va_end(va);
 
-    /* Only really need to calculate the date once. */
-    (void)rfc1123_date(date, now);
-
     conn->reply_length = xasprintf(&(conn->reply),
      "<html><head><title>%d %s</title></head><body>\n"
      "<h1>%s</h1>\n" /* errname */
      "%s\n" /* reason */
      "<hr>\n"
-     "Generated by %s on %s\n"
+     "Generated by %s"
      "</body></html>\n",
-     errcode, errname, errname, reason, server, date);
+     errcode, errname, errname, reason, server);
     free(reason);
 
-    conn->header_length = xasprintf(&(conn->header),
-     "HTTP/1.1 %d %s\r\n"
-     "Date: %s\r\n"
-     "Server: %s\r\n"
-     "Content-Length: %d\r\n"
-     "Content-Type: text/html\r\n"
-     "\r\n",
-     errcode, errname, date, server, conn->reply_length);
-
-    conn->http_code = errcode;
+    /* forget any dangling metadata */
+    conn->mime_type = mime_type_html;
+    conn->encoding = encoding_identity;
+
+    generate_header(conn, errcode, errname);
 }
 
 
@@ -426,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 */
@@ -499,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;
@@ -513,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.
@@ -540,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;
@@ -555,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;
     }
 
@@ -563,7 +626,6 @@ process_gzip(struct connection *conn)
         conn->reply_dont_free = 0;
     else
         free(conn->reply);
-
     conn->reply = buf;
     conn->reply_length -= zs.avail_out;
     conn->encoding = encoding_gzip;
@@ -575,23 +637,32 @@ process_gzip(struct connection *conn)
  */
 static void process_get(struct connection *conn)
 {
-    char *decoded_url, *safe_url;
-    char date[DATE_LEN];
+    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) {
@@ -605,6 +676,7 @@ static void process_get(struct connection *conn)
         if (buf == NULL) {
             default_reply(conn, 404, "Not Found",
                 "The page you requested could not be found.");
+            free(safe_url);
             return;
         }
         str_extract(buf, &(conn->reply_length), &(conn->reply));
@@ -617,33 +689,29 @@ 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);
         return;
     }
     free(safe_url);
 
     process_gzip(conn);
     assert(conn->mime_type != NULL);
-    conn->header_length = xasprintf(&(conn->header),
-        "HTTP/1.1 200 OK\r\n"
-        "Date: %s\r\n"
-        "Server: %s\r\n"
-        "Content-Length: %d\r\n"
-        "Content-Type: %s\r\n"
-        "%s"
-        "%s"
-        "\r\n"
-        ,
-        rfc1123_date(date, now), server,
-        conn->reply_length, conn->mime_type, conn->encoding,
-        conn->header_extra);
-    conn->http_code = 200;
+    generate_header(conn, 200, "OK");
 }
 
 
@@ -679,10 +747,6 @@ static void process_request(struct connection *conn)
         conn->state = SEND_HEADER;
     else
         conn->state = SEND_HEADER_AND_REPLY;
-
-    /* request not needed anymore */
-    free(conn->request);
-    conn->request = NULL; /* important: don't free it again later */
 }
 
 
@@ -692,11 +756,10 @@ static void process_request(struct connection *conn)
  */
 static void poll_recv_request(struct connection *conn)
 {
-    #define BUFSIZE 65536
-    char buf[BUFSIZE];
+    char buf[65536];
     ssize_t recvd;
 
-    recvd = recv(conn->socket, buf, BUFSIZE, 0);
+    recvd = recv(conn->socket, buf, sizeof(buf), 0);
     dverbosef("poll_recv_request(%d) got %d bytes", conn->socket, (int)recvd);
     if (recvd <= 0)
     {
@@ -705,8 +768,7 @@ static void poll_recv_request(struct connection *conn)
         conn->state = DONE;
         return;
     }
-    conn->last_active = now;
-    #undef BUFSIZE
+    conn->last_active_mono = now_mono();
 
     /* append to conn->request */
     conn->request = xrealloc(conn->request, conn->request_length+recvd+1);
@@ -714,17 +776,24 @@ static void poll_recv_request(struct connection *conn)
     conn->request_length += recvd;
     conn->request[conn->request_length] = 0;
 
-    /* process request if we have all of it */
-    if (conn->request_length > 4 &&
-        memcmp(conn->request+conn->request_length-4, "\r\n\r\n", 4) == 0)
-        process_request(conn);
-
     /* die if it's too long */
     if (conn->request_length > MAX_REQUEST_LENGTH)
     {
         default_reply(conn, 413, "Request Entity Too Large",
             "Your request was dropped because it was too long.");
         conn->state = SEND_HEADER;
+        return;
+    }
+
+    /* process request if we have all of it */
+    if (conn->request_length > 4 &&
+        memcmp(conn->request+conn->request_length-4, "\r\n\r\n", 4) == 0)
+    {
+        process_request(conn);
+
+        /* request not needed anymore */
+        free(conn->request);
+        conn->request = NULL; /* important: don't free it again later */
     }
 }
 
@@ -748,13 +817,11 @@ static void poll_send_header_and_reply(struct connection *conn)
     iov[0].iov_base = conn->header;
     iov[0].iov_len = conn->header_length;
 
-    iov[1].iov_base = conn->reply + conn->reply_sent;
-    iov[1].iov_len = conn->reply_length - conn->reply_sent;
+    iov[1].iov_base = conn->reply;
+    iov[1].iov_len = conn->reply_length;
 
     sent = writev(conn->socket, iov, 2);
-    conn->last_active = now;
-    verbosef("poll_send_header_and_reply(%d) sent %d bytes",
-        conn->socket, (int)sent);
+    conn->last_active_mono = now_mono();
 
     /* handle any errors (-1) or closure (0) in send() */
     if (sent < 1) {
@@ -776,7 +843,7 @@ static void poll_send_header_and_reply(struct connection *conn)
     conn->header_sent = conn->header_length;
     sent -= conn->header_length;
 
-    if (conn->reply_sent + sent < conn->reply_length) {
+    if (sent < (ssize_t)conn->reply_length) {
         verbosef("partially sent reply");
         conn->reply_sent += sent;
         conn->state = SEND_REPLY;
@@ -796,7 +863,7 @@ static void poll_send_header(struct connection *conn)
 
     sent = send(conn->socket, conn->header + conn->header_sent,
         conn->header_length - conn->header_sent, 0);
-    conn->last_active = now;
+    conn->last_active_mono = now_mono();
     dverbosef("poll_send_header(%d) sent %d bytes", conn->socket, (int)sent);
 
     /* handle any errors (-1) or closure (0) in send() */
@@ -832,7 +899,7 @@ static void poll_send_reply(struct connection *conn)
     sent = send(conn->socket,
         conn->reply + conn->reply_sent,
         conn->reply_length - conn->reply_sent, 0);
-    conn->last_active = now;
+    conn->last_active_mono = now_mono();
     dverbosef("poll_send_reply(%d) sent %d: [%d-%d] of %d",
         conn->socket, (int)sent,
         (int)conn->reply_sent,
@@ -858,68 +925,165 @@ static void poll_send_reply(struct connection *conn)
 
 
 
-/* ---------------------------------------------------------------------------
- * Initialize the sockin global.  This is the socket that we accept
- * connections from.  Pass -1 as max_conn for system limit.
+/* --------------------------------------------------------------------------
+ * 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.
  */
-void http_init(const char *bindaddr, const unsigned short bindport,
-   const int max_conn)
+static struct addrinfo *get_bind_addr(
+    const char *bindaddr, const unsigned short bindport)
 {
-    struct sockaddr_storage addrin;
-    struct addrinfo hints, *ai, *aiptr;
-    char ipaddr[INET6_ADDRSTRLEN], portstr[12];
-    int sockopt, ret;
+    struct addrinfo hints, *ai;
+    char portstr[6];
+    int ret;
 
-    memset(&hints, '\0', sizeof(hints));
+    memset(&hints, 0, sizeof(hints));
     hints.ai_family = AF_UNSPEC;
     hints.ai_socktype = SOCK_STREAM;
-    hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG;
+    hints.ai_flags = AI_PASSIVE;
+
     snprintf(portstr, sizeof(portstr), "%u", bindport);
+    if ((ret = getaddrinfo(bindaddr, portstr, &hints, &ai)))
+        err(1, "getaddrinfo(%s, %s) failed: %s",
+            bindaddr ? bindaddr : "NULL", portstr, gai_strerror(ret));
+    if (ai == NULL)
+        err(1, "getaddrinfo() returned NULL pointer");
+    return ai;
+}
+
+void http_add_bindaddr(const char *bindaddr)
+{
+    struct bindaddr_entry *ent;
 
-    if (ret = getaddrinfo(bindaddr, portstr, &hints, &aiptr))
-        err(1, "getaddrinfo(): %s", gai_strerror(ret));
+    ent = xmalloc(sizeof(*ent));
+    ent->s = bindaddr;
+    STAILQ_INSERT_TAIL(&bindaddrs, ent, entries);
+}
+
+static void http_listen_one(struct addrinfo *ai,
+    const unsigned short bindport)
+{
+    char ipaddr[INET6_ADDRSTRLEN];
+    int sockin, sockopt, ret;
+
+    /* format address into ipaddr string */
+    if ((ret = getnameinfo(ai->ai_addr, ai->ai_addrlen, ipaddr,
+                           sizeof(ipaddr), NULL, 0, NI_NUMERICHOST)) != 0)
+        err(1, "getnameinfo failed: %s", gai_strerror(ret));
+
+    /* create incoming socket */
+    if ((sockin = socket(ai->ai_family, ai->ai_socktype,
+            ai->ai_protocol)) == -1) {
+        warn("http_listen_one(%s, %u): socket(%d (%s), %d, %d) failed",
+          ipaddr, (unsigned int)bindport,
+          ai->ai_family,
+          (ai->ai_family == AF_INET6) ? "AF_INET6" :
+          (ai->ai_family == AF_INET) ? "AF_INET" :
+          "?",
+          ai->ai_socktype,  ai->ai_protocol);
+        return;
+    }
+
+    fd_set_nonblock(sockin);
 
-    for (ai = aiptr; ai; ai = ai->ai_next) {
-        /* create incoming socket */
-        sockin = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
-        if (sockin == -1)
-            continue;
+    /* reuse address */
+    sockopt = 1;
+    if (setsockopt(sockin, SOL_SOCKET, SO_REUSEADDR,
+            &sockopt, sizeof(sockopt)) == -1)
+        err(1, "can't set SO_REUSEADDR");
 
-        /* reuse address */
+#ifdef IPV6_V6ONLY
+    /* explicitly disallow IPv4 mapped addresses since OpenBSD doesn't allow
+     * dual stack sockets under any circumstances
+     */
+    if (ai->ai_family == AF_INET6) {
         sockopt = 1;
-        if (setsockopt(sockin, SOL_SOCKET, SO_REUSEADDR,
-                &sockopt, sizeof(sockopt)) == -1) {
-            close(sockin);
-            continue;
-        }
+        if (setsockopt(sockin, IPPROTO_IPV6, IPV6_V6ONLY,
+                &sockopt, sizeof(sockopt)) == -1)
+            err(1, "can't set IPV6_V6ONLY");
+    }
+#endif
 
-        /* Recover address and port strings. */
-        getnameinfo(ai->ai_addr, ai->ai_addrlen, ipaddr, sizeof(ipaddr),
-                NULL, 0, NI_NUMERICHOST);
+    /* bind socket */
+    if (bind(sockin, ai->ai_addr, ai->ai_addrlen) == -1) {
+        warn("bind(\"%s\") failed", ipaddr);
+        close(sockin);
+        return;
+    }
 
-        /* bind socket */
-        memcpy(&addrin, ai->ai_addr, ai->ai_addrlen);
-        if (bind(sockin, (struct sockaddr *)&addrin,
-                sizeof(addrin)) == -1) {
-            close(sockin);
-            continue;
-        }
+    /* listen on socket */
+    if (listen(sockin, 128) == -1)
+        err(1, "listen() failed");
 
-        verbosef("listening on %s:%u", ipaddr, bindport);
+    verbosef("listening on http://%s%s%s:%u%s",
+        (ai->ai_family == AF_INET6) ? "[" : "",
+        ipaddr,
+        (ai->ai_family == AF_INET6) ? "]" : "",
+        bindport,
+        http_base_url);
 
-        /* listen on socket */
-        if (listen(sockin, max_conn) >= 0)
-            /* Successfully bound and now listening. */
-            break;
+    /* add to insocks */
+    insocks = xrealloc(insocks, sizeof(*insocks) * (insock_num + 1));
+    insocks[insock_num++] = sockin;
+}
 
-        /* Next candidate. */
-        continue;
-    }
+/* Initialize the http sockets and listen on them. */
+void http_listen(const unsigned short bindport)
+{
+    /* If the user didn't specify any bind addresses, add a NULL.
+     * This will become a wildcard.
+     */
+    if (STAILQ_EMPTY(&bindaddrs))
+        http_add_bindaddr(NULL);
 
-    freeaddrinfo(aiptr);
+    /* Listen on every specified interface. */
+    while (!STAILQ_EMPTY(&bindaddrs)) {
+        struct bindaddr_entry *bindaddr = STAILQ_FIRST(&bindaddrs);
+        struct addrinfo *ai, *ais = get_bind_addr(bindaddr->s, bindport);
 
-    if (ai == NULL)
-        err(1, "getaddrinfo() unable to locate address");
+        /* There could be multiple addresses returned, handle them all. */
+        for (ai = ais; ai; ai = ai->ai_next)
+            http_listen_one(ai, bindport);
+
+        freeaddrinfo(ais);
+
+        STAILQ_REMOVE_HEAD(&bindaddrs, entries);
+        free(bindaddr);
+    }
+
+    if (insocks == NULL)
+        errx(1, "was not able to bind any ports for http interface");
 
     /* ignore SIGPIPE */
     if (signal(SIGPIPE, SIG_IGN) == SIG_ERR)
@@ -937,30 +1101,37 @@ http_fd_set(fd_set *recv_set, fd_set *send_set, int *max_fd,
 {
     struct connection *conn, *next;
     int minidle = idletime + 1;
+    unsigned int i;
 
     #define MAX_FD_SET(sock, fdset) do { \
-        FD_SET(sock, fdset); *max_fd = max(*max_fd, sock); } while(0)
+        FD_SET(sock, fdset); *max_fd = MAX(*max_fd, sock); } while(0)
 
-    MAX_FD_SET(sockin, recv_set);
+    for (i=0; i<insock_num; i++)
+        MAX_FD_SET(insocks[i], recv_set);
 
     LIST_FOREACH_SAFE(conn, &connlist, entries, next)
     {
-        int idlefor = now - conn->last_active;
+        int idlefor = now_mono() - conn->last_active_mono;
 
         /* Time out dead connections. */
-        if (idlefor >= idletime)
-        {
+        if (idlefor >= idletime) {
             char ipaddr[INET6_ADDRSTRLEN];
-            getnameinfo((struct sockaddr *) &conn->client, sizeof(conn->client),
-                    ipaddr, sizeof(ipaddr), NULL, 0, NI_NUMERICHOST);
-            verbosef("http socket timeout from %s (fd %d)",
-                    ipaddr, conn->socket);
+            /* FIXME: this is too late on FreeBSD, socket is invalid */
+            int ret = getnameinfo((struct sockaddr *)&conn->client,
+                sizeof(conn->client), ipaddr, sizeof(ipaddr),
+                NULL, 0, NI_NUMERICHOST);
+            if (ret == 0)
+                verbosef("http socket timeout from %s (fd %d)",
+                        ipaddr, conn->socket);
+            else
+                warn("http socket timeout: getnameinfo error: %s",
+                    gai_strerror(ret));
             conn->state = DONE;
         }
 
         /* Connections that need a timeout. */
         if (conn->state != DONE)
-            minidle = min(minidle, (idletime - idlefor));
+            minidle = MIN(minidle, (idletime - idlefor));
 
         switch (conn->state)
         {
@@ -1002,8 +1173,11 @@ http_fd_set(fd_set *recv_set, fd_set *send_set, int *max_fd,
 void http_poll(fd_set *recv_set, fd_set *send_set)
 {
     struct connection *conn;
+    unsigned int i;
 
-    if (FD_ISSET(sockin, recv_set)) accept_connection();
+    for (i=0; i<insock_num; i++)
+        if (FD_ISSET(insocks[i], recv_set))
+            accept_connection(insocks[i]);
 
     LIST_FOREACH(conn, &connlist, entries)
     switch (conn->state)
@@ -1024,8 +1198,30 @@ void http_poll(fd_set *recv_set, fd_set *send_set)
         if (FD_ISSET(conn->socket, send_set)) poll_send_reply(conn);
         break;
 
+    case DONE: /* fallthrough */
     default: errx(1, "invalid state");
     }
 }
 
+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]);
+    free(insocks);
+    insocks = NULL;
+
+    /* Close in-flight connections. */
+    LIST_FOREACH_SAFE(conn, &connlist, entries, next) {
+        LIST_REMOVE(conn, entries);
+        free_connection(conn);
+        free(conn);
+    }
+}
+
 /* vim:set ts=4 sw=4 et tw=78: */