Bug 1502025 - Add IPv6 support to httpd.js r=dragana
authorValentin Gosu <valentin.gosu@gmail.com>
Sun, 02 Dec 2018 23:29:45 +0000
changeset 505622 29385deef9ea018dd480de4f4c9ceaa4b1308c5d
parent 505621 389ecc0ddf76e316c9ec17e8847829f19cb49af6
child 505623 b3c8a3a052ea96b093839b5732661e6c1d006a77
push id10290
push userffxbld-merge
push dateMon, 03 Dec 2018 16:23:23 +0000
treeherdermozilla-beta@700bed2445e6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdragana
bugs1502025
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1502025 - Add IPv6 support to httpd.js r=dragana Differential Revision: https://phabricator.services.mozilla.com/D13335
netwerk/base/nsIServerSocket.idl
netwerk/base/nsServerSocket.cpp
netwerk/test/httpserver/httpd.js
netwerk/test/unit/test_network_connectivity_service.js
--- a/netwerk/base/nsIServerSocket.idl
+++ b/netwerk/base/nsIServerSocket.idl
@@ -55,16 +55,23 @@ interface nsIServerSocket : nsISupports
      *        This parameter may be silently limited by the operating system.
      *        Pass -1 to use the default value.
      */
     void init(in long aPort,
               in boolean aLoopbackOnly,
               in long aBackLog);
 
     /**
+     * the same as init(), but initializes an IPv6 server socket
+     */
+    void initIPv6(in long aPort,
+                  in boolean aLoopbackOnly,
+                  in long aBackLog);
+
+    /**
      * initSpecialConnection
      *
      * This method initializes a server socket and offers the ability to have
      * that socket not get terminated if Gecko is set offline.
      *
      * @param aPort
      *        The port of the server socket.  Pass -1 to indicate no preference,
      *        and a port will be selected automatically.
--- a/netwerk/base/nsServerSocket.cpp
+++ b/netwerk/base/nsServerSocket.cpp
@@ -250,16 +250,35 @@ NS_IMPL_ISUPPORTS(nsServerSocket, nsISer
 
 NS_IMETHODIMP
 nsServerSocket::Init(int32_t aPort, bool aLoopbackOnly, int32_t aBackLog) {
   return InitSpecialConnection(aPort, aLoopbackOnly ? LoopbackOnly : 0,
                                aBackLog);
 }
 
 NS_IMETHODIMP
+nsServerSocket::InitIPv6(int32_t aPort, bool aLoopbackOnly, int32_t aBackLog) {
+  PRNetAddrValue val;
+  PRNetAddr addr;
+
+  if (aPort < 0) {
+    aPort = 0;
+  }
+  if (aLoopbackOnly) {
+    val = PR_IpAddrLoopback;
+  } else {
+    val = PR_IpAddrAny;
+  }
+  PR_SetNetAddr(val, PR_AF_INET6, aPort, &addr);
+
+  mKeepWhenOffline = false;
+  return InitWithAddress(&addr, aBackLog);
+}
+
+NS_IMETHODIMP
 nsServerSocket::InitWithFilename(nsIFile *aPath, uint32_t aPermissions,
                                  int32_t aBacklog) {
 #if defined(XP_UNIX)
   nsresult rv;
 
   nsAutoCString path;
   rv = aPath->GetNativePath(path);
   if (NS_FAILED(rv)) return rv;
--- a/netwerk/test/httpserver/httpd.js
+++ b/netwerk/test/httpserver/httpd.js
@@ -190,16 +190,19 @@ var gThreadManager = null;
 /**
  * JavaScript constructors for commonly-used classes; precreating these is a
  * speedup over doing the same from base principles.  See the docs at
  * http://developer.mozilla.org/en/docs/Components.Constructor for details.
  */
 const ServerSocket = CC("@mozilla.org/network/server-socket;1",
                         "nsIServerSocket",
                         "init");
+const ServerSocketIPv6 = CC("@mozilla.org/network/server-socket;1",
+                            "nsIServerSocket",
+                            "initIPv6");
 const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1",
                                  "nsIScriptableInputStream",
                                  "init");
 const Pipe = CC("@mozilla.org/pipe;1",
                 "nsIPipe",
                 "init");
 const FileInputStream = CC("@mozilla.org/network/file-input-stream;1",
                            "nsIFileInputStream",
@@ -480,30 +483,38 @@ nsHttpServer.prototype =
     // connections, plus a safety margin in case some other process is
     // talking to the server as well.
     var maxConnections = 5 + Math.max(
       Services.prefs.getIntPref("network.http.max-persistent-connections-per-server"),
       Services.prefs.getIntPref("network.http.max-persistent-connections-per-proxy"));
 
     try {
       var loopback = true;
-      if (this._host != "127.0.0.1" && this._host != "localhost") {
+      if (this._host != "127.0.0.1" && this._host != "localhost" &&
+          this._host != "[::1]") {
         loopback = false;
       }
 
       // When automatically selecting a port, sometimes the chosen port is
       // "blocked" from clients. We don't want to use these ports because
       // tests will intermittently fail. So, we simply keep trying to to
       // get a server socket until a valid port is obtained. We limit
       // ourselves to finite attempts just so we don't loop forever.
       var socket;
       for (var i = 100; i; i--) {
-        var temp = new ServerSocket(this._port,
-                                    loopback, // true = localhost, false = everybody
-                                    maxConnections);
+        var temp = null;
+        if (this._host.includes(":")) {
+          temp = new ServerSocketIPv6(this._port,
+                                      loopback, // true = localhost, false = everybody
+                                      maxConnections);
+        } else {
+          temp = new ServerSocket(this._port,
+                                  loopback, // true = localhost, false = everybody
+                                  maxConnections);
+        }
 
         var allowed = Services.io.allowPort(temp.port, "http");
         if (!allowed) {
           dumpn(">>>Warning: obtained ServerSocket listens on a blocked " +
                 "port: " + temp.port);
         }
 
         if (!allowed && this._port == -1) {
@@ -515,22 +526,22 @@ nsHttpServer.prototype =
         socket = temp;
         break;
       }
 
       if (!socket) {
         throw new Error("No socket server available. Are there no available ports?");
       }
 
-      dumpn(">>> listening on port " + socket.port + ", " + maxConnections +
-            " pending connections");
       socket.asyncListen(this);
       this._port = socket.port;
       this._identity._initialize(socket.port, host, true);
       this._socket = socket;
+      dumpn(">>> listening on port " + socket.port + ", " + maxConnections +
+            " pending connections");
     } catch (e) {
       dump("\n!!! could not start server on port " + port + ": " + e + "\n\n");
       throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE);
     }
   },
 
   //
   // see nsIHttpServer.stop
@@ -971,18 +982,23 @@ ServerIdentity.prototype =
     this._host = host;
     if (this._primaryPort !== -1)
       this.add("http", host, port);
     else
       this.setPrimary("http", "localhost", port);
     this._defaultPort = port;
 
     // Only add this if we're being called at server startup
-    if (addSecondaryDefault && host != "127.0.0.1")
-      this.add("http", "127.0.0.1", port);
+    if (addSecondaryDefault && host != "127.0.0.1") {
+      if (host.includes(":")) {
+        this.add("http", "[::1]", port);
+      } else {
+        this.add("http", "127.0.0.1", port);
+      }
+    }
   },
 
   /**
    * Called at server shutdown time, unsets the primary location only if it was
    * the default-assigned location and removes the default location from the
    * set of locations used.
    */
   _teardown() {
@@ -1017,17 +1033,17 @@ ServerIdentity.prototype =
    *   if any argument doesn't match the corresponding production
    */
   _validate(scheme, host, port) {
     if (scheme !== "http" && scheme !== "https") {
       dumpn("*** server only supports http/https schemes: '" + scheme + "'");
       dumpStack();
       throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
     }
-    if (!HOST_REGEX.test(host)) {
+    if (!HOST_REGEX.test(host) && host != "[::1]") {
       dumpn("*** unexpected host: '" + host + "'");
       throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
     }
     if (port < 0 || port > 65535) {
       dumpn("*** unexpected port: '" + port + "'");
       throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
     }
   },
@@ -1430,29 +1446,32 @@ RequestReader.prototype =
 
       // If the Request-URI wasn't absolute, then we need to determine our host.
       // We have to determine what scheme was used to access us based on the
       // server identity data at this point, because the request just doesn't
       // contain enough data on its own to do this, sadly.
       if (!metadata._host) {
         var host, port;
         var hostPort = headers.getHeader("Host");
-        var colon = hostPort.indexOf(":");
+        var colon = hostPort.lastIndexOf(":");
+        if (hostPort.lastIndexOf("]") > colon) {
+          colon = -1;
+        }
         if (colon < 0) {
           host = hostPort;
           port = "";
         } else {
           host = hostPort.substring(0, colon);
           port = hostPort.substring(colon + 1);
         }
 
         // NB: We allow an empty port here because, oddly, a colon may be
         //     present even without a port number, e.g. "example.com:"; in this
         //     case the default port applies.
-        if (!HOST_REGEX.test(host) || !/^\d*$/.test(port)) {
+        if ((!HOST_REGEX.test(host) && host != "[::1]") || !/^\d*$/.test(port)) {
           dumpn("*** malformed hostname (" + hostPort + ") in Host " +
                 "header, 400 time");
           throw HTTP_400;
         }
 
         // If we're not given a port, we're stuck, because we don't know what
         // scheme to use to look up the correct port here, in general.  Since
         // the HTTPS case requires a tunnel/proxy and thus requires that the
--- a/netwerk/test/unit/test_network_connectivity_service.js
+++ b/netwerk/test/unit/test_network_connectivity_service.js
@@ -31,20 +31,25 @@ registerCleanupFunction(() => {
   Services.prefs.clearUserPref("network.connectivity-service.DNSv4.domain");
   Services.prefs.clearUserPref("network.connectivity-service.DNSv6.domain");
   Services.prefs.clearUserPref("network.captive-portal-service.testMode");
   Services.prefs.clearUserPref("network.connectivity-service.IPv4.url");
   Services.prefs.clearUserPref("network.connectivity-service.IPv6.url");
 });
 
 let httpserver = null;
+let httpserverv6 = null;
 XPCOMUtils.defineLazyGetter(this, "URL", function() {
   return "http://localhost:" + httpserver.identity.primaryPort + "/content";
 });
 
+XPCOMUtils.defineLazyGetter(this, "URLv6", function() {
+  return "http://[::1]:" + httpserverv6.identity.primaryPort + "/content";
+});
+
 function contentHandler(metadata, response)
 {
   response.setHeader("Content-Type", "text/plain");
   response.setHeader("Cache-Control", "no-cache");
 
   const responseBody = "anybody";
   response.bodyOutputStream.write(responseBody, responseBody.length);
 }
@@ -87,33 +92,37 @@ add_task(async function testDNS() {
   equal(ncs.DNSv6, Ci.nsINetworkConnectivityService.UNKNOWN, "Check DNSv6 support (expect UNKNOWN)");
 
   await promiseObserverNotification("network:connectivity-service:dns-checks-complete");
 
   equal(ncs.DNSv4, Ci.nsINetworkConnectivityService.OK, "Check DNSv4 support (expect OK)");
   equal(ncs.DNSv6, Ci.nsINetworkConnectivityService.OK, "Check DNSv6 support (expect OK)");
 
   httpserver = new HttpServer();
-  httpserver.registerPathHandler("/cps.txt", contentHandler);
+  httpserver.registerPathHandler("/content", contentHandler);
   httpserver.start(-1);
 
+  httpserverv6 = new HttpServer();
+  httpserverv6.registerPathHandler("/contentt", contentHandler);
+  httpserverv6._start(-1, "[::1]");
+
   // Before setting the pref, this status is unknown in automation
   equal(ncs.IPv4, Ci.nsINetworkConnectivityService.UNKNOWN, "Check IPv4 support (expect UNKNOWN)");
   equal(ncs.IPv6, Ci.nsINetworkConnectivityService.UNKNOWN, "Check IPv6 support (expect UNKNOWN)");
 
   Services.prefs.setBoolPref("network.captive-portal-service.testMode", true);
   Services.prefs.setCharPref("network.connectivity-service.IPv4.url", URL);
-  Services.prefs.setCharPref("network.connectivity-service.IPv6.url", URL);
+  Services.prefs.setCharPref("network.connectivity-service.IPv6.url", URLv6);
   ncs.recheckIPConnectivity();
   await promiseObserverNotification("network:connectivity-service:ip-checks-complete");
 
   equal(ncs.IPv4, Ci.nsINetworkConnectivityService.OK, "Check IPv4 support (expect OK)");
-  // httpserver doesn't yet support IPv6
-  equal(ncs.IPv6, Ci.nsINetworkConnectivityService.NOT_AVAILABLE, "Check IPv6 support (expect OK[TODO])");
+  equal(ncs.IPv6, Ci.nsINetworkConnectivityService.OK, "Check IPv6 support (expect OK)");
 
   // check that the CPS status is NOT_AVAILABLE when the endpoint is down.
   await new Promise(resolve => httpserver.stop(resolve));
+  await new Promise(resolve => httpserverv6.stop(resolve));
   Services.obs.notifyObservers(null, "network:captive-portal-connectivity", null);
   await promiseObserverNotification("network:connectivity-service:ip-checks-complete");
 
   equal(ncs.IPv4, Ci.nsINetworkConnectivityService.NOT_AVAILABLE, "Check IPv4 support (expect NOT_AVAILABLE)");
   equal(ncs.IPv6, Ci.nsINetworkConnectivityService.NOT_AVAILABLE, "Check IPv6 support (expect NOT_AVAILABLE)");
 });