Use multiple listening sockets, allow multiple bindaddrs.
[darkstat] / http.c
diff --git a/http.c b/http.c
index 1c4921a..91db646 100644 (file)
--- a/http.c
+++ b/http.c
@@ -10,6 +10,7 @@
 
 #include "darkstat.h"
 #include "http.h"
+#include "config.h"
 #include "conv.h"
 #include "hosts_db.h"
 #include "graph_db.h"
@@ -35,8 +36,6 @@
 #include <unistd.h>
 #include <zlib.h>
 
-char *base_url = NULL;
-
 static const char mime_type_xml[] = "text/xml";
 static const char mime_type_html[] = "text/html; charset=us-ascii";
 static const char mime_type_css[] = "text/css";
@@ -46,9 +45,11 @@ 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
 
+static int *insocks = NULL;
+static unsigned int insock_num = 0;
+
 #ifndef min
 #define min(a,b) (((a) < (b)) ? (a) : (b))
 #endif
@@ -90,6 +91,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.
@@ -297,7 +305,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;
@@ -618,17 +626,6 @@ static void process_get(struct connection *conn)
         return;
     }
 
-    /* make relative (or fail) */
-    decoded_url = safe_url;
-    if (!str_starts_with(decoded_url, base_url))
-    {
-        default_reply(conn, 404, "Not Found",
-            "The page you requested could not be found.");
-        free(decoded_url);
-        return;
-    }
-    safe_url = decoded_url + strlen(base_url) - 1;
-
     if (strcmp(safe_url, "/") == 0) {
         struct str *buf = html_front_page();
         str_extract(buf, &(conn->reply_length), &(conn->reply));
@@ -640,7 +637,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(decoded_url);
+            free(safe_url);
             return;
         }
         str_extract(buf, &(conn->reply_length), &(conn->reply));
@@ -660,10 +657,10 @@ static void process_get(struct connection *conn)
     else {
         default_reply(conn, 404, "Not Found",
             "The page you requested could not be found.");
-        free(decoded_url);
+        free(safe_url);
         return;
     }
-    free(decoded_url);
+    free(safe_url);
 
     process_gzip(conn);
     assert(conn->mime_type != NULL);
@@ -772,8 +769,8 @@ 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;
@@ -798,7 +795,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;
@@ -878,106 +875,109 @@ static void poll_send_reply(struct connection *conn)
     if (conn->reply_sent == conn->reply_length) conn->state = DONE;
 }
 
-
-
-/* --------------------------------------------------------------------------
- * Initialize the base path.
- */
-static void http_init_base(const char *url)
-{
-    char *slashed_url, *safe_url;
-    size_t urllen;
-
-    if (url == NULL) {
-        base_url = strdup("/");
-        return;
-    }
-
-    /* make sure that the url has leading and trailing slashes */
-    urllen = strlen(url);
-    slashed_url = xmalloc(urllen+3);
-    memset(slashed_url, '/', urllen+2);
-    memcpy(slashed_url+1, url, urllen); /* don't copy NULL */
-    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);
-        base_url = strdup("/"); /* set to default */
-        return;
-    }
-    else
-        base_url = safe_url;
-}
-
-/* --------------------------------------------------------------------------
- * Initialize the sockin global.  This is the socket that we accept
- * connections from.  Pass -1 as max_conn for system limit.
+/* 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 *base, 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;
-
-    http_init_base(base);
+    struct addrinfo hints, *ai;
+    char portstr[6];
+    int ret;
 
     memset(&hints, 0, sizeof(hints));
     hints.ai_family = AF_UNSPEC;
+#ifdef linux
+    if (bindaddr == NULL)
+        hints.ai_family = AF_INET6; /* dual stack socket */
+#endif
     hints.ai_socktype = SOCK_STREAM;
     hints.ai_flags = AI_PASSIVE;
 #ifdef AI_ADDRCONFIG
     hints.ai_flags |= AI_ADDRCONFIG;
 #endif
     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;
+}
 
-    if ((ret = getaddrinfo(bindaddr, portstr, &hints, &aiptr)))
-        err(1, "getaddrinfo(): %s", gai_strerror(ret));
-
-    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) {
-            close(sockin);
-            continue;
-        }
+void http_add_bindaddr(const char *bindaddr)
+{
+    struct bindaddr_entry *ent;
 
-        /* Recover address and port strings. */
-        getnameinfo(ai->ai_addr, ai->ai_addrlen, ipaddr, sizeof(ipaddr),
-                NULL, 0, NI_NUMERICHOST);
+    ent = xmalloc(sizeof(*ent));
+    ent->s = bindaddr;
+    STAILQ_INSERT_TAIL(&bindaddrs, ent, entries);
+}
 
-        /* bind socket */
-        memcpy(&addrin, ai->ai_addr, ai->ai_addrlen);
-        if (bind(sockin, (struct sockaddr *)&addrin, ai->ai_addrlen) == -1) {
-            close(sockin);
-            continue;
-        }
+static void http_listen_one(struct addrinfo *ai,
+    const unsigned short bindport)
+{
+    char ipaddr[INET6_ADDRSTRLEN];
+    int sockin, sockopt, ret;
+
+    /* create incoming socket */
+    if ((sockin = socket(ai->ai_family, SOCK_STREAM, 0)) == -1)
+        err(1, "socket() failed");
+
+    /* reuse address */
+    sockopt = 1;
+    if (setsockopt(sockin, SOL_SOCKET, SO_REUSEADDR,
+            &sockopt, sizeof(sockopt)) == -1)
+        err(1, "can't set SO_REUSEADDR");
+
+    /* 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));
+
+    /* bind socket */
+    if (bind(sockin, ai->ai_addr, ai->ai_addrlen) == -1)
+        err(1, "bind(\"%s\") failed", ipaddr);
+
+    /* listen on socket */
+    if (listen(sockin, -1) == -1)
+        err(1, "listen() failed");
+
+    verbosef("listening on http://%s%s%s:%u/",
+        (ai->ai_family == AF_INET6) ? "[" : "",
+        ipaddr,
+        (ai->ai_family == AF_INET6) ? "]" : "",
+        bindport);
+
+    /* add to insocks */
+    insocks = xrealloc(insocks, sizeof(*insocks) * (insock_num + 1));
+    insocks[insock_num++] = sockin;
+}
 
-        verbosef("listening on %s:%u", ipaddr, bindport);
+/* 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);
 
-        /* listen on socket */
-        if (listen(sockin, max_conn) >= 0)
-            /* Successfully bound and now listening. */
-            break;
+    /* 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);
 
-        /* Next candidate. */
-        continue;
-    }
+        /* There could be multiple addresses returned, handle them all. */
+        for (ai = ais; ai; ai = ai->ai_next)
+            http_listen_one(ai, bindport);
 
-    freeaddrinfo(aiptr);
+        freeaddrinfo(ais);
 
-    if (ai == NULL)
-        err(1, "getaddrinfo() unable to locate address");
+        STAILQ_REMOVE_HEAD(&bindaddrs, entries);
+        free(bindaddr);
+    }
 
     /* ignore SIGPIPE */
     if (signal(SIGPIPE, SIG_IGN) == SIG_ERR)
@@ -995,24 +995,31 @@ 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)
 
-    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;
 
         /* 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;
         }
 
@@ -1060,8 +1067,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)
@@ -1088,8 +1098,21 @@ void http_poll(fd_set *recv_set, fd_set *send_set)
 }
 
 void http_stop(void) {
-    free(base_url);
-    close(sockin);
+    struct connection *conn;
+    unsigned int i;
+
+    /* Close listening sockets. */
+    for (i=0; i<insock_num; i++)
+        close(insocks[i]);
+    free(insocks);
+    insocks = NULL;
+
+    /* Close in-flight connections. */
+    LIST_FOREACH(conn, &connlist, entries) {
+        LIST_REMOVE(conn, entries);
+        free_connection(conn);
+        free(conn);
+    }
 }
 
 /* vim:set ts=4 sw=4 et tw=78: */