Bug 428009 - Update the HTTP server to require a *correct* host be specified with it, not just *a* host via an absolute URI as Request-URI or a specified Host header. This also gives request handlers proper details about the location to which the request was targeted. r=biesi on the raw socket usage in the test code, r=ted on the build changes, r=sayrer on the server changes
authorJeff Walden <jwalden@mit.edu>
Sat, 07 Jun 2008 02:43:15 -0400
changeset 15290 3721c17088cc9a6ae7c0e8eff178cde801658899
parent 15289 7594e713db40ab7bed48616418960d9cafe2beb0
child 15291 75e1d5f3096d09ea2009fe6fff70e2c2197ae957
push idunknown
push userunknown
push dateunknown
reviewersbiesi, ted, sayrer
bugs428009
milestone1.9.1a1pre
Bug 428009 - Update the HTTP server to require a *correct* host be specified with it, not just *a* host via an absolute URI as Request-URI or a specified Host header. This also gives request handlers proper details about the location to which the request was targeted. r=biesi on the raw socket usage in the test code, r=ted on the build changes, r=sayrer on the server changes
build/pgo/Makefile.in
build/pgo/automation.py.in
build/pgo/server-locations.txt
netwerk/test/httpserver/README
netwerk/test/httpserver/httpd.js
netwerk/test/httpserver/nsIHttpServer.idl
netwerk/test/httpserver/test/head_utils.js
netwerk/test/httpserver/test/test_host.js
testing/mochitest/Makefile.in
testing/mochitest/server.js
--- a/build/pgo/Makefile.in
+++ b/build/pgo/Makefile.in
@@ -48,16 +48,17 @@ include $(topsrcdir)/config/rules.mk
 # Stuff to make a build with a profile
 _PROFILE_DIR = $(DEPTH)/_profile/pgo
 
 _PGO_FILES = 	\
 		automation.py \
 		profileserver.py \
 		index.html \
 		quit.js \
+		server-locations.txt \
 		$(NULL)
 
 
 ifeq ($(USE_SHORT_LIBNAME), 1)
 PROGRAM = $(MOZ_APP_NAME)$(BIN_SUFFIX)
 else
 PROGRAM = $(MOZ_APP_NAME)-bin$(BIN_SUFFIX)
 endif
--- a/build/pgo/automation.py.in
+++ b/build/pgo/automation.py.in
@@ -32,21 +32,23 @@
 # use your version of this file under the terms of the MPL, indicate your
 # decision by deleting the provisions above and replace them with the notice
 # and other provisions required by the GPL or the LGPL. If you do not delete
 # the provisions above, a recipient may use your version of this file under
 # the terms of any one of the MPL, the GPL or the LGPL.
 #
 # ***** END LICENSE BLOCK *****
 
+import codecs
 from datetime import datetime
 import itertools
 import logging
 import shutil
 import os
+import re
 import signal
 import sys
 import threading
 
 """
 Runs the browser from a script, and provides useful utilities
 for setting up the browser environment.
 """
@@ -57,66 +59,16 @@ for setting up the browser environment.
            "IS_MAC",
            "runApp",
            "Process",
            "initializeProfile",
            "DIST_BIN",
            "DEFAULT_APP",
           ]
 
-# Since some tests require cross-domain support in Mochitest, across ports,
-# domains, subdomains, etc. we use a proxy autoconfig hack to map a bunch of
-# servers onto localhost:8888.  We have to grant them the same privileges as
-# localhost:8888 here, since the browser only knows them as the URLs they're
-# pretending to be.  We also have two servers which are set up but don't have
-# privileges, for testing privilege functionality.
-#
-# These lists must be kept in sync with the following list:
-#
-# http://developer.mozilla.org/en/docs/Mochitest#How_do_I_test_issues_which_only_show_up_when_tests_are_run_across_domains.3F
-#
-servers = [
-           "localhost:8888", # MUST be first -- see PAC pref-setting code
-           "example.org:80",
-           "test1.example.org:80",
-           "test2.example.org:80",
-           "sub1.test1.example.org:80",
-           "sub1.test2.example.org:80",
-           "sub2.test1.example.org:80",
-           "sub2.test2.example.org:80",
-           "example.org:8000",
-           "test1.example.org:8000",
-           "test2.example.org:8000",
-           "sub1.test1.example.org:8000",
-           "sub1.test2.example.org:8000",
-           "sub2.test1.example.org:8000",
-           "sub2.test2.example.org:8000",
-           "example.com:80",
-           "test1.example.com:80",
-           "test2.example.com:80",
-           "sub1.test1.example.com:80",
-           "sub1.test2.example.com:80",
-           "sub2.test1.example.com:80",
-           "sub2.test2.example.com:80",
-           "sectest1.example.org:80",
-           "sub.sectest2.example.org:80",
-           "sub1.xn--lt-uia.example.org:8000", # U+00E4 U+006C U+0074
-           "sub2.xn--lt-uia.example.org:80",   # U+00E4 U+006C U+0074
-           "xn--exmple-cua.test:80",
-           "sub1.xn--exmple-cua.test:80",
-           "xn--hxajbheg2az3al.xn--jxalpdlp:80", # Greek IDN for example.test
-           "sub1.xn--hxajbheg2az3al.xn--jxalpdlp:80",
-          ]
-
-unprivilegedServers = [
-                       "sectest2.example.org:80",
-                       "sub.sectest1.example.org:80",
-                      ]
-
-
 # These are generated in mozilla/build/Makefile.in
 #expand DIST_BIN = "./" + __XPC_BIN_PATH__
 #expand IS_WIN32 = len("__WIN32__") != 0
 #expand IS_MAC = __IS_MAC__ != 0
 #ifdef IS_CYGWIN
 #expand IS_CYGWIN = __IS_CYGWIN__ == 1
 #else
 IS_CYGWIN = False
@@ -218,16 +170,98 @@ class Process:
     except:
       pass
 
 
 #################
 # PROFILE SETUP #
 #################
 
+class SyntaxError(Exception):
+  "Signifies a syntax error on a particular line in server-locations.txt."
+
+  def __init__(self, lineno, msg = None):
+    self.lineno = lineno
+    self.msg = msg
+
+  def __str__(self):
+    s = "Syntax error on line " + str(self.lineno)
+    if self.msg:
+      s += ": %s." % self.msg
+    else:
+      s += "."
+    return s
+
+
+class Location:
+  "Represents a location line in server-locations.txt."
+
+  def __init__(self, scheme, host, port, options):
+    self.scheme = scheme
+    self.host = host
+    self.port = port
+    self.options = options
+
+
+def readLocations():
+  """
+  Reads the locations at which the Mochitest HTTP server is available from
+  server-locations.txt.
+  """
+
+  locationFile = codecs.open("server-locations.txt", "r", "UTF-8")
+
+  # Perhaps more detail than necessary, but it's the easiest way to make sure
+  # we get exactly the format we want.  See server-locations.txt for the exact
+  # format guaranteed here.
+  lineRe = re.compile(r"^(?P<scheme>[a-z][-a-z0-9+.]*)"
+                      r"://"
+                      r"(?P<host>"
+                        r"\d+\.\d+\.\d+\.\d+"
+                        r"|"
+                        r"(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*"
+                        r"[a-z](?:[-a-z0-9]*[a-z0-9])?"
+                      r")"
+                      r":"
+                      r"(?P<port>\d+)"
+                      r"(?:"
+                      r"\s+"
+                      r"(?P<options>\w+(?:,\w+)*)"
+                      r")?$")
+  locations = []
+  lineno = 0
+  seenPrimary = False
+  for line in locationFile:
+    lineno += 1
+    if line.startswith("#") or line == "\n":
+      continue
+      
+    match = lineRe.match(line)
+    if not match:
+      raise SyntaxError(lineno)
+
+    options = match.group("options")
+    if options:
+      options = options.split(",")
+      if "primary" in options:
+        if seenPrimary:
+          raise SyntaxError(lineno, "multiple primary locations")
+        seenPrimary = True
+    else:
+      options = []
+
+    locations.append(Location(match.group("scheme"), match.group("host"),
+                              match.group("port"), options))
+
+  if not seenPrimary:
+    raise SyntaxError(lineno + 1, "missing primary location")
+
+  return locations
+
+
 def initializeProfile(profileDir):
   "Sets up the standard testing profile."
 
   # Start with a clean slate.
   shutil.rmtree(profileDir, True)
   os.mkdir(profileDir)
 
   prefs = []
@@ -245,52 +279,59 @@ user_pref("accessibility.typeaheadfind.a
 user_pref("javascript.options.showInConsole", true);
 user_pref("layout.debug.enable_data_xbl", true);
 user_pref("browser.EULA.override", true);
 
 user_pref("camino.warn_when_closing", false); // Camino-only, harmless to others
 """
   prefs.append(part)
 
-  # Grant God-power to all the servers on which tests can run.
-  for (i, server) in itertools.izip(itertools.count(1), servers):
+  locations = readLocations()
+
+  # Grant God-power to all the privileged servers on which tests run.
+  privileged = filter(lambda loc: "privileged" in loc.options, locations)
+  for (i, l) in itertools.izip(itertools.count(1), privileged):
     part = """
 user_pref("capability.principal.codebase.p%(i)d.granted",
           "UniversalXPConnect UniversalBrowserRead UniversalBrowserWrite \
            UniversalPreferencesRead UniversalPreferencesWrite \
            UniversalFileRead");
-user_pref("capability.principal.codebase.p%(i)d.id", "http://%(server)s");
+user_pref("capability.principal.codebase.p%(i)d.id", "%(origin)s");
 user_pref("capability.principal.codebase.p%(i)d.subjectName", "");
-"""  % {"i": i, "server": server}
+"""  % { "i": i,
+         "origin": (l.scheme + "://" + l.host + ":" + l.port) }
     prefs.append(part)
 
-  # Now add the two servers that do NOT have God-power so we can properly test
-  # the granting and receiving of God-power.  Strip off the first server because
-  # we proxy all the others to it.
-  allServers = servers[1:] + unprivilegedServers
-
-
-  # Now actually create the preference to make the proxying happen.
-  quotedServers = ", ".join(map(lambda x: "'" + x + "'", allServers))
+  # We need to proxy every server but the primary one.
+  origins = ["'%s://%s:%s'" % (l.scheme, l.host, l.port)
+             for l in filter(lambda l: "primary" not in l.options, locations)]
+  origins = ", ".join(origins)
 
   pacURL = """data:text/plain,
 function FindProxyForURL(url, host)
 {
-  var servers = [%(quotedServers)s];
-  var regex = new RegExp('http://(?:[^/@]*@)?(.*?(:\\\\\\\\d+)?)/');
+  var origins = [%(origins)s];
+  var regex = new RegExp('^([a-z][-a-z0-9+.]*)' +
+                         '://' +
+                         '(?:[^/@]*@)?' +
+                         '(.*?)' +
+                         '(?::(\\\\\\\\d+))?/');
   var matches = regex.exec(url);
   if (!matches)
     return 'DIRECT';
-  var hostport = matches[1], port = matches[2];
-  if (!port)
-    hostport += ':80';
-  if (servers.indexOf(hostport) >= 0)
+  var isHttp = matches[1] == 'http';
+  if (!matches[3])
+    matches[3] = isHttp ? '80' : '443';
+  var origin = matches[1] + '://' + matches[2] + ':' + matches[3];
+  if (origins.indexOf(origin) < 0)
+    return 'DIRECT';
+  if (isHttp)
     return 'PROXY localhost:8888';
   return 'DIRECT';
-}""" % {"quotedServers": quotedServers}
+}""" % { "origins": origins }
   pacURL = "".join(pacURL.splitlines())
 
   part = """
 user_pref("network.proxy.type", 2);
 user_pref("network.proxy.autoconfig_url", "%(pacURL)s");
 
 user_pref("camino.use_system_proxy_settings", false); // Camino-only, harmless to others
 """ % {"pacURL": pacURL}
new file mode 100644
--- /dev/null
+++ b/build/pgo/server-locations.txt
@@ -0,0 +1,116 @@
+#
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# Jeff Walden <jwalden+code@mit.edu>.
+# Portions created by the Initial Developer are Copyright (C) 2008
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+#
+# This file defines the locations at which this HTTP server may be accessed.
+# It is referred to by the following page, so if this file moves, that page must
+# be modified accordingly:
+#
+# http://developer.mozilla.org/en/docs/Mochitest#How_do_I_test_issues_which_only_show_up_when_tests_are_run_across_domains.3F
+#
+# Empty lines and lines which begin with "#" are ignored and may be used for
+# storing comments.  All other lines consist of an origin followed by whitespace
+# and a comma-separated list of options (if indeed any options are needed).
+#
+# The format of an origin is, referring to RFC 2396, a scheme (either "http" or
+# "https"), followed by "://", followed by a host, followed by ":", followed by
+# a port number.  The colon and port number must be present even if the port
+# number is the default for the protocol.
+#
+# Unrecognized options are ignored.  Recognized options are "primary" and
+# "privileged".  "primary" denotes a location which is the canonical location of
+# the server; this location is the one assumed for requests which don't
+# otherwise identify a particular origin (e.g. HTTP/1.0 requests).  "privileged"
+# denotes a location which should have the ability to request elevated
+# privileges; the default is no privileges.
+#
+
+#
+# This is the primary location from which tests run.
+#
+http://localhost:8888   primary,privileged
+
+#
+# These are a common set of prefixes scattered across one TLD with two ports and
+# another TLD on a single port.
+#
+http://example.org:80                privileged
+http://test1.example.org:80          privileged
+http://test2.example.org:80          privileged
+http://sub1.test1.example.org:80     privileged
+http://sub1.test2.example.org:80     privileged
+http://sub2.test1.example.org:80     privileged
+http://sub2.test2.example.org:80     privileged
+http://example.org:8000              privileged
+http://test1.example.org:8000        privileged
+http://test2.example.org:8000        privileged
+http://sub1.test1.example.org:8000   privileged
+http://sub1.test2.example.org:8000   privileged
+http://sub2.test1.example.org:8000   privileged
+http://sub2.test2.example.org:8000   privileged
+http://example.com:80                privileged
+http://test1.example.com:80          privileged
+http://test2.example.com:80          privileged
+http://sub1.test1.example.com:80     privileged
+http://sub1.test2.example.com:80     privileged
+http://sub2.test1.example.com:80     privileged
+http://sub2.test2.example.com:80     privileged
+
+#
+# These are subdomains of <ält.example.org>.
+#
+http://sub1.xn--lt-uia.example.org:8000   privileged
+http://sub2.xn--lt-uia.example.org:80     privileged
+http://xn--exmple-cua.test:80             privileged
+http://sub1.xn--exmple-cua.test:80        privileged
+
+#
+# These are subdomains of <παράδειγμα.δοκιμή>, the Greek IDN for example.test.
+#
+http://xn--hxajbheg2az3al.xn--jxalpdlp:80        privileged
+http://sub1.xn--hxajbheg2az3al.xn--jxalpdlp:80   privileged
+
+#
+# These hosts are used in tests which exercise privilege-granting functionality;
+# we could reuse some of the names above, but specific names make it easier to
+# distinguish one from the other in tests (as well as what functionality is
+# being tested).
+#
+http://sectest1.example.org:80       privileged
+http://sub.sectest2.example.org:80   privileged
+http://sectest2.example.org:80
+http://sub.sectest1.example.org:80
--- a/netwerk/test/httpserver/README
+++ b/netwerk/test/httpserver/README
@@ -41,16 +41,21 @@ Finally, you'll want to start (and later
 code which does this:
 
   server.start(8080);  // port on which server will operate
 
   // ...server now runs and serves requests...
 
   server.stop();
 
+This server will only respond to requests on 127.0.0.1:8080 or localhost:8080.
+If you want it to respond to requests at different hosts (say via a proxy
+mechanism), you must use server.identity.add() or server.identity.setPrimary()
+to add it.
+
 
 Using httpd.js as an Inline Script or from xpcshell
 ---------------------------------------------------
 
 Using httpd.js as a script or from xpcshell isn't very different from using it
 as a component; the only real difference lies in how you create an instance of
 the server.  To create an instance, do the following:
 
@@ -64,34 +69,29 @@ constants (specifically, Cc/Ci/Cr as abb
 and results properties of Components), it's possible this trampling could
 break your script.  In general you should use httpd.js as an XPCOM component
 whenever possible.
 
 
 Known Issues
 ------------
 
-While httpd.js runs on Mozilla 1.8 and 1.9 platforms, it doesn't run quite as
-well on 1.8 due to the absence of some APIs, specifically the threading APIs.
-The biggest problem here is that server shutdown (see nsIHttpServer.stop) is not
-guaranteed to complete after all pending requests have been served; if you are
-using the server in 1.8 code, you should probably wait a few seconds after
-calling server.stop() before the host application closes to ensure that all
-requests have completed.  Things probably aren't going to break too horribly if
-you don't do this, but better safe than sorry.
-
 httpd.js makes no effort to time out requests, beyond any the socket itself
 might or might not provide.  I don't believe it provides any by default, but
 I haven't verified this.
 
-To be clear: the guarantee that nsIHttpServer.stop says implementations should
-make when possible (that .stop returns only when all pending requests have been
-serviced) cannot be made in a 1.8 environment; it can be made in a 1.9
-environment.  Use 1.9 if this matters to you, or hack around it as described
-here.
+Every incoming request is processed by the corresponding request handler
+synchronously.  In other words, once the first CRLFCRLF of a request is
+received, the entire response is created before any new incoming requests can be
+served.  I anticipate adding asynchronous handler functionality in bug 396226,
+but it may be some time before that happens.
+
+There is no way to access the body of an incoming request.  This problem is
+merely a symptom of the previous one, and they will probably both be addressed
+at the same time.
 
 
 Other Goodies
 -------------
 
 A special testing function, |server|, is provided for use in xpcshell for quick
 testing of the server; see the source code for details on its use.  You don't
 want to use this in a script, however, because doing so will block until the
--- a/netwerk/test/httpserver/httpd.js
+++ b/netwerk/test/httpserver/httpd.js
@@ -202,19 +202,16 @@ const Pipe = CC("@mozilla.org/pipe;1",
                 "nsIPipe",
                 "init");
 const FileInputStream = CC("@mozilla.org/network/file-input-stream;1",
                            "nsIFileInputStream",
                            "init");
 const StreamCopier = CC("@mozilla.org/network/async-stream-copier;1",
                         "nsIAsyncStreamCopier",
                         "init");
-const Pump = CC("@mozilla.org/network/input-stream-pump;1",
-                "nsIInputStreamPump",
-                "init");
 const ConverterInputStream = CC("@mozilla.org/intl/converter-input-stream;1",
                                 "nsIConverterInputStream",
                                 "init");
 const WritablePropertyBag = CC("@mozilla.org/hash-property-bag;1",
                                "nsIWritablePropertyBag2");
 const SupportsString = CC("@mozilla.org/supports-string;1",
                           "nsISupportsString");
 
@@ -333,16 +330,19 @@ function nsHttpServer()
   this._port = undefined;
 
   /** The socket associated with this. */
   this._socket = null;
 
   /** The handler used to process requests to this server. */
   this._handler = new ServerHandler(this);
 
+  /** Naming information for this server. */
+  this._identity = new ServerIdentity();
+
   /**
    * Indicates when the server is to be shut down at the end of the request.
    */
   this._doQuit = false;
 
   /**
    * True if the socket in this is closed (and closure notifications have been
    * sent and processed if the socket was ever opened), false otherwise.
@@ -418,30 +418,36 @@ nsHttpServer.prototype =
     this._doQuit = this._socketClosed = false;
 
     var socket = new ServerSocket(this._port,
                                   true, // loopback only
                                   -1);  // default number of pending connections
 
     dumpn(">>> listening on port " + socket.port);
     socket.asyncListen(this);
+    this._identity._initialize(port, true);
     this._socket = socket;
   },
 
   //
   // see nsIHttpServer.stop
   //
   stop: function()
   {
     if (!this._socket)
       return;
 
     dumpn(">>> stopping listening on port " + this._socket.port);
     this._socket.close();
     this._socket = null;
+
+    // We can't have this identity any more, and the port on which we're running
+    // this server now could be meaningless the next time around.
+    this._identity._teardown();
+
     this._doQuit = false;
 
     // spin an event loop and wait for the socket-close notification
     var thr = gThreadManager.currentThread;
     while (!this._socketClosed || this._handler.hasPendingRequests())
       thr.processNextEvent(true);
   },
 
@@ -501,16 +507,24 @@ nsHttpServer.prototype =
   //
   // see nsIHttpServer.registerContentType
   //
   registerContentType: function(ext, type)
   {
     this._handler.registerContentType(ext, type);
   },
 
+  //
+  // see nsIHttpServer.serverIdentity
+  //
+  get identity()
+  {
+    return this._identity;
+  },
+
   // NSISUPPORTS
 
   //
   // see nsISupports.QueryInterface
   //
   QueryInterface: function(iid)
   {
     if (iid.equals(Ci.nsIHttpServer) ||
@@ -570,16 +584,273 @@ nsHttpServer.prototype =
     dumpn(">>> requesting a quit");
     dumpStack();
     this._doQuit = true;
   }
 
 };
 
 
+//
+// RFC 2396 section 3.2.2:
+//
+// host        = hostname | IPv4address
+// hostname    = *( domainlabel "." ) toplabel [ "." ]
+// domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
+// toplabel    = alpha | alpha *( alphanum | "-" ) alphanum
+// IPv4address = 1*digit "." 1*digit "." 1*digit "." 1*digit
+//
+
+const HOST_REGEX =
+  new RegExp("^(?:" +
+               // *( domainlabel "." )
+               "(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)*" +
+               // toplabel
+               "[a-z](?:[a-z0-9-]*[a-z0-9])?" +
+             "|" +
+               // IPv4 address 
+               "\\d+\\.\\d+\\.\\d+\\.\\d+" +
+             ")$",
+             "i");
+
+
+/**
+ * Represents the identity of a server.  An identity consists of a set of
+ * (scheme, host, port) tuples denoted as locations (allowing a single server to
+ * serve multiple sites or to be used behind both HTTP and HTTPS proxies for any
+ * host/port).  Any incoming request must be to one of these locations, or it
+ * will be rejected with an HTTP 400 error.  One location, denoted as the
+ * primary location, is the location assigned in contexts where a location
+ * cannot otherwise be endogenously derived, such as for HTTP/1.0 requests.
+ *
+ * A single identity may contain at most one location per unique host/port pair;
+ * other than that, no restrictions are placed upon what locations may
+ * constitute an identity.
+ */
+function ServerIdentity()
+{
+  /** The scheme of the primary location. */
+  this._primaryScheme = "http";
+
+  /** The hostname of the primary location. */
+  this._primaryHost = "127.0.0.1"
+
+  /** The port number of the primary location. */
+  this._primaryPort = -1;
+
+  /**
+   * The current port number for the corresponding server, stored so that a new
+   * primary location can always be set if the current one is removed.
+   */
+  this._defaultPort = -1;
+
+  /**
+   * Maps hosts to maps of ports to schemes, e.g. the following would represent
+   * https://example.com:789/ and http://example.org/:
+   *
+   *   {
+   *     "xexample.com": { 789: "https" },
+   *     "xexample.org": { 80: "http" }
+   *   }
+   *
+   * Note the "x" prefix on hostnames, which prevents collisions with special
+   * JS names like "prototype".
+   */
+  this._locations = { "xlocalhost": {} };
+}
+ServerIdentity.prototype =
+{
+  /**
+   * Initializes the primary name for the corresponding server, based on the
+   * provided port number.
+   */
+  _initialize: function(port, addSecondaryDefault)
+  {
+    if (this._primaryPort !== -1)
+      this.add("http", "localhost", port);
+    else
+      this.setPrimary("http", "localhost", port);
+    this._defaultPort = port;
+
+    // Only add this if we're being called at server startup
+    if (addSecondaryDefault)
+      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: function()
+  {
+    // Not the default primary location, nothing special to do here
+    this.remove("http", "127.0.0.1", this._defaultPort);
+
+    // This is a *very* tricky bit of reasoning here; make absolutely sure the
+    // tests for this code pass before you commit changes to it.
+    if (this._primaryScheme == "http" &&
+        this._primaryHost == "localhost" &&
+        this._primaryPort == this._defaultPort)
+    {
+      // Make sure we don't trigger the readding logic in .remove(), then remove
+      // the default location.
+      var port = this._defaultPort;
+      this._defaultPort = -1;
+      this.remove("http", "localhost", port);
+
+      // Ensure a server start triggers the setPrimary() path in ._initialize()
+      this._primaryPort = -1;
+    }
+    else
+    {
+      // No reason not to remove directly as it's not our primary location
+      this.remove("http", "localhost", this._defaultPort);
+    }
+  },
+
+  //
+  // see nsIHttpServerIdentity.primaryScheme
+  //
+  get primaryScheme()
+  {
+    if (this._primaryPort === -1)
+      throw Cr.NS_ERROR_NOT_INITIALIZED;
+    return this._primaryScheme;
+  },
+
+  //
+  // see nsIHttpServerIdentity.primaryHost
+  //
+  get primaryHost()
+  {
+    if (this._primaryPort === -1)
+      throw Cr.NS_ERROR_NOT_INITIALIZED;
+    return this._primaryHost;
+  },
+
+  //
+  // see nsIHttpServerIdentity.primaryPort
+  //
+  get primaryPort()
+  {
+    if (this._primaryPort === -1)
+      throw Cr.NS_ERROR_NOT_INITIALIZED;
+    return this._primaryPort;
+  },
+
+  //
+  // see nsIHttpServerIdentity.add
+  //
+  add: function(scheme, host, port)
+  {
+    this._validate(scheme, host, port);
+    
+    var entry = this._locations["x" + host];
+    if (!entry)
+      this._locations["x" + host] = entry = {};
+
+    entry[port] = scheme;
+  },
+
+  //
+  // see nsIHttpServerIdentity.remove
+  //
+  remove: function(scheme, host, port)
+  {
+    this._validate(scheme, host, port);
+
+    var entry = this._locations["x" + host];
+    if (!entry)
+      return false;
+
+    var present = port in entry;
+    delete entry[port];
+
+    if (this._primaryScheme == scheme &&
+        this._primaryHost == host &&
+        this._primaryPort == port &&
+        this._defaultPort !== -1)
+    {
+      // Always keep at least one identity in existence at any time, unless
+      // we're in the process of shutting down (the last condition above).
+      this._primaryPort = -1;
+      this._initialize(this._defaultPort, false);
+    }
+
+    return present;
+  },
+
+  //
+  // see nsIHttpServerIdentity.has
+  //
+  has: function(scheme, host, port)
+  {
+    this._validate(scheme, host, port);
+
+    return "x" + host in this._locations &&
+           scheme === this._locations["x" + host][port];
+  },
+  
+  //
+  // see nsIHttpServerIdentity.has
+  //
+  getScheme: function(host, port)
+  {
+    this._validate("http", host, port);
+
+    var entry = this._locations["x" + host];
+    if (!entry)
+      return "";
+
+    return entry[port] || "";
+  },
+  
+  //
+  // see nsIHttpServerIdentity.setPrimary
+  //
+  setPrimary: function(scheme, host, port)
+  {
+    this._validate(scheme, host, port);
+
+    this.add(scheme, host, port);
+
+    this._primaryScheme = scheme;
+    this._primaryHost = host;
+    this._primaryPort = port;
+  },
+
+  /**
+   * Ensures scheme, host, and port are all valid with respect to RFC 2396.
+   *
+   * @throws NS_ERROR_ILLEGAL_VALUE
+   *   if any argument doesn't match the corresponding production
+   */
+  _validate: function(scheme, host, port)
+  {
+    if (scheme !== "http" && scheme !== "https")
+    {
+      dumpn("*** server only supports http/https schemes: '" + scheme + "'");
+      dumpStack();
+      throw Cr.NS_ERROR_ILLEGAL_VALUE;
+    }
+    if (!HOST_REGEX.test(host))
+    {
+      dumpn("*** unexpected host: '" + host + "'");
+      throw Cr.NS_ERROR_ILLEGAL_VALUE;
+    }
+    if (port < 0 || port > 65535)
+    {
+      dumpn("*** unexpected port: '" + port + "'");
+      throw Cr.NS_ERROR_ILLEGAL_VALUE;
+    }
+  }
+};
+
+
 /**
  * Represents a connection to the server (and possibly in the future the thread
  * on which the connection is processed).
  *
  * @param input : nsIInputStream
  *   stream from which incoming data on the connection is read
  * @param output : nsIOutputStream
  *   stream to write data out the connection
@@ -860,18 +1131,16 @@ RequestReader.prototype =
    */
   _processHeaders: function(input, count)
   {
     NS_ASSERT(this._state == READER_IN_HEADERS);
 
     // XXX things to fix here:
     //
     // - need to support RFC 2047-encoded non-US-ASCII characters
-    // - really support absolute URLs (requires telling the server all its
-    //   hostnames, beyond just localhost:port or 127.0.0.1:port)
 
     this._data.appendBytes(readBytes(input, count));
 
     try
     {
       // do we have all the headers?
       if (!this._parseHeaders())
         return true;
@@ -897,21 +1166,88 @@ RequestReader.prototype =
   {
     NS_ASSERT(this._state == READER_IN_BODY);
 
     dumpn("*** _validateRequest");
 
     var metadata = this._metadata;
     var headers = metadata._headers;
 
-    var isHttp11 = metadata._httpVersion.equals(nsHttpVersion.HTTP_1_1);
-
     // 19.6.1.1 -- servers MUST report 400 to HTTP/1.1 requests w/o Host header
-    if (isHttp11 && !headers.hasHeader("Host"))
-      throw HTTP_400;
+    var identity = this._connection.server.identity;
+    if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1))
+    {
+      if (!headers.hasHeader("Host"))
+      {
+        dumpn("*** malformed HTTP/1.1 or greater request with no Host header!");
+        throw HTTP_400;
+      }
+
+      // 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(":");
+        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))
+        {
+          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
+        // requested URI be absolute (and thus contain the necessary
+        // information), let's assume HTTP will prevail and use that.
+        port = +port || 80;
+
+        var scheme = identity.getScheme(host, port);
+        if (!scheme)
+        {
+          dumpn("*** unrecognized hostname (" + hostPort + ") in Host " +
+                "header, 400 time");
+          throw HTTP_400;
+        }
+
+        metadata._scheme = scheme;
+        metadata._host = host;
+        metadata._port = port;
+      }
+    }
+    else
+    {
+      NS_ASSERT(metadata._host === undefined,
+                "HTTP/1.0 doesn't allow absolute paths in the request line!");
+
+      metadata._scheme = identity.primaryScheme;
+      metadata._host = identity.primaryHost;
+      metadata._port = identity.primaryPort;
+    }
+
+    NS_ASSERT(identity.has(metadata._scheme, metadata._host, metadata._port),
+              "must have a location we recognize by now!");
   },
 
   /**
    * Handles responses in case of error, either in the server or in the request.
    *
    * @param e
    *   the specific error encountered, which is an HttpError in the case where
    *   the request is in some way invalid or cannot be fulfilled; if this isn't
@@ -993,60 +1329,84 @@ RequestReader.prototype =
     var match = ver.match(/^HTTP\/(\d+\.\d+)$/);
     if (!match)
       throw HTTP_400;
 
     // determine HTTP version
     try
     {
       metadata._httpVersion = new nsHttpVersion(match[1]);
-      if (!metadata._httpVersion.equals(nsHttpVersion.HTTP_1_0) &&
-          !metadata._httpVersion.equals(nsHttpVersion.HTTP_1_1))
+      if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_0))
         throw "unsupported HTTP version";
     }
     catch (e)
     {
       // we support HTTP/1.0 and HTTP/1.1 only
       throw HTTP_501;
     }
 
 
     var fullPath = request[1];
+    var serverIdentity = this._connection.server.identity;
+
+    var scheme, host, port;
 
     if (fullPath.charAt(0) != "/")
     {
-      // XXX we don't really support absolute URIs yet -- a MUST for HTTP/1.1;
-      //     for now just get the path and use that, ignoring hostport
+      // No absolute paths in the request line in HTTP prior to 1.1
+      if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1))
+        throw HTTP_400;
+
       try
       {
         var uri = Cc["@mozilla.org/network/io-service;1"]
                     .getService(Ci.nsIIOService)
                     .newURI(fullPath, null, null);
         fullPath = uri.path;
+        scheme = uri.scheme;
+        host = metadata._host = uri.asciiHost;
+        port = uri.port;
+        if (port === -1)
+        {
+          if (scheme === "http")
+            port = 80;
+          else if (scheme === "https")
+            port = 443;
+          else
+            throw HTTP_400;
+        }
       }
-      catch (e) { /* invalid URI */ }
-      if (fullPath.charAt(0) != "/")
+      catch (e)
       {
-        this.errorCode = 400;
-        return;
+        // If the host is not a valid host on the server, the response MUST be a
+        // 400 (Bad Request) error message (section 5.2).  Alternately, the URI
+        // is malformed.
+        throw HTTP_400;
       }
+
+      if (!serverIdentity.has(scheme, host, port) || fullPath.charAt(0) != "/")
+        throw HTTP_400;
     }
 
     var splitter = fullPath.indexOf("?");
     if (splitter < 0)
     {
       // _queryString already set in ctor
       metadata._path = fullPath;
     }
     else
     {
       metadata._path = fullPath.substring(0, splitter);
       metadata._queryString = fullPath.substring(splitter + 1);
     }
 
+    metadata._scheme = scheme;
+    metadata._host = host;
+    metadata._port = port;
+
     // our work here is finished
     this._state = READER_IN_HEADERS;
   },
 
   /**
    * Parses all available HTTP headers in this until the header-ending CRLFCRLF,
    * adding them to the store of headers in the request.
    *
@@ -2394,17 +2754,20 @@ ServerHandler.prototype =
       response.bodyOutputStream.write(body, body.length);
     },
 
     "/trace": function(metadata, response)
     {
       response.setStatusLine(metadata.httpVersion, 200, "OK");
       response.setHeader("Content-Type", "text/plain", false);
 
-      var body = "Request (semantically equivalent, slightly reformatted):\n\n";
+      var body = "Request-URI: " +
+                 metadata.scheme + "://" + metadata.host + ":" + metadata.port +
+                 metadata.path + "\n\n";
+      body += "Request (semantically equivalent, slightly reformatted):\n\n";
       body += metadata.method + " " + metadata.path;
 
       if (metadata.queryString)
         body +=  "?" + metadata.queryString;
         
       body += " HTTP/" + metadata.httpVersion + "\r\n";
 
       var headEnum = metadata.headers;
@@ -2610,17 +2973,16 @@ Response.prototype =
   // POST-CONSTRUCTION API (not exposed externally)
 
   /**
    * The HTTP version number of this, as a string (e.g. "1.1").
    */
   get httpVersion()
   {
     this._ensureAlive();
-
     return this._httpVersion.toString();
   },
 
   /**
    * The HTTP status code of this response, as a string of three characters per
    * RFC 2616.
    */
   get httpCode()
@@ -2933,16 +3295,24 @@ nsHttpVersion.prototype =
    *
    * @param otherVersion : nsHttpVersion
    *   the version to compare against this
    */
   equals: function (otherVersion)
   {
     return this.major == otherVersion.major &&
            this.minor == otherVersion.minor;
+  },
+
+  /** True if this >= otherVersion, false otherwise. */
+  atLeast: function(otherVersion)
+  {
+    return this.major > otherVersion.major ||
+           (this.major == otherVersion.major &&
+            this.minor >= otherVersion.minor);
   }
 };
 
 nsHttpVersion.HTTP_1_0 = new nsHttpVersion("1.0");
 nsHttpVersion.HTTP_1_1 = new nsHttpVersion("1.1");
 
 
 /**
@@ -3091,22 +3461,33 @@ nsSimpleEnumerator.prototype =
 /**
  * A representation of the data in an HTTP request.
  *
  * @param port : uint
  *   the port on which the server receiving this request runs
  */
 function Request(port)
 {
+  /** Method of this request, e.g. GET or POST. */
   this._method = "";
+
+  /** Path of the requested resource; empty paths are converted to '/'. */
   this._path = "";
+
+  /** Query string, if any, associated with this request (not including '?'). */
   this._queryString = "";
-  this._host = "";
+
+  /** Scheme of requested resource, usually http, always lowercase. */
+  this._scheme = "http";
+
+  /** Hostname on which the requested resource resides. */
+  this._host = undefined;
+
+  /** Port number over which the request was received. */
   this._port = port;
-  this._host = "localhost"; // XXX or from environment or server itself?
 
   /**
    * The headers in this request.
    */
   this._headers = new nsHttpHeaders();
 
   /**
    * For the addition of ad-hoc properties and new functionality without having
@@ -3115,16 +3496,24 @@ function Request(port)
    */
   this._bag = null;
 }
 Request.prototype =
 {
   // SERVER METADATA
 
   //
+  // see nsIHttpRequestMetadata.scheme
+  //
+  get scheme()
+  {
+    return this._scheme;
+  },
+
+  //
   // see nsIHttpRequestMetadata.host
   //
   get host()
   {
     return this._host;
   },
 
   //
@@ -3361,16 +3750,17 @@ function server(port, basePath)
 
   // if you're running this, you probably want to see debugging info
   DEBUG = true;
 
   var srv = new nsHttpServer();
   if (lp)
     srv.registerDirectory("/", lp);
   srv.registerContentType("sjs", SJS_TYPE);
+  srv.identity.setPrimary("http", "localhost", port);
   srv.start(port);
 
   var thread = gThreadManager.currentThread;
   while (!srv.isStopped())
     thread.processNextEvent(true);
 
   // get rid of any pending requests
   while (thread.hasPendingEvents())
--- a/netwerk/test/httpserver/nsIHttpServer.idl
+++ b/netwerk/test/httpserver/nsIHttpServer.idl
@@ -31,33 +31,33 @@
  * use your version of this file under the terms of the MPL, indicate your
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
-#include "nsIServerSocket.idl"
 #include "nsIPropertyBag.idl"
 
 interface nsILocalFile;
 interface nsISimpleEnumerator;
 interface nsIOutputStream;
 
 interface nsIHttpServer;
 interface nsIHttpRequestHandler;
 interface nsIHttpRequestMetadata;
 interface nsIHttpResponse;
+interface nsIHttpServerIdentity;
 
 /**
  * An interface which represents an HTTP server.
  */
-[scriptable, uuid(5520f79e-ecd5-4c40-843b-97ee13a23747)]
-interface nsIHttpServer : nsIServerSocketListener
+[scriptable, uuid(9049C469-8402-4FA6-883C-826B0FE9CAF9)]
+interface nsIHttpServer : nsISupports
 {
   /**
    * Starts up this server, listening upon the given port.  This method may
    * throw if the process does not have sufficient privileges to open a socket
    * for the given port, and it also throws when called upon a server which has
    * already been started.
    *
    * @param port
@@ -178,16 +178,118 @@ interface nsIHttpServer : nsIServerSocke
    *   do not contain index pages, or null to reset to the default
    *   index handler; if while the server is running the handler
    *   throws an exception while responding to a request, an HTTP 500
    *   response will be returned.  An nsIFile corresponding to the
    *   directory is available from the metadata object passed to the
    *   handler, under the key "directory".
    */
   void setIndexHandler(in nsIHttpRequestHandler handler);
+
+  /** Represents the locations at which this server is reachable. */
+  readonly attribute nsIHttpServerIdentity identity;
+};
+
+/**
+ * Represents a set of names for a server, one of which is the primary name for
+ * the server and the rest of which are secondary.  By default every server will
+ * contain ("http", "localhost", port) and ("http", "127.0.0.1", port) as names,
+ * where port is what was provided to the corresponding server when started;
+ * however, except for their being removed when the corresponding server stops
+ * they have no special importance.
+ */
+[scriptable, uuid(a89de175-ae8e-4c46-91a5-0dba99bbd284)]
+interface nsIHttpServerIdentity : nsISupports
+{
+  /**
+   * The primary scheme at which the corresponding server is located, defaulting
+   * to 'http'.  This name will be the value of nsIHttpRequestMetadata.scheme
+   * for HTTP/1.0 requests.
+   *
+   * This value is always set when the corresponding server is running.  If the
+   * server is not running, this value is set only if it has been set to a
+   * non-default name using setPrimary.  In this case reading this value will
+   * throw NS_ERROR_NOT_INITIALIZED.
+   */
+  readonly attribute string primaryScheme;
+
+  /**
+   * The primary name by which the corresponding server is known, defaulting to
+   * 'localhost'.  This name will be the value of nsIHttpRequestMetadata.host
+   * for HTTP/1.0 requests.
+   *
+   * This value is always set when the corresponding server is running.  If the
+   * server is not running, this value is set only if it has been set to a
+   * non-default name using setPrimary.  In this case reading this value will
+   * throw NS_ERROR_NOT_INITIALIZED.
+   */
+  readonly attribute string primaryHost;
+
+  /**
+   * The primary port on which the corresponding server runs, defaulting to the
+   * associated server's port.  This name will be the value of
+   * nsIHttpRequestMetadata.port for HTTP/1.0 requests.
+   *
+   * This value is always set when the corresponding server is running.  If the
+   * server is not running, this value is set only if it has been set to a
+   * non-default name using setPrimary.  In this case reading this value will
+   * throw NS_ERROR_NOT_INITIALIZED.
+   */
+  readonly attribute long primaryPort;
+
+  /**
+   * Adds a location at which this server may be accessed.
+   *
+   * @throws NS_ERROR_ILLEGAL_VALUE
+   *   if scheme or host do not match the scheme or host productions imported
+   *   into RFC 2616 from RFC 2396, or if port is not a valid port number
+   */
+  void add(in string scheme, in string host, in long port);
+
+  /**
+   * Removes this name from the list of names by which the corresponding server
+   * is known.  If name is also the primary name for the server, the primary
+   * name reverts to 'http://127.0.0.1' with the associated server's port.
+   *
+   * @throws NS_ERROR_ILLEGAL_VALUE
+   *   if scheme or host do not match the scheme or host productions imported
+   *   into RFC 2616 from RFC 2396, or if port is not a valid port number
+   * @returns
+   *   true if the given name was a name for this server, false otherwise
+   */
+  PRBool remove(in string scheme, in string host, in long port);
+
+  /**
+   * Returns true if the given name is in this, false otherwise.
+   *
+   * @throws NS_ERROR_ILLEGAL_VALUE
+   *   if scheme or host do not match the scheme or host productions imported
+   *   into RFC 2616 from RFC 2396, or if port is not a valid port number
+   */
+  PRBool has(in string scheme, in string host, in long port);
+
+  /**
+   * Returns the scheme for the name with the given host and port, if one is
+   * present; otherwise returns the empty string.
+   *
+   * @throws NS_ERROR_ILLEGAL_VALUE
+   *   if host does not match the host production imported into RFC 2616 from
+   *   RFC 2396, or if port is not a valid port number
+   */
+  string getScheme(in string host, in long port);
+  
+  /**
+   * Designates the given name as the primary name in this and adds it to this
+   * if it is not already present.
+   *
+   * @throws NS_ERROR_ILLEGAL_VALUE
+   *   if scheme or host do not match the scheme or host productions imported
+   *   into RFC 2616 from RFC 2396, or if port is not a valid port number
+   */
+  void setPrimary(in string scheme, in string host, in long port);
 };
 
 /**
  * A representation of a handler for HTTP requests.  The handler is used by
  * calling its .handle method with data for an incoming request; it is the
  * handler's job to use that data as it sees fit to make the desired response.
  *
  * @note
@@ -215,28 +317,38 @@ interface nsIHttpRequestHandler : nsISup
    */
   void handle(in nsIHttpRequestMetadata metadata, in nsIHttpResponse response);
 };
 
 
 /**
  * A representation of the data included in an HTTP request.
  */
-[scriptable, uuid(3a899b17-b6eb-4333-8ef4-912df454a551)]
+[scriptable, uuid(45b92a9e-5e0a-42da-81a6-983e4b1bc1b0)]
 interface nsIHttpRequestMetadata : nsIPropertyBag
 {
   /**
    * The request type for this request (see RFC 2616, section 5.1.1).
    */
   readonly attribute string method;
-  
+
+  /**
+   * The scheme of the requested path, usually 'http' but might possibly be
+   * 'https' if some form of SSL tunneling is in use.  Note that this value
+   * cannot be accurately determined unless the incoming request used the
+   * absolute-path form of the request line; it defaults to 'http', so only
+   * if it is something else can you be entirely certain it's correct.
+   */
+  readonly attribute string scheme;
+
   /**
    * The host of the data being requested (e.g. "localhost" for the
    * http://localhost:8080/file resource).  Note that the relevant port on the
-   * host is specified in this.port.
+   * host is specified in this.port.  This value is in the ASCII character
+   * encoding.
    */
   readonly attribute string host;
 
   /**
    * The port on the server on which the request was received.
    */
   readonly attribute unsigned long port;
 
--- a/netwerk/test/httpserver/test/head_utils.js
+++ b/netwerk/test/httpserver/test/head_utils.js
@@ -39,20 +39,19 @@
 do_import_script("netwerk/test/httpserver/httpd.js");
 
 // if these tests fail, we'll want the debug output
 DEBUG = true;
 
 
 /**
  * Constructs a new nsHttpServer instance.  This function is intended to
- * encapsulate construction of a server so that at some point in the future
- * it is possible to run these tests (with at most slight modifications) against
- * the server when used as an XPCOM component (not as an inline script) with
- * only slight modifications.
+ * encapsulate construction of a server so that at some point in the future it
+ * is possible to run these tests (with at most slight modifications) against
+ * the server when used as an XPCOM component (not as an inline script).
  */
 function createServer()
 {
   return new nsHttpServer();
 }
 
 /**
  * Creates a new HTTP channel.
@@ -96,16 +95,75 @@ function fileContents(file)
   var fis = new FileInputStream(file, PR_RDONLY, 0444,
                                 Ci.nsIFileInputStream.CLOSE_ON_EOF);
   var sis = new ScriptableInputStream(fis);
   var contents = sis.read(file.fileSize);
   sis.close();
   return contents;
 }
 
+/**
+ * Iterates over the lines, delimited by CRLF, in data, returning each line
+ * without the trailing line separator.
+ *
+ * @param data : string
+ *   a string consisting of lines of data separated by CRLFs
+ * @returns Iterator
+ *   an Iterator which returns each line from data in turn; note that this
+ *   includes a final empty line if data ended with a CRLF
+ */
+function LineIterator(data)
+{
+  var start = 0, index = 0;
+  do
+  {
+    index = data.indexOf("\r\n");
+    if (index >= 0)
+      yield data.substring(0, index);
+    else
+      yield data;
+
+    data = data.substring(index + 2);
+  }
+  while (index >= 0);
+}
+
+/**
+ * Throws if iter does not contain exactly the CRLF-separated lines in the
+ * array expectedLines.
+ *
+ * @param iter : Iterator
+ *   an Iterator which returns lines of text
+ * @param expectedLines : [string]
+ *   an array of the expected lines of text
+ * @throws string
+ *   an error message if iter doesn't agree with expectedLines
+ */
+function expectLines(iter, expectedLines)
+{
+  var index = 0;
+  for (var line in iter)
+  {
+    if (expectedLines.length == index)
+      throw "Error: got more than " + expectedLines.length + " expected lines!";
+
+    var expected = expectedLines[index++];
+    if (expected !== line)
+      throw "Error on line " + index + "!\n" +
+            "  actual: '" + line + "',\n" +
+            "  expect: '" + expected + "'";
+  }
+
+  if (expectedLines.length !== index)
+  {
+    throw "Expected more lines!  Got " + index +
+          ", expected " + expectedLines.length;
+  }
+}
+
 
 /*******************************************************
  * SIMPLE SUPPORT FOR LOADING/TESTING A SERIES OF URLS *
  *******************************************************/
 
 /**
  * Represents a path to load from the tested HTTP server, along with actions to
  * take before, during, and after loading the associated page.
@@ -205,8 +263,173 @@ function runHttpTests(testArray, done)
           return this;
         throw Cr.NS_ERROR_NO_INTERFACE;
       }
     };
 
   performNextTest();
 }
 
+
+/****************************************
+ * RAW REQUEST FORMAT TESTING FUNCTIONS *
+ ****************************************/
+
+/**
+ * Sends a raw string of bytes to the given host and port and checks that the
+ * response is acceptable.
+ *
+ * @param host : string
+ *   the host to which a connection should be made
+ * @param port : PRUint16
+ *   the port to use for the connection
+ * @param data : string
+ *   the raw data to send, as a string of characters with codes in the range
+ *   0-255
+ * @param responseCheck : function(string) : void
+ *   a function which is provided with the data sent by the remote host which
+ *   conducts whatever tests it wants on that data; useful for tweaking the test
+ *   environment between tests
+ */
+function RawTest(host, port, data, responseCheck)
+{
+  if (0 > port || 65535 < port || port % 1 !== 0)
+    throw "bad port";
+  if (!/^[\x00-\xff]*$/.test(data))
+    throw "bad data contains non-byte-valued character";
+
+  this.host = host;
+  this.port = port;
+  this.data = data;
+  this.responseCheck = responseCheck;
+}
+
+/**
+ * Runs all the tests in testArray, an array of RawTests.
+ *
+ * @param testArray : [RawTest]
+ *   an array of RawTests to run, in order
+ * @param done
+ *   function to call when all tests have run (e.g. to shut down the server)
+ */
+function runRawTests(testArray, done)
+{
+  do_test_pending();
+
+  var sts = Cc["@mozilla.org/network/socket-transport-service;1"]
+              .getService(Ci.nsISocketTransportService);
+
+  var currentThread = Cc["@mozilla.org/thread-manager;1"]
+                        .getService()
+                        .currentThread;
+  
+  /** Kicks off running the next test in the array. */
+  function performNextTest()
+  {
+    if (++testIndex == testArray.length)
+    {
+      do_test_finished();
+      done();
+      return;
+    }
+
+
+    var rawTest = testArray[testIndex];
+
+    var transport =
+      sts.createTransport(null, 0, rawTest.host, rawTest.port, null);
+
+    var inStream = transport.openInputStream(0, 0, 0)
+                            .QueryInterface(Ci.nsIAsyncInputStream);
+    var outStream  = transport.openOutputStream(0, 0, 0)
+                              .QueryInterface(Ci.nsIAsyncOutputStream);
+
+    // reset
+    dataIndex = 0;
+    received = "";
+
+    waitForMoreInput(inStream);
+    waitToWriteOutput(outStream);
+  }
+
+  function waitForMoreInput(stream)
+  {
+    stream.asyncWait(reader, 0, 0, currentThread);
+  }
+
+  function waitToWriteOutput(stream)
+  {
+    stream.asyncWait(writer, 0, testArray[testIndex].data.length - dataIndex,
+                     currentThread);
+  }
+
+  /** Index of the test being run. */
+  var testIndex = -1;
+
+  /** Index of remaining data to be written to the socket in current test. */
+  var dataIndex = 0;
+
+  /** Data received so far from the server. */
+  var received = "";
+
+  /** Reads data from the socket. */
+  var reader =
+    {
+      onInputStreamReady: function(stream)
+      {
+        var bis = new BinaryInputStream(stream);
+
+        var av = 0;
+        try
+        {
+          av = bis.available();
+        }
+        catch (e) { /* default to 0 */ }
+
+        if (av > 0)
+        {
+          received += String.fromCharCode.apply(null, bis.readByteArray(av));
+          waitForMoreInput(stream);
+          return;
+        }
+
+        var rawTest = testArray[testIndex];
+        try
+        {
+          rawTest.responseCheck(received);
+        }
+        catch (e)
+        {
+          do_throw("error thrown by responseCheck: " + e);
+        }
+        finally
+        {
+          stream.close();
+          performNextTest();
+        }
+      }
+    };
+
+  /** Writes data to the socket. */
+  var writer = 
+    {
+      onOutputStreamReady: function(stream)
+      {
+        var data = testArray[testIndex].data.substring(dataIndex);
+
+        var written = 0;
+        try
+        {
+          written = stream.write(data, data.length);
+          dataIndex += written;
+        }
+        catch (e) { /* stream could have been closed, just ignore */ }
+
+        // Keep reading data until there's no more data to read
+        if (written != 0)
+          waitToWriteOutput(stream);
+        else
+          stream.close();
+      }
+    };
+
+  performNextTest();
+}
new file mode 100644
--- /dev/null
+++ b/netwerk/test/httpserver/test/test_host.js
@@ -0,0 +1,693 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is MozJSHTTP code.
+ *
+ * The Initial Developer of the Original Code is
+ * Jeff Walden <jwalden+code@mit.edu>.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Tests that the scheme, host, and port of the server are correctly recorded
+ * and used in HTTP requests and responses.
+ */
+
+const PORT = 4444;
+
+var srv;
+
+function run_test()
+{
+  srv = createServer();
+
+  srv.registerPathHandler("/http/1.0-request", http10Request);
+  srv.registerPathHandler("/http/1.1-good-host", http11goodHost);
+  srv.registerPathHandler("/http/1.1-good-host-wacky-port",
+                          http11goodHostWackyPort);
+  srv.registerPathHandler("/http/1.1-ip-host", http11ipHost);
+
+  const FAKE_PORT_ONE = 8888;
+  const FAKE_PORT_TWO = 8889;
+
+  srv.start(FAKE_PORT_ONE);
+
+  var id = srv.identity;
+
+  // The default location is http://localhost:PORT, where PORT is whatever you
+  // provided when you started the server.  http://127.0.0.1:PORT is also part
+  // of the default set of locations.
+  do_check_eq(id.primaryScheme, "http");
+  do_check_eq(id.primaryHost, "localhost");
+  do_check_eq(id.primaryPort, FAKE_PORT_ONE);
+  do_check_true(id.has("http", "localhost", FAKE_PORT_ONE));
+  do_check_true(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+  // This should be a nop.
+  id.add("http", "localhost", FAKE_PORT_ONE);
+  do_check_eq(id.primaryScheme, "http");
+  do_check_eq(id.primaryHost, "localhost");
+  do_check_eq(id.primaryPort, FAKE_PORT_ONE);
+  do_check_true(id.has("http", "localhost", FAKE_PORT_ONE));
+  do_check_true(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+  // Change the primary location and make sure all the getters work correctly.
+  id.setPrimary("http", "127.0.0.1", FAKE_PORT_ONE);
+  do_check_eq(id.primaryScheme, "http");
+  do_check_eq(id.primaryHost, "127.0.0.1");
+  do_check_eq(id.primaryPort, FAKE_PORT_ONE);
+  do_check_true(id.has("http", "localhost", FAKE_PORT_ONE));
+  do_check_true(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+  // Okay, now remove the primary location -- we fall back to the original
+  // location.
+  id.remove("http", "127.0.0.1", FAKE_PORT_ONE);
+  do_check_eq(id.primaryScheme, "http");
+  do_check_eq(id.primaryHost, "localhost");
+  do_check_eq(id.primaryPort, FAKE_PORT_ONE);
+  do_check_true(id.has("http", "localhost", FAKE_PORT_ONE));
+  do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+  // You can't remove every location -- try this and the original default
+  // location will be silently readded.
+  id.remove("http", "localhost", FAKE_PORT_ONE);
+  do_check_eq(id.primaryScheme, "http");
+  do_check_eq(id.primaryHost, "localhost");
+  do_check_eq(id.primaryPort, FAKE_PORT_ONE);
+  do_check_true(id.has("http", "localhost", FAKE_PORT_ONE));
+  do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+  // Okay, now that we've exercised that behavior, shut down the server and
+  // restart it on the correct port, to exercise port-changing behaviors at
+  // server start and stop.
+  srv.stop();
+
+  // Our primary location is gone because it was dependent on the port on which
+  // the server was running.
+  checkPrimariesThrow(id);
+  do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+  do_check_false(id.has("http", "localhost", FAKE_PORT_ONE));
+
+  srv.start(FAKE_PORT_TWO);
+
+  // We should have picked up http://localhost:8889 as our primary location now
+  // that we've restarted.
+  do_check_eq(id.primaryScheme, "http");
+  do_check_eq(id.primaryHost, "localhost", FAKE_PORT_TWO);
+  do_check_eq(id.primaryPort, FAKE_PORT_TWO);
+  do_check_false(id.has("http", "localhost", FAKE_PORT_ONE));
+  do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+  do_check_true(id.has("http", "localhost", FAKE_PORT_TWO));
+  do_check_true(id.has("http", "127.0.0.1", FAKE_PORT_TWO));
+
+  // Now we're going to see what happens when we shut down with a primary
+  // location that wasn't a default.  That location should persist, and the
+  // default we remove should still not be present.
+  id.setPrimary("http", "example.com", FAKE_PORT_TWO);
+  do_check_true(id.has("http", "example.com", FAKE_PORT_TWO));
+  do_check_true(id.has("http", "127.0.0.1", FAKE_PORT_TWO));
+  do_check_true(id.has("http", "localhost", FAKE_PORT_TWO));
+  do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+  do_check_false(id.has("http", "localhost", FAKE_PORT_ONE));
+
+  id.remove("http", "localhost", FAKE_PORT_TWO);
+  do_check_true(id.has("http", "example.com", FAKE_PORT_TWO));
+  do_check_false(id.has("http", "localhost", FAKE_PORT_TWO));
+  do_check_true(id.has("http", "127.0.0.1", FAKE_PORT_TWO));
+  do_check_false(id.has("http", "localhost", FAKE_PORT_ONE));
+  do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+  id.remove("http", "127.0.0.1", FAKE_PORT_TWO);
+  do_check_true(id.has("http", "example.com", FAKE_PORT_TWO));
+  do_check_false(id.has("http", "localhost", FAKE_PORT_TWO));
+  do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_TWO));
+  do_check_false(id.has("http", "localhost", FAKE_PORT_ONE));
+  do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+  srv.stop();
+
+  // Only the default added location disappears; any others stay around,
+  // possibly as the primary location.  We may have removed the default primary
+  // location, but the one we set manually should persist here.
+  do_check_eq(id.primaryScheme, "http");
+  do_check_eq(id.primaryHost, "example.com");
+  do_check_eq(id.primaryPort, FAKE_PORT_TWO);
+  do_check_true(id.has("http", "example.com", FAKE_PORT_TWO));
+  do_check_false(id.has("http", "localhost", FAKE_PORT_TWO));
+  do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_TWO));
+  do_check_false(id.has("http", "localhost", FAKE_PORT_ONE));
+  do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+
+  srv.start(PORT);
+
+  // Starting always adds HTTP entries for 127.0.0.1:port and localhost:port.
+  do_check_true(id.has("http", "example.com", FAKE_PORT_TWO));
+  do_check_false(id.has("http", "localhost", FAKE_PORT_TWO));
+  do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_TWO));
+  do_check_false(id.has("http", "localhost", FAKE_PORT_ONE));
+  do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+  do_check_true(id.has("http", "localhost", PORT));
+  do_check_true(id.has("http", "127.0.0.1", PORT));
+
+  // Remove the primary location we'd left set from last time.
+  id.remove("http", "example.com", FAKE_PORT_TWO);
+
+  // Default-port behavior testing requires the server responds to requests
+  // claiming to be on one such port.
+  id.add("http", "localhost", 80);
+
+  // Make sure we don't have anything lying around from running on either the
+  // first or the second port -- all we should have is our generated default,
+  // plus the additional port to test "portless" hostport variants.
+  do_check_true(id.has("http", "localhost", 80));
+  do_check_eq(id.primaryScheme, "http");
+  do_check_eq(id.primaryHost, "localhost");
+  do_check_eq(id.primaryPort, PORT);
+  do_check_true(id.has("http", "localhost", PORT));
+  do_check_true(id.has("http", "127.0.0.1", PORT));
+  do_check_false(id.has("http", "localhost", FAKE_PORT_ONE));
+  do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_ONE));
+  do_check_false(id.has("http", "example.com", FAKE_PORT_TWO));
+  do_check_false(id.has("http", "localhost", FAKE_PORT_TWO));
+  do_check_false(id.has("http", "127.0.0.1", FAKE_PORT_TWO));
+
+  // Okay, finally done with identity testing.  Our primary location is the one
+  // we want it to be, so we're off!
+  runRawTests(tests, function() { srv.stop(); });
+}
+
+
+/*********************
+ * UTILITY FUNCTIONS *
+ *********************/
+
+/**
+ * Verifies that all .primary* getters on a server identity correctly throw
+ * NS_ERROR_NOT_INITIALIZED.
+ *
+ * @param id : nsIHttpServerIdentity
+ *   the server identity to test
+ */
+function checkPrimariesThrow(id)
+{
+  var threw = false;
+  try
+  {
+    id.primaryScheme;
+  }
+  catch (e)
+  {
+    threw = e === Cr.NS_ERROR_NOT_INITIALIZED;
+  }
+  do_check_true(threw);
+
+  threw = false;
+  try
+  {
+    id.primaryHost;
+  }
+  catch (e)
+  {
+    threw = e === Cr.NS_ERROR_NOT_INITIALIZED;
+  }
+  do_check_true(threw);
+
+  threw = false;
+  try
+  {
+    id.primaryPort;
+  }
+  catch (e)
+  {
+    threw = e === Cr.NS_ERROR_NOT_INITIALIZED;
+  }
+  do_check_true(threw);
+}
+
+/**
+ * Spew a bunch of HTTP metadata from request into the body of response.
+ *
+ * @param request : nsIHttpRequestMetadata
+ *   the request whose metadata should be output
+ * @param response : nsIHttpResponse
+ *   the response to which the metadata is written
+ */
+function writeDetails(request, response)
+{
+  response.write("Method:  " + request.method + "\r\n");
+  response.write("Path:    " + request.path + "\r\n");
+  response.write("Query:   " + request.queryString + "\r\n");
+  response.write("Version: " + request.httpVersion + "\r\n");
+  response.write("Scheme:  " + request.scheme + "\r\n");
+  response.write("Host:    " + request.host + "\r\n");
+  response.write("Port:    " + request.port);
+}
+
+/**
+ * Advances iter past all non-blank lines and a single blank line, after which
+ * point the body of the response will be returned next from the iterator.
+ *
+ * @param iter : Iterator
+ *   an iterator over the CRLF-delimited lines in an HTTP response, currently
+ *   just after the Request-Line
+ */
+function skipHeaders(iter)
+{
+  var line = iter.next();
+  while (line !== "")
+    line = iter.next();
+}
+
+/**
+ * Utility function to check for a 400 response.
+ */
+function check400(data)
+{
+  var iter = LineIterator(data);
+
+  // Status-Line
+  var firstLine = iter.next();
+  do_check_eq(firstLine.substring(0, HTTP_400_LEADER_LENGTH), HTTP_400_LEADER);
+}
+
+
+/***************
+ * BEGIN TESTS *
+ ***************/
+
+const HTTP_400_LEADER = "HTTP/1.1 400 ";
+const HTTP_400_LEADER_LENGTH = HTTP_400_LEADER.length;
+
+var test, data;
+var tests = [];
+
+// HTTP/1.0 request, to ensure we see our default scheme/host/port
+
+function http10Request(request, response)
+{
+  writeDetails(request, response);
+  response.setStatusLine("1.0", 200, "TEST PASSED");
+}
+data = "GET /http/1.0-request HTTP/1.0\r\n" +
+       "\r\n";
+function check10(data)
+{
+  var iter = LineIterator(data);
+
+  // Status-Line
+  do_check_eq(iter.next(), "HTTP/1.0 200 TEST PASSED");
+
+  skipHeaders(iter);
+
+  // Okay, next line must be the data we expected to be written
+  var body =
+    [
+     "Method:  GET",
+     "Path:    /http/1.0-request",
+     "Query:   ",
+     "Version: 1.0",
+     "Scheme:  http",
+     "Host:    localhost",
+     "Port:    4444",
+    ];
+
+  expectLines(iter, body);
+}
+test = new RawTest("localhost", PORT, data, check10),
+tests.push(test);
+
+
+// HTTP/1.1 request, no Host header, expect a 400 response
+
+data = "GET /http/1.1-request HTTP/1.1\r\n" +
+       "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
+
+
+// HTTP/1.1 request, wrong host, expect a 400 response
+
+data = "GET /http/1.1-request HTTP/1.1\r\n" +
+       "Host: not-localhost\r\n" +
+       "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
+
+
+// HTTP/1.1 request, wrong host/right port, expect a 400 response
+
+data = "GET /http/1.1-request HTTP/1.1\r\n" +
+       "Host: not-localhost:4444\r\n" +
+       "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
+
+
+// HTTP/1.1 request, Host header has host but no port, expect a 400 response
+
+data = "GET /http/1.1-request HTTP/1.1\r\n" +
+       "Host: 127.0.0.1\r\n" +
+       "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
+
+
+// HTTP/1.1 request, Request-URI has wrong port, expect a 400 response
+
+data = "GET http://127.0.0.1/http/1.1-request HTTP/1.1\r\n" +
+       "Host: 127.0.0.1\r\n" +
+       "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
+
+
+// HTTP/1.1 request, Request-URI has wrong port, expect a 400 response
+
+data = "GET http://localhost:31337/http/1.1-request HTTP/1.1\r\n" +
+       "Host: localhost:31337\r\n" +
+       "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
+
+
+// HTTP/1.1 request, Request-URI has wrong scheme, expect a 400 response
+
+data = "GET https://localhost:4444/http/1.1-request HTTP/1.1\r\n" +
+       "Host: localhost:4444\r\n" +
+       "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
+
+
+// HTTP/1.1 request, correct Host header, expect handler's response
+
+function http11goodHost(request, response)
+{
+  writeDetails(request, response);
+  response.setStatusLine("1.1", 200, "TEST PASSED");
+}
+data = "GET /http/1.1-good-host HTTP/1.1\r\n" +
+       "Host: localhost:4444\r\n" +
+       "\r\n";
+function check11goodHost(data)
+{
+  var iter = LineIterator(data);
+
+  // Status-Line
+  do_check_eq(iter.next(), "HTTP/1.1 200 TEST PASSED");
+
+  skipHeaders(iter);
+
+  // Okay, next line must be the data we expected to be written
+  var body =
+    [
+     "Method:  GET",
+     "Path:    /http/1.1-good-host",
+     "Query:   ",
+     "Version: 1.1",
+     "Scheme:  http",
+     "Host:    localhost",
+     "Port:    4444",
+    ];
+
+  expectLines(iter, body);
+}
+test = new RawTest("localhost", PORT, data, check11goodHost),
+tests.push(test);
+
+
+// HTTP/1.1 request, Host header is secondary identity
+
+function http11ipHost(request, response)
+{
+  writeDetails(request, response);
+  response.setStatusLine("1.1", 200, "TEST PASSED");
+}
+data = "GET /http/1.1-ip-host HTTP/1.1\r\n" +
+       "Host: 127.0.0.1:4444\r\n" +
+       "\r\n";
+function check11ipHost(data)
+{
+  var iter = LineIterator(data);
+
+  // Status-Line
+  do_check_eq(iter.next(), "HTTP/1.1 200 TEST PASSED");
+
+  skipHeaders(iter);
+
+  // Okay, next line must be the data we expected to be written
+  var body =
+    [
+     "Method:  GET",
+     "Path:    /http/1.1-ip-host",
+     "Query:   ",
+     "Version: 1.1",
+     "Scheme:  http",
+     "Host:    127.0.0.1",
+     "Port:    4444",
+    ];
+
+  expectLines(iter, body);
+}
+test = new RawTest("localhost", PORT, data, check11ipHost),
+tests.push(test);
+
+
+// HTTP/1.1 request, absolute path, accurate Host header
+
+// reusing previous request handler so not defining a new one
+
+data = "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" +
+       "Host: localhost:4444\r\n" +
+       "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHost),
+tests.push(test);
+
+
+// HTTP/1.1 request, absolute path, inaccurate Host header
+
+// reusing previous request handler so not defining a new one
+
+data = "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" +
+       "Host: localhost:1234\r\n" +
+       "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHost),
+tests.push(test);
+
+
+// HTTP/1.1 request, absolute path, different inaccurate Host header
+
+// reusing previous request handler so not defining a new one
+
+data = "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" +
+       "Host: not-localhost:4444\r\n" +
+       "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHost),
+tests.push(test);
+
+
+// HTTP/1.1 request, absolute path, yet another inaccurate Host header
+
+// reusing previous request handler so not defining a new one
+
+data = "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" +
+       "Host: yippity-skippity\r\n" +
+       "\r\n";
+function checkInaccurate(data)
+{
+  check11goodHost(data);
+
+  // dynamism setup
+  srv.identity.setPrimary("http", "127.0.0.1", 4444);
+}
+test = new RawTest("localhost", PORT, data, checkInaccurate),
+tests.push(test);
+
+
+// HTTP/1.0 request, absolute path, different inaccurate Host header
+
+// reusing previous request handler so not defining a new one
+
+data = "GET /http/1.0-request HTTP/1.0\r\n" +
+       "Host: not-localhost:4444\r\n" +
+       "\r\n";
+function check10ip(data)
+{
+  var iter = LineIterator(data);
+
+  // Status-Line
+  do_check_eq(iter.next(), "HTTP/1.0 200 TEST PASSED");
+
+  skipHeaders(iter);
+
+  // Okay, next line must be the data we expected to be written
+  var body =
+    [
+     "Method:  GET",
+     "Path:    /http/1.0-request",
+     "Query:   ",
+     "Version: 1.0",
+     "Scheme:  http",
+     "Host:    127.0.0.1",
+     "Port:    4444",
+    ];
+
+  expectLines(iter, body);
+}
+test = new RawTest("localhost", PORT, data, check10ip),
+tests.push(test);
+
+
+// HTTP/1.1 request, Host header with implied port
+
+function http11goodHostWackyPort(request, response)
+{
+  writeDetails(request, response);
+  response.setStatusLine("1.1", 200, "TEST PASSED");
+}
+data = "GET /http/1.1-good-host-wacky-port HTTP/1.1\r\n" +
+       "Host: localhost\r\n" +
+       "\r\n";
+function check11goodHostWackyPort(data)
+{
+  var iter = LineIterator(data);
+
+  // Status-Line
+  do_check_eq(iter.next(), "HTTP/1.1 200 TEST PASSED");
+
+  skipHeaders(iter);
+
+  // Okay, next line must be the data we expected to be written
+  var body =
+    [
+     "Method:  GET",
+     "Path:    /http/1.1-good-host-wacky-port",
+     "Query:   ",
+     "Version: 1.1",
+     "Scheme:  http",
+     "Host:    localhost",
+     "Port:    80",
+    ];
+
+  expectLines(iter, body);
+}
+test = new RawTest("localhost", PORT, data, check11goodHostWackyPort),
+tests.push(test);
+
+
+// HTTP/1.1 request, Host header with wacky implied port
+
+data = "GET /http/1.1-good-host-wacky-port HTTP/1.1\r\n" +
+       "Host: localhost:\r\n" +
+       "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHostWackyPort),
+tests.push(test);
+
+
+// HTTP/1.1 request, absolute URI with implied port
+
+data = "GET http://localhost/http/1.1-good-host-wacky-port HTTP/1.1\r\n" +
+       "Host: localhost\r\n" +
+       "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHostWackyPort),
+tests.push(test);
+
+
+// HTTP/1.1 request, absolute URI with wacky implied port
+
+data = "GET http://localhost:/http/1.1-good-host-wacky-port HTTP/1.1\r\n" +
+       "Host: localhost\r\n" +
+       "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHostWackyPort),
+tests.push(test);
+
+
+// HTTP/1.1 request, absolute URI with explicit implied port, ignored Host
+
+data = "GET http://localhost:80/http/1.1-good-host-wacky-port HTTP/1.1\r\n" +
+       "Host: who-cares\r\n" +
+       "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHostWackyPort),
+tests.push(test);
+
+
+// HTTP/1.1 request, a malformed Request-URI
+
+data = "GET is-this-the-real-life-is-this-just-fantasy HTTP/1.1\r\n" +
+       "Host: localhost:4444\r\n" +
+       "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
+
+
+// HTTP/1.1 request, a malformed Host header
+
+data = "GET /http/1.1-request HTTP/1.1\r\n" +
+       "Host: la la la\r\n" +
+       "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
+
+
+// HTTP/1.1 request, a malformed Host header but absolute URI, 5.2 sez fine
+
+data = "GET http://localhost:4444/http/1.1-good-host HTTP/1.1\r\n" +
+       "Host: la la la\r\n" +
+       "\r\n";
+test = new RawTest("localhost", PORT, data, check11goodHost),
+tests.push(test);
+
+
+// HTTP/1.0 request, absolute URI, but those aren't valid in HTTP/1.0
+
+data = "GET http://localhost:4444/http/1.1-request HTTP/1.0\r\n" +
+       "Host: localhost:4444\r\n" +
+       "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
+
+
+// HTTP/1.1 request, absolute URI with unrecognized host
+
+data = "GET http://not-localhost:4444/http/1.1-request HTTP/1.1\r\n" +
+       "Host: not-localhost:4444\r\n" +
+       "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
+
+
+// HTTP/1.1 request, absolute URI with unrecognized host (but not in Host)
+
+data = "GET http://not-localhost:4444/http/1.1-request HTTP/1.1\r\n" +
+       "Host: localhost:4444\r\n" +
+       "\r\n";
+test = new RawTest("localhost", PORT, data, check400),
+tests.push(test);
--- a/testing/mochitest/Makefile.in
+++ b/testing/mochitest/Makefile.in
@@ -63,16 +63,17 @@ include $(topsrcdir)/config/rules.mk
 		harness-overlay.xul \
 		harness.xul \
 		browser-test-overlay.xul \
 		browser-test.js \
 		browser-harness.xul \
 		redirect-a11y.html \
 		redirect.html \
 		redirect.js \
+		$(topsrcdir)/build/pgo/server-locations.txt \
 		$(topsrcdir)/netwerk/test/httpserver/httpd.js \
 		$(NULL)	
 
 
 _DEST_DIR = $(DEPTH)/_tests/$(relativesrcdir)
 
 ifeq ($(USE_SHORT_LIBNAME), 1)
 PROGRAM = $(MOZ_APP_NAME)$(BIN_SUFFIX)
--- a/testing/mochitest/server.js
+++ b/testing/mochitest/server.js
@@ -129,24 +129,18 @@ function runServer()
   serverBasePath = Cc["@mozilla.org/file/local;1"]
                      .createInstance(Ci.nsILocalFile);
   var procDir = Cc["@mozilla.org/file/directory_service;1"]
                   .getService(Ci.nsIProperties).get("CurProcD", Ci.nsIFile);
   serverBasePath.initWithPath(procDir.parent.parent.path);
   serverBasePath.append("_tests");
   serverBasePath.append("testing");
   serverBasePath.append("mochitest");
-  server = new nsHttpServer();
-  server.registerDirectory("/", serverBasePath);
 
-  server.registerPathHandler("/server/shutdown", serverShutdown);
-
-  server.registerContentType("sjs", "sjs"); // .sjs == CGI-like functionality
-
-  server.setIndexHandler(defaultDirHandler);
+  server = createMochitestServer(serverBasePath);
   server.start(SERVER_PORT);
 
   // touch a file in the profile directory to indicate we're alive
   var foStream = Cc["@mozilla.org/network/file-output-stream;1"]
                    .createInstance(Ci.nsIFileOutputStream);
   var serverAlive = Cc["@mozilla.org/file/local;1"]
                       .createInstance(Ci.nsILocalFile);
   serverAlive.initWithFile(serverBasePath);
@@ -178,16 +172,103 @@ function runServer()
   // Server stopped by /server/shutdown handler -- go through pending events
   // and return.
 
   // get rid of any pending requests
   while (thread.hasPendingEvents())
     thread.processNextEvent(true);
 }
 
+/** Creates and returns an HTTP server configured to serve Mochitests. */
+function createMochitestServer(serverBasePath)
+{
+  var server = new nsHttpServer();
+
+  server.registerDirectory("/", serverBasePath);
+  server.registerPathHandler("/server/shutdown", serverShutdown);
+  server.registerContentType("sjs", "sjs"); // .sjs == CGI-like functionality
+  server.setIndexHandler(defaultDirHandler);
+
+  processLocations(server);
+
+  return server;
+}
+
+/**
+ * Notifies the HTTP server about all the locations at which it might receive
+ * requests, so that it can properly respond to requests on any of the hosts it
+ * serves.
+ */
+function processLocations(server)
+{
+  var serverLocations = serverBasePath.clone();
+  serverLocations.append("server-locations.txt");
+
+  const PR_RDONLY = 0x01;
+  var fis = new FileInputStream(serverLocations, PR_RDONLY, 0444,
+                                Ci.nsIFileInputStream.CLOSE_ON_EOF);
+
+  var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0);
+  lis.QueryInterface(Ci.nsIUnicharLineInputStream);
+
+  const LINE_REGEXP =
+    new RegExp("^([a-z][-a-z0-9+.]*)" +
+               "://" +
+               "(" +
+                 "\\d+\\.\\d+\\.\\d+\\.\\d+" +
+                 "|" +
+                 "(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\\.)*" +
+                 "[a-z](?:[-a-z0-9]*[a-z0-9])?" +
+               ")" +
+               ":" +
+               "(\\d+)" +
+               "(?:" +
+               "\\s+" +
+               "(\\w+(?:,\\w+)*)" +
+               ")?$");
+
+  var line = {};
+  var lineno = 0;
+  var seenPrimary = false;
+  do
+  {
+    var more = lis.readLine(line);
+    lineno++;
+
+    var lineValue = line.value;
+    if (lineValue.charAt(0) == "#" || lineValue == "")
+      continue;
+
+    var match = LINE_REGEXP.exec(lineValue);
+    if (!match)
+      throw "Syntax error in server-locations.txt, line " + lineno;
+
+    var [, scheme, host, port, options] = match;
+    if (options)
+    {
+      if (options.split(",").indexOf("primary") >= 0)
+      {
+        if (seenPrimary)
+        {
+          throw "Multiple primary locations in server-locations.txt, " +
+                "line " + lineno;
+        }
+  
+        server.identity.setPrimary(scheme, host, port);
+        seenPrimary = true;
+        continue;
+      }
+    }
+
+    server.identity.add(scheme, host, port);
+  }
+  while (more);
+}
+
+
 // PATH HANDLERS
 
 // /server/shutdown
 function serverShutdown(metadata, response)
 {
   response.setStatusLine("1.1", 200, "OK");
   response.setHeader("Content-type", "text/plain", false);