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

@@ -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"))
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!");
},
/**
@@ -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,24 +1345,45 @@ 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;
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("?");
@@ -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;