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