Bug 401649 - JS CGI support in httpd.js -- create responses conditioned on header values and HTTP method types! r=sayrer

This commit is contained in:
jwalden@mit.edu 2008-02-05 17:12:32 -08:00
parent e8ade30c42
commit 38eebf9225
9 changed files with 435 additions and 64 deletions

View File

@ -1,21 +1,21 @@
MozJSHTTP README
================
httpd.js README
===============
MozJSHTTP is a small cross-platform implementation of an HTTP/1.1 server in
httpd.js is a small cross-platform implementation of an HTTP/1.1 server in
JavaScript for the Mozilla platform.
MozJSHTTP may be used as an XPCOM component, as an inline script in a document
httpd.js may be used as an XPCOM component, as an inline script in a document
with XPCOM privileges, or from the XPCOM shell (xpcshell). Currently, its most-
supported method of use is from the XPCOM shell, where you can get all the
dynamicity of JS in adding request handlers and the like, but component-based
equivalent functionality is planned.
Using MozJSHTTP as an XPCOM Component
-------------------------------------
Using httpd.js as an XPCOM Component
------------------------------------
First, create an XPT file for nsIHttpServer.idl, using the xpidl tool included
in the Mozilla SDK for the environment in which you wish to run MozJSHTTP. See
in the Mozilla SDK for the environment in which you wish to run httpd.js. See
<http://developer.mozilla.org/en/docs/XPIDL:xpidl> for further details on how to
do this.
@ -47,10 +47,10 @@ code which does this:
server.stop();
Using MozJSHTTP as an Inline Script or from xpcshell
----------------------------------------------------
Using httpd.js as an Inline Script or from xpcshell
---------------------------------------------------
Using MozJSHTTP as a script or from xpcshell isn't very different from using it
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:
@ -58,18 +58,18 @@ the server. To create an instance, do the following:
You now can use |server| exactly as you would when |server| was created as an
XPCOM component. Note, however, that doing so will trample over the global
namespace, and global values defined in MozJSHTTP will leak into your script.
namespace, and global values defined in httpd.js will leak into your script.
This may typically be benign, but since some of the global values defined are
constants (specifically, Cc/Ci/Cr as abbreviations for the classes, interfaces,
and results properties of Components), it's possible this trampling could
break your script. In general you should use MozJSHTTP as an XPCOM component
break your script. In general you should use httpd.js as an XPCOM component
whenever possible.
Known Issues
------------
While MozJSHTTP runs on Mozilla 1.8 and 1.9 platforms, it doesn't run quite as
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
@ -78,7 +78,7 @@ 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.
MozJSHTTP makes no effort to time out requests, beyond any the socket itself
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.
@ -96,6 +96,6 @@ 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
server is shut down. It's also a good example of how to use the basic
functionality of MozJSHTTP, if you need one.
functionality of httpd.js, if you need one.
Have fun!

View File

@ -49,11 +49,14 @@
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
const CC = Components.Constructor;
/** True if debugging output is enabled, false otherwise. */
var DEBUG = false; // non-const *only* so tweakable in server tests
var gGlobalObject = this;
/**
* Asserts that the given condition holds. If it doesn't, the given message is
* dumped, a stack trace is printed, and an exception is thrown to attempt to
@ -157,6 +160,9 @@ const HIDDEN_CHAR = "^";
*/
const HEADERS_SUFFIX = HIDDEN_CHAR + "headers" + HIDDEN_CHAR;
/** Type used to denote SJS scripts for CGI-like functionality. */
const SJS_TYPE = "sjs";
/** dump(str) with a trailing "\n" -- only outputs if DEBUG */
function dumpn(str)
@ -189,6 +195,9 @@ const ServerSocket = CC("@mozilla.org/network/server-socket;1",
const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
"nsIBinaryInputStream",
"setInputStream");
const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1",
"nsIScriptableInputStream",
"init");
const Pipe = CC("@mozilla.org/pipe;1",
"nsIPipe",
"init");
@ -441,7 +450,7 @@ nsHttpServer.prototype =
//
registerFile: function(path, file)
{
if (!file.exists() || file.isDirectory())
if (file && (!file.exists() || file.isDirectory()))
throw Cr.NS_ERROR_INVALID_ARG;
this._handler.registerFile(path, file);
@ -489,6 +498,14 @@ nsHttpServer.prototype =
this._handler.setIndexHandler(handler);
},
//
// see nsIHttpServer.registerContentType
//
registerContentType: function(ext, type)
{
this._handler.registerContentType(ext, type);
},
// NSISUPPORTS
//
@ -1237,29 +1254,6 @@ LineData.prototype =
/**
* Gets a content-type for the given file, as best as it is possible to do so.
*
* @param file : nsIFile
* the nsIFile for which to get a file type
* @returns string
* the best content-type which can be determined for the file
*/
function getTypeFromFile(file)
{
try
{
return Cc["@mozilla.org/uriloader/external-helper-app-service;1"]
.getService(Ci.nsIMIMEService)
.getTypeFromFile(file);
}
catch (e)
{
return "application/octet-stream";
}
}
/**
* Creates a request-handling function for an nsIHttpRequestHandler object.
*/
@ -1525,6 +1519,12 @@ function ServerHandler(server)
*/
this._overrideErrors = {};
/**
* Maps file extensions to their MIME types in the server, overriding any
* mapping that might or might not exist in the MIME service.
*/
this._mimeMappings = {};
/**
* The default handler for requests for directories, used to serve directories
* when no index file is present.
@ -1620,6 +1620,13 @@ ServerHandler.prototype =
//
registerFile: function(path, file)
{
if (!file)
{
dumpn("*** unregistering '" + path + "' mapping");
delete this._overridePaths[path];
return;
}
dumpn("*** registering '" + path + "' as mapping to " + file.path);
file = file.clone();
@ -1631,8 +1638,7 @@ ServerHandler.prototype =
throw HTTP_404;
response.setStatusLine(metadata.httpVersion, 200, "OK");
self._writeFileResponse(file, response);
maybeAddHeaders(file, metadata, response);
self._writeFileResponse(metadata, file, response);
};
},
@ -1703,6 +1709,17 @@ ServerHandler.prototype =
this._indexHandler = handler;
},
//
// see nsIHttpServer.registerContentType
//
registerContentType: function(ext, type)
{
if (!type)
delete this._mimeMappings[ext];
else
this._mimeMappings[ext] = headerUtils.normalizeFieldValue(type);
},
// NON-XPCOM PUBLIC API
/**
@ -1784,37 +1801,94 @@ ServerHandler.prototype =
// finally...
dumpn("*** handling '" + path + "' as mapping to " + file.path);
this._writeFileResponse(file, response);
maybeAddHeaders(file, metadata, response);
this._writeFileResponse(metadata, file, response);
},
/**
* Writes an HTTP response for the given file, including setting headers for
* file metadata.
*
* @param metadata : Request
* the Request for which a response is being generated
* @param file : nsILocalFile
* the file which is to be sent in the response
* @param response : Response
* the response to which the file should be written
*/
_writeFileResponse: function(file, response)
_writeFileResponse: function(metadata, file, response)
{
const PR_RDONLY = 0x01;
var type = this._getTypeFromFile(file);
if (type == SJS_TYPE)
{
try
{
var fis = new FileInputStream(file, PR_RDONLY, 0444,
Ci.nsIFileInputStream.CLOSE_ON_EOF);
var sis = new ScriptableInputStream(fis);
var s = Cu.Sandbox(gGlobalObject);
Cu.evalInSandbox(sis.read(file.fileSize), s);
s.handleRequest(metadata, response);
}
catch (e)
{
dumpn("*** error running SJS: " + e);
throw HTTP_500;
}
}
else
{
try
{
response.setHeader("Last-Modified",
toDateString(file.lastModifiedTime),
false);
}
catch (e) { /* lastModifiedTime threw, ignore */ }
response.setHeader("Content-Type", type, false);
var fis = new FileInputStream(file, PR_RDONLY, 0444,
Ci.nsIFileInputStream.CLOSE_ON_EOF);
response.bodyOutputStream.writeFrom(fis, file.fileSize);
fis.close();
maybeAddHeaders(file, metadata, response);
}
},
/**
* Gets a content-type for the given file, first by checking for any custom
* MIME-types registered with this handler for the file's extension, second by
* asking the global MIME service for a content-type, and finally by failing
* over to application/octet-stream.
*
* @param file : nsIFile
* the nsIFile for which to get a file type
* @returns string
* the best content-type which can be determined for the file
*/
_getTypeFromFile: function(file)
{
try
{
response.setHeader("Last-Modified",
toDateString(file.lastModifiedTime),
false);
var name = file.leafName;
var dot = name.lastIndexOf(".");
if (dot > 0)
{
var ext = name.slice(dot + 1);
if (ext in this._mimeMappings)
return this._mimeMappings[ext];
}
return Cc["@mozilla.org/uriloader/external-helper-app-service;1"]
.getService(Ci.nsIMIMEService)
.getTypeFromFile(file);
}
catch (e)
{
return "application/octet-stream";
}
catch (e) { /* lastModifiedTime threw, ignore */ }
response.setHeader("Content-Type", getTypeFromFile(file), false);
const PR_RDONLY = 0x01;
var fis = new FileInputStream(file, PR_RDONLY, 0444,
Ci.nsIFileInputStream.CLOSE_ON_EOF);
response.bodyOutputStream.writeFrom(fis, file.fileSize);
fis.close();
},
/**
@ -2037,7 +2111,7 @@ ServerHandler.prototype =
{
// post-processing
response.setHeader("Connection", "close", false);
response.setHeader("Server", "MozJSHTTP", false);
response.setHeader("Server", "httpd.js", false);
response.setHeader("Date", toDateString(Date.now()), false);
var bodyStream = response.bodyInputStream;
@ -3291,6 +3365,7 @@ function server(port, basePath)
var srv = new nsHttpServer();
if (lp)
srv.registerDirectory("/", lp);
srv.registerContentType("sjs", SJS_TYPE);
srv.start(port);
var thread = gThreadManager.currentThread;

View File

@ -87,8 +87,8 @@ interface nsIHttpServer : nsIServerSocketListener
* the path which is to be mapped to the given file; must begin with "/" and
* be a valid URI path (i.e., no query string, hash reference, etc.)
* @param file
* the file to serve for the given path; this file must exist for the
* lifetime of the server
* the file to serve for the given path, or null to remove any mapping that
* might exist; this file must exist for the lifetime of the server
*/
void registerFile(in string path, in nsILocalFile file);
@ -148,6 +148,27 @@ interface nsIHttpServer : nsIServerSocketListener
*/
void registerDirectory(in string path, in nsILocalFile dir);
/**
* Associates files with the given extension with the given Content-Type when
* served by this server, in the absence of any file-specific information
* about the desired Content-Type. If type is empty, removes any extant
* mapping, if one is present.
*
* @throws NS_ERROR_INVALID_ARG
* if the given type is not a valid header field value, i.e. if it doesn't
* match the field-value production in RFC 2616
* @note
* No syntax checking is done of the given type, beyond ensuring that it is
* a valid header field value. Behavior when not given a string matching
* the media-type production in RFC 2616 section 3.7 is undefined.
* Implementations may choose to define specific behavior for types which do
* not match the production, such as for CGI functionality.
* @note
* Implementations MAY treat type as a trusted argument; users who fail to
* generate this string from trusted data risk security vulnerabilities.
*/
void registerContentType(in string extension, in string type);
/**
* Sets the handler used to display the contents of a directory if
* the directory contains no index page.

View File

@ -0,0 +1,8 @@
function handleRequest(request, response)
{
if (request.queryString == "throw")
throw "monkey wrench!";
response.setHeader("Content-Type", "text/plain", false);
response.write("PASS");
}

View File

@ -0,0 +1,2 @@
HTTP 500 Error
This-Header: SHOULD NOT APPEAR IN CGI.JSC RESPONSES!

View File

@ -0,0 +1,4 @@
function handleRequest(request, response)
{
response.write("FAIL");
}

View File

@ -82,6 +82,26 @@ function makeBIS(stream)
}
/**
* Returns the contents of the file as a string.
*
* @param file : nsILocalFile
* the file whose contents are to be read
* @returns string
* the contents of the file
*/
function fileContents(file)
{
const PR_RDONLY = 0x01;
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;
}
/*******************************************************
* SIMPLE SUPPORT FOR LOADING/TESTING A SERIES OF URLS *
*******************************************************/

View File

@ -40,10 +40,6 @@
const PREFIX = "http://localhost:4444";
const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1",
"nsIScriptableInputStream",
"init");
var tests =
[
new Test(PREFIX + "/test_both.html",

View File

@ -0,0 +1,245 @@
/* -*- 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 httpd.js code.
*
* The Initial Developer of the Original Code is
* Jeff Walden <jwalden+code@mit.edu>.
* Portions created by the Initial Developer are Copyright (C) 2007
* 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 support for server JS-generated pages
const BASE = "http://localhost:4444";
var sjs = do_get_file("netwerk/test/httpserver/test/data/sjs/cgi.sjs");
var srv;
var test;
var tests = [];
/*********************
* UTILITY FUNCTIONS *
*********************/
function isException(e, code)
{
if (e !== code && e.result !== code)
do_throw("unexpected error: " + e);
}
function bytesToString(bytes)
{
return bytes.map(function(v) { return String.fromCharCode(v); }).join("");
}
function skipCache(ch)
{
ch.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
}
/********************
* DEFINE THE TESTS *
********************/
/**
* Adds the set of tests defined in here, differentiating between tests with a
* SJS which throws an exception and creates a server error and tests with a
* normal, successful SJS.
*/
function setupTests(throwing)
{
const TEST_URL = BASE + "/cgi.sjs" + (throwing ? "?throw" : "");
// registerFile with SJS => raw text
function setupFile(ch)
{
srv.registerFile("/cgi.sjs", sjs);
skipCache(ch);
}
function verifyRawText(channel, cx, status, bytes)
{
dumpn(channel.originalURI.spec);
do_check_eq(bytesToString(bytes), fileContents(sjs));
}
test = new Test(TEST_URL, setupFile, null, verifyRawText);
tests.push(test);
// add mapping, => interpreted
function addTypeMapping(ch)
{
srv.registerContentType("sjs", "sjs");
skipCache(ch);
}
function checkType(ch, cx)
{
if (throwing)
{
do_check_false(ch.requestSucceeded);
do_check_eq(ch.responseStatus, 500);
}
else
{
do_check_eq(ch.contentType, "text/plain");
}
}
function checkContents(ch, cx, status, data)
{
if (!throwing)
do_check_eq("PASS", bytesToString(data));
}
test = new Test(TEST_URL, addTypeMapping, checkType, checkContents);
tests.push(test);
// remove file/type mapping, map containing directory => raw text
function setupDirectoryAndRemoveType(ch)
{
dumpn("removing type mapping");
srv.registerContentType("sjs", null);
srv.registerFile("/cgi.sjs", null);
srv.registerDirectory("/", sjs.parent);
skipCache(ch);
}
test = new Test(TEST_URL, setupDirectoryAndRemoveType, null, verifyRawText);
tests.push(test);
// add mapping, => interpreted
function contentAndCleanup(ch, cx, status, data)
{
checkContents(ch, cx, status, data);
// clean up state we've set up
srv.registerDirectory("/", null);
srv.registerContentType("sjs", null);
}
test = new Test(TEST_URL, addTypeMapping, checkType, contentAndCleanup);
tests.push(test);
// NB: No remaining state in the server right now! If we have any here,
// either the second run of tests (without ?throw) or the two tests
// added after the two sets will almost certainly fail.
}
/*****************
* ADD THE TESTS *
*****************/
setupTests(true);
setupTests(false);
// Test that when extension-mappings are used, the entire filename cannot be
// treated as an extension -- there must be at least one dot for a filename to
// match an extension.
function init(ch)
{
// clean up state we've set up
srv.registerDirectory("/", sjs.parent);
srv.registerContentType("sjs", "sjs");
skipCache(ch);
}
function checkNotSJS(ch, cx, status, data)
{
do_check_neq("FAIL", bytesToString(data));
}
test = new Test(BASE + "/sjs", init, null, checkNotSJS);
tests.push(test);
// One last test: for file mappings, the content-type is determined by the
// extension of the file on the server, not by the extension of the requested
// path.
function setupFileMapping(ch)
{
srv.registerFile("/script.html", sjs);
}
function onStart(ch, cx)
{
do_check_eq(ch.contentType, "text/plain");
}
function onStop(ch, cx, status, data)
{
do_check_eq("PASS", bytesToString(data));
}
test = new Test(BASE + "/script.html", setupFileMapping, onStart, onStop);
tests.push(test);
/*****************
* RUN THE TESTS *
*****************/
function run_test()
{
srv = createServer();
// Test for a content-type which isn't a field-value
try
{
srv.registerContentType("foo", "bar\nbaz");
throw "this server throws on content-types which aren't field-values";
}
catch (e)
{
isException(e, Cr.NS_ERROR_INVALID_ARG);
}
// NB: The server has no state at this point -- all state is set up and torn
// down in the tests, because we run the same tests twice with only a
// different query string on the requests, followed by the oddball
// test that doesn't care about throwing or not.
srv.start(4444);
runHttpTests(tests, function() { srv.stop(); });
}