1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/addon-sdk/source/lib/sdk/test/httpd.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,5212 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 +* License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +/* 1.9 +* An implementation of an HTTP server both as a loadable script and as an XPCOM 1.10 +* component. See the accompanying README file for user documentation on 1.11 +* httpd.js. 1.12 +*/ 1.13 + 1.14 +module.metadata = { 1.15 + "stability": "experimental" 1.16 +}; 1.17 + 1.18 +const { components, CC, Cc, Ci, Cr, Cu } = require("chrome"); 1.19 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.20 + 1.21 + 1.22 +const PR_UINT32_MAX = Math.pow(2, 32) - 1; 1.23 + 1.24 +/** True if debugging output is enabled, false otherwise. */ 1.25 +var DEBUG = false; // non-const *only* so tweakable in server tests 1.26 + 1.27 +/** True if debugging output should be timestamped. */ 1.28 +var DEBUG_TIMESTAMP = false; // non-const so tweakable in server tests 1.29 + 1.30 +var gGlobalObject = Cc["@mozilla.org/systemprincipal;1"].createInstance(); 1.31 + 1.32 +/** 1.33 +* Asserts that the given condition holds. If it doesn't, the given message is 1.34 +* dumped, a stack trace is printed, and an exception is thrown to attempt to 1.35 +* stop execution (which unfortunately must rely upon the exception not being 1.36 +* accidentally swallowed by the code that uses it). 1.37 +*/ 1.38 +function NS_ASSERT(cond, msg) 1.39 +{ 1.40 + if (DEBUG && !cond) 1.41 + { 1.42 + dumpn("###!!!"); 1.43 + dumpn("###!!! ASSERTION" + (msg ? ": " + msg : "!")); 1.44 + dumpn("###!!! Stack follows:"); 1.45 + 1.46 + var stack = new Error().stack.split(/\n/); 1.47 + dumpn(stack.map(function(val) { return "###!!! " + val; }).join("\n")); 1.48 + 1.49 + throw Cr.NS_ERROR_ABORT; 1.50 + } 1.51 +} 1.52 + 1.53 +/** Constructs an HTTP error object. */ 1.54 +function HttpError(code, description) 1.55 +{ 1.56 + this.code = code; 1.57 + this.description = description; 1.58 +} 1.59 +HttpError.prototype = 1.60 +{ 1.61 + toString: function() 1.62 + { 1.63 + return this.code + " " + this.description; 1.64 + } 1.65 +}; 1.66 + 1.67 +/** 1.68 +* Errors thrown to trigger specific HTTP server responses. 1.69 +*/ 1.70 +const HTTP_400 = new HttpError(400, "Bad Request"); 1.71 +const HTTP_401 = new HttpError(401, "Unauthorized"); 1.72 +const HTTP_402 = new HttpError(402, "Payment Required"); 1.73 +const HTTP_403 = new HttpError(403, "Forbidden"); 1.74 +const HTTP_404 = new HttpError(404, "Not Found"); 1.75 +const HTTP_405 = new HttpError(405, "Method Not Allowed"); 1.76 +const HTTP_406 = new HttpError(406, "Not Acceptable"); 1.77 +const HTTP_407 = new HttpError(407, "Proxy Authentication Required"); 1.78 +const HTTP_408 = new HttpError(408, "Request Timeout"); 1.79 +const HTTP_409 = new HttpError(409, "Conflict"); 1.80 +const HTTP_410 = new HttpError(410, "Gone"); 1.81 +const HTTP_411 = new HttpError(411, "Length Required"); 1.82 +const HTTP_412 = new HttpError(412, "Precondition Failed"); 1.83 +const HTTP_413 = new HttpError(413, "Request Entity Too Large"); 1.84 +const HTTP_414 = new HttpError(414, "Request-URI Too Long"); 1.85 +const HTTP_415 = new HttpError(415, "Unsupported Media Type"); 1.86 +const HTTP_417 = new HttpError(417, "Expectation Failed"); 1.87 + 1.88 +const HTTP_500 = new HttpError(500, "Internal Server Error"); 1.89 +const HTTP_501 = new HttpError(501, "Not Implemented"); 1.90 +const HTTP_502 = new HttpError(502, "Bad Gateway"); 1.91 +const HTTP_503 = new HttpError(503, "Service Unavailable"); 1.92 +const HTTP_504 = new HttpError(504, "Gateway Timeout"); 1.93 +const HTTP_505 = new HttpError(505, "HTTP Version Not Supported"); 1.94 + 1.95 +/** Creates a hash with fields corresponding to the values in arr. */ 1.96 +function array2obj(arr) 1.97 +{ 1.98 + var obj = {}; 1.99 + for (var i = 0; i < arr.length; i++) 1.100 + obj[arr[i]] = arr[i]; 1.101 + return obj; 1.102 +} 1.103 + 1.104 +/** Returns an array of the integers x through y, inclusive. */ 1.105 +function range(x, y) 1.106 +{ 1.107 + var arr = []; 1.108 + for (var i = x; i <= y; i++) 1.109 + arr.push(i); 1.110 + return arr; 1.111 +} 1.112 + 1.113 +/** An object (hash) whose fields are the numbers of all HTTP error codes. */ 1.114 +const HTTP_ERROR_CODES = array2obj(range(400, 417).concat(range(500, 505))); 1.115 + 1.116 + 1.117 +/** 1.118 +* The character used to distinguish hidden files from non-hidden files, a la 1.119 +* the leading dot in Apache. Since that mechanism also hides files from 1.120 +* easy display in LXR, ls output, etc. however, we choose instead to use a 1.121 +* suffix character. If a requested file ends with it, we append another 1.122 +* when getting the file on the server. If it doesn't, we just look up that 1.123 +* file. Therefore, any file whose name ends with exactly one of the character 1.124 +* is "hidden" and available for use by the server. 1.125 +*/ 1.126 +const HIDDEN_CHAR = "^"; 1.127 + 1.128 +/** 1.129 +* The file name suffix indicating the file containing overridden headers for 1.130 +* a requested file. 1.131 +*/ 1.132 +const HEADERS_SUFFIX = HIDDEN_CHAR + "headers" + HIDDEN_CHAR; 1.133 + 1.134 +/** Type used to denote SJS scripts for CGI-like functionality. */ 1.135 +const SJS_TYPE = "sjs"; 1.136 + 1.137 +/** Base for relative timestamps produced by dumpn(). */ 1.138 +var firstStamp = 0; 1.139 + 1.140 +/** dump(str) with a trailing "\n" -- only outputs if DEBUG. */ 1.141 +function dumpn(str) 1.142 +{ 1.143 + if (DEBUG) 1.144 + { 1.145 + var prefix = "HTTPD-INFO | "; 1.146 + if (DEBUG_TIMESTAMP) 1.147 + { 1.148 + if (firstStamp === 0) 1.149 + firstStamp = Date.now(); 1.150 + 1.151 + var elapsed = Date.now() - firstStamp; // milliseconds 1.152 + var min = Math.floor(elapsed / 60000); 1.153 + var sec = (elapsed % 60000) / 1000; 1.154 + 1.155 + if (sec < 10) 1.156 + prefix += min + ":0" + sec.toFixed(3) + " | "; 1.157 + else 1.158 + prefix += min + ":" + sec.toFixed(3) + " | "; 1.159 + } 1.160 + 1.161 + dump(prefix + str + "\n"); 1.162 + } 1.163 +} 1.164 + 1.165 +/** Dumps the current JS stack if DEBUG. */ 1.166 +function dumpStack() 1.167 +{ 1.168 + // peel off the frames for dumpStack() and Error() 1.169 + var stack = new Error().stack.split(/\n/).slice(2); 1.170 + stack.forEach(dumpn); 1.171 +} 1.172 + 1.173 + 1.174 +/** The XPCOM thread manager. */ 1.175 +var gThreadManager = null; 1.176 + 1.177 +/** The XPCOM prefs service. */ 1.178 +var gRootPrefBranch = null; 1.179 +function getRootPrefBranch() 1.180 +{ 1.181 + if (!gRootPrefBranch) 1.182 + { 1.183 + gRootPrefBranch = Cc["@mozilla.org/preferences-service;1"] 1.184 + .getService(Ci.nsIPrefBranch); 1.185 + } 1.186 + return gRootPrefBranch; 1.187 +} 1.188 + 1.189 +/** 1.190 +* JavaScript constructors for commonly-used classes; precreating these is a 1.191 +* speedup over doing the same from base principles. See the docs at 1.192 +* http://developer.mozilla.org/en/docs/components.Constructor for details. 1.193 +*/ 1.194 +const ServerSocket = CC("@mozilla.org/network/server-socket;1", 1.195 + "nsIServerSocket", 1.196 + "init"); 1.197 +const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1", 1.198 + "nsIScriptableInputStream", 1.199 + "init"); 1.200 +const Pipe = CC("@mozilla.org/pipe;1", 1.201 + "nsIPipe", 1.202 + "init"); 1.203 +const FileInputStream = CC("@mozilla.org/network/file-input-stream;1", 1.204 + "nsIFileInputStream", 1.205 + "init"); 1.206 +const ConverterInputStream = CC("@mozilla.org/intl/converter-input-stream;1", 1.207 + "nsIConverterInputStream", 1.208 + "init"); 1.209 +const WritablePropertyBag = CC("@mozilla.org/hash-property-bag;1", 1.210 + "nsIWritablePropertyBag2"); 1.211 +const SupportsString = CC("@mozilla.org/supports-string;1", 1.212 + "nsISupportsString"); 1.213 + 1.214 +/* These two are non-const only so a test can overwrite them. */ 1.215 +var BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", 1.216 + "nsIBinaryInputStream", 1.217 + "setInputStream"); 1.218 +var BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1", 1.219 + "nsIBinaryOutputStream", 1.220 + "setOutputStream"); 1.221 + 1.222 +/** 1.223 +* Returns the RFC 822/1123 representation of a date. 1.224 +* 1.225 +* @param date : Number 1.226 +* the date, in milliseconds from midnight (00:00:00), January 1, 1970 GMT 1.227 +* @returns string 1.228 +* the representation of the given date 1.229 +*/ 1.230 +function toDateString(date) 1.231 +{ 1.232 + // 1.233 + // rfc1123-date = wkday "," SP date1 SP time SP "GMT" 1.234 + // date1 = 2DIGIT SP month SP 4DIGIT 1.235 + // ; day month year (e.g., 02 Jun 1982) 1.236 + // time = 2DIGIT ":" 2DIGIT ":" 2DIGIT 1.237 + // ; 00:00:00 - 23:59:59 1.238 + // wkday = "Mon" | "Tue" | "Wed" 1.239 + // | "Thu" | "Fri" | "Sat" | "Sun" 1.240 + // month = "Jan" | "Feb" | "Mar" | "Apr" 1.241 + // | "May" | "Jun" | "Jul" | "Aug" 1.242 + // | "Sep" | "Oct" | "Nov" | "Dec" 1.243 + // 1.244 + 1.245 + const wkdayStrings = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; 1.246 + const monthStrings = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", 1.247 + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; 1.248 + 1.249 + /** 1.250 +* Processes a date and returns the encoded UTC time as a string according to 1.251 +* the format specified in RFC 2616. 1.252 +* 1.253 +* @param date : Date 1.254 +* the date to process 1.255 +* @returns string 1.256 +* a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59" 1.257 +*/ 1.258 + function toTime(date) 1.259 + { 1.260 + var hrs = date.getUTCHours(); 1.261 + var rv = (hrs < 10) ? "0" + hrs : hrs; 1.262 + 1.263 + var mins = date.getUTCMinutes(); 1.264 + rv += ":"; 1.265 + rv += (mins < 10) ? "0" + mins : mins; 1.266 + 1.267 + var secs = date.getUTCSeconds(); 1.268 + rv += ":"; 1.269 + rv += (secs < 10) ? "0" + secs : secs; 1.270 + 1.271 + return rv; 1.272 + } 1.273 + 1.274 + /** 1.275 +* Processes a date and returns the encoded UTC date as a string according to 1.276 +* the date1 format specified in RFC 2616. 1.277 +* 1.278 +* @param date : Date 1.279 +* the date to process 1.280 +* @returns string 1.281 +* a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59" 1.282 +*/ 1.283 + function toDate1(date) 1.284 + { 1.285 + var day = date.getUTCDate(); 1.286 + var month = date.getUTCMonth(); 1.287 + var year = date.getUTCFullYear(); 1.288 + 1.289 + var rv = (day < 10) ? "0" + day : day; 1.290 + rv += " " + monthStrings[month]; 1.291 + rv += " " + year; 1.292 + 1.293 + return rv; 1.294 + } 1.295 + 1.296 + date = new Date(date); 1.297 + 1.298 + const fmtString = "%wkday%, %date1% %time% GMT"; 1.299 + var rv = fmtString.replace("%wkday%", wkdayStrings[date.getUTCDay()]); 1.300 + rv = rv.replace("%time%", toTime(date)); 1.301 + return rv.replace("%date1%", toDate1(date)); 1.302 +} 1.303 + 1.304 +/** 1.305 +* Prints out a human-readable representation of the object o and its fields, 1.306 +* omitting those whose names begin with "_" if showMembers != true (to ignore 1.307 +* "private" properties exposed via getters/setters). 1.308 +*/ 1.309 +function printObj(o, showMembers) 1.310 +{ 1.311 + var s = "******************************\n"; 1.312 + s += "o = {\n"; 1.313 + for (var i in o) 1.314 + { 1.315 + if (typeof(i) != "string" || 1.316 + (showMembers || (i.length > 0 && i[0] != "_"))) 1.317 + s+= " " + i + ": " + o[i] + ",\n"; 1.318 + } 1.319 + s += " };\n"; 1.320 + s += "******************************"; 1.321 + dumpn(s); 1.322 +} 1.323 + 1.324 +/** 1.325 +* Instantiates a new HTTP server. 1.326 +*/ 1.327 +function nsHttpServer() 1.328 +{ 1.329 + if (!gThreadManager) 1.330 + gThreadManager = Cc["@mozilla.org/thread-manager;1"].getService(); 1.331 + 1.332 + /** The port on which this server listens. */ 1.333 + this._port = undefined; 1.334 + 1.335 + /** The socket associated with this. */ 1.336 + this._socket = null; 1.337 + 1.338 + /** The handler used to process requests to this server. */ 1.339 + this._handler = new ServerHandler(this); 1.340 + 1.341 + /** Naming information for this server. */ 1.342 + this._identity = new ServerIdentity(); 1.343 + 1.344 + /** 1.345 +* Indicates when the server is to be shut down at the end of the request. 1.346 +*/ 1.347 + this._doQuit = false; 1.348 + 1.349 + /** 1.350 +* True if the socket in this is closed (and closure notifications have been 1.351 +* sent and processed if the socket was ever opened), false otherwise. 1.352 +*/ 1.353 + this._socketClosed = true; 1.354 + 1.355 + /** 1.356 +* Used for tracking existing connections and ensuring that all connections 1.357 +* are properly cleaned up before server shutdown; increases by 1 for every 1.358 +* new incoming connection. 1.359 +*/ 1.360 + this._connectionGen = 0; 1.361 + 1.362 + /** 1.363 +* Hash of all open connections, indexed by connection number at time of 1.364 +* creation. 1.365 +*/ 1.366 + this._connections = {}; 1.367 +} 1.368 +nsHttpServer.prototype = 1.369 +{ 1.370 + classID: components.ID("{54ef6f81-30af-4b1d-ac55-8ba811293e41}"), 1.371 + 1.372 + // NSISERVERSOCKETLISTENER 1.373 + 1.374 + /** 1.375 +* Processes an incoming request coming in on the given socket and contained 1.376 +* in the given transport. 1.377 +* 1.378 +* @param socket : nsIServerSocket 1.379 +* the socket through which the request was served 1.380 +* @param trans : nsISocketTransport 1.381 +* the transport for the request/response 1.382 +* @see nsIServerSocketListener.onSocketAccepted 1.383 +*/ 1.384 + onSocketAccepted: function(socket, trans) 1.385 + { 1.386 + dumpn("*** onSocketAccepted(socket=" + socket + ", trans=" + trans + ")"); 1.387 + 1.388 + dumpn(">>> new connection on " + trans.host + ":" + trans.port); 1.389 + 1.390 + const SEGMENT_SIZE = 8192; 1.391 + const SEGMENT_COUNT = 1024; 1.392 + try 1.393 + { 1.394 + var input = trans.openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT) 1.395 + .QueryInterface(Ci.nsIAsyncInputStream); 1.396 + var output = trans.openOutputStream(0, 0, 0); 1.397 + } 1.398 + catch (e) 1.399 + { 1.400 + dumpn("*** error opening transport streams: " + e); 1.401 + trans.close(Cr.NS_BINDING_ABORTED); 1.402 + return; 1.403 + } 1.404 + 1.405 + var connectionNumber = ++this._connectionGen; 1.406 + 1.407 + try 1.408 + { 1.409 + var conn = new Connection(input, output, this, socket.port, trans.port, 1.410 + connectionNumber); 1.411 + var reader = new RequestReader(conn); 1.412 + 1.413 + // XXX add request timeout functionality here! 1.414 + 1.415 + // Note: must use main thread here, or we might get a GC that will cause 1.416 + // threadsafety assertions. We really need to fix XPConnect so that 1.417 + // you can actually do things in multi-threaded JS. :-( 1.418 + input.asyncWait(reader, 0, 0, gThreadManager.mainThread); 1.419 + } 1.420 + catch (e) 1.421 + { 1.422 + // Assume this connection can't be salvaged and bail on it completely; 1.423 + // don't attempt to close it so that we can assert that any connection 1.424 + // being closed is in this._connections. 1.425 + dumpn("*** error in initial request-processing stages: " + e); 1.426 + trans.close(Cr.NS_BINDING_ABORTED); 1.427 + return; 1.428 + } 1.429 + 1.430 + this._connections[connectionNumber] = conn; 1.431 + dumpn("*** starting connection " + connectionNumber); 1.432 + }, 1.433 + 1.434 + /** 1.435 +* Called when the socket associated with this is closed. 1.436 +* 1.437 +* @param socket : nsIServerSocket 1.438 +* the socket being closed 1.439 +* @param status : nsresult 1.440 +* the reason the socket stopped listening (NS_BINDING_ABORTED if the server 1.441 +* was stopped using nsIHttpServer.stop) 1.442 +* @see nsIServerSocketListener.onStopListening 1.443 +*/ 1.444 + onStopListening: function(socket, status) 1.445 + { 1.446 + dumpn(">>> shutting down server on port " + socket.port); 1.447 + this._socketClosed = true; 1.448 + if (!this._hasOpenConnections()) 1.449 + { 1.450 + dumpn("*** no open connections, notifying async from onStopListening"); 1.451 + 1.452 + // Notify asynchronously so that any pending teardown in stop() has a 1.453 + // chance to run first. 1.454 + var self = this; 1.455 + var stopEvent = 1.456 + { 1.457 + run: function() 1.458 + { 1.459 + dumpn("*** _notifyStopped async callback"); 1.460 + self._notifyStopped(); 1.461 + } 1.462 + }; 1.463 + gThreadManager.currentThread 1.464 + .dispatch(stopEvent, Ci.nsIThread.DISPATCH_NORMAL); 1.465 + } 1.466 + }, 1.467 + 1.468 + // NSIHTTPSERVER 1.469 + 1.470 + // 1.471 + // see nsIHttpServer.start 1.472 + // 1.473 + start: function(port) 1.474 + { 1.475 + this._start(port, "localhost") 1.476 + }, 1.477 + 1.478 + _start: function(port, host) 1.479 + { 1.480 + if (this._socket) 1.481 + throw Cr.NS_ERROR_ALREADY_INITIALIZED; 1.482 + 1.483 + this._port = port; 1.484 + this._doQuit = this._socketClosed = false; 1.485 + 1.486 + this._host = host; 1.487 + 1.488 + // The listen queue needs to be long enough to handle 1.489 + // network.http.max-persistent-connections-per-server concurrent connections, 1.490 + // plus a safety margin in case some other process is talking to 1.491 + // the server as well. 1.492 + var prefs = getRootPrefBranch(); 1.493 + var maxConnections; 1.494 + try { 1.495 + // Bug 776860: The original pref was removed in favor of this new one: 1.496 + maxConnections = prefs.getIntPref("network.http.max-persistent-connections-per-server") + 5; 1.497 + } 1.498 + catch(e) { 1.499 + maxConnections = prefs.getIntPref("network.http.max-connections-per-server") + 5; 1.500 + } 1.501 + 1.502 + try 1.503 + { 1.504 + var loopback = true; 1.505 + if (this._host != "127.0.0.1" && this._host != "localhost") { 1.506 + var loopback = false; 1.507 + } 1.508 + 1.509 + var socket = new ServerSocket(this._port, 1.510 + loopback, // true = localhost, false = everybody 1.511 + maxConnections); 1.512 + dumpn(">>> listening on port " + socket.port + ", " + maxConnections + 1.513 + " pending connections"); 1.514 + socket.asyncListen(this); 1.515 + this._identity._initialize(socket.port, host, true); 1.516 + this._socket = socket; 1.517 + } 1.518 + catch (e) 1.519 + { 1.520 + dumpn("!!! could not start server on port " + port + ": " + e); 1.521 + throw Cr.NS_ERROR_NOT_AVAILABLE; 1.522 + } 1.523 + }, 1.524 + 1.525 + // 1.526 + // see nsIHttpServer.stop 1.527 + // 1.528 + stop: function(callback) 1.529 + { 1.530 + if (!callback) 1.531 + throw Cr.NS_ERROR_NULL_POINTER; 1.532 + if (!this._socket) 1.533 + throw Cr.NS_ERROR_UNEXPECTED; 1.534 + 1.535 + this._stopCallback = typeof callback === "function" 1.536 + ? callback 1.537 + : function() { callback.onStopped(); }; 1.538 + 1.539 + dumpn(">>> stopping listening on port " + this._socket.port); 1.540 + this._socket.close(); 1.541 + this._socket = null; 1.542 + 1.543 + // We can't have this identity any more, and the port on which we're running 1.544 + // this server now could be meaningless the next time around. 1.545 + this._identity._teardown(); 1.546 + 1.547 + this._doQuit = false; 1.548 + 1.549 + // socket-close notification and pending request completion happen async 1.550 + }, 1.551 + 1.552 + // 1.553 + // see nsIHttpServer.registerFile 1.554 + // 1.555 + registerFile: function(path, file) 1.556 + { 1.557 + if (file && (!file.exists() || file.isDirectory())) 1.558 + throw Cr.NS_ERROR_INVALID_ARG; 1.559 + 1.560 + this._handler.registerFile(path, file); 1.561 + }, 1.562 + 1.563 + // 1.564 + // see nsIHttpServer.registerDirectory 1.565 + // 1.566 + registerDirectory: function(path, directory) 1.567 + { 1.568 + // XXX true path validation! 1.569 + if (path.charAt(0) != "/" || 1.570 + path.charAt(path.length - 1) != "/" || 1.571 + (directory && 1.572 + (!directory.exists() || !directory.isDirectory()))) 1.573 + throw Cr.NS_ERROR_INVALID_ARG; 1.574 + 1.575 + // XXX determine behavior of nonexistent /foo/bar when a /foo/bar/ mapping 1.576 + // exists! 1.577 + 1.578 + this._handler.registerDirectory(path, directory); 1.579 + }, 1.580 + 1.581 + // 1.582 + // see nsIHttpServer.registerPathHandler 1.583 + // 1.584 + registerPathHandler: function(path, handler) 1.585 + { 1.586 + this._handler.registerPathHandler(path, handler); 1.587 + }, 1.588 + 1.589 + // 1.590 + // see nsIHttpServer.registerPrefixHandler 1.591 + // 1.592 + registerPrefixHandler: function(prefix, handler) 1.593 + { 1.594 + this._handler.registerPrefixHandler(prefix, handler); 1.595 + }, 1.596 + 1.597 + // 1.598 + // see nsIHttpServer.registerErrorHandler 1.599 + // 1.600 + registerErrorHandler: function(code, handler) 1.601 + { 1.602 + this._handler.registerErrorHandler(code, handler); 1.603 + }, 1.604 + 1.605 + // 1.606 + // see nsIHttpServer.setIndexHandler 1.607 + // 1.608 + setIndexHandler: function(handler) 1.609 + { 1.610 + this._handler.setIndexHandler(handler); 1.611 + }, 1.612 + 1.613 + // 1.614 + // see nsIHttpServer.registerContentType 1.615 + // 1.616 + registerContentType: function(ext, type) 1.617 + { 1.618 + this._handler.registerContentType(ext, type); 1.619 + }, 1.620 + 1.621 + // 1.622 + // see nsIHttpServer.serverIdentity 1.623 + // 1.624 + get identity() 1.625 + { 1.626 + return this._identity; 1.627 + }, 1.628 + 1.629 + // 1.630 + // see nsIHttpServer.getState 1.631 + // 1.632 + getState: function(path, k) 1.633 + { 1.634 + return this._handler._getState(path, k); 1.635 + }, 1.636 + 1.637 + // 1.638 + // see nsIHttpServer.setState 1.639 + // 1.640 + setState: function(path, k, v) 1.641 + { 1.642 + return this._handler._setState(path, k, v); 1.643 + }, 1.644 + 1.645 + // 1.646 + // see nsIHttpServer.getSharedState 1.647 + // 1.648 + getSharedState: function(k) 1.649 + { 1.650 + return this._handler._getSharedState(k); 1.651 + }, 1.652 + 1.653 + // 1.654 + // see nsIHttpServer.setSharedState 1.655 + // 1.656 + setSharedState: function(k, v) 1.657 + { 1.658 + return this._handler._setSharedState(k, v); 1.659 + }, 1.660 + 1.661 + // 1.662 + // see nsIHttpServer.getObjectState 1.663 + // 1.664 + getObjectState: function(k) 1.665 + { 1.666 + return this._handler._getObjectState(k); 1.667 + }, 1.668 + 1.669 + // 1.670 + // see nsIHttpServer.setObjectState 1.671 + // 1.672 + setObjectState: function(k, v) 1.673 + { 1.674 + return this._handler._setObjectState(k, v); 1.675 + }, 1.676 + 1.677 + 1.678 + // NSISUPPORTS 1.679 + 1.680 + // 1.681 + // see nsISupports.QueryInterface 1.682 + // 1.683 + QueryInterface: function(iid) 1.684 + { 1.685 + if (iid.equals(Ci.nsIServerSocketListener) || iid.equals(Ci.nsISupports)) 1.686 + return this; 1.687 + 1.688 + throw Cr.NS_ERROR_NO_INTERFACE; 1.689 + }, 1.690 + 1.691 + 1.692 + // NON-XPCOM PUBLIC API 1.693 + 1.694 + /** 1.695 +* Returns true iff this server is not running (and is not in the process of 1.696 +* serving any requests still to be processed when the server was last 1.697 +* stopped after being run). 1.698 +*/ 1.699 + isStopped: function() 1.700 + { 1.701 + return this._socketClosed && !this._hasOpenConnections(); 1.702 + }, 1.703 + 1.704 + // PRIVATE IMPLEMENTATION 1.705 + 1.706 + /** True if this server has any open connections to it, false otherwise. */ 1.707 + _hasOpenConnections: function() 1.708 + { 1.709 + // 1.710 + // If we have any open connections, they're tracked as numeric properties on 1.711 + // |this._connections|. The non-standard __count__ property could be used 1.712 + // to check whether there are any properties, but standard-wise, even 1.713 + // looking forward to ES5, there's no less ugly yet still O(1) way to do 1.714 + // this. 1.715 + // 1.716 + for (var n in this._connections) 1.717 + return true; 1.718 + return false; 1.719 + }, 1.720 + 1.721 + /** Calls the server-stopped callback provided when stop() was called. */ 1.722 + _notifyStopped: function() 1.723 + { 1.724 + NS_ASSERT(this._stopCallback !== null, "double-notifying?"); 1.725 + NS_ASSERT(!this._hasOpenConnections(), "should be done serving by now"); 1.726 + 1.727 + // 1.728 + // NB: We have to grab this now, null out the member, *then* call the 1.729 + // callback here, or otherwise the callback could (indirectly) futz with 1.730 + // this._stopCallback by starting and immediately stopping this, at 1.731 + // which point we'd be nulling out a field we no longer have a right to 1.732 + // modify. 1.733 + // 1.734 + var callback = this._stopCallback; 1.735 + this._stopCallback = null; 1.736 + try 1.737 + { 1.738 + callback(); 1.739 + } 1.740 + catch (e) 1.741 + { 1.742 + // not throwing because this is specified as being usually (but not 1.743 + // always) asynchronous 1.744 + dump("!!! error running onStopped callback: " + e + "\n"); 1.745 + } 1.746 + }, 1.747 + 1.748 + /** 1.749 +* Notifies this server that the given connection has been closed. 1.750 +* 1.751 +* @param connection : Connection 1.752 +* the connection that was closed 1.753 +*/ 1.754 + _connectionClosed: function(connection) 1.755 + { 1.756 + NS_ASSERT(connection.number in this._connections, 1.757 + "closing a connection " + this + " that we never added to the " + 1.758 + "set of open connections?"); 1.759 + NS_ASSERT(this._connections[connection.number] === connection, 1.760 + "connection number mismatch? " + 1.761 + this._connections[connection.number]); 1.762 + delete this._connections[connection.number]; 1.763 + 1.764 + // Fire a pending server-stopped notification if it's our responsibility. 1.765 + if (!this._hasOpenConnections() && this._socketClosed) 1.766 + this._notifyStopped(); 1.767 + }, 1.768 + 1.769 + /** 1.770 +* Requests that the server be shut down when possible. 1.771 +*/ 1.772 + _requestQuit: function() 1.773 + { 1.774 + dumpn(">>> requesting a quit"); 1.775 + dumpStack(); 1.776 + this._doQuit = true; 1.777 + } 1.778 +}; 1.779 + 1.780 + 1.781 +// 1.782 +// RFC 2396 section 3.2.2: 1.783 +// 1.784 +// host = hostname | IPv4address 1.785 +// hostname = *( domainlabel "." ) toplabel [ "." ] 1.786 +// domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum 1.787 +// toplabel = alpha | alpha *( alphanum | "-" ) alphanum 1.788 +// IPv4address = 1*digit "." 1*digit "." 1*digit "." 1*digit 1.789 +// 1.790 + 1.791 +const HOST_REGEX = 1.792 + new RegExp("^(?:" + 1.793 + // *( domainlabel "." ) 1.794 + "(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)*" + 1.795 + // toplabel 1.796 + "[a-z](?:[a-z0-9-]*[a-z0-9])?" + 1.797 + "|" + 1.798 + // IPv4 address 1.799 + "\\d+\\.\\d+\\.\\d+\\.\\d+" + 1.800 + ")$", 1.801 + "i"); 1.802 + 1.803 + 1.804 +/** 1.805 +* Represents the identity of a server. An identity consists of a set of 1.806 +* (scheme, host, port) tuples denoted as locations (allowing a single server to 1.807 +* serve multiple sites or to be used behind both HTTP and HTTPS proxies for any 1.808 +* host/port). Any incoming request must be to one of these locations, or it 1.809 +* will be rejected with an HTTP 400 error. One location, denoted as the 1.810 +* primary location, is the location assigned in contexts where a location 1.811 +* cannot otherwise be endogenously derived, such as for HTTP/1.0 requests. 1.812 +* 1.813 +* A single identity may contain at most one location per unique host/port pair; 1.814 +* other than that, no restrictions are placed upon what locations may 1.815 +* constitute an identity. 1.816 +*/ 1.817 +function ServerIdentity() 1.818 +{ 1.819 + /** The scheme of the primary location. */ 1.820 + this._primaryScheme = "http"; 1.821 + 1.822 + /** The hostname of the primary location. */ 1.823 + this._primaryHost = "127.0.0.1" 1.824 + 1.825 + /** The port number of the primary location. */ 1.826 + this._primaryPort = -1; 1.827 + 1.828 + /** 1.829 +* The current port number for the corresponding server, stored so that a new 1.830 +* primary location can always be set if the current one is removed. 1.831 +*/ 1.832 + this._defaultPort = -1; 1.833 + 1.834 + /** 1.835 +* Maps hosts to maps of ports to schemes, e.g. the following would represent 1.836 +* https://example.com:789/ and http://example.org/: 1.837 +* 1.838 +* { 1.839 +* "xexample.com": { 789: "https" }, 1.840 +* "xexample.org": { 80: "http" } 1.841 +* } 1.842 +* 1.843 +* Note the "x" prefix on hostnames, which prevents collisions with special 1.844 +* JS names like "prototype". 1.845 +*/ 1.846 + this._locations = { "xlocalhost": {} }; 1.847 +} 1.848 +ServerIdentity.prototype = 1.849 +{ 1.850 + // NSIHTTPSERVERIDENTITY 1.851 + 1.852 + // 1.853 + // see nsIHttpServerIdentity.primaryScheme 1.854 + // 1.855 + get primaryScheme() 1.856 + { 1.857 + if (this._primaryPort === -1) 1.858 + throw Cr.NS_ERROR_NOT_INITIALIZED; 1.859 + return this._primaryScheme; 1.860 + }, 1.861 + 1.862 + // 1.863 + // see nsIHttpServerIdentity.primaryHost 1.864 + // 1.865 + get primaryHost() 1.866 + { 1.867 + if (this._primaryPort === -1) 1.868 + throw Cr.NS_ERROR_NOT_INITIALIZED; 1.869 + return this._primaryHost; 1.870 + }, 1.871 + 1.872 + // 1.873 + // see nsIHttpServerIdentity.primaryPort 1.874 + // 1.875 + get primaryPort() 1.876 + { 1.877 + if (this._primaryPort === -1) 1.878 + throw Cr.NS_ERROR_NOT_INITIALIZED; 1.879 + return this._primaryPort; 1.880 + }, 1.881 + 1.882 + // 1.883 + // see nsIHttpServerIdentity.add 1.884 + // 1.885 + add: function(scheme, host, port) 1.886 + { 1.887 + this._validate(scheme, host, port); 1.888 + 1.889 + var entry = this._locations["x" + host]; 1.890 + if (!entry) 1.891 + this._locations["x" + host] = entry = {}; 1.892 + 1.893 + entry[port] = scheme; 1.894 + }, 1.895 + 1.896 + // 1.897 + // see nsIHttpServerIdentity.remove 1.898 + // 1.899 + remove: function(scheme, host, port) 1.900 + { 1.901 + this._validate(scheme, host, port); 1.902 + 1.903 + var entry = this._locations["x" + host]; 1.904 + if (!entry) 1.905 + return false; 1.906 + 1.907 + var present = port in entry; 1.908 + delete entry[port]; 1.909 + 1.910 + if (this._primaryScheme == scheme && 1.911 + this._primaryHost == host && 1.912 + this._primaryPort == port && 1.913 + this._defaultPort !== -1) 1.914 + { 1.915 + // Always keep at least one identity in existence at any time, unless 1.916 + // we're in the process of shutting down (the last condition above). 1.917 + this._primaryPort = -1; 1.918 + this._initialize(this._defaultPort, host, false); 1.919 + } 1.920 + 1.921 + return present; 1.922 + }, 1.923 + 1.924 + // 1.925 + // see nsIHttpServerIdentity.has 1.926 + // 1.927 + has: function(scheme, host, port) 1.928 + { 1.929 + this._validate(scheme, host, port); 1.930 + 1.931 + return "x" + host in this._locations && 1.932 + scheme === this._locations["x" + host][port]; 1.933 + }, 1.934 + 1.935 + // 1.936 + // see nsIHttpServerIdentity.has 1.937 + // 1.938 + getScheme: function(host, port) 1.939 + { 1.940 + this._validate("http", host, port); 1.941 + 1.942 + var entry = this._locations["x" + host]; 1.943 + if (!entry) 1.944 + return ""; 1.945 + 1.946 + return entry[port] || ""; 1.947 + }, 1.948 + 1.949 + // 1.950 + // see nsIHttpServerIdentity.setPrimary 1.951 + // 1.952 + setPrimary: function(scheme, host, port) 1.953 + { 1.954 + this._validate(scheme, host, port); 1.955 + 1.956 + this.add(scheme, host, port); 1.957 + 1.958 + this._primaryScheme = scheme; 1.959 + this._primaryHost = host; 1.960 + this._primaryPort = port; 1.961 + }, 1.962 + 1.963 + 1.964 + // NSISUPPORTS 1.965 + 1.966 + // 1.967 + // see nsISupports.QueryInterface 1.968 + // 1.969 + QueryInterface: function(iid) 1.970 + { 1.971 + if (iid.equals(Ci.nsIHttpServerIdentity) || iid.equals(Ci.nsISupports)) 1.972 + return this; 1.973 + 1.974 + throw Cr.NS_ERROR_NO_INTERFACE; 1.975 + }, 1.976 + 1.977 + 1.978 + // PRIVATE IMPLEMENTATION 1.979 + 1.980 + /** 1.981 +* Initializes the primary name for the corresponding server, based on the 1.982 +* provided port number. 1.983 +*/ 1.984 + _initialize: function(port, host, addSecondaryDefault) 1.985 + { 1.986 + this._host = host; 1.987 + if (this._primaryPort !== -1) 1.988 + this.add("http", host, port); 1.989 + else 1.990 + this.setPrimary("http", "localhost", port); 1.991 + this._defaultPort = port; 1.992 + 1.993 + // Only add this if we're being called at server startup 1.994 + if (addSecondaryDefault && host != "127.0.0.1") 1.995 + this.add("http", "127.0.0.1", port); 1.996 + }, 1.997 + 1.998 + /** 1.999 +* Called at server shutdown time, unsets the primary location only if it was 1.1000 +* the default-assigned location and removes the default location from the 1.1001 +* set of locations used. 1.1002 +*/ 1.1003 + _teardown: function() 1.1004 + { 1.1005 + if (this._host != "127.0.0.1") { 1.1006 + // Not the default primary location, nothing special to do here 1.1007 + this.remove("http", "127.0.0.1", this._defaultPort); 1.1008 + } 1.1009 + 1.1010 + // This is a *very* tricky bit of reasoning here; make absolutely sure the 1.1011 + // tests for this code pass before you commit changes to it. 1.1012 + if (this._primaryScheme == "http" && 1.1013 + this._primaryHost == this._host && 1.1014 + this._primaryPort == this._defaultPort) 1.1015 + { 1.1016 + // Make sure we don't trigger the readding logic in .remove(), then remove 1.1017 + // the default location. 1.1018 + var port = this._defaultPort; 1.1019 + this._defaultPort = -1; 1.1020 + this.remove("http", this._host, port); 1.1021 + 1.1022 + // Ensure a server start triggers the setPrimary() path in ._initialize() 1.1023 + this._primaryPort = -1; 1.1024 + } 1.1025 + else 1.1026 + { 1.1027 + // No reason not to remove directly as it's not our primary location 1.1028 + this.remove("http", this._host, this._defaultPort); 1.1029 + } 1.1030 + }, 1.1031 + 1.1032 + /** 1.1033 +* Ensures scheme, host, and port are all valid with respect to RFC 2396. 1.1034 +* 1.1035 +* @throws NS_ERROR_ILLEGAL_VALUE 1.1036 +* if any argument doesn't match the corresponding production 1.1037 +*/ 1.1038 + _validate: function(scheme, host, port) 1.1039 + { 1.1040 + if (scheme !== "http" && scheme !== "https") 1.1041 + { 1.1042 + dumpn("*** server only supports http/https schemes: '" + scheme + "'"); 1.1043 + dumpStack(); 1.1044 + throw Cr.NS_ERROR_ILLEGAL_VALUE; 1.1045 + } 1.1046 + if (!HOST_REGEX.test(host)) 1.1047 + { 1.1048 + dumpn("*** unexpected host: '" + host + "'"); 1.1049 + throw Cr.NS_ERROR_ILLEGAL_VALUE; 1.1050 + } 1.1051 + if (port < 0 || port > 65535) 1.1052 + { 1.1053 + dumpn("*** unexpected port: '" + port + "'"); 1.1054 + throw Cr.NS_ERROR_ILLEGAL_VALUE; 1.1055 + } 1.1056 + } 1.1057 +}; 1.1058 + 1.1059 + 1.1060 +/** 1.1061 +* Represents a connection to the server (and possibly in the future the thread 1.1062 +* on which the connection is processed). 1.1063 +* 1.1064 +* @param input : nsIInputStream 1.1065 +* stream from which incoming data on the connection is read 1.1066 +* @param output : nsIOutputStream 1.1067 +* stream to write data out the connection 1.1068 +* @param server : nsHttpServer 1.1069 +* the server handling the connection 1.1070 +* @param port : int 1.1071 +* the port on which the server is running 1.1072 +* @param outgoingPort : int 1.1073 +* the outgoing port used by this connection 1.1074 +* @param number : uint 1.1075 +* a serial number used to uniquely identify this connection 1.1076 +*/ 1.1077 +function Connection(input, output, server, port, outgoingPort, number) 1.1078 +{ 1.1079 + dumpn("*** opening new connection " + number + " on port " + outgoingPort); 1.1080 + 1.1081 + /** Stream of incoming data. */ 1.1082 + this.input = input; 1.1083 + 1.1084 + /** Stream for outgoing data. */ 1.1085 + this.output = output; 1.1086 + 1.1087 + /** The server associated with this request. */ 1.1088 + this.server = server; 1.1089 + 1.1090 + /** The port on which the server is running. */ 1.1091 + this.port = port; 1.1092 + 1.1093 + /** The outgoing poort used by this connection. */ 1.1094 + this._outgoingPort = outgoingPort; 1.1095 + 1.1096 + /** The serial number of this connection. */ 1.1097 + this.number = number; 1.1098 + 1.1099 + /** 1.1100 +* The request for which a response is being generated, null if the 1.1101 +* incoming request has not been fully received or if it had errors. 1.1102 +*/ 1.1103 + this.request = null; 1.1104 + 1.1105 + /** State variables for debugging. */ 1.1106 + this._closed = this._processed = false; 1.1107 +} 1.1108 +Connection.prototype = 1.1109 +{ 1.1110 + /** Closes this connection's input/output streams. */ 1.1111 + close: function() 1.1112 + { 1.1113 + dumpn("*** closing connection " + this.number + 1.1114 + " on port " + this._outgoingPort); 1.1115 + 1.1116 + this.input.close(); 1.1117 + this.output.close(); 1.1118 + this._closed = true; 1.1119 + 1.1120 + var server = this.server; 1.1121 + server._connectionClosed(this); 1.1122 + 1.1123 + // If an error triggered a server shutdown, act on it now 1.1124 + if (server._doQuit) 1.1125 + server.stop(function() { /* not like we can do anything better */ }); 1.1126 + }, 1.1127 + 1.1128 + /** 1.1129 +* Initiates processing of this connection, using the data in the given 1.1130 +* request. 1.1131 +* 1.1132 +* @param request : Request 1.1133 +* the request which should be processed 1.1134 +*/ 1.1135 + process: function(request) 1.1136 + { 1.1137 + NS_ASSERT(!this._closed && !this._processed); 1.1138 + 1.1139 + this._processed = true; 1.1140 + 1.1141 + this.request = request; 1.1142 + this.server._handler.handleResponse(this); 1.1143 + }, 1.1144 + 1.1145 + /** 1.1146 +* Initiates processing of this connection, generating a response with the 1.1147 +* given HTTP error code. 1.1148 +* 1.1149 +* @param code : uint 1.1150 +* an HTTP code, so in the range [0, 1000) 1.1151 +* @param request : Request 1.1152 +* incomplete data about the incoming request (since there were errors 1.1153 +* during its processing 1.1154 +*/ 1.1155 + processError: function(code, request) 1.1156 + { 1.1157 + NS_ASSERT(!this._closed && !this._processed); 1.1158 + 1.1159 + this._processed = true; 1.1160 + this.request = request; 1.1161 + this.server._handler.handleError(code, this); 1.1162 + }, 1.1163 + 1.1164 + /** Converts this to a string for debugging purposes. */ 1.1165 + toString: function() 1.1166 + { 1.1167 + return "<Connection(" + this.number + 1.1168 + (this.request ? ", " + this.request.path : "") +"): " + 1.1169 + (this._closed ? "closed" : "open") + ">"; 1.1170 + } 1.1171 +}; 1.1172 + 1.1173 + 1.1174 + 1.1175 +/** Returns an array of count bytes from the given input stream. */ 1.1176 +function readBytes(inputStream, count) 1.1177 +{ 1.1178 + return new BinaryInputStream(inputStream).readByteArray(count); 1.1179 +} 1.1180 + 1.1181 + 1.1182 + 1.1183 +/** Request reader processing states; see RequestReader for details. */ 1.1184 +const READER_IN_REQUEST_LINE = 0; 1.1185 +const READER_IN_HEADERS = 1; 1.1186 +const READER_IN_BODY = 2; 1.1187 +const READER_FINISHED = 3; 1.1188 + 1.1189 + 1.1190 +/** 1.1191 +* Reads incoming request data asynchronously, does any necessary preprocessing, 1.1192 +* and forwards it to the request handler. Processing occurs in three states: 1.1193 +* 1.1194 +* READER_IN_REQUEST_LINE Reading the request's status line 1.1195 +* READER_IN_HEADERS Reading headers in the request 1.1196 +* READER_IN_BODY Reading the body of the request 1.1197 +* READER_FINISHED Entire request has been read and processed 1.1198 +* 1.1199 +* During the first two stages, initial metadata about the request is gathered 1.1200 +* into a Request object. Once the status line and headers have been processed, 1.1201 +* we start processing the body of the request into the Request. Finally, when 1.1202 +* the entire body has been read, we create a Response and hand it off to the 1.1203 +* ServerHandler to be given to the appropriate request handler. 1.1204 +* 1.1205 +* @param connection : Connection 1.1206 +* the connection for the request being read 1.1207 +*/ 1.1208 +function RequestReader(connection) 1.1209 +{ 1.1210 + /** Connection metadata for this request. */ 1.1211 + this._connection = connection; 1.1212 + 1.1213 + /** 1.1214 +* A container providing line-by-line access to the raw bytes that make up the 1.1215 +* data which has been read from the connection but has not yet been acted 1.1216 +* upon (by passing it to the request handler or by extracting request 1.1217 +* metadata from it). 1.1218 +*/ 1.1219 + this._data = new LineData(); 1.1220 + 1.1221 + /** 1.1222 +* The amount of data remaining to be read from the body of this request. 1.1223 +* After all headers in the request have been read this is the value in the 1.1224 +* Content-Length header, but as the body is read its value decreases to zero. 1.1225 +*/ 1.1226 + this._contentLength = 0; 1.1227 + 1.1228 + /** The current state of parsing the incoming request. */ 1.1229 + this._state = READER_IN_REQUEST_LINE; 1.1230 + 1.1231 + /** Metadata constructed from the incoming request for the request handler. */ 1.1232 + this._metadata = new Request(connection.port); 1.1233 + 1.1234 + /** 1.1235 +* Used to preserve state if we run out of line data midway through a 1.1236 +* multi-line header. _lastHeaderName stores the name of the header, while 1.1237 +* _lastHeaderValue stores the value we've seen so far for the header. 1.1238 +* 1.1239 +* These fields are always either both undefined or both strings. 1.1240 +*/ 1.1241 + this._lastHeaderName = this._lastHeaderValue = undefined; 1.1242 +} 1.1243 +RequestReader.prototype = 1.1244 +{ 1.1245 + // NSIINPUTSTREAMCALLBACK 1.1246 + 1.1247 + /** 1.1248 +* Called when more data from the incoming request is available. This method 1.1249 +* then reads the available data from input and deals with that data as 1.1250 +* necessary, depending upon the syntax of already-downloaded data. 1.1251 +* 1.1252 +* @param input : nsIAsyncInputStream 1.1253 +* the stream of incoming data from the connection 1.1254 +*/ 1.1255 + onInputStreamReady: function(input) 1.1256 + { 1.1257 + dumpn("*** onInputStreamReady(input=" + input + ") on thread " + 1.1258 + gThreadManager.currentThread + " (main is " + 1.1259 + gThreadManager.mainThread + ")"); 1.1260 + dumpn("*** this._state == " + this._state); 1.1261 + 1.1262 + // Handle cases where we get more data after a request error has been 1.1263 + // discovered but *before* we can close the connection. 1.1264 + var data = this._data; 1.1265 + if (!data) 1.1266 + return; 1.1267 + 1.1268 + try 1.1269 + { 1.1270 + data.appendBytes(readBytes(input, input.available())); 1.1271 + } 1.1272 + catch (e) 1.1273 + { 1.1274 + if (streamClosed(e)) 1.1275 + { 1.1276 + dumpn("*** WARNING: unexpected error when reading from socket; will " + 1.1277 + "be treated as if the input stream had been closed"); 1.1278 + dumpn("*** WARNING: actual error was: " + e); 1.1279 + } 1.1280 + 1.1281 + // We've lost a race -- input has been closed, but we're still expecting 1.1282 + // to read more data. available() will throw in this case, and since 1.1283 + // we're dead in the water now, destroy the connection. 1.1284 + dumpn("*** onInputStreamReady called on a closed input, destroying " + 1.1285 + "connection"); 1.1286 + this._connection.close(); 1.1287 + return; 1.1288 + } 1.1289 + 1.1290 + switch (this._state) 1.1291 + { 1.1292 + default: 1.1293 + NS_ASSERT(false, "invalid state: " + this._state); 1.1294 + break; 1.1295 + 1.1296 + case READER_IN_REQUEST_LINE: 1.1297 + if (!this._processRequestLine()) 1.1298 + break; 1.1299 + /* fall through */ 1.1300 + 1.1301 + case READER_IN_HEADERS: 1.1302 + if (!this._processHeaders()) 1.1303 + break; 1.1304 + /* fall through */ 1.1305 + 1.1306 + case READER_IN_BODY: 1.1307 + this._processBody(); 1.1308 + } 1.1309 + 1.1310 + if (this._state != READER_FINISHED) 1.1311 + input.asyncWait(this, 0, 0, gThreadManager.currentThread); 1.1312 + }, 1.1313 + 1.1314 + // 1.1315 + // see nsISupports.QueryInterface 1.1316 + // 1.1317 + QueryInterface: function(aIID) 1.1318 + { 1.1319 + if (aIID.equals(Ci.nsIInputStreamCallback) || 1.1320 + aIID.equals(Ci.nsISupports)) 1.1321 + return this; 1.1322 + 1.1323 + throw Cr.NS_ERROR_NO_INTERFACE; 1.1324 + }, 1.1325 + 1.1326 + 1.1327 + // PRIVATE API 1.1328 + 1.1329 + /** 1.1330 +* Processes unprocessed, downloaded data as a request line. 1.1331 +* 1.1332 +* @returns boolean 1.1333 +* true iff the request line has been fully processed 1.1334 +*/ 1.1335 + _processRequestLine: function() 1.1336 + { 1.1337 + NS_ASSERT(this._state == READER_IN_REQUEST_LINE); 1.1338 + 1.1339 + // Servers SHOULD ignore any empty line(s) received where a Request-Line 1.1340 + // is expected (section 4.1). 1.1341 + var data = this._data; 1.1342 + var line = {}; 1.1343 + var readSuccess; 1.1344 + while ((readSuccess = data.readLine(line)) && line.value == "") 1.1345 + dumpn("*** ignoring beginning blank line..."); 1.1346 + 1.1347 + // if we don't have a full line, wait until we do 1.1348 + if (!readSuccess) 1.1349 + return false; 1.1350 + 1.1351 + // we have the first non-blank line 1.1352 + try 1.1353 + { 1.1354 + this._parseRequestLine(line.value); 1.1355 + this._state = READER_IN_HEADERS; 1.1356 + return true; 1.1357 + } 1.1358 + catch (e) 1.1359 + { 1.1360 + this._handleError(e); 1.1361 + return false; 1.1362 + } 1.1363 + }, 1.1364 + 1.1365 + /** 1.1366 +* Processes stored data, assuming it is either at the beginning or in 1.1367 +* the middle of processing request headers. 1.1368 +* 1.1369 +* @returns boolean 1.1370 +* true iff header data in the request has been fully processed 1.1371 +*/ 1.1372 + _processHeaders: function() 1.1373 + { 1.1374 + NS_ASSERT(this._state == READER_IN_HEADERS); 1.1375 + 1.1376 + // XXX things to fix here: 1.1377 + // 1.1378 + // - need to support RFC 2047-encoded non-US-ASCII characters 1.1379 + 1.1380 + try 1.1381 + { 1.1382 + var done = this._parseHeaders(); 1.1383 + if (done) 1.1384 + { 1.1385 + var request = this._metadata; 1.1386 + 1.1387 + // XXX this is wrong for requests with transfer-encodings applied to 1.1388 + // them, particularly chunked (which by its nature can have no 1.1389 + // meaningful Content-Length header)! 1.1390 + this._contentLength = request.hasHeader("Content-Length") 1.1391 + ? parseInt(request.getHeader("Content-Length"), 10) 1.1392 + : 0; 1.1393 + dumpn("_processHeaders, Content-length=" + this._contentLength); 1.1394 + 1.1395 + this._state = READER_IN_BODY; 1.1396 + } 1.1397 + return done; 1.1398 + } 1.1399 + catch (e) 1.1400 + { 1.1401 + this._handleError(e); 1.1402 + return false; 1.1403 + } 1.1404 + }, 1.1405 + 1.1406 + /** 1.1407 +* Processes stored data, assuming it is either at the beginning or in 1.1408 +* the middle of processing the request body. 1.1409 +* 1.1410 +* @returns boolean 1.1411 +* true iff the request body has been fully processed 1.1412 +*/ 1.1413 + _processBody: function() 1.1414 + { 1.1415 + NS_ASSERT(this._state == READER_IN_BODY); 1.1416 + 1.1417 + // XXX handle chunked transfer-coding request bodies! 1.1418 + 1.1419 + try 1.1420 + { 1.1421 + if (this._contentLength > 0) 1.1422 + { 1.1423 + var data = this._data.purge(); 1.1424 + var count = Math.min(data.length, this._contentLength); 1.1425 + dumpn("*** loading data=" + data + " len=" + data.length + 1.1426 + " excess=" + (data.length - count)); 1.1427 + 1.1428 + var bos = new BinaryOutputStream(this._metadata._bodyOutputStream); 1.1429 + bos.writeByteArray(data, count); 1.1430 + this._contentLength -= count; 1.1431 + } 1.1432 + 1.1433 + dumpn("*** remaining body data len=" + this._contentLength); 1.1434 + if (this._contentLength == 0) 1.1435 + { 1.1436 + this._validateRequest(); 1.1437 + this._state = READER_FINISHED; 1.1438 + this._handleResponse(); 1.1439 + return true; 1.1440 + } 1.1441 + 1.1442 + return false; 1.1443 + } 1.1444 + catch (e) 1.1445 + { 1.1446 + this._handleError(e); 1.1447 + return false; 1.1448 + } 1.1449 + }, 1.1450 + 1.1451 + /** 1.1452 +* Does various post-header checks on the data in this request. 1.1453 +* 1.1454 +* @throws : HttpError 1.1455 +* if the request was malformed in some way 1.1456 +*/ 1.1457 + _validateRequest: function() 1.1458 + { 1.1459 + NS_ASSERT(this._state == READER_IN_BODY); 1.1460 + 1.1461 + dumpn("*** _validateRequest"); 1.1462 + 1.1463 + var metadata = this._metadata; 1.1464 + var headers = metadata._headers; 1.1465 + 1.1466 + // 19.6.1.1 -- servers MUST report 400 to HTTP/1.1 requests w/o Host header 1.1467 + var identity = this._connection.server.identity; 1.1468 + if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1)) 1.1469 + { 1.1470 + if (!headers.hasHeader("Host")) 1.1471 + { 1.1472 + dumpn("*** malformed HTTP/1.1 or greater request with no Host header!"); 1.1473 + throw HTTP_400; 1.1474 + } 1.1475 + 1.1476 + // If the Request-URI wasn't absolute, then we need to determine our host. 1.1477 + // We have to determine what scheme was used to access us based on the 1.1478 + // server identity data at this point, because the request just doesn't 1.1479 + // contain enough data on its own to do this, sadly. 1.1480 + if (!metadata._host) 1.1481 + { 1.1482 + var host, port; 1.1483 + var hostPort = headers.getHeader("Host"); 1.1484 + var colon = hostPort.indexOf(":"); 1.1485 + if (colon < 0) 1.1486 + { 1.1487 + host = hostPort; 1.1488 + port = ""; 1.1489 + } 1.1490 + else 1.1491 + { 1.1492 + host = hostPort.substring(0, colon); 1.1493 + port = hostPort.substring(colon + 1); 1.1494 + } 1.1495 + 1.1496 + // NB: We allow an empty port here because, oddly, a colon may be 1.1497 + // present even without a port number, e.g. "example.com:"; in this 1.1498 + // case the default port applies. 1.1499 + if (!HOST_REGEX.test(host) || !/^\d*$/.test(port)) 1.1500 + { 1.1501 + dumpn("*** malformed hostname (" + hostPort + ") in Host " + 1.1502 + "header, 400 time"); 1.1503 + throw HTTP_400; 1.1504 + } 1.1505 + 1.1506 + // If we're not given a port, we're stuck, because we don't know what 1.1507 + // scheme to use to look up the correct port here, in general. Since 1.1508 + // the HTTPS case requires a tunnel/proxy and thus requires that the 1.1509 + // requested URI be absolute (and thus contain the necessary 1.1510 + // information), let's assume HTTP will prevail and use that. 1.1511 + port = +port || 80; 1.1512 + 1.1513 + var scheme = identity.getScheme(host, port); 1.1514 + if (!scheme) 1.1515 + { 1.1516 + dumpn("*** unrecognized hostname (" + hostPort + ") in Host " + 1.1517 + "header, 400 time"); 1.1518 + throw HTTP_400; 1.1519 + } 1.1520 + 1.1521 + metadata._scheme = scheme; 1.1522 + metadata._host = host; 1.1523 + metadata._port = port; 1.1524 + } 1.1525 + } 1.1526 + else 1.1527 + { 1.1528 + NS_ASSERT(metadata._host === undefined, 1.1529 + "HTTP/1.0 doesn't allow absolute paths in the request line!"); 1.1530 + 1.1531 + metadata._scheme = identity.primaryScheme; 1.1532 + metadata._host = identity.primaryHost; 1.1533 + metadata._port = identity.primaryPort; 1.1534 + } 1.1535 + 1.1536 + NS_ASSERT(identity.has(metadata._scheme, metadata._host, metadata._port), 1.1537 + "must have a location we recognize by now!"); 1.1538 + }, 1.1539 + 1.1540 + /** 1.1541 +* Handles responses in case of error, either in the server or in the request. 1.1542 +* 1.1543 +* @param e 1.1544 +* the specific error encountered, which is an HttpError in the case where 1.1545 +* the request is in some way invalid or cannot be fulfilled; if this isn't 1.1546 +* an HttpError we're going to be paranoid and shut down, because that 1.1547 +* shouldn't happen, ever 1.1548 +*/ 1.1549 + _handleError: function(e) 1.1550 + { 1.1551 + // Don't fall back into normal processing! 1.1552 + this._state = READER_FINISHED; 1.1553 + 1.1554 + var server = this._connection.server; 1.1555 + if (e instanceof HttpError) 1.1556 + { 1.1557 + var code = e.code; 1.1558 + } 1.1559 + else 1.1560 + { 1.1561 + dumpn("!!! UNEXPECTED ERROR: " + e + 1.1562 + (e.lineNumber ? ", line " + e.lineNumber : "")); 1.1563 + 1.1564 + // no idea what happened -- be paranoid and shut down 1.1565 + code = 500; 1.1566 + server._requestQuit(); 1.1567 + } 1.1568 + 1.1569 + // make attempted reuse of data an error 1.1570 + this._data = null; 1.1571 + 1.1572 + this._connection.processError(code, this._metadata); 1.1573 + }, 1.1574 + 1.1575 + /** 1.1576 +* Now that we've read the request line and headers, we can actually hand off 1.1577 +* the request to be handled. 1.1578 +* 1.1579 +* This method is called once per request, after the request line and all 1.1580 +* headers and the body, if any, have been received. 1.1581 +*/ 1.1582 + _handleResponse: function() 1.1583 + { 1.1584 + NS_ASSERT(this._state == READER_FINISHED); 1.1585 + 1.1586 + // We don't need the line-based data any more, so make attempted reuse an 1.1587 + // error. 1.1588 + this._data = null; 1.1589 + 1.1590 + this._connection.process(this._metadata); 1.1591 + }, 1.1592 + 1.1593 + 1.1594 + // PARSING 1.1595 + 1.1596 + /** 1.1597 +* Parses the request line for the HTTP request associated with this. 1.1598 +* 1.1599 +* @param line : string 1.1600 +* the request line 1.1601 +*/ 1.1602 + _parseRequestLine: function(line) 1.1603 + { 1.1604 + NS_ASSERT(this._state == READER_IN_REQUEST_LINE); 1.1605 + 1.1606 + dumpn("*** _parseRequestLine('" + line + "')"); 1.1607 + 1.1608 + var metadata = this._metadata; 1.1609 + 1.1610 + // clients and servers SHOULD accept any amount of SP or HT characters 1.1611 + // between fields, even though only a single SP is required (section 19.3) 1.1612 + var request = line.split(/[ \t]+/); 1.1613 + if (!request || request.length != 3) 1.1614 + throw HTTP_400; 1.1615 + 1.1616 + metadata._method = request[0]; 1.1617 + 1.1618 + // get the HTTP version 1.1619 + var ver = request[2]; 1.1620 + var match = ver.match(/^HTTP\/(\d+\.\d+)$/); 1.1621 + if (!match) 1.1622 + throw HTTP_400; 1.1623 + 1.1624 + // determine HTTP version 1.1625 + try 1.1626 + { 1.1627 + metadata._httpVersion = new nsHttpVersion(match[1]); 1.1628 + if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_0)) 1.1629 + throw "unsupported HTTP version"; 1.1630 + } 1.1631 + catch (e) 1.1632 + { 1.1633 + // we support HTTP/1.0 and HTTP/1.1 only 1.1634 + throw HTTP_501; 1.1635 + } 1.1636 + 1.1637 + 1.1638 + var fullPath = request[1]; 1.1639 + var serverIdentity = this._connection.server.identity; 1.1640 + 1.1641 + var scheme, host, port; 1.1642 + 1.1643 + if (fullPath.charAt(0) != "/") 1.1644 + { 1.1645 + // No absolute paths in the request line in HTTP prior to 1.1 1.1646 + if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1)) 1.1647 + throw HTTP_400; 1.1648 + 1.1649 + try 1.1650 + { 1.1651 + var uri = Cc["@mozilla.org/network/io-service;1"] 1.1652 + .getService(Ci.nsIIOService) 1.1653 + .newURI(fullPath, null, null); 1.1654 + fullPath = uri.path; 1.1655 + scheme = uri.scheme; 1.1656 + host = metadata._host = uri.asciiHost; 1.1657 + port = uri.port; 1.1658 + if (port === -1) 1.1659 + { 1.1660 + if (scheme === "http") 1.1661 + port = 80; 1.1662 + else if (scheme === "https") 1.1663 + port = 443; 1.1664 + else 1.1665 + throw HTTP_400; 1.1666 + } 1.1667 + } 1.1668 + catch (e) 1.1669 + { 1.1670 + // If the host is not a valid host on the server, the response MUST be a 1.1671 + // 400 (Bad Request) error message (section 5.2). Alternately, the URI 1.1672 + // is malformed. 1.1673 + throw HTTP_400; 1.1674 + } 1.1675 + 1.1676 + if (!serverIdentity.has(scheme, host, port) || fullPath.charAt(0) != "/") 1.1677 + throw HTTP_400; 1.1678 + } 1.1679 + 1.1680 + var splitter = fullPath.indexOf("?"); 1.1681 + if (splitter < 0) 1.1682 + { 1.1683 + // _queryString already set in ctor 1.1684 + metadata._path = fullPath; 1.1685 + } 1.1686 + else 1.1687 + { 1.1688 + metadata._path = fullPath.substring(0, splitter); 1.1689 + metadata._queryString = fullPath.substring(splitter + 1); 1.1690 + } 1.1691 + 1.1692 + metadata._scheme = scheme; 1.1693 + metadata._host = host; 1.1694 + metadata._port = port; 1.1695 + }, 1.1696 + 1.1697 + /** 1.1698 +* Parses all available HTTP headers in this until the header-ending CRLFCRLF, 1.1699 +* adding them to the store of headers in the request. 1.1700 +* 1.1701 +* @throws 1.1702 +* HTTP_400 if the headers are malformed 1.1703 +* @returns boolean 1.1704 +* true if all headers have now been processed, false otherwise 1.1705 +*/ 1.1706 + _parseHeaders: function() 1.1707 + { 1.1708 + NS_ASSERT(this._state == READER_IN_HEADERS); 1.1709 + 1.1710 + dumpn("*** _parseHeaders"); 1.1711 + 1.1712 + var data = this._data; 1.1713 + 1.1714 + var headers = this._metadata._headers; 1.1715 + var lastName = this._lastHeaderName; 1.1716 + var lastVal = this._lastHeaderValue; 1.1717 + 1.1718 + var line = {}; 1.1719 + while (true) 1.1720 + { 1.1721 + NS_ASSERT(!((lastVal === undefined) ^ (lastName === undefined)), 1.1722 + lastName === undefined ? 1.1723 + "lastVal without lastName? lastVal: '" + lastVal + "'" : 1.1724 + "lastName without lastVal? lastName: '" + lastName + "'"); 1.1725 + 1.1726 + if (!data.readLine(line)) 1.1727 + { 1.1728 + // save any data we have from the header we might still be processing 1.1729 + this._lastHeaderName = lastName; 1.1730 + this._lastHeaderValue = lastVal; 1.1731 + return false; 1.1732 + } 1.1733 + 1.1734 + var lineText = line.value; 1.1735 + var firstChar = lineText.charAt(0); 1.1736 + 1.1737 + // blank line means end of headers 1.1738 + if (lineText == "") 1.1739 + { 1.1740 + // we're finished with the previous header 1.1741 + if (lastName) 1.1742 + { 1.1743 + try 1.1744 + { 1.1745 + headers.setHeader(lastName, lastVal, true); 1.1746 + } 1.1747 + catch (e) 1.1748 + { 1.1749 + dumpn("*** e == " + e); 1.1750 + throw HTTP_400; 1.1751 + } 1.1752 + } 1.1753 + else 1.1754 + { 1.1755 + // no headers in request -- valid for HTTP/1.0 requests 1.1756 + } 1.1757 + 1.1758 + // either way, we're done processing headers 1.1759 + this._state = READER_IN_BODY; 1.1760 + return true; 1.1761 + } 1.1762 + else if (firstChar == " " || firstChar == "\t") 1.1763 + { 1.1764 + // multi-line header if we've already seen a header line 1.1765 + if (!lastName) 1.1766 + { 1.1767 + // we don't have a header to continue! 1.1768 + throw HTTP_400; 1.1769 + } 1.1770 + 1.1771 + // append this line's text to the value; starts with SP/HT, so no need 1.1772 + // for separating whitespace 1.1773 + lastVal += lineText; 1.1774 + } 1.1775 + else 1.1776 + { 1.1777 + // we have a new header, so set the old one (if one existed) 1.1778 + if (lastName) 1.1779 + { 1.1780 + try 1.1781 + { 1.1782 + headers.setHeader(lastName, lastVal, true); 1.1783 + } 1.1784 + catch (e) 1.1785 + { 1.1786 + dumpn("*** e == " + e); 1.1787 + throw HTTP_400; 1.1788 + } 1.1789 + } 1.1790 + 1.1791 + var colon = lineText.indexOf(":"); // first colon must be splitter 1.1792 + if (colon < 1) 1.1793 + { 1.1794 + // no colon or missing header field-name 1.1795 + throw HTTP_400; 1.1796 + } 1.1797 + 1.1798 + // set header name, value (to be set in the next loop, usually) 1.1799 + lastName = lineText.substring(0, colon); 1.1800 + lastVal = lineText.substring(colon + 1); 1.1801 + } // empty, continuation, start of header 1.1802 + } // while (true) 1.1803 + } 1.1804 +}; 1.1805 + 1.1806 + 1.1807 +/** The character codes for CR and LF. */ 1.1808 +const CR = 0x0D, LF = 0x0A; 1.1809 + 1.1810 +/** 1.1811 +* Calculates the number of characters before the first CRLF pair in array, or 1.1812 +* -1 if the array contains no CRLF pair. 1.1813 +* 1.1814 +* @param array : Array 1.1815 +* an array of numbers in the range [0, 256), each representing a single 1.1816 +* character; the first CRLF is the lowest index i where 1.1817 +* |array[i] == "\r".charCodeAt(0)| and |array[i+1] == "\n".charCodeAt(0)|, 1.1818 +* if such an |i| exists, and -1 otherwise 1.1819 +* @returns int 1.1820 +* the index of the first CRLF if any were present, -1 otherwise 1.1821 +*/ 1.1822 +function findCRLF(array) 1.1823 +{ 1.1824 + for (var i = array.indexOf(CR); i >= 0; i = array.indexOf(CR, i + 1)) 1.1825 + { 1.1826 + if (array[i + 1] == LF) 1.1827 + return i; 1.1828 + } 1.1829 + return -1; 1.1830 +} 1.1831 + 1.1832 + 1.1833 +/** 1.1834 +* A container which provides line-by-line access to the arrays of bytes with 1.1835 +* which it is seeded. 1.1836 +*/ 1.1837 +function LineData() 1.1838 +{ 1.1839 + /** An array of queued bytes from which to get line-based characters. */ 1.1840 + this._data = []; 1.1841 +} 1.1842 +LineData.prototype = 1.1843 +{ 1.1844 + /** 1.1845 +* Appends the bytes in the given array to the internal data cache maintained 1.1846 +* by this. 1.1847 +*/ 1.1848 + appendBytes: function(bytes) 1.1849 + { 1.1850 + Array.prototype.push.apply(this._data, bytes); 1.1851 + }, 1.1852 + 1.1853 + /** 1.1854 +* Removes and returns a line of data, delimited by CRLF, from this. 1.1855 +* 1.1856 +* @param out 1.1857 +* an object whose "value" property will be set to the first line of text 1.1858 +* present in this, sans CRLF, if this contains a full CRLF-delimited line 1.1859 +* of text; if this doesn't contain enough data, the value of the property 1.1860 +* is undefined 1.1861 +* @returns boolean 1.1862 +* true if a full line of data could be read from the data in this, false 1.1863 +* otherwise 1.1864 +*/ 1.1865 + readLine: function(out) 1.1866 + { 1.1867 + var data = this._data; 1.1868 + var length = findCRLF(data); 1.1869 + if (length < 0) 1.1870 + return false; 1.1871 + 1.1872 + // 1.1873 + // We have the index of the CR, so remove all the characters, including 1.1874 + // CRLF, from the array with splice, and convert the removed array into the 1.1875 + // corresponding string, from which we then strip the trailing CRLF. 1.1876 + // 1.1877 + // Getting the line in this matter acknowledges that substring is an O(1) 1.1878 + // operation in SpiderMonkey because strings are immutable, whereas two 1.1879 + // splices, both from the beginning of the data, are less likely to be as 1.1880 + // cheap as a single splice plus two extra character conversions. 1.1881 + // 1.1882 + var line = String.fromCharCode.apply(null, data.splice(0, length + 2)); 1.1883 + out.value = line.substring(0, length); 1.1884 + 1.1885 + return true; 1.1886 + }, 1.1887 + 1.1888 + /** 1.1889 +* Removes the bytes currently within this and returns them in an array. 1.1890 +* 1.1891 +* @returns Array 1.1892 +* the bytes within this when this method is called 1.1893 +*/ 1.1894 + purge: function() 1.1895 + { 1.1896 + var data = this._data; 1.1897 + this._data = []; 1.1898 + return data; 1.1899 + } 1.1900 +}; 1.1901 + 1.1902 + 1.1903 + 1.1904 +/** 1.1905 +* Creates a request-handling function for an nsIHttpRequestHandler object. 1.1906 +*/ 1.1907 +function createHandlerFunc(handler) 1.1908 +{ 1.1909 + return function(metadata, response) { handler.handle(metadata, response); }; 1.1910 +} 1.1911 + 1.1912 + 1.1913 +/** 1.1914 +* The default handler for directories; writes an HTML response containing a 1.1915 +* slightly-formatted directory listing. 1.1916 +*/ 1.1917 +function defaultIndexHandler(metadata, response) 1.1918 +{ 1.1919 + response.setHeader("Content-Type", "text/html", false); 1.1920 + 1.1921 + var path = htmlEscape(decodeURI(metadata.path)); 1.1922 + 1.1923 + // 1.1924 + // Just do a very basic bit of directory listings -- no need for too much 1.1925 + // fanciness, especially since we don't have a style sheet in which we can 1.1926 + // stick rules (don't want to pollute the default path-space). 1.1927 + // 1.1928 + 1.1929 + var body = '<html>\ 1.1930 +<head>\ 1.1931 +<title>' + path + '</title>\ 1.1932 +</head>\ 1.1933 +<body>\ 1.1934 +<h1>' + path + '</h1>\ 1.1935 +<ol style="list-style-type: none">'; 1.1936 + 1.1937 + var directory = metadata.getProperty("directory").QueryInterface(Ci.nsILocalFile); 1.1938 + NS_ASSERT(directory && directory.isDirectory()); 1.1939 + 1.1940 + var fileList = []; 1.1941 + var files = directory.directoryEntries; 1.1942 + while (files.hasMoreElements()) 1.1943 + { 1.1944 + var f = files.getNext().QueryInterface(Ci.nsIFile); 1.1945 + var name = f.leafName; 1.1946 + if (!f.isHidden() && 1.1947 + (name.charAt(name.length - 1) != HIDDEN_CHAR || 1.1948 + name.charAt(name.length - 2) == HIDDEN_CHAR)) 1.1949 + fileList.push(f); 1.1950 + } 1.1951 + 1.1952 + fileList.sort(fileSort); 1.1953 + 1.1954 + for (var i = 0; i < fileList.length; i++) 1.1955 + { 1.1956 + var file = fileList[i]; 1.1957 + try 1.1958 + { 1.1959 + var name = file.leafName; 1.1960 + if (name.charAt(name.length - 1) == HIDDEN_CHAR) 1.1961 + name = name.substring(0, name.length - 1); 1.1962 + var sep = file.isDirectory() ? "/" : ""; 1.1963 + 1.1964 + // Note: using " to delimit the attribute here because encodeURIComponent 1.1965 + // passes through '. 1.1966 + var item = '<li><a href="' + encodeURIComponent(name) + sep + '">' + 1.1967 + htmlEscape(name) + sep + 1.1968 + '</a></li>'; 1.1969 + 1.1970 + body += item; 1.1971 + } 1.1972 + catch (e) { /* some file system error, ignore the file */ } 1.1973 + } 1.1974 + 1.1975 + body += ' </ol>\ 1.1976 +</body>\ 1.1977 +</html>'; 1.1978 + 1.1979 + response.bodyOutputStream.write(body, body.length); 1.1980 +} 1.1981 + 1.1982 +/** 1.1983 +* Sorts a and b (nsIFile objects) into an aesthetically pleasing order. 1.1984 +*/ 1.1985 +function fileSort(a, b) 1.1986 +{ 1.1987 + var dira = a.isDirectory(), dirb = b.isDirectory(); 1.1988 + 1.1989 + if (dira && !dirb) 1.1990 + return -1; 1.1991 + if (dirb && !dira) 1.1992 + return 1; 1.1993 + 1.1994 + var namea = a.leafName.toLowerCase(), nameb = b.leafName.toLowerCase(); 1.1995 + return nameb > namea ? -1 : 1; 1.1996 +} 1.1997 + 1.1998 + 1.1999 +/** 1.2000 +* Converts an externally-provided path into an internal path for use in 1.2001 +* determining file mappings. 1.2002 +* 1.2003 +* @param path 1.2004 +* the path to convert 1.2005 +* @param encoded 1.2006 +* true if the given path should be passed through decodeURI prior to 1.2007 +* conversion 1.2008 +* @throws URIError 1.2009 +* if path is incorrectly encoded 1.2010 +*/ 1.2011 +function toInternalPath(path, encoded) 1.2012 +{ 1.2013 + if (encoded) 1.2014 + path = decodeURI(path); 1.2015 + 1.2016 + var comps = path.split("/"); 1.2017 + for (var i = 0, sz = comps.length; i < sz; i++) 1.2018 + { 1.2019 + var comp = comps[i]; 1.2020 + if (comp.charAt(comp.length - 1) == HIDDEN_CHAR) 1.2021 + comps[i] = comp + HIDDEN_CHAR; 1.2022 + } 1.2023 + return comps.join("/"); 1.2024 +} 1.2025 + 1.2026 + 1.2027 +/** 1.2028 +* Adds custom-specified headers for the given file to the given response, if 1.2029 +* any such headers are specified. 1.2030 +* 1.2031 +* @param file 1.2032 +* the file on the disk which is to be written 1.2033 +* @param metadata 1.2034 +* metadata about the incoming request 1.2035 +* @param response 1.2036 +* the Response to which any specified headers/data should be written 1.2037 +* @throws HTTP_500 1.2038 +* if an error occurred while processing custom-specified headers 1.2039 +*/ 1.2040 +function maybeAddHeaders(file, metadata, response) 1.2041 +{ 1.2042 + var name = file.leafName; 1.2043 + if (name.charAt(name.length - 1) == HIDDEN_CHAR) 1.2044 + name = name.substring(0, name.length - 1); 1.2045 + 1.2046 + var headerFile = file.parent; 1.2047 + headerFile.append(name + HEADERS_SUFFIX); 1.2048 + 1.2049 + if (!headerFile.exists()) 1.2050 + return; 1.2051 + 1.2052 + const PR_RDONLY = 0x01; 1.2053 + var fis = new FileInputStream(headerFile, PR_RDONLY, parseInt("444", 8), 1.2054 + Ci.nsIFileInputStream.CLOSE_ON_EOF); 1.2055 + 1.2056 + try 1.2057 + { 1.2058 + var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0); 1.2059 + lis.QueryInterface(Ci.nsIUnicharLineInputStream); 1.2060 + 1.2061 + var line = {value: ""}; 1.2062 + var more = lis.readLine(line); 1.2063 + 1.2064 + if (!more && line.value == "") 1.2065 + return; 1.2066 + 1.2067 + 1.2068 + // request line 1.2069 + 1.2070 + var status = line.value; 1.2071 + if (status.indexOf("HTTP ") == 0) 1.2072 + { 1.2073 + status = status.substring(5); 1.2074 + var space = status.indexOf(" "); 1.2075 + var code, description; 1.2076 + if (space < 0) 1.2077 + { 1.2078 + code = status; 1.2079 + description = ""; 1.2080 + } 1.2081 + else 1.2082 + { 1.2083 + code = status.substring(0, space); 1.2084 + description = status.substring(space + 1, status.length); 1.2085 + } 1.2086 + 1.2087 + response.setStatusLine(metadata.httpVersion, parseInt(code, 10), description); 1.2088 + 1.2089 + line.value = ""; 1.2090 + more = lis.readLine(line); 1.2091 + } 1.2092 + 1.2093 + // headers 1.2094 + while (more || line.value != "") 1.2095 + { 1.2096 + var header = line.value; 1.2097 + var colon = header.indexOf(":"); 1.2098 + 1.2099 + response.setHeader(header.substring(0, colon), 1.2100 + header.substring(colon + 1, header.length), 1.2101 + false); // allow overriding server-set headers 1.2102 + 1.2103 + line.value = ""; 1.2104 + more = lis.readLine(line); 1.2105 + } 1.2106 + } 1.2107 + catch (e) 1.2108 + { 1.2109 + dumpn("WARNING: error in headers for " + metadata.path + ": " + e); 1.2110 + throw HTTP_500; 1.2111 + } 1.2112 + finally 1.2113 + { 1.2114 + fis.close(); 1.2115 + } 1.2116 +} 1.2117 + 1.2118 + 1.2119 +/** 1.2120 +* An object which handles requests for a server, executing default and 1.2121 +* overridden behaviors as instructed by the code which uses and manipulates it. 1.2122 +* Default behavior includes the paths / and /trace (diagnostics), with some 1.2123 +* support for HTTP error pages for various codes and fallback to HTTP 500 if 1.2124 +* those codes fail for any reason. 1.2125 +* 1.2126 +* @param server : nsHttpServer 1.2127 +* the server in which this handler is being used 1.2128 +*/ 1.2129 +function ServerHandler(server) 1.2130 +{ 1.2131 + // FIELDS 1.2132 + 1.2133 + /** 1.2134 +* The nsHttpServer instance associated with this handler. 1.2135 +*/ 1.2136 + this._server = server; 1.2137 + 1.2138 + /** 1.2139 +* A FileMap object containing the set of path->nsILocalFile mappings for 1.2140 +* all directory mappings set in the server (e.g., "/" for /var/www/html/, 1.2141 +* "/foo/bar/" for /local/path/, and "/foo/bar/baz/" for /local/path2). 1.2142 +* 1.2143 +* Note carefully: the leading and trailing "/" in each path (not file) are 1.2144 +* removed before insertion to simplify the code which uses this. You have 1.2145 +* been warned! 1.2146 +*/ 1.2147 + this._pathDirectoryMap = new FileMap(); 1.2148 + 1.2149 + /** 1.2150 +* Custom request handlers for the server in which this resides. Path-handler 1.2151 +* pairs are stored as property-value pairs in this property. 1.2152 +* 1.2153 +* @see ServerHandler.prototype._defaultPaths 1.2154 +*/ 1.2155 + this._overridePaths = {}; 1.2156 + 1.2157 + /** 1.2158 +* Custom request handlers for the server in which this resides. Prefix-handler 1.2159 +* pairs are stored as property-value pairs in this property. 1.2160 +*/ 1.2161 + this._overridePrefixes = {}; 1.2162 + 1.2163 + /** 1.2164 +* Custom request handlers for the error handlers in the server in which this 1.2165 +* resides. Path-handler pairs are stored as property-value pairs in this 1.2166 +* property. 1.2167 +* 1.2168 +* @see ServerHandler.prototype._defaultErrors 1.2169 +*/ 1.2170 + this._overrideErrors = {}; 1.2171 + 1.2172 + /** 1.2173 +* Maps file extensions to their MIME types in the server, overriding any 1.2174 +* mapping that might or might not exist in the MIME service. 1.2175 +*/ 1.2176 + this._mimeMappings = {}; 1.2177 + 1.2178 + /** 1.2179 +* The default handler for requests for directories, used to serve directories 1.2180 +* when no index file is present. 1.2181 +*/ 1.2182 + this._indexHandler = defaultIndexHandler; 1.2183 + 1.2184 + /** Per-path state storage for the server. */ 1.2185 + this._state = {}; 1.2186 + 1.2187 + /** Entire-server state storage. */ 1.2188 + this._sharedState = {}; 1.2189 + 1.2190 + /** Entire-server state storage for nsISupports values. */ 1.2191 + this._objectState = {}; 1.2192 +} 1.2193 +ServerHandler.prototype = 1.2194 +{ 1.2195 + // PUBLIC API 1.2196 + 1.2197 + /** 1.2198 +* Handles a request to this server, responding to the request appropriately 1.2199 +* and initiating server shutdown if necessary. 1.2200 +* 1.2201 +* This method never throws an exception. 1.2202 +* 1.2203 +* @param connection : Connection 1.2204 +* the connection for this request 1.2205 +*/ 1.2206 + handleResponse: function(connection) 1.2207 + { 1.2208 + var request = connection.request; 1.2209 + var response = new Response(connection); 1.2210 + 1.2211 + var path = request.path; 1.2212 + dumpn("*** path == " + path); 1.2213 + 1.2214 + try 1.2215 + { 1.2216 + try 1.2217 + { 1.2218 + if (path in this._overridePaths) 1.2219 + { 1.2220 + // explicit paths first, then files based on existing directory mappings, 1.2221 + // then (if the file doesn't exist) built-in server default paths 1.2222 + dumpn("calling override for " + path); 1.2223 + this._overridePaths[path](request, response); 1.2224 + } 1.2225 + else 1.2226 + { 1.2227 + let longestPrefix = ""; 1.2228 + for (let prefix in this._overridePrefixes) 1.2229 + { 1.2230 + if (prefix.length > longestPrefix.length && path.startsWith(prefix)) 1.2231 + { 1.2232 + longestPrefix = prefix; 1.2233 + } 1.2234 + } 1.2235 + if (longestPrefix.length > 0) 1.2236 + { 1.2237 + dumpn("calling prefix override for " + longestPrefix); 1.2238 + this._overridePrefixes[longestPrefix](request, response); 1.2239 + } 1.2240 + else 1.2241 + { 1.2242 + this._handleDefault(request, response); 1.2243 + } 1.2244 + } 1.2245 + } 1.2246 + catch (e) 1.2247 + { 1.2248 + if (response.partiallySent()) 1.2249 + { 1.2250 + response.abort(e); 1.2251 + return; 1.2252 + } 1.2253 + 1.2254 + if (!(e instanceof HttpError)) 1.2255 + { 1.2256 + dumpn("*** unexpected error: e == " + e); 1.2257 + throw HTTP_500; 1.2258 + } 1.2259 + if (e.code !== 404) 1.2260 + throw e; 1.2261 + 1.2262 + dumpn("*** default: " + (path in this._defaultPaths)); 1.2263 + 1.2264 + response = new Response(connection); 1.2265 + if (path in this._defaultPaths) 1.2266 + this._defaultPaths[path](request, response); 1.2267 + else 1.2268 + throw HTTP_404; 1.2269 + } 1.2270 + } 1.2271 + catch (e) 1.2272 + { 1.2273 + if (response.partiallySent()) 1.2274 + { 1.2275 + response.abort(e); 1.2276 + return; 1.2277 + } 1.2278 + 1.2279 + var errorCode = "internal"; 1.2280 + 1.2281 + try 1.2282 + { 1.2283 + if (!(e instanceof HttpError)) 1.2284 + throw e; 1.2285 + 1.2286 + errorCode = e.code; 1.2287 + dumpn("*** errorCode == " + errorCode); 1.2288 + 1.2289 + response = new Response(connection); 1.2290 + if (e.customErrorHandling) 1.2291 + e.customErrorHandling(response); 1.2292 + this._handleError(errorCode, request, response); 1.2293 + return; 1.2294 + } 1.2295 + catch (e2) 1.2296 + { 1.2297 + dumpn("*** error handling " + errorCode + " error: " + 1.2298 + "e2 == " + e2 + ", shutting down server"); 1.2299 + 1.2300 + connection.server._requestQuit(); 1.2301 + response.abort(e2); 1.2302 + return; 1.2303 + } 1.2304 + } 1.2305 + 1.2306 + response.complete(); 1.2307 + }, 1.2308 + 1.2309 + // 1.2310 + // see nsIHttpServer.registerFile 1.2311 + // 1.2312 + registerFile: function(path, file) 1.2313 + { 1.2314 + if (!file) 1.2315 + { 1.2316 + dumpn("*** unregistering '" + path + "' mapping"); 1.2317 + delete this._overridePaths[path]; 1.2318 + return; 1.2319 + } 1.2320 + 1.2321 + dumpn("*** registering '" + path + "' as mapping to " + file.path); 1.2322 + file = file.clone(); 1.2323 + 1.2324 + var self = this; 1.2325 + this._overridePaths[path] = 1.2326 + function(request, response) 1.2327 + { 1.2328 + if (!file.exists()) 1.2329 + throw HTTP_404; 1.2330 + 1.2331 + response.setStatusLine(request.httpVersion, 200, "OK"); 1.2332 + self._writeFileResponse(request, file, response, 0, file.fileSize); 1.2333 + }; 1.2334 + }, 1.2335 + 1.2336 + // 1.2337 + // see nsIHttpServer.registerPathHandler 1.2338 + // 1.2339 + registerPathHandler: function(path, handler) 1.2340 + { 1.2341 + // XXX true path validation! 1.2342 + if (path.charAt(0) != "/") 1.2343 + throw Cr.NS_ERROR_INVALID_ARG; 1.2344 + 1.2345 + this._handlerToField(handler, this._overridePaths, path); 1.2346 + }, 1.2347 + 1.2348 + // 1.2349 + // see nsIHttpServer.registerPrefixHandler 1.2350 + // 1.2351 + registerPrefixHandler: function(prefix, handler) 1.2352 + { 1.2353 + // XXX true prefix validation! 1.2354 + if (!(prefix.startsWith("/") && prefix.endsWith("/"))) 1.2355 + throw Cr.NS_ERROR_INVALID_ARG; 1.2356 + 1.2357 + this._handlerToField(handler, this._overridePrefixes, prefix); 1.2358 + }, 1.2359 + 1.2360 + // 1.2361 + // see nsIHttpServer.registerDirectory 1.2362 + // 1.2363 + registerDirectory: function(path, directory) 1.2364 + { 1.2365 + // strip off leading and trailing '/' so that we can use lastIndexOf when 1.2366 + // determining exactly how a path maps onto a mapped directory -- 1.2367 + // conditional is required here to deal with "/".substring(1, 0) being 1.2368 + // converted to "/".substring(0, 1) per the JS specification 1.2369 + var key = path.length == 1 ? "" : path.substring(1, path.length - 1); 1.2370 + 1.2371 + // the path-to-directory mapping code requires that the first character not 1.2372 + // be "/", or it will go into an infinite loop 1.2373 + if (key.charAt(0) == "/") 1.2374 + throw Cr.NS_ERROR_INVALID_ARG; 1.2375 + 1.2376 + key = toInternalPath(key, false); 1.2377 + 1.2378 + if (directory) 1.2379 + { 1.2380 + dumpn("*** mapping '" + path + "' to the location " + directory.path); 1.2381 + this._pathDirectoryMap.put(key, directory); 1.2382 + } 1.2383 + else 1.2384 + { 1.2385 + dumpn("*** removing mapping for '" + path + "'"); 1.2386 + this._pathDirectoryMap.put(key, null); 1.2387 + } 1.2388 + }, 1.2389 + 1.2390 + // 1.2391 + // see nsIHttpServer.registerErrorHandler 1.2392 + // 1.2393 + registerErrorHandler: function(err, handler) 1.2394 + { 1.2395 + if (!(err in HTTP_ERROR_CODES)) 1.2396 + dumpn("*** WARNING: registering non-HTTP/1.1 error code " + 1.2397 + "(" + err + ") handler -- was this intentional?"); 1.2398 + 1.2399 + this._handlerToField(handler, this._overrideErrors, err); 1.2400 + }, 1.2401 + 1.2402 + // 1.2403 + // see nsIHttpServer.setIndexHandler 1.2404 + // 1.2405 + setIndexHandler: function(handler) 1.2406 + { 1.2407 + if (!handler) 1.2408 + handler = defaultIndexHandler; 1.2409 + else if (typeof(handler) != "function") 1.2410 + handler = createHandlerFunc(handler); 1.2411 + 1.2412 + this._indexHandler = handler; 1.2413 + }, 1.2414 + 1.2415 + // 1.2416 + // see nsIHttpServer.registerContentType 1.2417 + // 1.2418 + registerContentType: function(ext, type) 1.2419 + { 1.2420 + if (!type) 1.2421 + delete this._mimeMappings[ext]; 1.2422 + else 1.2423 + this._mimeMappings[ext] = headerUtils.normalizeFieldValue(type); 1.2424 + }, 1.2425 + 1.2426 + // PRIVATE API 1.2427 + 1.2428 + /** 1.2429 +* Sets or remove (if handler is null) a handler in an object with a key. 1.2430 +* 1.2431 +* @param handler 1.2432 +* a handler, either function or an nsIHttpRequestHandler 1.2433 +* @param dict 1.2434 +* The object to attach the handler to. 1.2435 +* @param key 1.2436 +* The field name of the handler. 1.2437 +*/ 1.2438 + _handlerToField: function(handler, dict, key) 1.2439 + { 1.2440 + // for convenience, handler can be a function if this is run from xpcshell 1.2441 + if (typeof(handler) == "function") 1.2442 + dict[key] = handler; 1.2443 + else if (handler) 1.2444 + dict[key] = createHandlerFunc(handler); 1.2445 + else 1.2446 + delete dict[key]; 1.2447 + }, 1.2448 + 1.2449 + /** 1.2450 +* Handles a request which maps to a file in the local filesystem (if a base 1.2451 +* path has already been set; otherwise the 404 error is thrown). 1.2452 +* 1.2453 +* @param metadata : Request 1.2454 +* metadata for the incoming request 1.2455 +* @param response : Response 1.2456 +* an uninitialized Response to the given request, to be initialized by a 1.2457 +* request handler 1.2458 +* @throws HTTP_### 1.2459 +* if an HTTP error occurred (usually HTTP_404); note that in this case the 1.2460 +* calling code must handle post-processing of the response 1.2461 +*/ 1.2462 + _handleDefault: function(metadata, response) 1.2463 + { 1.2464 + dumpn("*** _handleDefault()"); 1.2465 + 1.2466 + response.setStatusLine(metadata.httpVersion, 200, "OK"); 1.2467 + 1.2468 + var path = metadata.path; 1.2469 + NS_ASSERT(path.charAt(0) == "/", "invalid path: <" + path + ">"); 1.2470 + 1.2471 + // determine the actual on-disk file; this requires finding the deepest 1.2472 + // path-to-directory mapping in the requested URL 1.2473 + var file = this._getFileForPath(path); 1.2474 + 1.2475 + // the "file" might be a directory, in which case we either serve the 1.2476 + // contained index.html or make the index handler write the response 1.2477 + if (file.exists() && file.isDirectory()) 1.2478 + { 1.2479 + file.append("index.html"); // make configurable? 1.2480 + if (!file.exists() || file.isDirectory()) 1.2481 + { 1.2482 + metadata._ensurePropertyBag(); 1.2483 + metadata._bag.setPropertyAsInterface("directory", file.parent); 1.2484 + this._indexHandler(metadata, response); 1.2485 + return; 1.2486 + } 1.2487 + } 1.2488 + 1.2489 + // alternately, the file might not exist 1.2490 + if (!file.exists()) 1.2491 + throw HTTP_404; 1.2492 + 1.2493 + var start, end; 1.2494 + if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1) && 1.2495 + metadata.hasHeader("Range") && 1.2496 + this._getTypeFromFile(file) !== SJS_TYPE) 1.2497 + { 1.2498 + var rangeMatch = metadata.getHeader("Range").match(/^bytes=(\d+)?-(\d+)?$/); 1.2499 + if (!rangeMatch) 1.2500 + throw HTTP_400; 1.2501 + 1.2502 + if (rangeMatch[1] !== undefined) 1.2503 + start = parseInt(rangeMatch[1], 10); 1.2504 + 1.2505 + if (rangeMatch[2] !== undefined) 1.2506 + end = parseInt(rangeMatch[2], 10); 1.2507 + 1.2508 + if (start === undefined && end === undefined) 1.2509 + throw HTTP_400; 1.2510 + 1.2511 + // No start given, so the end is really the count of bytes from the 1.2512 + // end of the file. 1.2513 + if (start === undefined) 1.2514 + { 1.2515 + start = Math.max(0, file.fileSize - end); 1.2516 + end = file.fileSize - 1; 1.2517 + } 1.2518 + 1.2519 + // start and end are inclusive 1.2520 + if (end === undefined || end >= file.fileSize) 1.2521 + end = file.fileSize - 1; 1.2522 + 1.2523 + if (start !== undefined && start >= file.fileSize) { 1.2524 + var HTTP_416 = new HttpError(416, "Requested Range Not Satisfiable"); 1.2525 + HTTP_416.customErrorHandling = function(errorResponse) 1.2526 + { 1.2527 + maybeAddHeaders(file, metadata, errorResponse); 1.2528 + }; 1.2529 + throw HTTP_416; 1.2530 + } 1.2531 + 1.2532 + if (end < start) 1.2533 + { 1.2534 + response.setStatusLine(metadata.httpVersion, 200, "OK"); 1.2535 + start = 0; 1.2536 + end = file.fileSize - 1; 1.2537 + } 1.2538 + else 1.2539 + { 1.2540 + response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); 1.2541 + var contentRange = "bytes " + start + "-" + end + "/" + file.fileSize; 1.2542 + response.setHeader("Content-Range", contentRange); 1.2543 + } 1.2544 + } 1.2545 + else 1.2546 + { 1.2547 + start = 0; 1.2548 + end = file.fileSize - 1; 1.2549 + } 1.2550 + 1.2551 + // finally... 1.2552 + dumpn("*** handling '" + path + "' as mapping to " + file.path + " from " + 1.2553 + start + " to " + end + " inclusive"); 1.2554 + this._writeFileResponse(metadata, file, response, start, end - start + 1); 1.2555 + }, 1.2556 + 1.2557 + /** 1.2558 +* Writes an HTTP response for the given file, including setting headers for 1.2559 +* file metadata. 1.2560 +* 1.2561 +* @param metadata : Request 1.2562 +* the Request for which a response is being generated 1.2563 +* @param file : nsILocalFile 1.2564 +* the file which is to be sent in the response 1.2565 +* @param response : Response 1.2566 +* the response to which the file should be written 1.2567 +* @param offset: uint 1.2568 +* the byte offset to skip to when writing 1.2569 +* @param count: uint 1.2570 +* the number of bytes to write 1.2571 +*/ 1.2572 + _writeFileResponse: function(metadata, file, response, offset, count) 1.2573 + { 1.2574 + const PR_RDONLY = 0x01; 1.2575 + 1.2576 + var type = this._getTypeFromFile(file); 1.2577 + if (type === SJS_TYPE) 1.2578 + { 1.2579 + var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8), 1.2580 + Ci.nsIFileInputStream.CLOSE_ON_EOF); 1.2581 + 1.2582 + try 1.2583 + { 1.2584 + var sis = new ScriptableInputStream(fis); 1.2585 + var s = Cu.Sandbox(gGlobalObject); 1.2586 + s.importFunction(dump, "dump"); 1.2587 + 1.2588 + // Define a basic key-value state-preservation API across requests, with 1.2589 + // keys initially corresponding to the empty string. 1.2590 + var self = this; 1.2591 + var path = metadata.path; 1.2592 + s.importFunction(function getState(k) 1.2593 + { 1.2594 + return self._getState(path, k); 1.2595 + }); 1.2596 + s.importFunction(function setState(k, v) 1.2597 + { 1.2598 + self._setState(path, k, v); 1.2599 + }); 1.2600 + s.importFunction(function getSharedState(k) 1.2601 + { 1.2602 + return self._getSharedState(k); 1.2603 + }); 1.2604 + s.importFunction(function setSharedState(k, v) 1.2605 + { 1.2606 + self._setSharedState(k, v); 1.2607 + }); 1.2608 + s.importFunction(function getObjectState(k, callback) 1.2609 + { 1.2610 + callback(self._getObjectState(k)); 1.2611 + }); 1.2612 + s.importFunction(function setObjectState(k, v) 1.2613 + { 1.2614 + self._setObjectState(k, v); 1.2615 + }); 1.2616 + s.importFunction(function registerPathHandler(p, h) 1.2617 + { 1.2618 + self.registerPathHandler(p, h); 1.2619 + }); 1.2620 + 1.2621 + // Make it possible for sjs files to access their location 1.2622 + this._setState(path, "__LOCATION__", file.path); 1.2623 + 1.2624 + try 1.2625 + { 1.2626 + // Alas, the line number in errors dumped to console when calling the 1.2627 + // request handler is simply an offset from where we load the SJS file. 1.2628 + // Work around this in a reasonably non-fragile way by dynamically 1.2629 + // getting the line number where we evaluate the SJS file. Don't 1.2630 + // separate these two lines! 1.2631 + var line = new Error().lineNumber; 1.2632 + Cu.evalInSandbox(sis.read(file.fileSize), s); 1.2633 + } 1.2634 + catch (e) 1.2635 + { 1.2636 + dumpn("*** syntax error in SJS at " + file.path + ": " + e); 1.2637 + throw HTTP_500; 1.2638 + } 1.2639 + 1.2640 + try 1.2641 + { 1.2642 + s.handleRequest(metadata, response); 1.2643 + } 1.2644 + catch (e) 1.2645 + { 1.2646 + dump("*** error running SJS at " + file.path + ": " + 1.2647 + e + " on line " + 1.2648 + (e instanceof Error 1.2649 + ? e.lineNumber + " in httpd.js" 1.2650 + : (e.lineNumber - line)) + "\n"); 1.2651 + throw HTTP_500; 1.2652 + } 1.2653 + } 1.2654 + finally 1.2655 + { 1.2656 + fis.close(); 1.2657 + } 1.2658 + } 1.2659 + else 1.2660 + { 1.2661 + try 1.2662 + { 1.2663 + response.setHeader("Last-Modified", 1.2664 + toDateString(file.lastModifiedTime), 1.2665 + false); 1.2666 + } 1.2667 + catch (e) { /* lastModifiedTime threw, ignore */ } 1.2668 + 1.2669 + response.setHeader("Content-Type", type, false); 1.2670 + maybeAddHeaders(file, metadata, response); 1.2671 + response.setHeader("Content-Length", "" + count, false); 1.2672 + 1.2673 + var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8), 1.2674 + Ci.nsIFileInputStream.CLOSE_ON_EOF); 1.2675 + 1.2676 + offset = offset || 0; 1.2677 + count = count || file.fileSize; 1.2678 + NS_ASSERT(offset === 0 || offset < file.fileSize, "bad offset"); 1.2679 + NS_ASSERT(count >= 0, "bad count"); 1.2680 + NS_ASSERT(offset + count <= file.fileSize, "bad total data size"); 1.2681 + 1.2682 + try 1.2683 + { 1.2684 + if (offset !== 0) 1.2685 + { 1.2686 + // Seek (or read, if seeking isn't supported) to the correct offset so 1.2687 + // the data sent to the client matches the requested range. 1.2688 + if (fis instanceof Ci.nsISeekableStream) 1.2689 + fis.seek(Ci.nsISeekableStream.NS_SEEK_SET, offset); 1.2690 + else 1.2691 + new ScriptableInputStream(fis).read(offset); 1.2692 + } 1.2693 + } 1.2694 + catch (e) 1.2695 + { 1.2696 + fis.close(); 1.2697 + throw e; 1.2698 + } 1.2699 + 1.2700 + let writeMore = function writeMore() 1.2701 + { 1.2702 + gThreadManager.currentThread 1.2703 + .dispatch(writeData, Ci.nsIThread.DISPATCH_NORMAL); 1.2704 + } 1.2705 + 1.2706 + var input = new BinaryInputStream(fis); 1.2707 + var output = new BinaryOutputStream(response.bodyOutputStream); 1.2708 + var writeData = 1.2709 + { 1.2710 + run: function() 1.2711 + { 1.2712 + var chunkSize = Math.min(65536, count); 1.2713 + count -= chunkSize; 1.2714 + NS_ASSERT(count >= 0, "underflow"); 1.2715 + 1.2716 + try 1.2717 + { 1.2718 + var data = input.readByteArray(chunkSize); 1.2719 + NS_ASSERT(data.length === chunkSize, 1.2720 + "incorrect data returned? got " + data.length + 1.2721 + ", expected " + chunkSize); 1.2722 + output.writeByteArray(data, data.length); 1.2723 + if (count === 0) 1.2724 + { 1.2725 + fis.close(); 1.2726 + response.finish(); 1.2727 + } 1.2728 + else 1.2729 + { 1.2730 + writeMore(); 1.2731 + } 1.2732 + } 1.2733 + catch (e) 1.2734 + { 1.2735 + try 1.2736 + { 1.2737 + fis.close(); 1.2738 + } 1.2739 + finally 1.2740 + { 1.2741 + response.finish(); 1.2742 + } 1.2743 + throw e; 1.2744 + } 1.2745 + } 1.2746 + }; 1.2747 + 1.2748 + writeMore(); 1.2749 + 1.2750 + // Now that we know copying will start, flag the response as async. 1.2751 + response.processAsync(); 1.2752 + } 1.2753 + }, 1.2754 + 1.2755 + /** 1.2756 +* Get the value corresponding to a given key for the given path for SJS state 1.2757 +* preservation across requests. 1.2758 +* 1.2759 +* @param path : string 1.2760 +* the path from which the given state is to be retrieved 1.2761 +* @param k : string 1.2762 +* the key whose corresponding value is to be returned 1.2763 +* @returns string 1.2764 +* the corresponding value, which is initially the empty string 1.2765 +*/ 1.2766 + _getState: function(path, k) 1.2767 + { 1.2768 + var state = this._state; 1.2769 + if (path in state && k in state[path]) 1.2770 + return state[path][k]; 1.2771 + return ""; 1.2772 + }, 1.2773 + 1.2774 + /** 1.2775 +* Set the value corresponding to a given key for the given path for SJS state 1.2776 +* preservation across requests. 1.2777 +* 1.2778 +* @param path : string 1.2779 +* the path from which the given state is to be retrieved 1.2780 +* @param k : string 1.2781 +* the key whose corresponding value is to be set 1.2782 +* @param v : string 1.2783 +* the value to be set 1.2784 +*/ 1.2785 + _setState: function(path, k, v) 1.2786 + { 1.2787 + if (typeof v !== "string") 1.2788 + throw new Error("non-string value passed"); 1.2789 + var state = this._state; 1.2790 + if (!(path in state)) 1.2791 + state[path] = {}; 1.2792 + state[path][k] = v; 1.2793 + }, 1.2794 + 1.2795 + /** 1.2796 +* Get the value corresponding to a given key for SJS state preservation 1.2797 +* across requests. 1.2798 +* 1.2799 +* @param k : string 1.2800 +* the key whose corresponding value is to be returned 1.2801 +* @returns string 1.2802 +* the corresponding value, which is initially the empty string 1.2803 +*/ 1.2804 + _getSharedState: function(k) 1.2805 + { 1.2806 + var state = this._sharedState; 1.2807 + if (k in state) 1.2808 + return state[k]; 1.2809 + return ""; 1.2810 + }, 1.2811 + 1.2812 + /** 1.2813 +* Set the value corresponding to a given key for SJS state preservation 1.2814 +* across requests. 1.2815 +* 1.2816 +* @param k : string 1.2817 +* the key whose corresponding value is to be set 1.2818 +* @param v : string 1.2819 +* the value to be set 1.2820 +*/ 1.2821 + _setSharedState: function(k, v) 1.2822 + { 1.2823 + if (typeof v !== "string") 1.2824 + throw new Error("non-string value passed"); 1.2825 + this._sharedState[k] = v; 1.2826 + }, 1.2827 + 1.2828 + /** 1.2829 +* Returns the object associated with the given key in the server for SJS 1.2830 +* state preservation across requests. 1.2831 +* 1.2832 +* @param k : string 1.2833 +* the key whose corresponding object is to be returned 1.2834 +* @returns nsISupports 1.2835 +* the corresponding object, or null if none was present 1.2836 +*/ 1.2837 + _getObjectState: function(k) 1.2838 + { 1.2839 + if (typeof k !== "string") 1.2840 + throw new Error("non-string key passed"); 1.2841 + return this._objectState[k] || null; 1.2842 + }, 1.2843 + 1.2844 + /** 1.2845 +* Sets the object associated with the given key in the server for SJS 1.2846 +* state preservation across requests. 1.2847 +* 1.2848 +* @param k : string 1.2849 +* the key whose corresponding object is to be set 1.2850 +* @param v : nsISupports 1.2851 +* the object to be associated with the given key; may be null 1.2852 +*/ 1.2853 + _setObjectState: function(k, v) 1.2854 + { 1.2855 + if (typeof k !== "string") 1.2856 + throw new Error("non-string key passed"); 1.2857 + if (typeof v !== "object") 1.2858 + throw new Error("non-object value passed"); 1.2859 + if (v && !("QueryInterface" in v)) 1.2860 + { 1.2861 + throw new Error("must pass an nsISupports; use wrappedJSObject to ease " + 1.2862 + "pain when using the server from JS"); 1.2863 + } 1.2864 + 1.2865 + this._objectState[k] = v; 1.2866 + }, 1.2867 + 1.2868 + /** 1.2869 +* Gets a content-type for the given file, first by checking for any custom 1.2870 +* MIME-types registered with this handler for the file's extension, second by 1.2871 +* asking the global MIME service for a content-type, and finally by failing 1.2872 +* over to application/octet-stream. 1.2873 +* 1.2874 +* @param file : nsIFile 1.2875 +* the nsIFile for which to get a file type 1.2876 +* @returns string 1.2877 +* the best content-type which can be determined for the file 1.2878 +*/ 1.2879 + _getTypeFromFile: function(file) 1.2880 + { 1.2881 + try 1.2882 + { 1.2883 + var name = file.leafName; 1.2884 + var dot = name.lastIndexOf("."); 1.2885 + if (dot > 0) 1.2886 + { 1.2887 + var ext = name.slice(dot + 1); 1.2888 + if (ext in this._mimeMappings) 1.2889 + return this._mimeMappings[ext]; 1.2890 + } 1.2891 + return Cc["@mozilla.org/uriloader/external-helper-app-service;1"] 1.2892 + .getService(Ci.nsIMIMEService) 1.2893 + .getTypeFromFile(file); 1.2894 + } 1.2895 + catch (e) 1.2896 + { 1.2897 + return "application/octet-stream"; 1.2898 + } 1.2899 + }, 1.2900 + 1.2901 + /** 1.2902 +* Returns the nsILocalFile which corresponds to the path, as determined using 1.2903 +* all registered path->directory mappings and any paths which are explicitly 1.2904 +* overridden. 1.2905 +* 1.2906 +* @param path : string 1.2907 +* the server path for which a file should be retrieved, e.g. "/foo/bar" 1.2908 +* @throws HttpError 1.2909 +* when the correct action is the corresponding HTTP error (i.e., because no 1.2910 +* mapping was found for a directory in path, the referenced file doesn't 1.2911 +* exist, etc.) 1.2912 +* @returns nsILocalFile 1.2913 +* the file to be sent as the response to a request for the path 1.2914 +*/ 1.2915 + _getFileForPath: function(path) 1.2916 + { 1.2917 + // decode and add underscores as necessary 1.2918 + try 1.2919 + { 1.2920 + path = toInternalPath(path, true); 1.2921 + } 1.2922 + catch (e) 1.2923 + { 1.2924 + throw HTTP_400; // malformed path 1.2925 + } 1.2926 + 1.2927 + // next, get the directory which contains this path 1.2928 + var pathMap = this._pathDirectoryMap; 1.2929 + 1.2930 + // An example progression of tmp for a path "/foo/bar/baz/" might be: 1.2931 + // "foo/bar/baz/", "foo/bar/baz", "foo/bar", "foo", "" 1.2932 + var tmp = path.substring(1); 1.2933 + while (true) 1.2934 + { 1.2935 + // do we have a match for current head of the path? 1.2936 + var file = pathMap.get(tmp); 1.2937 + if (file) 1.2938 + { 1.2939 + // XXX hack; basically disable showing mapping for /foo/bar/ when the 1.2940 + // requested path was /foo/bar, because relative links on the page 1.2941 + // will all be incorrect -- we really need the ability to easily 1.2942 + // redirect here instead 1.2943 + if (tmp == path.substring(1) && 1.2944 + tmp.length != 0 && 1.2945 + tmp.charAt(tmp.length - 1) != "/") 1.2946 + file = null; 1.2947 + else 1.2948 + break; 1.2949 + } 1.2950 + 1.2951 + // if we've finished trying all prefixes, exit 1.2952 + if (tmp == "") 1.2953 + break; 1.2954 + 1.2955 + tmp = tmp.substring(0, tmp.lastIndexOf("/")); 1.2956 + } 1.2957 + 1.2958 + // no mapping applies, so 404 1.2959 + if (!file) 1.2960 + throw HTTP_404; 1.2961 + 1.2962 + 1.2963 + // last, get the file for the path within the determined directory 1.2964 + var parentFolder = file.parent; 1.2965 + var dirIsRoot = (parentFolder == null); 1.2966 + 1.2967 + // Strategy here is to append components individually, making sure we 1.2968 + // never move above the given directory; this allows paths such as 1.2969 + // "<file>/foo/../bar" but prevents paths such as "<file>/../base-sibling"; 1.2970 + // this component-wise approach also means the code works even on platforms 1.2971 + // which don't use "/" as the directory separator, such as Windows 1.2972 + var leafPath = path.substring(tmp.length + 1); 1.2973 + var comps = leafPath.split("/"); 1.2974 + for (var i = 0, sz = comps.length; i < sz; i++) 1.2975 + { 1.2976 + var comp = comps[i]; 1.2977 + 1.2978 + if (comp == "..") 1.2979 + file = file.parent; 1.2980 + else if (comp == "." || comp == "") 1.2981 + continue; 1.2982 + else 1.2983 + file.append(comp); 1.2984 + 1.2985 + if (!dirIsRoot && file.equals(parentFolder)) 1.2986 + throw HTTP_403; 1.2987 + } 1.2988 + 1.2989 + return file; 1.2990 + }, 1.2991 + 1.2992 + /** 1.2993 +* Writes the error page for the given HTTP error code over the given 1.2994 +* connection. 1.2995 +* 1.2996 +* @param errorCode : uint 1.2997 +* the HTTP error code to be used 1.2998 +* @param connection : Connection 1.2999 +* the connection on which the error occurred 1.3000 +*/ 1.3001 + handleError: function(errorCode, connection) 1.3002 + { 1.3003 + var response = new Response(connection); 1.3004 + 1.3005 + dumpn("*** error in request: " + errorCode); 1.3006 + 1.3007 + this._handleError(errorCode, new Request(connection.port), response); 1.3008 + }, 1.3009 + 1.3010 + /** 1.3011 +* Handles a request which generates the given error code, using the 1.3012 +* user-defined error handler if one has been set, gracefully falling back to 1.3013 +* the x00 status code if the code has no handler, and failing to status code 1.3014 +* 500 if all else fails. 1.3015 +* 1.3016 +* @param errorCode : uint 1.3017 +* the HTTP error which is to be returned 1.3018 +* @param metadata : Request 1.3019 +* metadata for the request, which will often be incomplete since this is an 1.3020 +* error 1.3021 +* @param response : Response 1.3022 +* an uninitialized Response should be initialized when this method 1.3023 +* completes with information which represents the desired error code in the 1.3024 +* ideal case or a fallback code in abnormal circumstances (i.e., 500 is a 1.3025 +* fallback for 505, per HTTP specs) 1.3026 +*/ 1.3027 + _handleError: function(errorCode, metadata, response) 1.3028 + { 1.3029 + if (!metadata) 1.3030 + throw Cr.NS_ERROR_NULL_POINTER; 1.3031 + 1.3032 + var errorX00 = errorCode - (errorCode % 100); 1.3033 + 1.3034 + try 1.3035 + { 1.3036 + if (!(errorCode in HTTP_ERROR_CODES)) 1.3037 + dumpn("*** WARNING: requested invalid error: " + errorCode); 1.3038 + 1.3039 + // RFC 2616 says that we should try to handle an error by its class if we 1.3040 + // can't otherwise handle it -- if that fails, we revert to handling it as 1.3041 + // a 500 internal server error, and if that fails we throw and shut down 1.3042 + // the server 1.3043 + 1.3044 + // actually handle the error 1.3045 + try 1.3046 + { 1.3047 + if (errorCode in this._overrideErrors) 1.3048 + this._overrideErrors[errorCode](metadata, response); 1.3049 + else 1.3050 + this._defaultErrors[errorCode](metadata, response); 1.3051 + } 1.3052 + catch (e) 1.3053 + { 1.3054 + if (response.partiallySent()) 1.3055 + { 1.3056 + response.abort(e); 1.3057 + return; 1.3058 + } 1.3059 + 1.3060 + // don't retry the handler that threw 1.3061 + if (errorX00 == errorCode) 1.3062 + throw HTTP_500; 1.3063 + 1.3064 + dumpn("*** error in handling for error code " + errorCode + ", " + 1.3065 + "falling back to " + errorX00 + "..."); 1.3066 + response = new Response(response._connection); 1.3067 + if (errorX00 in this._overrideErrors) 1.3068 + this._overrideErrors[errorX00](metadata, response); 1.3069 + else if (errorX00 in this._defaultErrors) 1.3070 + this._defaultErrors[errorX00](metadata, response); 1.3071 + else 1.3072 + throw HTTP_500; 1.3073 + } 1.3074 + } 1.3075 + catch (e) 1.3076 + { 1.3077 + if (response.partiallySent()) 1.3078 + { 1.3079 + response.abort(); 1.3080 + return; 1.3081 + } 1.3082 + 1.3083 + // we've tried everything possible for a meaningful error -- now try 500 1.3084 + dumpn("*** error in handling for error code " + errorX00 + ", falling " + 1.3085 + "back to 500..."); 1.3086 + 1.3087 + try 1.3088 + { 1.3089 + response = new Response(response._connection); 1.3090 + if (500 in this._overrideErrors) 1.3091 + this._overrideErrors[500](metadata, response); 1.3092 + else 1.3093 + this._defaultErrors[500](metadata, response); 1.3094 + } 1.3095 + catch (e2) 1.3096 + { 1.3097 + dumpn("*** multiple errors in default error handlers!"); 1.3098 + dumpn("*** e == " + e + ", e2 == " + e2); 1.3099 + response.abort(e2); 1.3100 + return; 1.3101 + } 1.3102 + } 1.3103 + 1.3104 + response.complete(); 1.3105 + }, 1.3106 + 1.3107 + // FIELDS 1.3108 + 1.3109 + /** 1.3110 +* This object contains the default handlers for the various HTTP error codes. 1.3111 +*/ 1.3112 + _defaultErrors: 1.3113 + { 1.3114 + 400: function(metadata, response) 1.3115 + { 1.3116 + // none of the data in metadata is reliable, so hard-code everything here 1.3117 + response.setStatusLine("1.1", 400, "Bad Request"); 1.3118 + response.setHeader("Content-Type", "text/plain", false); 1.3119 + 1.3120 + var body = "Bad request\n"; 1.3121 + response.bodyOutputStream.write(body, body.length); 1.3122 + }, 1.3123 + 403: function(metadata, response) 1.3124 + { 1.3125 + response.setStatusLine(metadata.httpVersion, 403, "Forbidden"); 1.3126 + response.setHeader("Content-Type", "text/html", false); 1.3127 + 1.3128 + var body = "<html>\ 1.3129 +<head><title>403 Forbidden</title></head>\ 1.3130 +<body>\ 1.3131 +<h1>403 Forbidden</h1>\ 1.3132 +</body>\ 1.3133 +</html>"; 1.3134 + response.bodyOutputStream.write(body, body.length); 1.3135 + }, 1.3136 + 404: function(metadata, response) 1.3137 + { 1.3138 + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); 1.3139 + response.setHeader("Content-Type", "text/html", false); 1.3140 + 1.3141 + var body = "<html>\ 1.3142 +<head><title>404 Not Found</title></head>\ 1.3143 +<body>\ 1.3144 +<h1>404 Not Found</h1>\ 1.3145 +<p>\ 1.3146 +<span style='font-family: monospace;'>" + 1.3147 + htmlEscape(metadata.path) + 1.3148 + "</span> was not found.\ 1.3149 +</p>\ 1.3150 +</body>\ 1.3151 +</html>"; 1.3152 + response.bodyOutputStream.write(body, body.length); 1.3153 + }, 1.3154 + 416: function(metadata, response) 1.3155 + { 1.3156 + response.setStatusLine(metadata.httpVersion, 1.3157 + 416, 1.3158 + "Requested Range Not Satisfiable"); 1.3159 + response.setHeader("Content-Type", "text/html", false); 1.3160 + 1.3161 + var body = "<html>\ 1.3162 +<head>\ 1.3163 +<title>416 Requested Range Not Satisfiable</title></head>\ 1.3164 +<body>\ 1.3165 +<h1>416 Requested Range Not Satisfiable</h1>\ 1.3166 +<p>The byte range was not valid for the\ 1.3167 +requested resource.\ 1.3168 +</p>\ 1.3169 +</body>\ 1.3170 +</html>"; 1.3171 + response.bodyOutputStream.write(body, body.length); 1.3172 + }, 1.3173 + 500: function(metadata, response) 1.3174 + { 1.3175 + response.setStatusLine(metadata.httpVersion, 1.3176 + 500, 1.3177 + "Internal Server Error"); 1.3178 + response.setHeader("Content-Type", "text/html", false); 1.3179 + 1.3180 + var body = "<html>\ 1.3181 +<head><title>500 Internal Server Error</title></head>\ 1.3182 +<body>\ 1.3183 +<h1>500 Internal Server Error</h1>\ 1.3184 +<p>Something's broken in this server and\ 1.3185 +needs to be fixed.</p>\ 1.3186 +</body>\ 1.3187 +</html>"; 1.3188 + response.bodyOutputStream.write(body, body.length); 1.3189 + }, 1.3190 + 501: function(metadata, response) 1.3191 + { 1.3192 + response.setStatusLine(metadata.httpVersion, 501, "Not Implemented"); 1.3193 + response.setHeader("Content-Type", "text/html", false); 1.3194 + 1.3195 + var body = "<html>\ 1.3196 +<head><title>501 Not Implemented</title></head>\ 1.3197 +<body>\ 1.3198 +<h1>501 Not Implemented</h1>\ 1.3199 +<p>This server is not (yet) Apache.</p>\ 1.3200 +</body>\ 1.3201 +</html>"; 1.3202 + response.bodyOutputStream.write(body, body.length); 1.3203 + }, 1.3204 + 505: function(metadata, response) 1.3205 + { 1.3206 + response.setStatusLine("1.1", 505, "HTTP Version Not Supported"); 1.3207 + response.setHeader("Content-Type", "text/html", false); 1.3208 + 1.3209 + var body = "<html>\ 1.3210 +<head><title>505 HTTP Version Not Supported</title></head>\ 1.3211 +<body>\ 1.3212 +<h1>505 HTTP Version Not Supported</h1>\ 1.3213 +<p>This server only supports HTTP/1.0 and HTTP/1.1\ 1.3214 +connections.</p>\ 1.3215 +</body>\ 1.3216 +</html>"; 1.3217 + response.bodyOutputStream.write(body, body.length); 1.3218 + } 1.3219 + }, 1.3220 + 1.3221 + /** 1.3222 +* Contains handlers for the default set of URIs contained in this server. 1.3223 +*/ 1.3224 + _defaultPaths: 1.3225 + { 1.3226 + "/": function(metadata, response) 1.3227 + { 1.3228 + response.setStatusLine(metadata.httpVersion, 200, "OK"); 1.3229 + response.setHeader("Content-Type", "text/html", false); 1.3230 + 1.3231 + var body = "<html>\ 1.3232 +<head><title>httpd.js</title></head>\ 1.3233 +<body>\ 1.3234 +<h1>httpd.js</h1>\ 1.3235 +<p>If you're seeing this page, httpd.js is up and\ 1.3236 +serving requests! Now set a base path and serve some\ 1.3237 +files!</p>\ 1.3238 +</body>\ 1.3239 +</html>"; 1.3240 + 1.3241 + response.bodyOutputStream.write(body, body.length); 1.3242 + }, 1.3243 + 1.3244 + "/trace": function(metadata, response) 1.3245 + { 1.3246 + response.setStatusLine(metadata.httpVersion, 200, "OK"); 1.3247 + response.setHeader("Content-Type", "text/plain", false); 1.3248 + 1.3249 + var body = "Request-URI: " + 1.3250 + metadata.scheme + "://" + metadata.host + ":" + metadata.port + 1.3251 + metadata.path + "\n\n"; 1.3252 + body += "Request (semantically equivalent, slightly reformatted):\n\n"; 1.3253 + body += metadata.method + " " + metadata.path; 1.3254 + 1.3255 + if (metadata.queryString) 1.3256 + body += "?" + metadata.queryString; 1.3257 + 1.3258 + body += " HTTP/" + metadata.httpVersion + "\r\n"; 1.3259 + 1.3260 + var headEnum = metadata.headers; 1.3261 + while (headEnum.hasMoreElements()) 1.3262 + { 1.3263 + var fieldName = headEnum.getNext() 1.3264 + .QueryInterface(Ci.nsISupportsString) 1.3265 + .data; 1.3266 + body += fieldName + ": " + metadata.getHeader(fieldName) + "\r\n"; 1.3267 + } 1.3268 + 1.3269 + response.bodyOutputStream.write(body, body.length); 1.3270 + } 1.3271 + } 1.3272 +}; 1.3273 + 1.3274 + 1.3275 +/** 1.3276 +* Maps absolute paths to files on the local file system (as nsILocalFiles). 1.3277 +*/ 1.3278 +function FileMap() 1.3279 +{ 1.3280 + /** Hash which will map paths to nsILocalFiles. */ 1.3281 + this._map = {}; 1.3282 +} 1.3283 +FileMap.prototype = 1.3284 +{ 1.3285 + // PUBLIC API 1.3286 + 1.3287 + /** 1.3288 +* Maps key to a clone of the nsILocalFile value if value is non-null; 1.3289 +* otherwise, removes any extant mapping for key. 1.3290 +* 1.3291 +* @param key : string 1.3292 +* string to which a clone of value is mapped 1.3293 +* @param value : nsILocalFile 1.3294 +* the file to map to key, or null to remove a mapping 1.3295 +*/ 1.3296 + put: function(key, value) 1.3297 + { 1.3298 + if (value) 1.3299 + this._map[key] = value.clone(); 1.3300 + else 1.3301 + delete this._map[key]; 1.3302 + }, 1.3303 + 1.3304 + /** 1.3305 +* Returns a clone of the nsILocalFile mapped to key, or null if no such 1.3306 +* mapping exists. 1.3307 +* 1.3308 +* @param key : string 1.3309 +* key to which the returned file maps 1.3310 +* @returns nsILocalFile 1.3311 +* a clone of the mapped file, or null if no mapping exists 1.3312 +*/ 1.3313 + get: function(key) 1.3314 + { 1.3315 + var val = this._map[key]; 1.3316 + return val ? val.clone() : null; 1.3317 + } 1.3318 +}; 1.3319 + 1.3320 + 1.3321 +// Response CONSTANTS 1.3322 + 1.3323 +// token = *<any CHAR except CTLs or separators> 1.3324 +// CHAR = <any US-ASCII character (0-127)> 1.3325 +// CTL = <any US-ASCII control character (0-31) and DEL (127)> 1.3326 +// separators = "(" | ")" | "<" | ">" | "@" 1.3327 +// | "," | ";" | ":" | "\" | <"> 1.3328 +// | "/" | "[" | "]" | "?" | "=" 1.3329 +// | "{" | "}" | SP | HT 1.3330 +const IS_TOKEN_ARRAY = 1.3331 + [0, 0, 0, 0, 0, 0, 0, 0, // 0 1.3332 + 0, 0, 0, 0, 0, 0, 0, 0, // 8 1.3333 + 0, 0, 0, 0, 0, 0, 0, 0, // 16 1.3334 + 0, 0, 0, 0, 0, 0, 0, 0, // 24 1.3335 + 1.3336 + 0, 1, 0, 1, 1, 1, 1, 1, // 32 1.3337 + 0, 0, 1, 1, 0, 1, 1, 0, // 40 1.3338 + 1, 1, 1, 1, 1, 1, 1, 1, // 48 1.3339 + 1, 1, 0, 0, 0, 0, 0, 0, // 56 1.3340 + 1.3341 + 0, 1, 1, 1, 1, 1, 1, 1, // 64 1.3342 + 1, 1, 1, 1, 1, 1, 1, 1, // 72 1.3343 + 1, 1, 1, 1, 1, 1, 1, 1, // 80 1.3344 + 1, 1, 1, 0, 0, 0, 1, 1, // 88 1.3345 + 1.3346 + 1, 1, 1, 1, 1, 1, 1, 1, // 96 1.3347 + 1, 1, 1, 1, 1, 1, 1, 1, // 104 1.3348 + 1, 1, 1, 1, 1, 1, 1, 1, // 112 1.3349 + 1, 1, 1, 0, 1, 0, 1]; // 120 1.3350 + 1.3351 + 1.3352 +/** 1.3353 +* Determines whether the given character code is a CTL. 1.3354 +* 1.3355 +* @param code : uint 1.3356 +* the character code 1.3357 +* @returns boolean 1.3358 +* true if code is a CTL, false otherwise 1.3359 +*/ 1.3360 +function isCTL(code) 1.3361 +{ 1.3362 + return (code >= 0 && code <= 31) || (code == 127); 1.3363 +} 1.3364 + 1.3365 +/** 1.3366 +* Represents a response to an HTTP request, encapsulating all details of that 1.3367 +* response. This includes all headers, the HTTP version, status code and 1.3368 +* explanation, and the entity itself. 1.3369 +* 1.3370 +* @param connection : Connection 1.3371 +* the connection over which this response is to be written 1.3372 +*/ 1.3373 +function Response(connection) 1.3374 +{ 1.3375 + /** The connection over which this response will be written. */ 1.3376 + this._connection = connection; 1.3377 + 1.3378 + /** 1.3379 +* The HTTP version of this response; defaults to 1.1 if not set by the 1.3380 +* handler. 1.3381 +*/ 1.3382 + this._httpVersion = nsHttpVersion.HTTP_1_1; 1.3383 + 1.3384 + /** 1.3385 +* The HTTP code of this response; defaults to 200. 1.3386 +*/ 1.3387 + this._httpCode = 200; 1.3388 + 1.3389 + /** 1.3390 +* The description of the HTTP code in this response; defaults to "OK". 1.3391 +*/ 1.3392 + this._httpDescription = "OK"; 1.3393 + 1.3394 + /** 1.3395 +* An nsIHttpHeaders object in which the headers in this response should be 1.3396 +* stored. This property is null after the status line and headers have been 1.3397 +* written to the network, and it may be modified up until it is cleared, 1.3398 +* except if this._finished is set first (in which case headers are written 1.3399 +* asynchronously in response to a finish() call not preceded by 1.3400 +* flushHeaders()). 1.3401 +*/ 1.3402 + this._headers = new nsHttpHeaders(); 1.3403 + 1.3404 + /** 1.3405 +* Set to true when this response is ended (completely constructed if possible 1.3406 +* and the connection closed); further actions on this will then fail. 1.3407 +*/ 1.3408 + this._ended = false; 1.3409 + 1.3410 + /** 1.3411 +* A stream used to hold data written to the body of this response. 1.3412 +*/ 1.3413 + this._bodyOutputStream = null; 1.3414 + 1.3415 + /** 1.3416 +* A stream containing all data that has been written to the body of this 1.3417 +* response so far. (Async handlers make the data contained in this 1.3418 +* unreliable as a way of determining content length in general, but auxiliary 1.3419 +* saved information can sometimes be used to guarantee reliability.) 1.3420 +*/ 1.3421 + this._bodyInputStream = null; 1.3422 + 1.3423 + /** 1.3424 +* A stream copier which copies data to the network. It is initially null 1.3425 +* until replaced with a copier for response headers; when headers have been 1.3426 +* fully sent it is replaced with a copier for the response body, remaining 1.3427 +* so for the duration of response processing. 1.3428 +*/ 1.3429 + this._asyncCopier = null; 1.3430 + 1.3431 + /** 1.3432 +* True if this response has been designated as being processed 1.3433 +* asynchronously rather than for the duration of a single call to 1.3434 +* nsIHttpRequestHandler.handle. 1.3435 +*/ 1.3436 + this._processAsync = false; 1.3437 + 1.3438 + /** 1.3439 +* True iff finish() has been called on this, signaling that no more changes 1.3440 +* to this may be made. 1.3441 +*/ 1.3442 + this._finished = false; 1.3443 + 1.3444 + /** 1.3445 +* True iff powerSeized() has been called on this, signaling that this 1.3446 +* response is to be handled manually by the response handler (which may then 1.3447 +* send arbitrary data in response, even non-HTTP responses). 1.3448 +*/ 1.3449 + this._powerSeized = false; 1.3450 +} 1.3451 +Response.prototype = 1.3452 +{ 1.3453 + // PUBLIC CONSTRUCTION API 1.3454 + 1.3455 + // 1.3456 + // see nsIHttpResponse.bodyOutputStream 1.3457 + // 1.3458 + get bodyOutputStream() 1.3459 + { 1.3460 + if (this._finished) 1.3461 + throw Cr.NS_ERROR_NOT_AVAILABLE; 1.3462 + 1.3463 + if (!this._bodyOutputStream) 1.3464 + { 1.3465 + var pipe = new Pipe(true, false, Response.SEGMENT_SIZE, PR_UINT32_MAX, 1.3466 + null); 1.3467 + this._bodyOutputStream = pipe.outputStream; 1.3468 + this._bodyInputStream = pipe.inputStream; 1.3469 + if (this._processAsync || this._powerSeized) 1.3470 + this._startAsyncProcessor(); 1.3471 + } 1.3472 + 1.3473 + return this._bodyOutputStream; 1.3474 + }, 1.3475 + 1.3476 + // 1.3477 + // see nsIHttpResponse.write 1.3478 + // 1.3479 + write: function(data) 1.3480 + { 1.3481 + if (this._finished) 1.3482 + throw Cr.NS_ERROR_NOT_AVAILABLE; 1.3483 + 1.3484 + var dataAsString = String(data); 1.3485 + this.bodyOutputStream.write(dataAsString, dataAsString.length); 1.3486 + }, 1.3487 + 1.3488 + // 1.3489 + // see nsIHttpResponse.setStatusLine 1.3490 + // 1.3491 + setStatusLine: function(httpVersion, code, description) 1.3492 + { 1.3493 + if (!this._headers || this._finished || this._powerSeized) 1.3494 + throw Cr.NS_ERROR_NOT_AVAILABLE; 1.3495 + this._ensureAlive(); 1.3496 + 1.3497 + if (!(code >= 0 && code < 1000)) 1.3498 + throw Cr.NS_ERROR_INVALID_ARG; 1.3499 + 1.3500 + try 1.3501 + { 1.3502 + var httpVer; 1.3503 + // avoid version construction for the most common cases 1.3504 + if (!httpVersion || httpVersion == "1.1") 1.3505 + httpVer = nsHttpVersion.HTTP_1_1; 1.3506 + else if (httpVersion == "1.0") 1.3507 + httpVer = nsHttpVersion.HTTP_1_0; 1.3508 + else 1.3509 + httpVer = new nsHttpVersion(httpVersion); 1.3510 + } 1.3511 + catch (e) 1.3512 + { 1.3513 + throw Cr.NS_ERROR_INVALID_ARG; 1.3514 + } 1.3515 + 1.3516 + // Reason-Phrase = *<TEXT, excluding CR, LF> 1.3517 + // TEXT = <any OCTET except CTLs, but including LWS> 1.3518 + // 1.3519 + // XXX this ends up disallowing octets which aren't Unicode, I think -- not 1.3520 + // much to do if description is IDL'd as string 1.3521 + if (!description) 1.3522 + description = ""; 1.3523 + for (var i = 0; i < description.length; i++) 1.3524 + if (isCTL(description.charCodeAt(i)) && description.charAt(i) != "\t") 1.3525 + throw Cr.NS_ERROR_INVALID_ARG; 1.3526 + 1.3527 + // set the values only after validation to preserve atomicity 1.3528 + this._httpDescription = description; 1.3529 + this._httpCode = code; 1.3530 + this._httpVersion = httpVer; 1.3531 + }, 1.3532 + 1.3533 + // 1.3534 + // see nsIHttpResponse.setHeader 1.3535 + // 1.3536 + setHeader: function(name, value, merge) 1.3537 + { 1.3538 + if (!this._headers || this._finished || this._powerSeized) 1.3539 + throw Cr.NS_ERROR_NOT_AVAILABLE; 1.3540 + this._ensureAlive(); 1.3541 + 1.3542 + this._headers.setHeader(name, value, merge); 1.3543 + }, 1.3544 + 1.3545 + // 1.3546 + // see nsIHttpResponse.processAsync 1.3547 + // 1.3548 + processAsync: function() 1.3549 + { 1.3550 + if (this._finished) 1.3551 + throw Cr.NS_ERROR_UNEXPECTED; 1.3552 + if (this._powerSeized) 1.3553 + throw Cr.NS_ERROR_NOT_AVAILABLE; 1.3554 + if (this._processAsync) 1.3555 + return; 1.3556 + this._ensureAlive(); 1.3557 + 1.3558 + dumpn("*** processing connection " + this._connection.number + " async"); 1.3559 + this._processAsync = true; 1.3560 + 1.3561 + /* 1.3562 +* Either the bodyOutputStream getter or this method is responsible for 1.3563 +* starting the asynchronous processor and catching writes of data to the 1.3564 +* response body of async responses as they happen, for the purpose of 1.3565 +* forwarding those writes to the actual connection's output stream. 1.3566 +* If bodyOutputStream is accessed first, calling this method will create 1.3567 +* the processor (when it first is clear that body data is to be written 1.3568 +* immediately, not buffered). If this method is called first, accessing 1.3569 +* bodyOutputStream will create the processor. If only this method is 1.3570 +* called, we'll write nothing, neither headers nor the nonexistent body, 1.3571 +* until finish() is called. Since that delay is easily avoided by simply 1.3572 +* getting bodyOutputStream or calling write(""), we don't worry about it. 1.3573 +*/ 1.3574 + if (this._bodyOutputStream && !this._asyncCopier) 1.3575 + this._startAsyncProcessor(); 1.3576 + }, 1.3577 + 1.3578 + // 1.3579 + // see nsIHttpResponse.seizePower 1.3580 + // 1.3581 + seizePower: function() 1.3582 + { 1.3583 + if (this._processAsync) 1.3584 + throw Cr.NS_ERROR_NOT_AVAILABLE; 1.3585 + if (this._finished) 1.3586 + throw Cr.NS_ERROR_UNEXPECTED; 1.3587 + if (this._powerSeized) 1.3588 + return; 1.3589 + this._ensureAlive(); 1.3590 + 1.3591 + dumpn("*** forcefully seizing power over connection " + 1.3592 + this._connection.number + "..."); 1.3593 + 1.3594 + // Purge any already-written data without sending it. We could as easily 1.3595 + // swap out the streams entirely, but that makes it possible to acquire and 1.3596 + // unknowingly use a stale reference, so we require there only be one of 1.3597 + // each stream ever for any response to avoid this complication. 1.3598 + if (this._asyncCopier) 1.3599 + this._asyncCopier.cancel(Cr.NS_BINDING_ABORTED); 1.3600 + this._asyncCopier = null; 1.3601 + if (this._bodyOutputStream) 1.3602 + { 1.3603 + var input = new BinaryInputStream(this._bodyInputStream); 1.3604 + var avail; 1.3605 + while ((avail = input.available()) > 0) 1.3606 + input.readByteArray(avail); 1.3607 + } 1.3608 + 1.3609 + this._powerSeized = true; 1.3610 + if (this._bodyOutputStream) 1.3611 + this._startAsyncProcessor(); 1.3612 + }, 1.3613 + 1.3614 + // 1.3615 + // see nsIHttpResponse.finish 1.3616 + // 1.3617 + finish: function() 1.3618 + { 1.3619 + if (!this._processAsync && !this._powerSeized) 1.3620 + throw Cr.NS_ERROR_UNEXPECTED; 1.3621 + if (this._finished) 1.3622 + return; 1.3623 + 1.3624 + dumpn("*** finishing connection " + this._connection.number); 1.3625 + this._startAsyncProcessor(); // in case bodyOutputStream was never accessed 1.3626 + if (this._bodyOutputStream) 1.3627 + this._bodyOutputStream.close(); 1.3628 + this._finished = true; 1.3629 + }, 1.3630 + 1.3631 + 1.3632 + // NSISUPPORTS 1.3633 + 1.3634 + // 1.3635 + // see nsISupports.QueryInterface 1.3636 + // 1.3637 + QueryInterface: function(iid) 1.3638 + { 1.3639 + if (iid.equals(Ci.nsIHttpResponse) || iid.equals(Ci.nsISupports)) 1.3640 + return this; 1.3641 + 1.3642 + throw Cr.NS_ERROR_NO_INTERFACE; 1.3643 + }, 1.3644 + 1.3645 + 1.3646 + // POST-CONSTRUCTION API (not exposed externally) 1.3647 + 1.3648 + /** 1.3649 +* The HTTP version number of this, as a string (e.g. "1.1"). 1.3650 +*/ 1.3651 + get httpVersion() 1.3652 + { 1.3653 + this._ensureAlive(); 1.3654 + return this._httpVersion.toString(); 1.3655 + }, 1.3656 + 1.3657 + /** 1.3658 +* The HTTP status code of this response, as a string of three characters per 1.3659 +* RFC 2616. 1.3660 +*/ 1.3661 + get httpCode() 1.3662 + { 1.3663 + this._ensureAlive(); 1.3664 + 1.3665 + var codeString = (this._httpCode < 10 ? "0" : "") + 1.3666 + (this._httpCode < 100 ? "0" : "") + 1.3667 + this._httpCode; 1.3668 + return codeString; 1.3669 + }, 1.3670 + 1.3671 + /** 1.3672 +* The description of the HTTP status code of this response, or "" if none is 1.3673 +* set. 1.3674 +*/ 1.3675 + get httpDescription() 1.3676 + { 1.3677 + this._ensureAlive(); 1.3678 + 1.3679 + return this._httpDescription; 1.3680 + }, 1.3681 + 1.3682 + /** 1.3683 +* The headers in this response, as an nsHttpHeaders object. 1.3684 +*/ 1.3685 + get headers() 1.3686 + { 1.3687 + this._ensureAlive(); 1.3688 + 1.3689 + return this._headers; 1.3690 + }, 1.3691 + 1.3692 + // 1.3693 + // see nsHttpHeaders.getHeader 1.3694 + // 1.3695 + getHeader: function(name) 1.3696 + { 1.3697 + this._ensureAlive(); 1.3698 + 1.3699 + return this._headers.getHeader(name); 1.3700 + }, 1.3701 + 1.3702 + /** 1.3703 +* Determines whether this response may be abandoned in favor of a newly 1.3704 +* constructed response. A response may be abandoned only if it is not being 1.3705 +* sent asynchronously and if raw control over it has not been taken from the 1.3706 +* server. 1.3707 +* 1.3708 +* @returns boolean 1.3709 +* true iff no data has been written to the network 1.3710 +*/ 1.3711 + partiallySent: function() 1.3712 + { 1.3713 + dumpn("*** partiallySent()"); 1.3714 + return this._processAsync || this._powerSeized; 1.3715 + }, 1.3716 + 1.3717 + /** 1.3718 +* If necessary, kicks off the remaining request processing needed to be done 1.3719 +* after a request handler performs its initial work upon this response. 1.3720 +*/ 1.3721 + complete: function() 1.3722 + { 1.3723 + dumpn("*** complete()"); 1.3724 + if (this._processAsync || this._powerSeized) 1.3725 + { 1.3726 + NS_ASSERT(this._processAsync ^ this._powerSeized, 1.3727 + "can't both send async and relinquish power"); 1.3728 + return; 1.3729 + } 1.3730 + 1.3731 + NS_ASSERT(!this.partiallySent(), "completing a partially-sent response?"); 1.3732 + 1.3733 + this._startAsyncProcessor(); 1.3734 + 1.3735 + // Now make sure we finish processing this request! 1.3736 + if (this._bodyOutputStream) 1.3737 + this._bodyOutputStream.close(); 1.3738 + }, 1.3739 + 1.3740 + /** 1.3741 +* Abruptly ends processing of this response, usually due to an error in an 1.3742 +* incoming request but potentially due to a bad error handler. Since we 1.3743 +* cannot handle the error in the usual way (giving an HTTP error page in 1.3744 +* response) because data may already have been sent (or because the response 1.3745 +* might be expected to have been generated asynchronously or completely from 1.3746 +* scratch by the handler), we stop processing this response and abruptly 1.3747 +* close the connection. 1.3748 +* 1.3749 +* @param e : Error 1.3750 +* the exception which precipitated this abort, or null if no such exception 1.3751 +* was generated 1.3752 +*/ 1.3753 + abort: function(e) 1.3754 + { 1.3755 + dumpn("*** abort(<" + e + ">)"); 1.3756 + 1.3757 + // This response will be ended by the processor if one was created. 1.3758 + var copier = this._asyncCopier; 1.3759 + if (copier) 1.3760 + { 1.3761 + // We dispatch asynchronously here so that any pending writes of data to 1.3762 + // the connection will be deterministically written. This makes it easier 1.3763 + // to specify exact behavior, and it makes observable behavior more 1.3764 + // predictable for clients. Note that the correctness of this depends on 1.3765 + // callbacks in response to _waitToReadData in WriteThroughCopier 1.3766 + // happening asynchronously with respect to the actual writing of data to 1.3767 + // bodyOutputStream, as they currently do; if they happened synchronously, 1.3768 + // an event which ran before this one could write more data to the 1.3769 + // response body before we get around to canceling the copier. We have 1.3770 + // tests for this in test_seizepower.js, however, and I can't think of a 1.3771 + // way to handle both cases without removing bodyOutputStream access and 1.3772 + // moving its effective write(data, length) method onto Response, which 1.3773 + // would be slower and require more code than this anyway. 1.3774 + gThreadManager.currentThread.dispatch({ 1.3775 + run: function() 1.3776 + { 1.3777 + dumpn("*** canceling copy asynchronously..."); 1.3778 + copier.cancel(Cr.NS_ERROR_UNEXPECTED); 1.3779 + } 1.3780 + }, Ci.nsIThread.DISPATCH_NORMAL); 1.3781 + } 1.3782 + else 1.3783 + { 1.3784 + this.end(); 1.3785 + } 1.3786 + }, 1.3787 + 1.3788 + /** 1.3789 +* Closes this response's network connection, marks the response as finished, 1.3790 +* and notifies the server handler that the request is done being processed. 1.3791 +*/ 1.3792 + end: function() 1.3793 + { 1.3794 + NS_ASSERT(!this._ended, "ending this response twice?!?!"); 1.3795 + 1.3796 + this._connection.close(); 1.3797 + if (this._bodyOutputStream) 1.3798 + this._bodyOutputStream.close(); 1.3799 + 1.3800 + this._finished = true; 1.3801 + this._ended = true; 1.3802 + }, 1.3803 + 1.3804 + // PRIVATE IMPLEMENTATION 1.3805 + 1.3806 + /** 1.3807 +* Sends the status line and headers of this response if they haven't been 1.3808 +* sent and initiates the process of copying data written to this response's 1.3809 +* body to the network. 1.3810 +*/ 1.3811 + _startAsyncProcessor: function() 1.3812 + { 1.3813 + dumpn("*** _startAsyncProcessor()"); 1.3814 + 1.3815 + // Handle cases where we're being called a second time. The former case 1.3816 + // happens when this is triggered both by complete() and by processAsync(), 1.3817 + // while the latter happens when processAsync() in conjunction with sent 1.3818 + // data causes abort() to be called. 1.3819 + if (this._asyncCopier || this._ended) 1.3820 + { 1.3821 + dumpn("*** ignoring second call to _startAsyncProcessor"); 1.3822 + return; 1.3823 + } 1.3824 + 1.3825 + // Send headers if they haven't been sent already and should be sent, then 1.3826 + // asynchronously continue to send the body. 1.3827 + if (this._headers && !this._powerSeized) 1.3828 + { 1.3829 + this._sendHeaders(); 1.3830 + return; 1.3831 + } 1.3832 + 1.3833 + this._headers = null; 1.3834 + this._sendBody(); 1.3835 + }, 1.3836 + 1.3837 + /** 1.3838 +* Signals that all modifications to the response status line and headers are 1.3839 +* complete and then sends that data over the network to the client. Once 1.3840 +* this method completes, a different response to the request that resulted 1.3841 +* in this response cannot be sent -- the only possible action in case of 1.3842 +* error is to abort the response and close the connection. 1.3843 +*/ 1.3844 + _sendHeaders: function() 1.3845 + { 1.3846 + dumpn("*** _sendHeaders()"); 1.3847 + 1.3848 + NS_ASSERT(this._headers); 1.3849 + NS_ASSERT(!this._powerSeized); 1.3850 + 1.3851 + // request-line 1.3852 + var statusLine = "HTTP/" + this.httpVersion + " " + 1.3853 + this.httpCode + " " + 1.3854 + this.httpDescription + "\r\n"; 1.3855 + 1.3856 + // header post-processing 1.3857 + 1.3858 + var headers = this._headers; 1.3859 + headers.setHeader("Connection", "close", false); 1.3860 + headers.setHeader("Server", "httpd.js", false); 1.3861 + if (!headers.hasHeader("Date")) 1.3862 + headers.setHeader("Date", toDateString(Date.now()), false); 1.3863 + 1.3864 + // Any response not being processed asynchronously must have an associated 1.3865 + // Content-Length header for reasons of backwards compatibility with the 1.3866 + // initial server, which fully buffered every response before sending it. 1.3867 + // Beyond that, however, it's good to do this anyway because otherwise it's 1.3868 + // impossible to test behaviors that depend on the presence or absence of a 1.3869 + // Content-Length header. 1.3870 + if (!this._processAsync) 1.3871 + { 1.3872 + dumpn("*** non-async response, set Content-Length"); 1.3873 + 1.3874 + var bodyStream = this._bodyInputStream; 1.3875 + var avail = bodyStream ? bodyStream.available() : 0; 1.3876 + 1.3877 + // XXX assumes stream will always report the full amount of data available 1.3878 + headers.setHeader("Content-Length", "" + avail, false); 1.3879 + } 1.3880 + 1.3881 + 1.3882 + // construct and send response 1.3883 + dumpn("*** header post-processing completed, sending response head..."); 1.3884 + 1.3885 + // request-line 1.3886 + var preambleData = [statusLine]; 1.3887 + 1.3888 + // headers 1.3889 + var headEnum = headers.enumerator; 1.3890 + while (headEnum.hasMoreElements()) 1.3891 + { 1.3892 + var fieldName = headEnum.getNext() 1.3893 + .QueryInterface(Ci.nsISupportsString) 1.3894 + .data; 1.3895 + var values = headers.getHeaderValues(fieldName); 1.3896 + for (var i = 0, sz = values.length; i < sz; i++) 1.3897 + preambleData.push(fieldName + ": " + values[i] + "\r\n"); 1.3898 + } 1.3899 + 1.3900 + // end request-line/headers 1.3901 + preambleData.push("\r\n"); 1.3902 + 1.3903 + var preamble = preambleData.join(""); 1.3904 + 1.3905 + var responseHeadPipe = new Pipe(true, false, 0, PR_UINT32_MAX, null); 1.3906 + responseHeadPipe.outputStream.write(preamble, preamble.length); 1.3907 + 1.3908 + var response = this; 1.3909 + var copyObserver = 1.3910 + { 1.3911 + onStartRequest: function(request, cx) 1.3912 + { 1.3913 + dumpn("*** preamble copying started"); 1.3914 + }, 1.3915 + 1.3916 + onStopRequest: function(request, cx, statusCode) 1.3917 + { 1.3918 + dumpn("*** preamble copying complete " + 1.3919 + "[status=0x" + statusCode.toString(16) + "]"); 1.3920 + 1.3921 + if (!components.isSuccessCode(statusCode)) 1.3922 + { 1.3923 + dumpn("!!! header copying problems: non-success statusCode, " + 1.3924 + "ending response"); 1.3925 + 1.3926 + response.end(); 1.3927 + } 1.3928 + else 1.3929 + { 1.3930 + response._sendBody(); 1.3931 + } 1.3932 + }, 1.3933 + 1.3934 + QueryInterface: function(aIID) 1.3935 + { 1.3936 + if (aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsISupports)) 1.3937 + return this; 1.3938 + 1.3939 + throw Cr.NS_ERROR_NO_INTERFACE; 1.3940 + } 1.3941 + }; 1.3942 + 1.3943 + var headerCopier = this._asyncCopier = 1.3944 + new WriteThroughCopier(responseHeadPipe.inputStream, 1.3945 + this._connection.output, 1.3946 + copyObserver, null); 1.3947 + 1.3948 + responseHeadPipe.outputStream.close(); 1.3949 + 1.3950 + // Forbid setting any more headers or modifying the request line. 1.3951 + this._headers = null; 1.3952 + }, 1.3953 + 1.3954 + /** 1.3955 +* Asynchronously writes the body of the response (or the entire response, if 1.3956 +* seizePower() has been called) to the network. 1.3957 +*/ 1.3958 + _sendBody: function() 1.3959 + { 1.3960 + dumpn("*** _sendBody"); 1.3961 + 1.3962 + NS_ASSERT(!this._headers, "still have headers around but sending body?"); 1.3963 + 1.3964 + // If no body data was written, we're done 1.3965 + if (!this._bodyInputStream) 1.3966 + { 1.3967 + dumpn("*** empty body, response finished"); 1.3968 + this.end(); 1.3969 + return; 1.3970 + } 1.3971 + 1.3972 + var response = this; 1.3973 + var copyObserver = 1.3974 + { 1.3975 + onStartRequest: function(request, context) 1.3976 + { 1.3977 + dumpn("*** onStartRequest"); 1.3978 + }, 1.3979 + 1.3980 + onStopRequest: function(request, cx, statusCode) 1.3981 + { 1.3982 + dumpn("*** onStopRequest [status=0x" + statusCode.toString(16) + "]"); 1.3983 + 1.3984 + if (statusCode === Cr.NS_BINDING_ABORTED) 1.3985 + { 1.3986 + dumpn("*** terminating copy observer without ending the response"); 1.3987 + } 1.3988 + else 1.3989 + { 1.3990 + if (!components.isSuccessCode(statusCode)) 1.3991 + dumpn("*** WARNING: non-success statusCode in onStopRequest"); 1.3992 + 1.3993 + response.end(); 1.3994 + } 1.3995 + }, 1.3996 + 1.3997 + QueryInterface: function(aIID) 1.3998 + { 1.3999 + if (aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsISupports)) 1.4000 + return this; 1.4001 + 1.4002 + throw Cr.NS_ERROR_NO_INTERFACE; 1.4003 + } 1.4004 + }; 1.4005 + 1.4006 + dumpn("*** starting async copier of body data..."); 1.4007 + this._asyncCopier = 1.4008 + new WriteThroughCopier(this._bodyInputStream, this._connection.output, 1.4009 + copyObserver, null); 1.4010 + }, 1.4011 + 1.4012 + /** Ensures that this hasn't been ended. */ 1.4013 + _ensureAlive: function() 1.4014 + { 1.4015 + NS_ASSERT(!this._ended, "not handling response lifetime correctly"); 1.4016 + } 1.4017 +}; 1.4018 + 1.4019 +/** 1.4020 +* Size of the segments in the buffer used in storing response data and writing 1.4021 +* it to the socket. 1.4022 +*/ 1.4023 +Response.SEGMENT_SIZE = 8192; 1.4024 + 1.4025 +/** Serves double duty in WriteThroughCopier implementation. */ 1.4026 +function notImplemented() 1.4027 +{ 1.4028 + throw Cr.NS_ERROR_NOT_IMPLEMENTED; 1.4029 +} 1.4030 + 1.4031 +/** Returns true iff the given exception represents stream closure. */ 1.4032 +function streamClosed(e) 1.4033 +{ 1.4034 + return e === Cr.NS_BASE_STREAM_CLOSED || 1.4035 + (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_CLOSED); 1.4036 +} 1.4037 + 1.4038 +/** Returns true iff the given exception represents a blocked stream. */ 1.4039 +function wouldBlock(e) 1.4040 +{ 1.4041 + return e === Cr.NS_BASE_STREAM_WOULD_BLOCK || 1.4042 + (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_WOULD_BLOCK); 1.4043 +} 1.4044 + 1.4045 +/** 1.4046 +* Copies data from source to sink as it becomes available, when that data can 1.4047 +* be written to sink without blocking. 1.4048 +* 1.4049 +* @param source : nsIAsyncInputStream 1.4050 +* the stream from which data is to be read 1.4051 +* @param sink : nsIAsyncOutputStream 1.4052 +* the stream to which data is to be copied 1.4053 +* @param observer : nsIRequestObserver 1.4054 +* an observer which will be notified when the copy starts and finishes 1.4055 +* @param context : nsISupports 1.4056 +* context passed to observer when notified of start/stop 1.4057 +* @throws NS_ERROR_NULL_POINTER 1.4058 +* if source, sink, or observer are null 1.4059 +*/ 1.4060 +function WriteThroughCopier(source, sink, observer, context) 1.4061 +{ 1.4062 + if (!source || !sink || !observer) 1.4063 + throw Cr.NS_ERROR_NULL_POINTER; 1.4064 + 1.4065 + /** Stream from which data is being read. */ 1.4066 + this._source = source; 1.4067 + 1.4068 + /** Stream to which data is being written. */ 1.4069 + this._sink = sink; 1.4070 + 1.4071 + /** Observer watching this copy. */ 1.4072 + this._observer = observer; 1.4073 + 1.4074 + /** Context for the observer watching this. */ 1.4075 + this._context = context; 1.4076 + 1.4077 + /** 1.4078 +* True iff this is currently being canceled (cancel has been called, the 1.4079 +* callback may not yet have been made). 1.4080 +*/ 1.4081 + this._canceled = false; 1.4082 + 1.4083 + /** 1.4084 +* False until all data has been read from input and written to output, at 1.4085 +* which point this copy is completed and cancel() is asynchronously called. 1.4086 +*/ 1.4087 + this._completed = false; 1.4088 + 1.4089 + /** Required by nsIRequest, meaningless. */ 1.4090 + this.loadFlags = 0; 1.4091 + /** Required by nsIRequest, meaningless. */ 1.4092 + this.loadGroup = null; 1.4093 + /** Required by nsIRequest, meaningless. */ 1.4094 + this.name = "response-body-copy"; 1.4095 + 1.4096 + /** Status of this request. */ 1.4097 + this.status = Cr.NS_OK; 1.4098 + 1.4099 + /** Arrays of byte strings waiting to be written to output. */ 1.4100 + this._pendingData = []; 1.4101 + 1.4102 + // start copying 1.4103 + try 1.4104 + { 1.4105 + observer.onStartRequest(this, context); 1.4106 + this._waitToReadData(); 1.4107 + this._waitForSinkClosure(); 1.4108 + } 1.4109 + catch (e) 1.4110 + { 1.4111 + dumpn("!!! error starting copy: " + e + 1.4112 + ("lineNumber" in e ? ", line " + e.lineNumber : "")); 1.4113 + dumpn(e.stack); 1.4114 + this.cancel(Cr.NS_ERROR_UNEXPECTED); 1.4115 + } 1.4116 +} 1.4117 +WriteThroughCopier.prototype = 1.4118 +{ 1.4119 + /* nsISupports implementation */ 1.4120 + 1.4121 + QueryInterface: function(iid) 1.4122 + { 1.4123 + if (iid.equals(Ci.nsIInputStreamCallback) || 1.4124 + iid.equals(Ci.nsIOutputStreamCallback) || 1.4125 + iid.equals(Ci.nsIRequest) || 1.4126 + iid.equals(Ci.nsISupports)) 1.4127 + { 1.4128 + return this; 1.4129 + } 1.4130 + 1.4131 + throw Cr.NS_ERROR_NO_INTERFACE; 1.4132 + }, 1.4133 + 1.4134 + 1.4135 + // NSIINPUTSTREAMCALLBACK 1.4136 + 1.4137 + /** 1.4138 +* Receives a more-data-in-input notification and writes the corresponding 1.4139 +* data to the output. 1.4140 +* 1.4141 +* @param input : nsIAsyncInputStream 1.4142 +* the input stream on whose data we have been waiting 1.4143 +*/ 1.4144 + onInputStreamReady: function(input) 1.4145 + { 1.4146 + if (this._source === null) 1.4147 + return; 1.4148 + 1.4149 + dumpn("*** onInputStreamReady"); 1.4150 + 1.4151 + // 1.4152 + // Ordinarily we'll read a non-zero amount of data from input, queue it up 1.4153 + // to be written and then wait for further callbacks. The complications in 1.4154 + // this method are the cases where we deviate from that behavior when errors 1.4155 + // occur or when copying is drawing to a finish. 1.4156 + // 1.4157 + // The edge cases when reading data are: 1.4158 + // 1.4159 + // Zero data is read 1.4160 + // If zero data was read, we're at the end of available data, so we can 1.4161 + // should stop reading and move on to writing out what we have (or, if 1.4162 + // we've already done that, onto notifying of completion). 1.4163 + // A stream-closed exception is thrown 1.4164 + // This is effectively a less kind version of zero data being read; the 1.4165 + // only difference is that we notify of completion with that result 1.4166 + // rather than with NS_OK. 1.4167 + // Some other exception is thrown 1.4168 + // This is the least kind result. We don't know what happened, so we 1.4169 + // act as though the stream closed except that we notify of completion 1.4170 + // with the result NS_ERROR_UNEXPECTED. 1.4171 + // 1.4172 + 1.4173 + var bytesWanted = 0, bytesConsumed = -1; 1.4174 + try 1.4175 + { 1.4176 + input = new BinaryInputStream(input); 1.4177 + 1.4178 + bytesWanted = Math.min(input.available(), Response.SEGMENT_SIZE); 1.4179 + dumpn("*** input wanted: " + bytesWanted); 1.4180 + 1.4181 + if (bytesWanted > 0) 1.4182 + { 1.4183 + var data = input.readByteArray(bytesWanted); 1.4184 + bytesConsumed = data.length; 1.4185 + this._pendingData.push(String.fromCharCode.apply(String, data)); 1.4186 + } 1.4187 + 1.4188 + dumpn("*** " + bytesConsumed + " bytes read"); 1.4189 + 1.4190 + // Handle the zero-data edge case in the same place as all other edge 1.4191 + // cases are handled. 1.4192 + if (bytesWanted === 0) 1.4193 + throw Cr.NS_BASE_STREAM_CLOSED; 1.4194 + } 1.4195 + catch (e) 1.4196 + { 1.4197 + if (streamClosed(e)) 1.4198 + { 1.4199 + dumpn("*** input stream closed"); 1.4200 + e = bytesWanted === 0 ? Cr.NS_OK : Cr.NS_ERROR_UNEXPECTED; 1.4201 + } 1.4202 + else 1.4203 + { 1.4204 + dumpn("!!! unexpected error reading from input, canceling: " + e); 1.4205 + e = Cr.NS_ERROR_UNEXPECTED; 1.4206 + } 1.4207 + 1.4208 + this._doneReadingSource(e); 1.4209 + return; 1.4210 + } 1.4211 + 1.4212 + var pendingData = this._pendingData; 1.4213 + 1.4214 + NS_ASSERT(bytesConsumed > 0); 1.4215 + NS_ASSERT(pendingData.length > 0, "no pending data somehow?"); 1.4216 + NS_ASSERT(pendingData[pendingData.length - 1].length > 0, 1.4217 + "buffered zero bytes of data?"); 1.4218 + 1.4219 + NS_ASSERT(this._source !== null); 1.4220 + 1.4221 + // Reading has gone great, and we've gotten data to write now. What if we 1.4222 + // don't have a place to write that data, because output went away just 1.4223 + // before this read? Drop everything on the floor, including new data, and 1.4224 + // cancel at this point. 1.4225 + if (this._sink === null) 1.4226 + { 1.4227 + pendingData.length = 0; 1.4228 + this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED); 1.4229 + return; 1.4230 + } 1.4231 + 1.4232 + // Okay, we've read the data, and we know we have a place to write it. We 1.4233 + // need to queue up the data to be written, but *only* if none is queued 1.4234 + // already -- if data's already queued, the code that actually writes the 1.4235 + // data will make sure to wait on unconsumed pending data. 1.4236 + try 1.4237 + { 1.4238 + if (pendingData.length === 1) 1.4239 + this._waitToWriteData(); 1.4240 + } 1.4241 + catch (e) 1.4242 + { 1.4243 + dumpn("!!! error waiting to write data just read, swallowing and " + 1.4244 + "writing only what we already have: " + e); 1.4245 + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); 1.4246 + return; 1.4247 + } 1.4248 + 1.4249 + // Whee! We successfully read some data, and it's successfully queued up to 1.4250 + // be written. All that remains now is to wait for more data to read. 1.4251 + try 1.4252 + { 1.4253 + this._waitToReadData(); 1.4254 + } 1.4255 + catch (e) 1.4256 + { 1.4257 + dumpn("!!! error waiting to read more data: " + e); 1.4258 + this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED); 1.4259 + } 1.4260 + }, 1.4261 + 1.4262 + 1.4263 + // NSIOUTPUTSTREAMCALLBACK 1.4264 + 1.4265 + /** 1.4266 +* Callback when data may be written to the output stream without blocking, or 1.4267 +* when the output stream has been closed. 1.4268 +* 1.4269 +* @param output : nsIAsyncOutputStream 1.4270 +* the output stream on whose writability we've been waiting, also known as 1.4271 +* this._sink 1.4272 +*/ 1.4273 + onOutputStreamReady: function(output) 1.4274 + { 1.4275 + if (this._sink === null) 1.4276 + return; 1.4277 + 1.4278 + dumpn("*** onOutputStreamReady"); 1.4279 + 1.4280 + var pendingData = this._pendingData; 1.4281 + if (pendingData.length === 0) 1.4282 + { 1.4283 + // There's no pending data to write. The only way this can happen is if 1.4284 + // we're waiting on the output stream's closure, so we can respond to a 1.4285 + // copying failure as quickly as possible (rather than waiting for data to 1.4286 + // be available to read and then fail to be copied). Therefore, we must 1.4287 + // be done now -- don't bother to attempt to write anything and wrap 1.4288 + // things up. 1.4289 + dumpn("!!! output stream closed prematurely, ending copy"); 1.4290 + 1.4291 + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); 1.4292 + return; 1.4293 + } 1.4294 + 1.4295 + 1.4296 + NS_ASSERT(pendingData[0].length > 0, "queued up an empty quantum?"); 1.4297 + 1.4298 + // 1.4299 + // Write out the first pending quantum of data. The possible errors here 1.4300 + // are: 1.4301 + // 1.4302 + // The write might fail because we can't write that much data 1.4303 + // Okay, we've written what we can now, so re-queue what's left and 1.4304 + // finish writing it out later. 1.4305 + // The write failed because the stream was closed 1.4306 + // Discard pending data that we can no longer write, stop reading, and 1.4307 + // signal that copying finished. 1.4308 + // Some other error occurred. 1.4309 + // Same as if the stream were closed, but notify with the status 1.4310 + // NS_ERROR_UNEXPECTED so the observer knows something was wonky. 1.4311 + // 1.4312 + 1.4313 + try 1.4314 + { 1.4315 + var quantum = pendingData[0]; 1.4316 + 1.4317 + // XXX |quantum| isn't guaranteed to be ASCII, so we're relying on 1.4318 + // undefined behavior! We're only using this because writeByteArray 1.4319 + // is unusably broken for asynchronous output streams; see bug 532834 1.4320 + // for details. 1.4321 + var bytesWritten = output.write(quantum, quantum.length); 1.4322 + if (bytesWritten === quantum.length) 1.4323 + pendingData.shift(); 1.4324 + else 1.4325 + pendingData[0] = quantum.substring(bytesWritten); 1.4326 + 1.4327 + dumpn("*** wrote " + bytesWritten + " bytes of data"); 1.4328 + } 1.4329 + catch (e) 1.4330 + { 1.4331 + if (wouldBlock(e)) 1.4332 + { 1.4333 + NS_ASSERT(pendingData.length > 0, 1.4334 + "stream-blocking exception with no data to write?"); 1.4335 + NS_ASSERT(pendingData[0].length > 0, 1.4336 + "stream-blocking exception with empty quantum?"); 1.4337 + this._waitToWriteData(); 1.4338 + return; 1.4339 + } 1.4340 + 1.4341 + if (streamClosed(e)) 1.4342 + dumpn("!!! output stream prematurely closed, signaling error..."); 1.4343 + else 1.4344 + dumpn("!!! unknown error: " + e + ", quantum=" + quantum); 1.4345 + 1.4346 + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); 1.4347 + return; 1.4348 + } 1.4349 + 1.4350 + // The day is ours! Quantum written, now let's see if we have more data 1.4351 + // still to write. 1.4352 + try 1.4353 + { 1.4354 + if (pendingData.length > 0) 1.4355 + { 1.4356 + this._waitToWriteData(); 1.4357 + return; 1.4358 + } 1.4359 + } 1.4360 + catch (e) 1.4361 + { 1.4362 + dumpn("!!! unexpected error waiting to write pending data: " + e); 1.4363 + this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED); 1.4364 + return; 1.4365 + } 1.4366 + 1.4367 + // Okay, we have no more pending data to write -- but might we get more in 1.4368 + // the future? 1.4369 + if (this._source !== null) 1.4370 + { 1.4371 + /* 1.4372 +* If we might, then wait for the output stream to be closed. (We wait 1.4373 +* only for closure because we have no data to write -- and if we waited 1.4374 +* for a specific amount of data, we would get repeatedly notified for no 1.4375 +* reason if over time the output stream permitted more and more data to 1.4376 +* be written to it without blocking.) 1.4377 +*/ 1.4378 + this._waitForSinkClosure(); 1.4379 + } 1.4380 + else 1.4381 + { 1.4382 + /* 1.4383 +* On the other hand, if we can't have more data because the input 1.4384 +* stream's gone away, then it's time to notify of copy completion. 1.4385 +* Victory! 1.4386 +*/ 1.4387 + this._sink = null; 1.4388 + this._cancelOrDispatchCancelCallback(Cr.NS_OK); 1.4389 + } 1.4390 + }, 1.4391 + 1.4392 + 1.4393 + // NSIREQUEST 1.4394 + 1.4395 + /** Returns true if the cancel observer hasn't been notified yet. */ 1.4396 + isPending: function() 1.4397 + { 1.4398 + return !this._completed; 1.4399 + }, 1.4400 + 1.4401 + /** Not implemented, don't use! */ 1.4402 + suspend: notImplemented, 1.4403 + /** Not implemented, don't use! */ 1.4404 + resume: notImplemented, 1.4405 + 1.4406 + /** 1.4407 +* Cancels data reading from input, asynchronously writes out any pending 1.4408 +* data, and causes the observer to be notified with the given error code when 1.4409 +* all writing has finished. 1.4410 +* 1.4411 +* @param status : nsresult 1.4412 +* the status to pass to the observer when data copying has been canceled 1.4413 +*/ 1.4414 + cancel: function(status) 1.4415 + { 1.4416 + dumpn("*** cancel(" + status.toString(16) + ")"); 1.4417 + 1.4418 + if (this._canceled) 1.4419 + { 1.4420 + dumpn("*** suppressing a late cancel"); 1.4421 + return; 1.4422 + } 1.4423 + 1.4424 + this._canceled = true; 1.4425 + this.status = status; 1.4426 + 1.4427 + // We could be in the middle of absolutely anything at this point. Both 1.4428 + // input and output might still be around, we might have pending data to 1.4429 + // write, and in general we know nothing about the state of the world. We 1.4430 + // therefore must assume everything's in progress and take everything to its 1.4431 + // final steady state (or so far as it can go before we need to finish 1.4432 + // writing out remaining data). 1.4433 + 1.4434 + this._doneReadingSource(status); 1.4435 + }, 1.4436 + 1.4437 + 1.4438 + // PRIVATE IMPLEMENTATION 1.4439 + 1.4440 + /** 1.4441 +* Stop reading input if we haven't already done so, passing e as the status 1.4442 +* when closing the stream, and kick off a copy-completion notice if no more 1.4443 +* data remains to be written. 1.4444 +* 1.4445 +* @param e : nsresult 1.4446 +* the status to be used when closing the input stream 1.4447 +*/ 1.4448 + _doneReadingSource: function(e) 1.4449 + { 1.4450 + dumpn("*** _doneReadingSource(0x" + e.toString(16) + ")"); 1.4451 + 1.4452 + this._finishSource(e); 1.4453 + if (this._pendingData.length === 0) 1.4454 + this._sink = null; 1.4455 + else 1.4456 + NS_ASSERT(this._sink !== null, "null output?"); 1.4457 + 1.4458 + // If we've written out all data read up to this point, then it's time to 1.4459 + // signal completion. 1.4460 + if (this._sink === null) 1.4461 + { 1.4462 + NS_ASSERT(this._pendingData.length === 0, "pending data still?"); 1.4463 + this._cancelOrDispatchCancelCallback(e); 1.4464 + } 1.4465 + }, 1.4466 + 1.4467 + /** 1.4468 +* Stop writing output if we haven't already done so, discard any data that 1.4469 +* remained to be sent, close off input if it wasn't already closed, and kick 1.4470 +* off a copy-completion notice. 1.4471 +* 1.4472 +* @param e : nsresult 1.4473 +* the status to be used when closing input if it wasn't already closed 1.4474 +*/ 1.4475 + _doneWritingToSink: function(e) 1.4476 + { 1.4477 + dumpn("*** _doneWritingToSink(0x" + e.toString(16) + ")"); 1.4478 + 1.4479 + this._pendingData.length = 0; 1.4480 + this._sink = null; 1.4481 + this._doneReadingSource(e); 1.4482 + }, 1.4483 + 1.4484 + /** 1.4485 +* Completes processing of this copy: either by canceling the copy if it 1.4486 +* hasn't already been canceled using the provided status, or by dispatching 1.4487 +* the cancel callback event (with the originally provided status, of course) 1.4488 +* if it already has been canceled. 1.4489 +* 1.4490 +* @param status : nsresult 1.4491 +* the status code to use to cancel this, if this hasn't already been 1.4492 +* canceled 1.4493 +*/ 1.4494 + _cancelOrDispatchCancelCallback: function(status) 1.4495 + { 1.4496 + dumpn("*** _cancelOrDispatchCancelCallback(" + status + ")"); 1.4497 + 1.4498 + NS_ASSERT(this._source === null, "should have finished input"); 1.4499 + NS_ASSERT(this._sink === null, "should have finished output"); 1.4500 + NS_ASSERT(this._pendingData.length === 0, "should have no pending data"); 1.4501 + 1.4502 + if (!this._canceled) 1.4503 + { 1.4504 + this.cancel(status); 1.4505 + return; 1.4506 + } 1.4507 + 1.4508 + var self = this; 1.4509 + var event = 1.4510 + { 1.4511 + run: function() 1.4512 + { 1.4513 + dumpn("*** onStopRequest async callback"); 1.4514 + 1.4515 + self._completed = true; 1.4516 + try 1.4517 + { 1.4518 + self._observer.onStopRequest(self, self._context, self.status); 1.4519 + } 1.4520 + catch (e) 1.4521 + { 1.4522 + NS_ASSERT(false, 1.4523 + "how are we throwing an exception here? we control " + 1.4524 + "all the callers! " + e); 1.4525 + } 1.4526 + } 1.4527 + }; 1.4528 + 1.4529 + gThreadManager.currentThread.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL); 1.4530 + }, 1.4531 + 1.4532 + /** 1.4533 +* Kicks off another wait for more data to be available from the input stream. 1.4534 +*/ 1.4535 + _waitToReadData: function() 1.4536 + { 1.4537 + dumpn("*** _waitToReadData"); 1.4538 + this._source.asyncWait(this, 0, Response.SEGMENT_SIZE, 1.4539 + gThreadManager.mainThread); 1.4540 + }, 1.4541 + 1.4542 + /** 1.4543 +* Kicks off another wait until data can be written to the output stream. 1.4544 +*/ 1.4545 + _waitToWriteData: function() 1.4546 + { 1.4547 + dumpn("*** _waitToWriteData"); 1.4548 + 1.4549 + var pendingData = this._pendingData; 1.4550 + NS_ASSERT(pendingData.length > 0, "no pending data to write?"); 1.4551 + NS_ASSERT(pendingData[0].length > 0, "buffered an empty write?"); 1.4552 + 1.4553 + this._sink.asyncWait(this, 0, pendingData[0].length, 1.4554 + gThreadManager.mainThread); 1.4555 + }, 1.4556 + 1.4557 + /** 1.4558 +* Kicks off a wait for the sink to which data is being copied to be closed. 1.4559 +* We wait for stream closure when we don't have any data to be copied, rather 1.4560 +* than waiting to write a specific amount of data. We can't wait to write 1.4561 +* data because the sink might be infinitely writable, and if no data appears 1.4562 +* in the source for a long time we might have to spin quite a bit waiting to 1.4563 +* write, waiting to write again, &c. Waiting on stream closure instead means 1.4564 +* we'll get just one notification if the sink dies. Note that when data 1.4565 +* starts arriving from the sink we'll resume waiting for data to be written, 1.4566 +* dropping this closure-only callback entirely. 1.4567 +*/ 1.4568 + _waitForSinkClosure: function() 1.4569 + { 1.4570 + dumpn("*** _waitForSinkClosure"); 1.4571 + 1.4572 + this._sink.asyncWait(this, Ci.nsIAsyncOutputStream.WAIT_CLOSURE_ONLY, 0, 1.4573 + gThreadManager.mainThread); 1.4574 + }, 1.4575 + 1.4576 + /** 1.4577 +* Closes input with the given status, if it hasn't already been closed; 1.4578 +* otherwise a no-op. 1.4579 +* 1.4580 +* @param status : nsresult 1.4581 +* status code use to close the source stream if necessary 1.4582 +*/ 1.4583 + _finishSource: function(status) 1.4584 + { 1.4585 + dumpn("*** _finishSource(" + status.toString(16) + ")"); 1.4586 + 1.4587 + if (this._source !== null) 1.4588 + { 1.4589 + this._source.closeWithStatus(status); 1.4590 + this._source = null; 1.4591 + } 1.4592 + } 1.4593 +}; 1.4594 + 1.4595 + 1.4596 +/** 1.4597 +* A container for utility functions used with HTTP headers. 1.4598 +*/ 1.4599 +const headerUtils = 1.4600 +{ 1.4601 + /** 1.4602 +* Normalizes fieldName (by converting it to lowercase) and ensures it is a 1.4603 +* valid header field name (although not necessarily one specified in RFC 1.4604 +* 2616). 1.4605 +* 1.4606 +* @throws NS_ERROR_INVALID_ARG 1.4607 +* if fieldName does not match the field-name production in RFC 2616 1.4608 +* @returns string 1.4609 +* fieldName converted to lowercase if it is a valid header, for characters 1.4610 +* where case conversion is possible 1.4611 +*/ 1.4612 + normalizeFieldName: function(fieldName) 1.4613 + { 1.4614 + if (fieldName == "") 1.4615 + throw Cr.NS_ERROR_INVALID_ARG; 1.4616 + 1.4617 + for (var i = 0, sz = fieldName.length; i < sz; i++) 1.4618 + { 1.4619 + if (!IS_TOKEN_ARRAY[fieldName.charCodeAt(i)]) 1.4620 + { 1.4621 + dumpn(fieldName + " is not a valid header field name!"); 1.4622 + throw Cr.NS_ERROR_INVALID_ARG; 1.4623 + } 1.4624 + } 1.4625 + 1.4626 + return fieldName.toLowerCase(); 1.4627 + }, 1.4628 + 1.4629 + /** 1.4630 +* Ensures that fieldValue is a valid header field value (although not 1.4631 +* necessarily as specified in RFC 2616 if the corresponding field name is 1.4632 +* part of the HTTP protocol), normalizes the value if it is, and 1.4633 +* returns the normalized value. 1.4634 +* 1.4635 +* @param fieldValue : string 1.4636 +* a value to be normalized as an HTTP header field value 1.4637 +* @throws NS_ERROR_INVALID_ARG 1.4638 +* if fieldValue does not match the field-value production in RFC 2616 1.4639 +* @returns string 1.4640 +* fieldValue as a normalized HTTP header field value 1.4641 +*/ 1.4642 + normalizeFieldValue: function(fieldValue) 1.4643 + { 1.4644 + // field-value = *( field-content | LWS ) 1.4645 + // field-content = <the OCTETs making up the field-value 1.4646 + // and consisting of either *TEXT or combinations 1.4647 + // of token, separators, and quoted-string> 1.4648 + // TEXT = <any OCTET except CTLs, 1.4649 + // but including LWS> 1.4650 + // LWS = [CRLF] 1*( SP | HT ) 1.4651 + // 1.4652 + // quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) 1.4653 + // qdtext = <any TEXT except <">> 1.4654 + // quoted-pair = "\" CHAR 1.4655 + // CHAR = <any US-ASCII character (octets 0 - 127)> 1.4656 + 1.4657 + // Any LWS that occurs between field-content MAY be replaced with a single 1.4658 + // SP before interpreting the field value or forwarding the message 1.4659 + // downstream (section 4.2); we replace 1*LWS with a single SP 1.4660 + var val = fieldValue.replace(/(?:(?:\r\n)?[ \t]+)+/g, " "); 1.4661 + 1.4662 + // remove leading/trailing LWS (which has been converted to SP) 1.4663 + val = val.replace(/^ +/, "").replace(/ +$/, ""); 1.4664 + 1.4665 + // that should have taken care of all CTLs, so val should contain no CTLs 1.4666 + for (var i = 0, len = val.length; i < len; i++) 1.4667 + if (isCTL(val.charCodeAt(i))) 1.4668 + throw Cr.NS_ERROR_INVALID_ARG; 1.4669 + 1.4670 + // XXX disallows quoted-pair where CHAR is a CTL -- will not invalidly 1.4671 + // normalize, however, so this can be construed as a tightening of the 1.4672 + // spec and not entirely as a bug 1.4673 + return val; 1.4674 + } 1.4675 +}; 1.4676 + 1.4677 + 1.4678 + 1.4679 +/** 1.4680 +* Converts the given string into a string which is safe for use in an HTML 1.4681 +* context. 1.4682 +* 1.4683 +* @param str : string 1.4684 +* the string to make HTML-safe 1.4685 +* @returns string 1.4686 +* an HTML-safe version of str 1.4687 +*/ 1.4688 +function htmlEscape(str) 1.4689 +{ 1.4690 + // this is naive, but it'll work 1.4691 + var s = ""; 1.4692 + for (var i = 0; i < str.length; i++) 1.4693 + s += "&#" + str.charCodeAt(i) + ";"; 1.4694 + return s; 1.4695 +} 1.4696 + 1.4697 + 1.4698 +/** 1.4699 +* Constructs an object representing an HTTP version (see section 3.1). 1.4700 +* 1.4701 +* @param versionString 1.4702 +* a string of the form "#.#", where # is an non-negative decimal integer with 1.4703 +* or without leading zeros 1.4704 +* @throws 1.4705 +* if versionString does not specify a valid HTTP version number 1.4706 +*/ 1.4707 +function nsHttpVersion(versionString) 1.4708 +{ 1.4709 + var matches = /^(\d+)\.(\d+)$/.exec(versionString); 1.4710 + if (!matches) 1.4711 + throw "Not a valid HTTP version!"; 1.4712 + 1.4713 + /** The major version number of this, as a number. */ 1.4714 + this.major = parseInt(matches[1], 10); 1.4715 + 1.4716 + /** The minor version number of this, as a number. */ 1.4717 + this.minor = parseInt(matches[2], 10); 1.4718 + 1.4719 + if (isNaN(this.major) || isNaN(this.minor) || 1.4720 + this.major < 0 || this.minor < 0) 1.4721 + throw "Not a valid HTTP version!"; 1.4722 +} 1.4723 +nsHttpVersion.prototype = 1.4724 +{ 1.4725 + /** 1.4726 +* Returns the standard string representation of the HTTP version represented 1.4727 +* by this (e.g., "1.1"). 1.4728 +*/ 1.4729 + toString: function () 1.4730 + { 1.4731 + return this.major + "." + this.minor; 1.4732 + }, 1.4733 + 1.4734 + /** 1.4735 +* Returns true if this represents the same HTTP version as otherVersion, 1.4736 +* false otherwise. 1.4737 +* 1.4738 +* @param otherVersion : nsHttpVersion 1.4739 +* the version to compare against this 1.4740 +*/ 1.4741 + equals: function (otherVersion) 1.4742 + { 1.4743 + return this.major == otherVersion.major && 1.4744 + this.minor == otherVersion.minor; 1.4745 + }, 1.4746 + 1.4747 + /** True if this >= otherVersion, false otherwise. */ 1.4748 + atLeast: function(otherVersion) 1.4749 + { 1.4750 + return this.major > otherVersion.major || 1.4751 + (this.major == otherVersion.major && 1.4752 + this.minor >= otherVersion.minor); 1.4753 + } 1.4754 +}; 1.4755 + 1.4756 +nsHttpVersion.HTTP_1_0 = new nsHttpVersion("1.0"); 1.4757 +nsHttpVersion.HTTP_1_1 = new nsHttpVersion("1.1"); 1.4758 + 1.4759 + 1.4760 +/** 1.4761 +* An object which stores HTTP headers for a request or response. 1.4762 +* 1.4763 +* Note that since headers are case-insensitive, this object converts headers to 1.4764 +* lowercase before storing them. This allows the getHeader and hasHeader 1.4765 +* methods to work correctly for any case of a header, but it means that the 1.4766 +* values returned by .enumerator may not be equal case-sensitively to the 1.4767 +* values passed to setHeader when adding headers to this. 1.4768 +*/ 1.4769 +function nsHttpHeaders() 1.4770 +{ 1.4771 + /** 1.4772 +* A hash of headers, with header field names as the keys and header field 1.4773 +* values as the values. Header field names are case-insensitive, but upon 1.4774 +* insertion here they are converted to lowercase. Header field values are 1.4775 +* normalized upon insertion to contain no leading or trailing whitespace. 1.4776 +* 1.4777 +* Note also that per RFC 2616, section 4.2, two headers with the same name in 1.4778 +* a message may be treated as one header with the same field name and a field 1.4779 +* value consisting of the separate field values joined together with a "," in 1.4780 +* their original order. This hash stores multiple headers with the same name 1.4781 +* in this manner. 1.4782 +*/ 1.4783 + this._headers = {}; 1.4784 +} 1.4785 +nsHttpHeaders.prototype = 1.4786 +{ 1.4787 + /** 1.4788 +* Sets the header represented by name and value in this. 1.4789 +* 1.4790 +* @param name : string 1.4791 +* the header name 1.4792 +* @param value : string 1.4793 +* the header value 1.4794 +* @throws NS_ERROR_INVALID_ARG 1.4795 +* if name or value is not a valid header component 1.4796 +*/ 1.4797 + setHeader: function(fieldName, fieldValue, merge) 1.4798 + { 1.4799 + var name = headerUtils.normalizeFieldName(fieldName); 1.4800 + var value = headerUtils.normalizeFieldValue(fieldValue); 1.4801 + 1.4802 + // The following three headers are stored as arrays because their real-world 1.4803 + // syntax prevents joining individual headers into a single header using 1.4804 + // ",". See also <http://hg.mozilla.org/mozilla-central/diff/9b2a99adc05e/netwerk/protocol/http/src/nsHttpHeaderArray.cpp#l77> 1.4805 + if (merge && name in this._headers) 1.4806 + { 1.4807 + if (name === "www-authenticate" || 1.4808 + name === "proxy-authenticate" || 1.4809 + name === "set-cookie") 1.4810 + { 1.4811 + this._headers[name].push(value); 1.4812 + } 1.4813 + else 1.4814 + { 1.4815 + this._headers[name][0] += "," + value; 1.4816 + NS_ASSERT(this._headers[name].length === 1, 1.4817 + "how'd a non-special header have multiple values?") 1.4818 + } 1.4819 + } 1.4820 + else 1.4821 + { 1.4822 + this._headers[name] = [value]; 1.4823 + } 1.4824 + }, 1.4825 + 1.4826 + /** 1.4827 +* Returns the value for the header specified by this. 1.4828 +* 1.4829 +* @throws NS_ERROR_INVALID_ARG 1.4830 +* if fieldName does not constitute a valid header field name 1.4831 +* @throws NS_ERROR_NOT_AVAILABLE 1.4832 +* if the given header does not exist in this 1.4833 +* @returns string 1.4834 +* the field value for the given header, possibly with non-semantic changes 1.4835 +* (i.e., leading/trailing whitespace stripped, whitespace runs replaced 1.4836 +* with spaces, etc.) at the option of the implementation; multiple 1.4837 +* instances of the header will be combined with a comma, except for 1.4838 +* the three headers noted in the description of getHeaderValues 1.4839 +*/ 1.4840 + getHeader: function(fieldName) 1.4841 + { 1.4842 + return this.getHeaderValues(fieldName).join("\n"); 1.4843 + }, 1.4844 + 1.4845 + /** 1.4846 +* Returns the value for the header specified by fieldName as an array. 1.4847 +* 1.4848 +* @throws NS_ERROR_INVALID_ARG 1.4849 +* if fieldName does not constitute a valid header field name 1.4850 +* @throws NS_ERROR_NOT_AVAILABLE 1.4851 +* if the given header does not exist in this 1.4852 +* @returns [string] 1.4853 +* an array of all the header values in this for the given 1.4854 +* header name. Header values will generally be collapsed 1.4855 +* into a single header by joining all header values together 1.4856 +* with commas, but certain headers (Proxy-Authenticate, 1.4857 +* WWW-Authenticate, and Set-Cookie) violate the HTTP spec 1.4858 +* and cannot be collapsed in this manner. For these headers 1.4859 +* only, the returned array may contain multiple elements if 1.4860 +* that header has been added more than once. 1.4861 +*/ 1.4862 + getHeaderValues: function(fieldName) 1.4863 + { 1.4864 + var name = headerUtils.normalizeFieldName(fieldName); 1.4865 + 1.4866 + if (name in this._headers) 1.4867 + return this._headers[name]; 1.4868 + else 1.4869 + throw Cr.NS_ERROR_NOT_AVAILABLE; 1.4870 + }, 1.4871 + 1.4872 + /** 1.4873 +* Returns true if a header with the given field name exists in this, false 1.4874 +* otherwise. 1.4875 +* 1.4876 +* @param fieldName : string 1.4877 +* the field name whose existence is to be determined in this 1.4878 +* @throws NS_ERROR_INVALID_ARG 1.4879 +* if fieldName does not constitute a valid header field name 1.4880 +* @returns boolean 1.4881 +* true if the header's present, false otherwise 1.4882 +*/ 1.4883 + hasHeader: function(fieldName) 1.4884 + { 1.4885 + var name = headerUtils.normalizeFieldName(fieldName); 1.4886 + return (name in this._headers); 1.4887 + }, 1.4888 + 1.4889 + /** 1.4890 +* Returns a new enumerator over the field names of the headers in this, as 1.4891 +* nsISupportsStrings. The names returned will be in lowercase, regardless of 1.4892 +* how they were input using setHeader (header names are case-insensitive per 1.4893 +* RFC 2616). 1.4894 +*/ 1.4895 + get enumerator() 1.4896 + { 1.4897 + var headers = []; 1.4898 + for (var i in this._headers) 1.4899 + { 1.4900 + var supports = new SupportsString(); 1.4901 + supports.data = i; 1.4902 + headers.push(supports); 1.4903 + } 1.4904 + 1.4905 + return new nsSimpleEnumerator(headers); 1.4906 + } 1.4907 +}; 1.4908 + 1.4909 + 1.4910 +/** 1.4911 +* Constructs an nsISimpleEnumerator for the given array of items. 1.4912 +* 1.4913 +* @param items : Array 1.4914 +* the items, which must all implement nsISupports 1.4915 +*/ 1.4916 +function nsSimpleEnumerator(items) 1.4917 +{ 1.4918 + this._items = items; 1.4919 + this._nextIndex = 0; 1.4920 +} 1.4921 +nsSimpleEnumerator.prototype = 1.4922 +{ 1.4923 + hasMoreElements: function() 1.4924 + { 1.4925 + return this._nextIndex < this._items.length; 1.4926 + }, 1.4927 + getNext: function() 1.4928 + { 1.4929 + if (!this.hasMoreElements()) 1.4930 + throw Cr.NS_ERROR_NOT_AVAILABLE; 1.4931 + 1.4932 + return this._items[this._nextIndex++]; 1.4933 + }, 1.4934 + QueryInterface: function(aIID) 1.4935 + { 1.4936 + if (Ci.nsISimpleEnumerator.equals(aIID) || 1.4937 + Ci.nsISupports.equals(aIID)) 1.4938 + return this; 1.4939 + 1.4940 + throw Cr.NS_ERROR_NO_INTERFACE; 1.4941 + } 1.4942 +}; 1.4943 + 1.4944 + 1.4945 +/** 1.4946 +* A representation of the data in an HTTP request. 1.4947 +* 1.4948 +* @param port : uint 1.4949 +* the port on which the server receiving this request runs 1.4950 +*/ 1.4951 +function Request(port) 1.4952 +{ 1.4953 + /** Method of this request, e.g. GET or POST. */ 1.4954 + this._method = ""; 1.4955 + 1.4956 + /** Path of the requested resource; empty paths are converted to '/'. */ 1.4957 + this._path = ""; 1.4958 + 1.4959 + /** Query string, if any, associated with this request (not including '?'). */ 1.4960 + this._queryString = ""; 1.4961 + 1.4962 + /** Scheme of requested resource, usually http, always lowercase. */ 1.4963 + this._scheme = "http"; 1.4964 + 1.4965 + /** Hostname on which the requested resource resides. */ 1.4966 + this._host = undefined; 1.4967 + 1.4968 + /** Port number over which the request was received. */ 1.4969 + this._port = port; 1.4970 + 1.4971 + var bodyPipe = new Pipe(false, false, 0, PR_UINT32_MAX, null); 1.4972 + 1.4973 + /** Stream from which data in this request's body may be read. */ 1.4974 + this._bodyInputStream = bodyPipe.inputStream; 1.4975 + 1.4976 + /** Stream to which data in this request's body is written. */ 1.4977 + this._bodyOutputStream = bodyPipe.outputStream; 1.4978 + 1.4979 + /** 1.4980 +* The headers in this request. 1.4981 +*/ 1.4982 + this._headers = new nsHttpHeaders(); 1.4983 + 1.4984 + /** 1.4985 +* For the addition of ad-hoc properties and new functionality without having 1.4986 +* to change nsIHttpRequest every time; currently lazily created, as its only 1.4987 +* use is in directory listings. 1.4988 +*/ 1.4989 + this._bag = null; 1.4990 +} 1.4991 +Request.prototype = 1.4992 +{ 1.4993 + // SERVER METADATA 1.4994 + 1.4995 + // 1.4996 + // see nsIHttpRequest.scheme 1.4997 + // 1.4998 + get scheme() 1.4999 + { 1.5000 + return this._scheme; 1.5001 + }, 1.5002 + 1.5003 + // 1.5004 + // see nsIHttpRequest.host 1.5005 + // 1.5006 + get host() 1.5007 + { 1.5008 + return this._host; 1.5009 + }, 1.5010 + 1.5011 + // 1.5012 + // see nsIHttpRequest.port 1.5013 + // 1.5014 + get port() 1.5015 + { 1.5016 + return this._port; 1.5017 + }, 1.5018 + 1.5019 + // REQUEST LINE 1.5020 + 1.5021 + // 1.5022 + // see nsIHttpRequest.method 1.5023 + // 1.5024 + get method() 1.5025 + { 1.5026 + return this._method; 1.5027 + }, 1.5028 + 1.5029 + // 1.5030 + // see nsIHttpRequest.httpVersion 1.5031 + // 1.5032 + get httpVersion() 1.5033 + { 1.5034 + return this._httpVersion.toString(); 1.5035 + }, 1.5036 + 1.5037 + // 1.5038 + // see nsIHttpRequest.path 1.5039 + // 1.5040 + get path() 1.5041 + { 1.5042 + return this._path; 1.5043 + }, 1.5044 + 1.5045 + // 1.5046 + // see nsIHttpRequest.queryString 1.5047 + // 1.5048 + get queryString() 1.5049 + { 1.5050 + return this._queryString; 1.5051 + }, 1.5052 + 1.5053 + // HEADERS 1.5054 + 1.5055 + // 1.5056 + // see nsIHttpRequest.getHeader 1.5057 + // 1.5058 + getHeader: function(name) 1.5059 + { 1.5060 + return this._headers.getHeader(name); 1.5061 + }, 1.5062 + 1.5063 + // 1.5064 + // see nsIHttpRequest.hasHeader 1.5065 + // 1.5066 + hasHeader: function(name) 1.5067 + { 1.5068 + return this._headers.hasHeader(name); 1.5069 + }, 1.5070 + 1.5071 + // 1.5072 + // see nsIHttpRequest.headers 1.5073 + // 1.5074 + get headers() 1.5075 + { 1.5076 + return this._headers.enumerator; 1.5077 + }, 1.5078 + 1.5079 + // 1.5080 + // see nsIPropertyBag.enumerator 1.5081 + // 1.5082 + get enumerator() 1.5083 + { 1.5084 + this._ensurePropertyBag(); 1.5085 + return this._bag.enumerator; 1.5086 + }, 1.5087 + 1.5088 + // 1.5089 + // see nsIHttpRequest.headers 1.5090 + // 1.5091 + get bodyInputStream() 1.5092 + { 1.5093 + return this._bodyInputStream; 1.5094 + }, 1.5095 + 1.5096 + // 1.5097 + // see nsIPropertyBag.getProperty 1.5098 + // 1.5099 + getProperty: function(name) 1.5100 + { 1.5101 + this._ensurePropertyBag(); 1.5102 + return this._bag.getProperty(name); 1.5103 + }, 1.5104 + 1.5105 + 1.5106 + // NSISUPPORTS 1.5107 + 1.5108 + // 1.5109 + // see nsISupports.QueryInterface 1.5110 + // 1.5111 + QueryInterface: function(iid) 1.5112 + { 1.5113 + if (iid.equals(Ci.nsIHttpRequest) || iid.equals(Ci.nsISupports)) 1.5114 + return this; 1.5115 + 1.5116 + throw Cr.NS_ERROR_NO_INTERFACE; 1.5117 + }, 1.5118 + 1.5119 + 1.5120 + // PRIVATE IMPLEMENTATION 1.5121 + 1.5122 + /** Ensures a property bag has been created for ad-hoc behaviors. */ 1.5123 + _ensurePropertyBag: function() 1.5124 + { 1.5125 + if (!this._bag) 1.5126 + this._bag = new WritablePropertyBag(); 1.5127 + } 1.5128 +}; 1.5129 + 1.5130 + 1.5131 +// XPCOM trappings 1.5132 +if ("XPCOMUtils" in this && // Firefox 3.6 doesn't load XPCOMUtils in this scope for some reason... 1.5133 + "generateNSGetFactory" in XPCOMUtils) { 1.5134 + var NSGetFactory = XPCOMUtils.generateNSGetFactory([nsHttpServer]); 1.5135 +} 1.5136 + 1.5137 + 1.5138 + 1.5139 +/** 1.5140 +* Creates a new HTTP server listening for loopback traffic on the given port, 1.5141 +* starts it, and runs the server until the server processes a shutdown request, 1.5142 +* spinning an event loop so that events posted by the server's socket are 1.5143 +* processed. 1.5144 +* 1.5145 +* This method is primarily intended for use in running this script from within 1.5146 +* xpcshell and running a functional HTTP server without having to deal with 1.5147 +* non-essential details. 1.5148 +* 1.5149 +* Note that running multiple servers using variants of this method probably 1.5150 +* doesn't work, simply due to how the internal event loop is spun and stopped. 1.5151 +* 1.5152 +* @note 1.5153 +* This method only works with Mozilla 1.9 (i.e., Firefox 3 or trunk code); 1.5154 +* you should use this server as a component in Mozilla 1.8. 1.5155 +* @param port 1.5156 +* the port on which the server will run, or -1 if there exists no preference 1.5157 +* for a specific port; note that attempting to use some values for this 1.5158 +* parameter (particularly those below 1024) may cause this method to throw or 1.5159 +* may result in the server being prematurely shut down 1.5160 +* @param basePath 1.5161 +* a local directory from which requests will be served (i.e., if this is 1.5162 +* "/home/jwalden/" then a request to /index.html will load 1.5163 +* /home/jwalden/index.html); if this is omitted, only the default URLs in 1.5164 +* this server implementation will be functional 1.5165 +*/ 1.5166 +function server(port, basePath) 1.5167 +{ 1.5168 + if (basePath) 1.5169 + { 1.5170 + var lp = Cc["@mozilla.org/file/local;1"] 1.5171 + .createInstance(Ci.nsILocalFile); 1.5172 + lp.initWithPath(basePath); 1.5173 + } 1.5174 + 1.5175 + // if you're running this, you probably want to see debugging info 1.5176 + DEBUG = true; 1.5177 + 1.5178 + var srv = new nsHttpServer(); 1.5179 + if (lp) 1.5180 + srv.registerDirectory("/", lp); 1.5181 + srv.registerContentType("sjs", SJS_TYPE); 1.5182 + srv.start(port); 1.5183 + 1.5184 + var thread = gThreadManager.currentThread; 1.5185 + while (!srv.isStopped()) 1.5186 + thread.processNextEvent(true); 1.5187 + 1.5188 + // get rid of any pending requests 1.5189 + while (thread.hasPendingEvents()) 1.5190 + thread.processNextEvent(true); 1.5191 + 1.5192 + DEBUG = false; 1.5193 +} 1.5194 + 1.5195 +function startServerAsync(port, basePath) 1.5196 +{ 1.5197 + if (basePath) 1.5198 + { 1.5199 + var lp = Cc["@mozilla.org/file/local;1"] 1.5200 + .createInstance(Ci.nsILocalFile); 1.5201 + lp.initWithPath(basePath); 1.5202 + } 1.5203 + 1.5204 + var srv = new nsHttpServer(); 1.5205 + if (lp) 1.5206 + srv.registerDirectory("/", lp); 1.5207 + srv.registerContentType("sjs", "sjs"); 1.5208 + srv.start(port); 1.5209 + return srv; 1.5210 +} 1.5211 + 1.5212 +exports.nsHttpServer = nsHttpServer; 1.5213 +exports.ScriptableInputStream = ScriptableInputStream; 1.5214 +exports.server = server; 1.5215 +exports.startServerAsync = startServerAsync;