Revamp the networking code in httpd.c to support IPv6. Side effects
authorsimon <simon@cda61777-01e9-0310-a592-d414129be87e>
Sun, 4 Mar 2012 15:20:57 +0000 (15:20 +0000)
committersimon <simon@cda61777-01e9-0310-a592-d414129be87e>
Sun, 4 Mar 2012 15:20:57 +0000 (15:20 +0000)
include slight changes to the output saying where the URL is, and
support for port numbers specified as service names. Also I've
invented a nicer syntax 'ANY' for the magic server address 0.0.0.0.

git-svn-id: svn://svn.tartarus.org/sgt/agedu@9424 cda61777-01e9-0310-a592-d414129be87e

TODO
agedu.but
agedu.c
agedu.h
configure.ac
httpd.c
httpd.h

diff --git a/TODO b/TODO
index 4f04fe9..cc9fb0b 100644 (file)
--- a/TODO
+++ b/TODO
@@ -55,11 +55,6 @@ TODO list for agedu
       controversial; IIRC it's all in POSIX, for one thing. So more
       likely this should simply wait until somebody complains.
 
- - IPv6 support in the HTTP server
-    * of course, Linux magic auth can still work in this context; we
-      merely have to be prepared to open one of /proc/net/tcp or
-      /proc/net/tcp6 as appropriate.
-
  - run-time configuration in the HTTP server
     * I think this probably works by having a configuration form, or
       a link pointing to one, somewhere on the report page. If you
index f73f333..65ceaca 100644 (file)
--- a/agedu.but
+++ b/agedu.but
@@ -578,12 +578,11 @@ three months ago or later.
 
 \dt \cw{--address} \e{addr}[\cw{:}\e{port}]
 
-\dd Specifies the network address and port number on which
-\cw{agedu} should listen when running its web server. If you want
-\cw{agedu} to listen for connections coming in from any source, you
-should probably specify the special IP address \cw{0.0.0.0}. If the
-port number is omitted, an arbitrary unused port will be chosen for
-you and displayed.
+\dd Specifies the network address and port number on which \cw{agedu}
+should listen when running its web server. If you want \cw{agedu} to
+listen for connections coming in from any source, specify the address
+as the special value \cw{ANY}. If the port number is omitted, an
+arbitrary unused port will be chosen for you and displayed.
 
 \lcont{
 
diff --git a/agedu.c b/agedu.c
index 9a354f8..7fd2314 100644 (file)
--- a/agedu.c
+++ b/agedu.c
@@ -502,8 +502,8 @@ int main(int argc, char **argv)
     time_t now = time(NULL);
     time_t textcutoff = now, htmlnewest = now, htmloldest = now;
     int htmlautoagerange = 1;
-    const char *httpserveraddr = NULL;
-    int httpserverport = 0;
+    const char *httpserveraddr = "localhost";
+    const char *httpserverport = NULL;
     const char *httpauthdata = NULL;
     const char *outfile = NULL;
     const char *html_title = PNAME;
@@ -821,10 +821,13 @@ int main(int argc, char **argv)
                        else
                            port = optval;
                        port += strcspn(port, ":");
-                       if (port)
+                       if (port && *port)
                            *port++ = '\0';
-                       httpserveraddr = optval;
-                       httpserverport = atoi(port);
+                        if (!strcmp(optval, "ANY"))
+                            httpserveraddr = NULL;
+                        else
+                            httpserveraddr = optval;
+                       httpserverport = port;
                    }
                    break;
                  case OPT_AUTH:
diff --git a/agedu.h b/agedu.h
index 5a4e72b..8e2548c 100644 (file)
--- a/agedu.h
+++ b/agedu.h
@@ -88,6 +88,9 @@
 #ifdef HAVE_SYS_SELECT_H
 #  include <sys/select.h>
 #endif
+#ifdef HAVE_NETDB_H
+#  include <netdb.h>
+#endif
     
 #define PNAME "agedu"
 
index 96a813f..87c153b 100644 (file)
@@ -19,7 +19,7 @@ AM_CONDITIONAL([HAVE_HALIBUT],[test "x$HALIBUT" = "xyes"])
 AC_HEADER_DIRENT
 AC_HEADER_STDC
 AC_HEADER_SYS_WAIT
-AC_CHECK_HEADERS([assert.h arpa/inet.h ctype.h errno.h fcntl.h features.h fnmatch.h limits.h netinet/in.h pwd.h stdarg.h stddef.h stdint.h stdio.h stdlib.h string.h sys/ioctl.h sys/mman.h sys/socket.h syslog.h termios.h time.h unistd.h])
+AC_CHECK_HEADERS([assert.h arpa/inet.h ctype.h errno.h fcntl.h features.h fnmatch.h limits.h netdb.h netinet/in.h pwd.h stdarg.h stddef.h stdint.h stdio.h stdlib.h string.h sys/ioctl.h sys/mman.h sys/socket.h syslog.h termios.h time.h unistd.h])
 
 # Checks for typedefs, structures, and compiler characteristics.
 AC_C_CONST
@@ -38,9 +38,25 @@ AC_FUNC_VPRINTF
 AC_SEARCH_LIBS(connect, socket nsl)
 AC_SEARCH_LIBS(inet_ntoa, socket nsl)
 AC_SEARCH_LIBS(inet_addr, socket nsl)
-# AC_SEARCH_LIBS(gethostbyname, socket nsl resolv) # (if we used gethostbyname)
-
-AC_CHECK_FUNCS([ftruncate fdopendir lstat64 stat64 memchr munmap select socket strcasecmp strchr strcspn strerror strrchr strspn strtoul strtoull connect inet_ntoa inet_addr])
+AC_SEARCH_LIBS(gethostbyname, socket nsl resolv)
+AC_SEARCH_LIBS(getaddrinfo, socket nsl resolv)
+
+AC_CHECK_FUNCS([ftruncate fdopendir lstat64 stat64 memchr munmap select socket strcasecmp strchr strcspn strerror strrchr strspn strtoul strtoull connect inet_ntoa inet_addr gethostbyname getaddrinfo])
+
+AC_ARG_ENABLE([ipv6],
+              AS_HELP_STRING([--disable-ipv6],
+                             [disable IPv6 in the built-in web server]),
+              [ipv6=$enableval],[ipv6=$ac_cv_func_getaddrinfo])
+AC_ARG_ENABLE([ipv4],
+              AS_HELP_STRING([--disable-ipv4],
+                             [disable IPv4 in the built-in web server]),
+              [ipv4=$enableval],[ipv4=yes])
+if test "$ipv6" = "no"; then
+  AC_DEFINE([NO_IPV6], [1], [define if IPv6 is disabled at configure time])
+fi
+if test "$ipv4" = "no"; then
+  AC_DEFINE([NO_IPV4], [1], [define if IPv4 is disabled at configure time])
+fi
 
 AC_CONFIG_FILES([Makefile])
 AC_OUTPUT
diff --git a/httpd.c b/httpd.c
index 6c90e66..daaea67 100644 (file)
--- a/httpd.c
+++ b/httpd.c
@@ -388,9 +388,12 @@ struct fd *new_fdstruct(int fd, int type)
 
 int check_owning_uid(int fd, int flip)
 {
-    struct sockaddr_in sock, peer;
+    struct sockaddr_storage sock, peer;
+    int connected;
     socklen_t addrlen;
-    char linebuf[4096], matchbuf[80];
+    char linebuf[4096], matchbuf[128];
+    char *filename;
+    int matchcol, uidcol;
     FILE *fp;
 
     addrlen = sizeof(sock);
@@ -399,10 +402,12 @@ int check_owning_uid(int fd, int flip)
        exit(1);
     }
     addrlen = sizeof(peer);
+    connected = 1;
     if (getpeername(fd, (struct sockaddr *)&peer, &addrlen)) {
        if (errno == ENOTCONN) {
-           peer.sin_addr.s_addr = htonl(0);
-           peer.sin_port = htons(0);
+            connected = 0;
+            memset(&peer, 0, sizeof(peer));
+            peer.ss_family = sock.ss_family;
        } else {
            fprintf(stderr, "getpeername: %s\n", strerror(errno));
            exit(1);
@@ -410,21 +415,60 @@ int check_owning_uid(int fd, int flip)
     }
 
     if (flip) {
-       struct sockaddr_in tmp = sock;
+       struct sockaddr_storage tmp = sock;
        sock = peer;
        peer = tmp;
     }
 
-    sprintf(matchbuf, "%08X:%04X %08X:%04X",
-           peer.sin_addr.s_addr, ntohs(peer.sin_port),
-           sock.sin_addr.s_addr, ntohs(sock.sin_port));
-    fp = fopen("/proc/net/tcp", "r");
+#ifndef NO_IPV4
+    if (peer.ss_family == AF_INET) {
+        struct sockaddr_in *sock4 = (struct sockaddr_in *)&sock;
+        struct sockaddr_in *peer4 = (struct sockaddr_in *)&peer;
+
+        assert(peer4->sin_family == AF_INET);
+
+        sprintf(matchbuf, "%08X:%04X %08X:%04X",
+                peer4->sin_addr.s_addr, ntohs(peer4->sin_port),
+                sock4->sin_addr.s_addr, ntohs(sock4->sin_port));
+        filename = "/proc/net/tcp";
+        matchcol = 6;
+        uidcol = 75;
+    } else
+#endif
+#ifndef NO_IPV6
+    if (peer.ss_family == AF_INET6) {
+        struct sockaddr_in6 *sock6 = (struct sockaddr_in6 *)&sock;
+        struct sockaddr_in6 *peer6 = (struct sockaddr_in6 *)&peer;
+        char *p;
+
+        assert(peer6->sin6_family == AF_INET6);
+
+        p = matchbuf;
+        for (int i = 0; i < 4; i++)
+            p += sprintf(p, "%08X",
+                         ((uint32_t *)peer6->sin6_addr.s6_addr)[i]);
+        p += sprintf(p, ":%04X ", ntohs(peer6->sin6_port));
+        for (int i = 0; i < 4; i++)
+            p += sprintf(p, "%08X",
+                         ((uint32_t *)sock6->sin6_addr.s6_addr)[i]);
+        p += sprintf(p, ":%04X", ntohs(sock6->sin6_port));
+
+        filename = "/proc/net/tcp6";
+        matchcol = 6;
+        uidcol = 123;
+    } else
+#endif
+    {
+        return -1;                     /* unidentified family */
+    }
+
+    fp = fopen(filename, "r");
     if (fp) {
        while (fgets(linebuf, sizeof(linebuf), fp)) {
-           if (strlen(linebuf) >= 75 &&
-               !strncmp(linebuf+6, matchbuf, strlen(matchbuf))) {
+           if (strlen(linebuf) >= uidcol &&
+               !strncmp(linebuf+matchcol, matchbuf, strlen(matchbuf))) {
                fclose(fp);
-               return atoi(linebuf + 75);
+               return atoi(linebuf + uidcol);
            }
        }
        fclose(fp);
@@ -463,73 +507,264 @@ static void base64_encode_atom(unsigned char *data, int n, char *out)
        out[3] = '=';
 }
 
-void run_httpd(const void *t, int authmask, const struct httpd_config *dcfg,
-              const struct html_config *incfg)
+struct listenfds {
+    int v4, v6;
+};
+
+static int make_listening_sockets(struct listenfds *fds, const char *address,
+                                  const char *portstr, char **outhostname)
 {
-    int fd, ret;
-    int authtype;
-    char *authstring = NULL;
-    struct sockaddr_in addr;
+    /*
+     * Establish up to 2 listening sockets, for IPv4 and IPv6, on the
+     * same arbitrarily selected port. Return them in fds.v4 and
+     * fds.v6, with each entry being -1 if that socket was not
+     * established at all. Main return value is the port chosen, or <0
+     * if the whole process failed.
+     */
+    struct sockaddr_in6 addr6;
+    struct sockaddr_in addr4;
+    int got_v6, got_v4;
     socklen_t addrlen;
-    struct html_config cfg = *incfg;
+    int ret, port = 0;
 
     /*
-     * Establish the listening socket and retrieve its port
-     * number.
+     * Special case of the address parameter: if it's "0.0.0.0", treat
+     * it like NULL, because that was how you specified listen-on-any-
+     * address in versions before the IPv6 revamp.
      */
-    fd = socket(PF_INET, SOCK_STREAM, 0);
-    if (fd < 0) {
-       fprintf(stderr, "socket(PF_INET): %s\n", strerror(errno));
-       exit(1);
+    {
+        int u,v,w,x;
+        if (address && 
+            4 == sscanf(address, "%d.%d.%d.%d", &u, &v, &w, &x) &&
+            u==0 && v==0 && w==0 && x==0)
+            address = NULL;
     }
-    memset(&addr, 0, sizeof(addr));
-    addr.sin_family = AF_INET;
-    if (!dcfg->address) {
-#ifdef RANDOM_LOCALHOST
-       unsigned long ipaddr;
-       srand(0L);
-       ipaddr = 0x7f000000;
-       ipaddr += (1 + rand() % 255) << 16;
-       ipaddr += (1 + rand() % 255) << 8;
-       ipaddr += (1 + rand() % 255);
-       addr.sin_addr.s_addr = htonl(ipaddr);
-#else
-       addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
-#endif
-       addr.sin_port = htons(0);
+
+    if (portstr && !*portstr)
+        portstr = NULL;                /* normalise NULL and empty string */
+
+    if (!address) {
+        char hostname[HOST_NAME_MAX];
+        if (gethostname(hostname, sizeof(hostname)) < 0) {
+            perror("hostname");
+            return -1;
+        }
+        *outhostname = dupstr(hostname);
     } else {
-       addr.sin_addr.s_addr = inet_addr(dcfg->address);
-       addr.sin_port = dcfg->port ? htons(dcfg->port) : 0;
+        *outhostname = dupstr(address);
     }
-    addrlen = sizeof(addr);
-    ret = bind(fd, (const struct sockaddr *)&addr, addrlen);
-#ifdef RANDOM_LOCALHOST
-    if (ret < 0 && errno == EADDRNOTAVAIL && !dcfg->address) {
-       /*
-        * Some systems don't like us binding to random weird
-        * localhost-space addresses. Try again with the official
-        * INADDR_LOOPBACK.
-        */
-       addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
-       addr.sin_port = htons(0);
-       ret = bind(fd, (const struct sockaddr *)&addr, addrlen);
+
+    fds->v6 = fds->v4 = -1;
+    got_v6 = got_v4 = 0;
+
+#if defined HAVE_GETADDRINFO
+
+    /*
+     * Resolve the given address using getaddrinfo, yielding an IPv6
+     * address or an IPv4 one or both.
+     */
+
+    struct addrinfo hints;
+    struct addrinfo *addrs, *ai;
+    hints.ai_family = AF_UNSPEC;
+    hints.ai_socktype = SOCK_STREAM;
+    hints.ai_protocol = 0;
+    hints.ai_flags = AI_PASSIVE;
+    ret = getaddrinfo(address, portstr ? portstr : "http", &hints, &addrs);
+    if (ret) {
+        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret));
+        return -1;
     }
+    for (ai = addrs; ai; ai = ai->ai_next) {
+#ifndef NO_IPV6
+        if (!got_v6 && ai->ai_family == AF_INET6) {
+            memcpy(&addr6, ai->ai_addr, ai->ai_addrlen);
+            if (portstr && !port)
+                port = ntohs(addr6.sin6_port);
+            got_v6 = 1;
+        }
+#endif
+#ifndef NO_IPV4
+        if (!got_v4 && ai->ai_family == AF_INET) {
+            memcpy(&addr4, ai->ai_addr, ai->ai_addrlen);
+            if (portstr && !port)
+                port = ntohs(addr4.sin_port);
+            got_v4 = 1;
+        }
 #endif
-    if (ret < 0) {
-       fprintf(stderr, "bind: %s\n", strerror(errno));
-       exit(1);
     }
-    if (listen(fd, 5) < 0) {
-       fprintf(stderr, "listen: %s\n", strerror(errno));
-       exit(1);
+
+#elif defined HAVE_GETHOSTBYNAME
+
+    /*
+     * IPv4-only setup using inet_addr and gethostbyname.
+     */
+    struct hostent *h;
+
+    memset(&addr4, 0, sizeof(addr4));
+    addr4.sin_family = AF_INET;
+
+    if (!address) {
+        addr4.sin_addr.s_addr = htons(INADDR_ANY);
+        got_v4 = 1;
+    } else if (inet_aton(address, &addr4.sin_addr)) {
+        got_v4 = 1;                    /* numeric address */
+    } else if ((h = gethostbyname(address)) != NULL) {
+        memcpy(&addr4.sin_addr, h->h_addr, sizeof(addr4.sin_addr));
+        got_v4 = 1;
+    } else {
+        fprintf(stderr, "gethostbyname: %s\n", hstrerror(h_errno));
+        return -1;
     }
-    addrlen = sizeof(addr);
-    if (getsockname(fd, (struct sockaddr *)&addr, &addrlen)) {
-       fprintf(stderr, "getsockname: %s\n", strerror(errno));
-       exit(1);
+
+    if (portstr) {
+        struct servent *s;
+        if (!portstr[strspn(portstr, "0123456789")]) {
+            port = atoi(portstr);
+        } else if ((s = getservbyname(portstr, NULL)) != NULL) {
+            port = ntohs(s->s_port);
+        } else {
+            fprintf(stderr, "getservbyname: port '%s' not understood\n",
+                    portstr);
+            return -1;
+        }
+    }
+
+#endif
+
+#ifndef NO_IPV6
+#ifndef NO_IPV4
+  retry:
+#endif
+    if (got_v6) {
+        fds->v6 = socket(PF_INET6, SOCK_STREAM, 0);
+        if (fds->v6 < 0) {
+            fprintf(stderr, "socket(PF_INET6): %s\n", strerror(errno));
+            goto done_v6;
+        }
+#ifdef IPV6_V6ONLY
+        {
+            int i = 1;
+            if (setsockopt(fds->v6, IPPROTO_IPV6, IPV6_V6ONLY,
+                           (char *)&i, sizeof(i)) < 0) {
+                fprintf(stderr, "setsockopt(IPV6_V6ONLY): %s\n",
+                        strerror(errno));
+                close(fds->v6);
+                fds->v6 = -1;
+                goto done_v6;
+            }
+        }
+#endif /* IPV6_V6ONLY */
+        addr6.sin6_port = htons(port);
+        addrlen = sizeof(addr6);
+        if (bind(fds->v6, (const struct sockaddr *)&addr6, addrlen) < 0) {
+            fprintf(stderr, "bind: %s\n", strerror(errno));
+            close(fds->v6);
+            fds->v6 = -1;
+            goto done_v6;
+        }
+        if (listen(fds->v6, 5) < 0) {
+            fprintf(stderr, "listen: %s\n", strerror(errno));
+            close(fds->v6);
+            fds->v6 = -1;
+            goto done_v6;
+        }
+        if (port == 0) {
+            addrlen = sizeof(addr6);
+            if (getsockname(fds->v6, (struct sockaddr *)&addr6,
+                            &addrlen) < 0) {
+                fprintf(stderr, "getsockname: %s\n", strerror(errno));
+                close(fds->v6);
+                fds->v6 = -1;
+                goto done_v6;
+            }
+            port = ntohs(addr6.sin6_port);
+        }
+    }
+  done_v6:
+#endif
+
+#ifndef NO_IPV4
+    if (got_v4) {
+        fds->v4 = socket(PF_INET, SOCK_STREAM, 0);
+        if (fds->v4 < 0) {
+            fprintf(stderr, "socket(PF_INET): %s\n", strerror(errno));
+            goto done_v4;
+        }
+        addr4.sin_port = htons(port);
+        addrlen = sizeof(addr4);
+        if (bind(fds->v4, (const struct sockaddr *)&addr4, addrlen) < 0) {
+#ifndef NO_IPV6
+            if (fds->v6 >= 0) {
+                /*
+                 * If we support both v6 and v4, it's a failure
+                 * condition if we didn't manage to bind to both. If
+                 * the port number was arbitrary, we go round and try
+                 * again. Otherwise, give up.
+                 */
+                close(fds->v6);
+                close(fds->v4);
+                fds->v6 = fds->v4 = -1;
+                port = 0;
+                if (!portstr)
+                    goto retry;
+            }
+#endif
+            fprintf(stderr, "bind: %s\n", strerror(errno));
+            close(fds->v4);
+            fds->v4 = -1;
+            goto done_v4;
+        }
+        if (listen(fds->v4, 5) < 0) {
+            fprintf(stderr, "listen: %s\n", strerror(errno));
+            close(fds->v4);
+            fds->v4 = -1;
+            goto done_v4;
+        }
+        if (port == 0) {
+            addrlen = sizeof(addr4);
+            if (getsockname(fds->v4, (struct sockaddr *)&addr4,
+                            &addrlen) < 0) {
+                fprintf(stderr, "getsockname: %s\n", strerror(errno));
+                close(fds->v4);
+                fds->v4 = -1;
+                goto done_v4;
+            }
+            port = ntohs(addr4.sin_port);
+        }
     }
+  done_v4:
+#endif
+
+    if (fds->v6 >= 0 || fds->v4 >= 0)
+        return port;
+    else
+        return -1;
+}
+
+void run_httpd(const void *t, int authmask, const struct httpd_config *dcfg,
+              const struct html_config *incfg)
+{
+    struct listenfds lfds;
+    int ret, port;
+    int authtype;
+    char *authstring = NULL;
+    char *hostname;
+    struct sockaddr_in addr;
+    socklen_t addrlen;
+    struct html_config cfg = *incfg;
+
+    /*
+     * Establish the listening socket(s) and retrieve its port
+     * number.
+     */
+    port = make_listening_sockets(&lfds, dcfg->address, dcfg->port, &hostname);
+    if (port < 0)
+        exit(1);                       /* already reported an error */
+
     if ((authmask & HTTPD_AUTH_MAGIC) &&
-       (check_owning_uid(fd, 1) == getuid())) {
+       (lfds.v4 < 0 || check_owning_uid(lfds.v4, 1) == getuid()) &&
+        (lfds.v6 < 0 || check_owning_uid(lfds.v6, 1) == getuid())) {
        authtype = HTTPD_AUTH_MAGIC;
        if (authmask != HTTPD_AUTH_MAGIC)
            printf("Using Linux /proc/net magic authentication\n");
@@ -615,20 +850,20 @@ void run_httpd(const void *t, int authmask, const struct httpd_config *dcfg,
        fprintf(stderr, PNAME ": authentication method not supported\n");
        exit(1);
     }
-    if (ntohs(addr.sin_addr.s_addr) == INADDR_ANY) {
-       printf("Server port: %d\n", ntohs(addr.sin_port));
-    } else if (ntohs(addr.sin_port) == 80) {
-       printf("URL: http://%s/\n", inet_ntoa(addr.sin_addr));
+    if (port == 80) {
+       printf("URL: http://%s/\n", hostname);
     } else {
-       printf("URL: http://%s:%d/\n",
-              inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
+       printf("URL: http://%s:%d/\n", hostname, port);
     }
     fflush(stdout);
 
     /*
-     * Now construct an fd structure to hold it.
+     * Now construct fd structure(s) to hold the listening sockets.
      */
-    new_fdstruct(fd, FD_LISTENER);
+    if (lfds.v4 >= 0)
+        new_fdstruct(lfds.v4, FD_LISTENER);
+    if (lfds.v6 >= 0)
+        new_fdstruct(lfds.v6, FD_LISTENER);
 
     if (dcfg->closeoneof) {
         /*
diff --git a/httpd.h b/httpd.h
index a964e2f..d719931 100644 (file)
--- a/httpd.h
+++ b/httpd.h
@@ -8,8 +8,7 @@
 #define HTTPD_AUTH_NONE  4
 
 struct httpd_config {
-    const char *address;
-    int port;
+    const char *address, *port;
     int closeoneof;
     const char *basicauthdata;
 };