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

This commit is contained in:
Jeff Walden
2008-06-07 02:43:15 -04:00
parent b36c7fa43d
commit c1031870fd
10 changed files with 1779 additions and 121 deletions

View File

@@ -53,6 +53,7 @@ _PGO_FILES = \
profileserver.py \
index.html \
quit.js \
server-locations.txt \
$(NULL)

View File

@@ -37,11 +37,13 @@
#
# ***** 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
@@ -62,56 +64,6 @@ __all__ = [
"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
@@ -223,6 +175,88 @@ class Process:
# 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."
@@ -250,42 +284,49 @@ 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 = """

View File

@@ -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

View File

@@ -46,6 +46,11 @@ code which does this:
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
---------------------------------------------------
@@ -69,24 +74,19 @@ 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

View File

@@ -207,9 +207,6 @@ const FileInputStream = CC("@mozilla.org/network/file-input-stream;1",
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");
@@ -338,6 +335,9 @@ function nsHttpServer()
/** 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.
*/
@@ -423,6 +423,7 @@ nsHttpServer.prototype =
dumpn(">>> listening on port " + socket.port);
socket.asyncListen(this);
this._identity._initialize(port, true);
this._socket = socket;
},
@@ -437,6 +438,11 @@ nsHttpServer.prototype =
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
@@ -506,6 +512,14 @@ nsHttpServer.prototype =
this._handler.registerContentType(ext, type);
},
//
// see nsIHttpServer.serverIdentity
//
get identity()
{
return this._identity;
},
// NSISUPPORTS
//
@@ -575,6 +589,263 @@ nsHttpServer.prototype =
};
//
// 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).
@@ -865,8 +1136,6 @@ RequestReader.prototype =
// 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));
@@ -902,11 +1171,78 @@ RequestReader.prototype =
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"))
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!");
},
/**
@@ -998,8 +1334,7 @@ RequestReader.prototype =
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)
@@ -1010,25 +1345,46 @@ RequestReader.prototype =
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;
}
catch (e) { /* invalid URI */ }
if (fullPath.charAt(0) != "/")
scheme = uri.scheme;
host = metadata._host = uri.asciiHost;
port = uri.port;
if (port === -1)
{
this.errorCode = 400;
return;
if (scheme === "http")
port = 80;
else if (scheme === "https")
port = 443;
else
throw HTTP_400;
}
}
catch (e)
{
// 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)
@@ -1042,6 +1398,10 @@ RequestReader.prototype =
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;
},
@@ -2399,7 +2759,10 @@ ServerHandler.prototype =
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)
@@ -2615,7 +2978,6 @@ Response.prototype =
get httpVersion()
{
this._ensureAlive();
return this._httpVersion.toString();
},
@@ -2938,6 +3300,14 @@ nsHttpVersion.prototype =
{
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);
}
};
@@ -3096,12 +3466,23 @@ nsSimpleEnumerator.prototype =
*/
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.
@@ -3119,6 +3500,14 @@ Request.prototype =
{
// SERVER METADATA
//
// see nsIHttpRequestMetadata.scheme
//
get scheme()
{
return this._scheme;
},
//
// see nsIHttpRequestMetadata.host
//
@@ -3366,6 +3755,7 @@ function server(port, basePath)
if (lp)
srv.registerDirectory("/", lp);
srv.registerContentType("sjs", SJS_TYPE);
srv.identity.setPrimary("http", "localhost", port);
srv.start(port);
var thread = gThreadManager.currentThread;

View File

@@ -36,7 +36,6 @@
*
* ***** END LICENSE BLOCK ***** */
#include "nsIServerSocket.idl"
#include "nsIPropertyBag.idl"
interface nsILocalFile;
@@ -47,12 +46,13 @@ 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
@@ -183,6 +183,108 @@ interface nsIHttpServer : nsIServerSocketListener
* 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);
};
/**
@@ -220,7 +322,7 @@ interface nsIHttpRequestHandler : nsISupports
/**
* 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
{
/**
@@ -228,10 +330,20 @@ interface nsIHttpRequestMetadata : nsIPropertyBag
*/
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;

View File

@@ -44,10 +44,9 @@ 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()
{
@@ -101,6 +100,65 @@ function fileContents(file)
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 *
@@ -210,3 +268,168 @@ function runHttpTests(testArray, done)
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();
}

View File

@@ -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);

View File

@@ -68,6 +68,7 @@ _SERV_FILES = \
redirect-a11y.html \
redirect.html \
redirect.js \
$(topsrcdir)/build/pgo/server-locations.txt \
$(topsrcdir)/netwerk/test/httpserver/httpd.js \
$(NULL)

View File

@@ -134,14 +134,8 @@ function runServer()
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
@@ -183,6 +177,93 @@ function runServer()
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