addon-sdk/source/lib/sdk/test/httpd.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 /*
michael@0 6 * An implementation of an HTTP server both as a loadable script and as an XPCOM
michael@0 7 * component. See the accompanying README file for user documentation on
michael@0 8 * httpd.js.
michael@0 9 */
michael@0 10
michael@0 11 module.metadata = {
michael@0 12 "stability": "experimental"
michael@0 13 };
michael@0 14
michael@0 15 const { components, CC, Cc, Ci, Cr, Cu } = require("chrome");
michael@0 16 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 17
michael@0 18
michael@0 19 const PR_UINT32_MAX = Math.pow(2, 32) - 1;
michael@0 20
michael@0 21 /** True if debugging output is enabled, false otherwise. */
michael@0 22 var DEBUG = false; // non-const *only* so tweakable in server tests
michael@0 23
michael@0 24 /** True if debugging output should be timestamped. */
michael@0 25 var DEBUG_TIMESTAMP = false; // non-const so tweakable in server tests
michael@0 26
michael@0 27 var gGlobalObject = Cc["@mozilla.org/systemprincipal;1"].createInstance();
michael@0 28
michael@0 29 /**
michael@0 30 * Asserts that the given condition holds. If it doesn't, the given message is
michael@0 31 * dumped, a stack trace is printed, and an exception is thrown to attempt to
michael@0 32 * stop execution (which unfortunately must rely upon the exception not being
michael@0 33 * accidentally swallowed by the code that uses it).
michael@0 34 */
michael@0 35 function NS_ASSERT(cond, msg)
michael@0 36 {
michael@0 37 if (DEBUG && !cond)
michael@0 38 {
michael@0 39 dumpn("###!!!");
michael@0 40 dumpn("###!!! ASSERTION" + (msg ? ": " + msg : "!"));
michael@0 41 dumpn("###!!! Stack follows:");
michael@0 42
michael@0 43 var stack = new Error().stack.split(/\n/);
michael@0 44 dumpn(stack.map(function(val) { return "###!!! " + val; }).join("\n"));
michael@0 45
michael@0 46 throw Cr.NS_ERROR_ABORT;
michael@0 47 }
michael@0 48 }
michael@0 49
michael@0 50 /** Constructs an HTTP error object. */
michael@0 51 function HttpError(code, description)
michael@0 52 {
michael@0 53 this.code = code;
michael@0 54 this.description = description;
michael@0 55 }
michael@0 56 HttpError.prototype =
michael@0 57 {
michael@0 58 toString: function()
michael@0 59 {
michael@0 60 return this.code + " " + this.description;
michael@0 61 }
michael@0 62 };
michael@0 63
michael@0 64 /**
michael@0 65 * Errors thrown to trigger specific HTTP server responses.
michael@0 66 */
michael@0 67 const HTTP_400 = new HttpError(400, "Bad Request");
michael@0 68 const HTTP_401 = new HttpError(401, "Unauthorized");
michael@0 69 const HTTP_402 = new HttpError(402, "Payment Required");
michael@0 70 const HTTP_403 = new HttpError(403, "Forbidden");
michael@0 71 const HTTP_404 = new HttpError(404, "Not Found");
michael@0 72 const HTTP_405 = new HttpError(405, "Method Not Allowed");
michael@0 73 const HTTP_406 = new HttpError(406, "Not Acceptable");
michael@0 74 const HTTP_407 = new HttpError(407, "Proxy Authentication Required");
michael@0 75 const HTTP_408 = new HttpError(408, "Request Timeout");
michael@0 76 const HTTP_409 = new HttpError(409, "Conflict");
michael@0 77 const HTTP_410 = new HttpError(410, "Gone");
michael@0 78 const HTTP_411 = new HttpError(411, "Length Required");
michael@0 79 const HTTP_412 = new HttpError(412, "Precondition Failed");
michael@0 80 const HTTP_413 = new HttpError(413, "Request Entity Too Large");
michael@0 81 const HTTP_414 = new HttpError(414, "Request-URI Too Long");
michael@0 82 const HTTP_415 = new HttpError(415, "Unsupported Media Type");
michael@0 83 const HTTP_417 = new HttpError(417, "Expectation Failed");
michael@0 84
michael@0 85 const HTTP_500 = new HttpError(500, "Internal Server Error");
michael@0 86 const HTTP_501 = new HttpError(501, "Not Implemented");
michael@0 87 const HTTP_502 = new HttpError(502, "Bad Gateway");
michael@0 88 const HTTP_503 = new HttpError(503, "Service Unavailable");
michael@0 89 const HTTP_504 = new HttpError(504, "Gateway Timeout");
michael@0 90 const HTTP_505 = new HttpError(505, "HTTP Version Not Supported");
michael@0 91
michael@0 92 /** Creates a hash with fields corresponding to the values in arr. */
michael@0 93 function array2obj(arr)
michael@0 94 {
michael@0 95 var obj = {};
michael@0 96 for (var i = 0; i < arr.length; i++)
michael@0 97 obj[arr[i]] = arr[i];
michael@0 98 return obj;
michael@0 99 }
michael@0 100
michael@0 101 /** Returns an array of the integers x through y, inclusive. */
michael@0 102 function range(x, y)
michael@0 103 {
michael@0 104 var arr = [];
michael@0 105 for (var i = x; i <= y; i++)
michael@0 106 arr.push(i);
michael@0 107 return arr;
michael@0 108 }
michael@0 109
michael@0 110 /** An object (hash) whose fields are the numbers of all HTTP error codes. */
michael@0 111 const HTTP_ERROR_CODES = array2obj(range(400, 417).concat(range(500, 505)));
michael@0 112
michael@0 113
michael@0 114 /**
michael@0 115 * The character used to distinguish hidden files from non-hidden files, a la
michael@0 116 * the leading dot in Apache. Since that mechanism also hides files from
michael@0 117 * easy display in LXR, ls output, etc. however, we choose instead to use a
michael@0 118 * suffix character. If a requested file ends with it, we append another
michael@0 119 * when getting the file on the server. If it doesn't, we just look up that
michael@0 120 * file. Therefore, any file whose name ends with exactly one of the character
michael@0 121 * is "hidden" and available for use by the server.
michael@0 122 */
michael@0 123 const HIDDEN_CHAR = "^";
michael@0 124
michael@0 125 /**
michael@0 126 * The file name suffix indicating the file containing overridden headers for
michael@0 127 * a requested file.
michael@0 128 */
michael@0 129 const HEADERS_SUFFIX = HIDDEN_CHAR + "headers" + HIDDEN_CHAR;
michael@0 130
michael@0 131 /** Type used to denote SJS scripts for CGI-like functionality. */
michael@0 132 const SJS_TYPE = "sjs";
michael@0 133
michael@0 134 /** Base for relative timestamps produced by dumpn(). */
michael@0 135 var firstStamp = 0;
michael@0 136
michael@0 137 /** dump(str) with a trailing "\n" -- only outputs if DEBUG. */
michael@0 138 function dumpn(str)
michael@0 139 {
michael@0 140 if (DEBUG)
michael@0 141 {
michael@0 142 var prefix = "HTTPD-INFO | ";
michael@0 143 if (DEBUG_TIMESTAMP)
michael@0 144 {
michael@0 145 if (firstStamp === 0)
michael@0 146 firstStamp = Date.now();
michael@0 147
michael@0 148 var elapsed = Date.now() - firstStamp; // milliseconds
michael@0 149 var min = Math.floor(elapsed / 60000);
michael@0 150 var sec = (elapsed % 60000) / 1000;
michael@0 151
michael@0 152 if (sec < 10)
michael@0 153 prefix += min + ":0" + sec.toFixed(3) + " | ";
michael@0 154 else
michael@0 155 prefix += min + ":" + sec.toFixed(3) + " | ";
michael@0 156 }
michael@0 157
michael@0 158 dump(prefix + str + "\n");
michael@0 159 }
michael@0 160 }
michael@0 161
michael@0 162 /** Dumps the current JS stack if DEBUG. */
michael@0 163 function dumpStack()
michael@0 164 {
michael@0 165 // peel off the frames for dumpStack() and Error()
michael@0 166 var stack = new Error().stack.split(/\n/).slice(2);
michael@0 167 stack.forEach(dumpn);
michael@0 168 }
michael@0 169
michael@0 170
michael@0 171 /** The XPCOM thread manager. */
michael@0 172 var gThreadManager = null;
michael@0 173
michael@0 174 /** The XPCOM prefs service. */
michael@0 175 var gRootPrefBranch = null;
michael@0 176 function getRootPrefBranch()
michael@0 177 {
michael@0 178 if (!gRootPrefBranch)
michael@0 179 {
michael@0 180 gRootPrefBranch = Cc["@mozilla.org/preferences-service;1"]
michael@0 181 .getService(Ci.nsIPrefBranch);
michael@0 182 }
michael@0 183 return gRootPrefBranch;
michael@0 184 }
michael@0 185
michael@0 186 /**
michael@0 187 * JavaScript constructors for commonly-used classes; precreating these is a
michael@0 188 * speedup over doing the same from base principles. See the docs at
michael@0 189 * http://developer.mozilla.org/en/docs/components.Constructor for details.
michael@0 190 */
michael@0 191 const ServerSocket = CC("@mozilla.org/network/server-socket;1",
michael@0 192 "nsIServerSocket",
michael@0 193 "init");
michael@0 194 const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1",
michael@0 195 "nsIScriptableInputStream",
michael@0 196 "init");
michael@0 197 const Pipe = CC("@mozilla.org/pipe;1",
michael@0 198 "nsIPipe",
michael@0 199 "init");
michael@0 200 const FileInputStream = CC("@mozilla.org/network/file-input-stream;1",
michael@0 201 "nsIFileInputStream",
michael@0 202 "init");
michael@0 203 const ConverterInputStream = CC("@mozilla.org/intl/converter-input-stream;1",
michael@0 204 "nsIConverterInputStream",
michael@0 205 "init");
michael@0 206 const WritablePropertyBag = CC("@mozilla.org/hash-property-bag;1",
michael@0 207 "nsIWritablePropertyBag2");
michael@0 208 const SupportsString = CC("@mozilla.org/supports-string;1",
michael@0 209 "nsISupportsString");
michael@0 210
michael@0 211 /* These two are non-const only so a test can overwrite them. */
michael@0 212 var BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
michael@0 213 "nsIBinaryInputStream",
michael@0 214 "setInputStream");
michael@0 215 var BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1",
michael@0 216 "nsIBinaryOutputStream",
michael@0 217 "setOutputStream");
michael@0 218
michael@0 219 /**
michael@0 220 * Returns the RFC 822/1123 representation of a date.
michael@0 221 *
michael@0 222 * @param date : Number
michael@0 223 * the date, in milliseconds from midnight (00:00:00), January 1, 1970 GMT
michael@0 224 * @returns string
michael@0 225 * the representation of the given date
michael@0 226 */
michael@0 227 function toDateString(date)
michael@0 228 {
michael@0 229 //
michael@0 230 // rfc1123-date = wkday "," SP date1 SP time SP "GMT"
michael@0 231 // date1 = 2DIGIT SP month SP 4DIGIT
michael@0 232 // ; day month year (e.g., 02 Jun 1982)
michael@0 233 // time = 2DIGIT ":" 2DIGIT ":" 2DIGIT
michael@0 234 // ; 00:00:00 - 23:59:59
michael@0 235 // wkday = "Mon" | "Tue" | "Wed"
michael@0 236 // | "Thu" | "Fri" | "Sat" | "Sun"
michael@0 237 // month = "Jan" | "Feb" | "Mar" | "Apr"
michael@0 238 // | "May" | "Jun" | "Jul" | "Aug"
michael@0 239 // | "Sep" | "Oct" | "Nov" | "Dec"
michael@0 240 //
michael@0 241
michael@0 242 const wkdayStrings = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
michael@0 243 const monthStrings = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
michael@0 244 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
michael@0 245
michael@0 246 /**
michael@0 247 * Processes a date and returns the encoded UTC time as a string according to
michael@0 248 * the format specified in RFC 2616.
michael@0 249 *
michael@0 250 * @param date : Date
michael@0 251 * the date to process
michael@0 252 * @returns string
michael@0 253 * a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59"
michael@0 254 */
michael@0 255 function toTime(date)
michael@0 256 {
michael@0 257 var hrs = date.getUTCHours();
michael@0 258 var rv = (hrs < 10) ? "0" + hrs : hrs;
michael@0 259
michael@0 260 var mins = date.getUTCMinutes();
michael@0 261 rv += ":";
michael@0 262 rv += (mins < 10) ? "0" + mins : mins;
michael@0 263
michael@0 264 var secs = date.getUTCSeconds();
michael@0 265 rv += ":";
michael@0 266 rv += (secs < 10) ? "0" + secs : secs;
michael@0 267
michael@0 268 return rv;
michael@0 269 }
michael@0 270
michael@0 271 /**
michael@0 272 * Processes a date and returns the encoded UTC date as a string according to
michael@0 273 * the date1 format specified in RFC 2616.
michael@0 274 *
michael@0 275 * @param date : Date
michael@0 276 * the date to process
michael@0 277 * @returns string
michael@0 278 * a string of the form "HH:MM:SS", ranging from "00:00:00" to "23:59:59"
michael@0 279 */
michael@0 280 function toDate1(date)
michael@0 281 {
michael@0 282 var day = date.getUTCDate();
michael@0 283 var month = date.getUTCMonth();
michael@0 284 var year = date.getUTCFullYear();
michael@0 285
michael@0 286 var rv = (day < 10) ? "0" + day : day;
michael@0 287 rv += " " + monthStrings[month];
michael@0 288 rv += " " + year;
michael@0 289
michael@0 290 return rv;
michael@0 291 }
michael@0 292
michael@0 293 date = new Date(date);
michael@0 294
michael@0 295 const fmtString = "%wkday%, %date1% %time% GMT";
michael@0 296 var rv = fmtString.replace("%wkday%", wkdayStrings[date.getUTCDay()]);
michael@0 297 rv = rv.replace("%time%", toTime(date));
michael@0 298 return rv.replace("%date1%", toDate1(date));
michael@0 299 }
michael@0 300
michael@0 301 /**
michael@0 302 * Prints out a human-readable representation of the object o and its fields,
michael@0 303 * omitting those whose names begin with "_" if showMembers != true (to ignore
michael@0 304 * "private" properties exposed via getters/setters).
michael@0 305 */
michael@0 306 function printObj(o, showMembers)
michael@0 307 {
michael@0 308 var s = "******************************\n";
michael@0 309 s += "o = {\n";
michael@0 310 for (var i in o)
michael@0 311 {
michael@0 312 if (typeof(i) != "string" ||
michael@0 313 (showMembers || (i.length > 0 && i[0] != "_")))
michael@0 314 s+= " " + i + ": " + o[i] + ",\n";
michael@0 315 }
michael@0 316 s += " };\n";
michael@0 317 s += "******************************";
michael@0 318 dumpn(s);
michael@0 319 }
michael@0 320
michael@0 321 /**
michael@0 322 * Instantiates a new HTTP server.
michael@0 323 */
michael@0 324 function nsHttpServer()
michael@0 325 {
michael@0 326 if (!gThreadManager)
michael@0 327 gThreadManager = Cc["@mozilla.org/thread-manager;1"].getService();
michael@0 328
michael@0 329 /** The port on which this server listens. */
michael@0 330 this._port = undefined;
michael@0 331
michael@0 332 /** The socket associated with this. */
michael@0 333 this._socket = null;
michael@0 334
michael@0 335 /** The handler used to process requests to this server. */
michael@0 336 this._handler = new ServerHandler(this);
michael@0 337
michael@0 338 /** Naming information for this server. */
michael@0 339 this._identity = new ServerIdentity();
michael@0 340
michael@0 341 /**
michael@0 342 * Indicates when the server is to be shut down at the end of the request.
michael@0 343 */
michael@0 344 this._doQuit = false;
michael@0 345
michael@0 346 /**
michael@0 347 * True if the socket in this is closed (and closure notifications have been
michael@0 348 * sent and processed if the socket was ever opened), false otherwise.
michael@0 349 */
michael@0 350 this._socketClosed = true;
michael@0 351
michael@0 352 /**
michael@0 353 * Used for tracking existing connections and ensuring that all connections
michael@0 354 * are properly cleaned up before server shutdown; increases by 1 for every
michael@0 355 * new incoming connection.
michael@0 356 */
michael@0 357 this._connectionGen = 0;
michael@0 358
michael@0 359 /**
michael@0 360 * Hash of all open connections, indexed by connection number at time of
michael@0 361 * creation.
michael@0 362 */
michael@0 363 this._connections = {};
michael@0 364 }
michael@0 365 nsHttpServer.prototype =
michael@0 366 {
michael@0 367 classID: components.ID("{54ef6f81-30af-4b1d-ac55-8ba811293e41}"),
michael@0 368
michael@0 369 // NSISERVERSOCKETLISTENER
michael@0 370
michael@0 371 /**
michael@0 372 * Processes an incoming request coming in on the given socket and contained
michael@0 373 * in the given transport.
michael@0 374 *
michael@0 375 * @param socket : nsIServerSocket
michael@0 376 * the socket through which the request was served
michael@0 377 * @param trans : nsISocketTransport
michael@0 378 * the transport for the request/response
michael@0 379 * @see nsIServerSocketListener.onSocketAccepted
michael@0 380 */
michael@0 381 onSocketAccepted: function(socket, trans)
michael@0 382 {
michael@0 383 dumpn("*** onSocketAccepted(socket=" + socket + ", trans=" + trans + ")");
michael@0 384
michael@0 385 dumpn(">>> new connection on " + trans.host + ":" + trans.port);
michael@0 386
michael@0 387 const SEGMENT_SIZE = 8192;
michael@0 388 const SEGMENT_COUNT = 1024;
michael@0 389 try
michael@0 390 {
michael@0 391 var input = trans.openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT)
michael@0 392 .QueryInterface(Ci.nsIAsyncInputStream);
michael@0 393 var output = trans.openOutputStream(0, 0, 0);
michael@0 394 }
michael@0 395 catch (e)
michael@0 396 {
michael@0 397 dumpn("*** error opening transport streams: " + e);
michael@0 398 trans.close(Cr.NS_BINDING_ABORTED);
michael@0 399 return;
michael@0 400 }
michael@0 401
michael@0 402 var connectionNumber = ++this._connectionGen;
michael@0 403
michael@0 404 try
michael@0 405 {
michael@0 406 var conn = new Connection(input, output, this, socket.port, trans.port,
michael@0 407 connectionNumber);
michael@0 408 var reader = new RequestReader(conn);
michael@0 409
michael@0 410 // XXX add request timeout functionality here!
michael@0 411
michael@0 412 // Note: must use main thread here, or we might get a GC that will cause
michael@0 413 // threadsafety assertions. We really need to fix XPConnect so that
michael@0 414 // you can actually do things in multi-threaded JS. :-(
michael@0 415 input.asyncWait(reader, 0, 0, gThreadManager.mainThread);
michael@0 416 }
michael@0 417 catch (e)
michael@0 418 {
michael@0 419 // Assume this connection can't be salvaged and bail on it completely;
michael@0 420 // don't attempt to close it so that we can assert that any connection
michael@0 421 // being closed is in this._connections.
michael@0 422 dumpn("*** error in initial request-processing stages: " + e);
michael@0 423 trans.close(Cr.NS_BINDING_ABORTED);
michael@0 424 return;
michael@0 425 }
michael@0 426
michael@0 427 this._connections[connectionNumber] = conn;
michael@0 428 dumpn("*** starting connection " + connectionNumber);
michael@0 429 },
michael@0 430
michael@0 431 /**
michael@0 432 * Called when the socket associated with this is closed.
michael@0 433 *
michael@0 434 * @param socket : nsIServerSocket
michael@0 435 * the socket being closed
michael@0 436 * @param status : nsresult
michael@0 437 * the reason the socket stopped listening (NS_BINDING_ABORTED if the server
michael@0 438 * was stopped using nsIHttpServer.stop)
michael@0 439 * @see nsIServerSocketListener.onStopListening
michael@0 440 */
michael@0 441 onStopListening: function(socket, status)
michael@0 442 {
michael@0 443 dumpn(">>> shutting down server on port " + socket.port);
michael@0 444 this._socketClosed = true;
michael@0 445 if (!this._hasOpenConnections())
michael@0 446 {
michael@0 447 dumpn("*** no open connections, notifying async from onStopListening");
michael@0 448
michael@0 449 // Notify asynchronously so that any pending teardown in stop() has a
michael@0 450 // chance to run first.
michael@0 451 var self = this;
michael@0 452 var stopEvent =
michael@0 453 {
michael@0 454 run: function()
michael@0 455 {
michael@0 456 dumpn("*** _notifyStopped async callback");
michael@0 457 self._notifyStopped();
michael@0 458 }
michael@0 459 };
michael@0 460 gThreadManager.currentThread
michael@0 461 .dispatch(stopEvent, Ci.nsIThread.DISPATCH_NORMAL);
michael@0 462 }
michael@0 463 },
michael@0 464
michael@0 465 // NSIHTTPSERVER
michael@0 466
michael@0 467 //
michael@0 468 // see nsIHttpServer.start
michael@0 469 //
michael@0 470 start: function(port)
michael@0 471 {
michael@0 472 this._start(port, "localhost")
michael@0 473 },
michael@0 474
michael@0 475 _start: function(port, host)
michael@0 476 {
michael@0 477 if (this._socket)
michael@0 478 throw Cr.NS_ERROR_ALREADY_INITIALIZED;
michael@0 479
michael@0 480 this._port = port;
michael@0 481 this._doQuit = this._socketClosed = false;
michael@0 482
michael@0 483 this._host = host;
michael@0 484
michael@0 485 // The listen queue needs to be long enough to handle
michael@0 486 // network.http.max-persistent-connections-per-server concurrent connections,
michael@0 487 // plus a safety margin in case some other process is talking to
michael@0 488 // the server as well.
michael@0 489 var prefs = getRootPrefBranch();
michael@0 490 var maxConnections;
michael@0 491 try {
michael@0 492 // Bug 776860: The original pref was removed in favor of this new one:
michael@0 493 maxConnections = prefs.getIntPref("network.http.max-persistent-connections-per-server") + 5;
michael@0 494 }
michael@0 495 catch(e) {
michael@0 496 maxConnections = prefs.getIntPref("network.http.max-connections-per-server") + 5;
michael@0 497 }
michael@0 498
michael@0 499 try
michael@0 500 {
michael@0 501 var loopback = true;
michael@0 502 if (this._host != "127.0.0.1" && this._host != "localhost") {
michael@0 503 var loopback = false;
michael@0 504 }
michael@0 505
michael@0 506 var socket = new ServerSocket(this._port,
michael@0 507 loopback, // true = localhost, false = everybody
michael@0 508 maxConnections);
michael@0 509 dumpn(">>> listening on port " + socket.port + ", " + maxConnections +
michael@0 510 " pending connections");
michael@0 511 socket.asyncListen(this);
michael@0 512 this._identity._initialize(socket.port, host, true);
michael@0 513 this._socket = socket;
michael@0 514 }
michael@0 515 catch (e)
michael@0 516 {
michael@0 517 dumpn("!!! could not start server on port " + port + ": " + e);
michael@0 518 throw Cr.NS_ERROR_NOT_AVAILABLE;
michael@0 519 }
michael@0 520 },
michael@0 521
michael@0 522 //
michael@0 523 // see nsIHttpServer.stop
michael@0 524 //
michael@0 525 stop: function(callback)
michael@0 526 {
michael@0 527 if (!callback)
michael@0 528 throw Cr.NS_ERROR_NULL_POINTER;
michael@0 529 if (!this._socket)
michael@0 530 throw Cr.NS_ERROR_UNEXPECTED;
michael@0 531
michael@0 532 this._stopCallback = typeof callback === "function"
michael@0 533 ? callback
michael@0 534 : function() { callback.onStopped(); };
michael@0 535
michael@0 536 dumpn(">>> stopping listening on port " + this._socket.port);
michael@0 537 this._socket.close();
michael@0 538 this._socket = null;
michael@0 539
michael@0 540 // We can't have this identity any more, and the port on which we're running
michael@0 541 // this server now could be meaningless the next time around.
michael@0 542 this._identity._teardown();
michael@0 543
michael@0 544 this._doQuit = false;
michael@0 545
michael@0 546 // socket-close notification and pending request completion happen async
michael@0 547 },
michael@0 548
michael@0 549 //
michael@0 550 // see nsIHttpServer.registerFile
michael@0 551 //
michael@0 552 registerFile: function(path, file)
michael@0 553 {
michael@0 554 if (file && (!file.exists() || file.isDirectory()))
michael@0 555 throw Cr.NS_ERROR_INVALID_ARG;
michael@0 556
michael@0 557 this._handler.registerFile(path, file);
michael@0 558 },
michael@0 559
michael@0 560 //
michael@0 561 // see nsIHttpServer.registerDirectory
michael@0 562 //
michael@0 563 registerDirectory: function(path, directory)
michael@0 564 {
michael@0 565 // XXX true path validation!
michael@0 566 if (path.charAt(0) != "/" ||
michael@0 567 path.charAt(path.length - 1) != "/" ||
michael@0 568 (directory &&
michael@0 569 (!directory.exists() || !directory.isDirectory())))
michael@0 570 throw Cr.NS_ERROR_INVALID_ARG;
michael@0 571
michael@0 572 // XXX determine behavior of nonexistent /foo/bar when a /foo/bar/ mapping
michael@0 573 // exists!
michael@0 574
michael@0 575 this._handler.registerDirectory(path, directory);
michael@0 576 },
michael@0 577
michael@0 578 //
michael@0 579 // see nsIHttpServer.registerPathHandler
michael@0 580 //
michael@0 581 registerPathHandler: function(path, handler)
michael@0 582 {
michael@0 583 this._handler.registerPathHandler(path, handler);
michael@0 584 },
michael@0 585
michael@0 586 //
michael@0 587 // see nsIHttpServer.registerPrefixHandler
michael@0 588 //
michael@0 589 registerPrefixHandler: function(prefix, handler)
michael@0 590 {
michael@0 591 this._handler.registerPrefixHandler(prefix, handler);
michael@0 592 },
michael@0 593
michael@0 594 //
michael@0 595 // see nsIHttpServer.registerErrorHandler
michael@0 596 //
michael@0 597 registerErrorHandler: function(code, handler)
michael@0 598 {
michael@0 599 this._handler.registerErrorHandler(code, handler);
michael@0 600 },
michael@0 601
michael@0 602 //
michael@0 603 // see nsIHttpServer.setIndexHandler
michael@0 604 //
michael@0 605 setIndexHandler: function(handler)
michael@0 606 {
michael@0 607 this._handler.setIndexHandler(handler);
michael@0 608 },
michael@0 609
michael@0 610 //
michael@0 611 // see nsIHttpServer.registerContentType
michael@0 612 //
michael@0 613 registerContentType: function(ext, type)
michael@0 614 {
michael@0 615 this._handler.registerContentType(ext, type);
michael@0 616 },
michael@0 617
michael@0 618 //
michael@0 619 // see nsIHttpServer.serverIdentity
michael@0 620 //
michael@0 621 get identity()
michael@0 622 {
michael@0 623 return this._identity;
michael@0 624 },
michael@0 625
michael@0 626 //
michael@0 627 // see nsIHttpServer.getState
michael@0 628 //
michael@0 629 getState: function(path, k)
michael@0 630 {
michael@0 631 return this._handler._getState(path, k);
michael@0 632 },
michael@0 633
michael@0 634 //
michael@0 635 // see nsIHttpServer.setState
michael@0 636 //
michael@0 637 setState: function(path, k, v)
michael@0 638 {
michael@0 639 return this._handler._setState(path, k, v);
michael@0 640 },
michael@0 641
michael@0 642 //
michael@0 643 // see nsIHttpServer.getSharedState
michael@0 644 //
michael@0 645 getSharedState: function(k)
michael@0 646 {
michael@0 647 return this._handler._getSharedState(k);
michael@0 648 },
michael@0 649
michael@0 650 //
michael@0 651 // see nsIHttpServer.setSharedState
michael@0 652 //
michael@0 653 setSharedState: function(k, v)
michael@0 654 {
michael@0 655 return this._handler._setSharedState(k, v);
michael@0 656 },
michael@0 657
michael@0 658 //
michael@0 659 // see nsIHttpServer.getObjectState
michael@0 660 //
michael@0 661 getObjectState: function(k)
michael@0 662 {
michael@0 663 return this._handler._getObjectState(k);
michael@0 664 },
michael@0 665
michael@0 666 //
michael@0 667 // see nsIHttpServer.setObjectState
michael@0 668 //
michael@0 669 setObjectState: function(k, v)
michael@0 670 {
michael@0 671 return this._handler._setObjectState(k, v);
michael@0 672 },
michael@0 673
michael@0 674
michael@0 675 // NSISUPPORTS
michael@0 676
michael@0 677 //
michael@0 678 // see nsISupports.QueryInterface
michael@0 679 //
michael@0 680 QueryInterface: function(iid)
michael@0 681 {
michael@0 682 if (iid.equals(Ci.nsIServerSocketListener) || iid.equals(Ci.nsISupports))
michael@0 683 return this;
michael@0 684
michael@0 685 throw Cr.NS_ERROR_NO_INTERFACE;
michael@0 686 },
michael@0 687
michael@0 688
michael@0 689 // NON-XPCOM PUBLIC API
michael@0 690
michael@0 691 /**
michael@0 692 * Returns true iff this server is not running (and is not in the process of
michael@0 693 * serving any requests still to be processed when the server was last
michael@0 694 * stopped after being run).
michael@0 695 */
michael@0 696 isStopped: function()
michael@0 697 {
michael@0 698 return this._socketClosed && !this._hasOpenConnections();
michael@0 699 },
michael@0 700
michael@0 701 // PRIVATE IMPLEMENTATION
michael@0 702
michael@0 703 /** True if this server has any open connections to it, false otherwise. */
michael@0 704 _hasOpenConnections: function()
michael@0 705 {
michael@0 706 //
michael@0 707 // If we have any open connections, they're tracked as numeric properties on
michael@0 708 // |this._connections|. The non-standard __count__ property could be used
michael@0 709 // to check whether there are any properties, but standard-wise, even
michael@0 710 // looking forward to ES5, there's no less ugly yet still O(1) way to do
michael@0 711 // this.
michael@0 712 //
michael@0 713 for (var n in this._connections)
michael@0 714 return true;
michael@0 715 return false;
michael@0 716 },
michael@0 717
michael@0 718 /** Calls the server-stopped callback provided when stop() was called. */
michael@0 719 _notifyStopped: function()
michael@0 720 {
michael@0 721 NS_ASSERT(this._stopCallback !== null, "double-notifying?");
michael@0 722 NS_ASSERT(!this._hasOpenConnections(), "should be done serving by now");
michael@0 723
michael@0 724 //
michael@0 725 // NB: We have to grab this now, null out the member, *then* call the
michael@0 726 // callback here, or otherwise the callback could (indirectly) futz with
michael@0 727 // this._stopCallback by starting and immediately stopping this, at
michael@0 728 // which point we'd be nulling out a field we no longer have a right to
michael@0 729 // modify.
michael@0 730 //
michael@0 731 var callback = this._stopCallback;
michael@0 732 this._stopCallback = null;
michael@0 733 try
michael@0 734 {
michael@0 735 callback();
michael@0 736 }
michael@0 737 catch (e)
michael@0 738 {
michael@0 739 // not throwing because this is specified as being usually (but not
michael@0 740 // always) asynchronous
michael@0 741 dump("!!! error running onStopped callback: " + e + "\n");
michael@0 742 }
michael@0 743 },
michael@0 744
michael@0 745 /**
michael@0 746 * Notifies this server that the given connection has been closed.
michael@0 747 *
michael@0 748 * @param connection : Connection
michael@0 749 * the connection that was closed
michael@0 750 */
michael@0 751 _connectionClosed: function(connection)
michael@0 752 {
michael@0 753 NS_ASSERT(connection.number in this._connections,
michael@0 754 "closing a connection " + this + " that we never added to the " +
michael@0 755 "set of open connections?");
michael@0 756 NS_ASSERT(this._connections[connection.number] === connection,
michael@0 757 "connection number mismatch? " +
michael@0 758 this._connections[connection.number]);
michael@0 759 delete this._connections[connection.number];
michael@0 760
michael@0 761 // Fire a pending server-stopped notification if it's our responsibility.
michael@0 762 if (!this._hasOpenConnections() && this._socketClosed)
michael@0 763 this._notifyStopped();
michael@0 764 },
michael@0 765
michael@0 766 /**
michael@0 767 * Requests that the server be shut down when possible.
michael@0 768 */
michael@0 769 _requestQuit: function()
michael@0 770 {
michael@0 771 dumpn(">>> requesting a quit");
michael@0 772 dumpStack();
michael@0 773 this._doQuit = true;
michael@0 774 }
michael@0 775 };
michael@0 776
michael@0 777
michael@0 778 //
michael@0 779 // RFC 2396 section 3.2.2:
michael@0 780 //
michael@0 781 // host = hostname | IPv4address
michael@0 782 // hostname = *( domainlabel "." ) toplabel [ "." ]
michael@0 783 // domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
michael@0 784 // toplabel = alpha | alpha *( alphanum | "-" ) alphanum
michael@0 785 // IPv4address = 1*digit "." 1*digit "." 1*digit "." 1*digit
michael@0 786 //
michael@0 787
michael@0 788 const HOST_REGEX =
michael@0 789 new RegExp("^(?:" +
michael@0 790 // *( domainlabel "." )
michael@0 791 "(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)*" +
michael@0 792 // toplabel
michael@0 793 "[a-z](?:[a-z0-9-]*[a-z0-9])?" +
michael@0 794 "|" +
michael@0 795 // IPv4 address
michael@0 796 "\\d+\\.\\d+\\.\\d+\\.\\d+" +
michael@0 797 ")$",
michael@0 798 "i");
michael@0 799
michael@0 800
michael@0 801 /**
michael@0 802 * Represents the identity of a server. An identity consists of a set of
michael@0 803 * (scheme, host, port) tuples denoted as locations (allowing a single server to
michael@0 804 * serve multiple sites or to be used behind both HTTP and HTTPS proxies for any
michael@0 805 * host/port). Any incoming request must be to one of these locations, or it
michael@0 806 * will be rejected with an HTTP 400 error. One location, denoted as the
michael@0 807 * primary location, is the location assigned in contexts where a location
michael@0 808 * cannot otherwise be endogenously derived, such as for HTTP/1.0 requests.
michael@0 809 *
michael@0 810 * A single identity may contain at most one location per unique host/port pair;
michael@0 811 * other than that, no restrictions are placed upon what locations may
michael@0 812 * constitute an identity.
michael@0 813 */
michael@0 814 function ServerIdentity()
michael@0 815 {
michael@0 816 /** The scheme of the primary location. */
michael@0 817 this._primaryScheme = "http";
michael@0 818
michael@0 819 /** The hostname of the primary location. */
michael@0 820 this._primaryHost = "127.0.0.1"
michael@0 821
michael@0 822 /** The port number of the primary location. */
michael@0 823 this._primaryPort = -1;
michael@0 824
michael@0 825 /**
michael@0 826 * The current port number for the corresponding server, stored so that a new
michael@0 827 * primary location can always be set if the current one is removed.
michael@0 828 */
michael@0 829 this._defaultPort = -1;
michael@0 830
michael@0 831 /**
michael@0 832 * Maps hosts to maps of ports to schemes, e.g. the following would represent
michael@0 833 * https://example.com:789/ and http://example.org/:
michael@0 834 *
michael@0 835 * {
michael@0 836 * "xexample.com": { 789: "https" },
michael@0 837 * "xexample.org": { 80: "http" }
michael@0 838 * }
michael@0 839 *
michael@0 840 * Note the "x" prefix on hostnames, which prevents collisions with special
michael@0 841 * JS names like "prototype".
michael@0 842 */
michael@0 843 this._locations = { "xlocalhost": {} };
michael@0 844 }
michael@0 845 ServerIdentity.prototype =
michael@0 846 {
michael@0 847 // NSIHTTPSERVERIDENTITY
michael@0 848
michael@0 849 //
michael@0 850 // see nsIHttpServerIdentity.primaryScheme
michael@0 851 //
michael@0 852 get primaryScheme()
michael@0 853 {
michael@0 854 if (this._primaryPort === -1)
michael@0 855 throw Cr.NS_ERROR_NOT_INITIALIZED;
michael@0 856 return this._primaryScheme;
michael@0 857 },
michael@0 858
michael@0 859 //
michael@0 860 // see nsIHttpServerIdentity.primaryHost
michael@0 861 //
michael@0 862 get primaryHost()
michael@0 863 {
michael@0 864 if (this._primaryPort === -1)
michael@0 865 throw Cr.NS_ERROR_NOT_INITIALIZED;
michael@0 866 return this._primaryHost;
michael@0 867 },
michael@0 868
michael@0 869 //
michael@0 870 // see nsIHttpServerIdentity.primaryPort
michael@0 871 //
michael@0 872 get primaryPort()
michael@0 873 {
michael@0 874 if (this._primaryPort === -1)
michael@0 875 throw Cr.NS_ERROR_NOT_INITIALIZED;
michael@0 876 return this._primaryPort;
michael@0 877 },
michael@0 878
michael@0 879 //
michael@0 880 // see nsIHttpServerIdentity.add
michael@0 881 //
michael@0 882 add: function(scheme, host, port)
michael@0 883 {
michael@0 884 this._validate(scheme, host, port);
michael@0 885
michael@0 886 var entry = this._locations["x" + host];
michael@0 887 if (!entry)
michael@0 888 this._locations["x" + host] = entry = {};
michael@0 889
michael@0 890 entry[port] = scheme;
michael@0 891 },
michael@0 892
michael@0 893 //
michael@0 894 // see nsIHttpServerIdentity.remove
michael@0 895 //
michael@0 896 remove: function(scheme, host, port)
michael@0 897 {
michael@0 898 this._validate(scheme, host, port);
michael@0 899
michael@0 900 var entry = this._locations["x" + host];
michael@0 901 if (!entry)
michael@0 902 return false;
michael@0 903
michael@0 904 var present = port in entry;
michael@0 905 delete entry[port];
michael@0 906
michael@0 907 if (this._primaryScheme == scheme &&
michael@0 908 this._primaryHost == host &&
michael@0 909 this._primaryPort == port &&
michael@0 910 this._defaultPort !== -1)
michael@0 911 {
michael@0 912 // Always keep at least one identity in existence at any time, unless
michael@0 913 // we're in the process of shutting down (the last condition above).
michael@0 914 this._primaryPort = -1;
michael@0 915 this._initialize(this._defaultPort, host, false);
michael@0 916 }
michael@0 917
michael@0 918 return present;
michael@0 919 },
michael@0 920
michael@0 921 //
michael@0 922 // see nsIHttpServerIdentity.has
michael@0 923 //
michael@0 924 has: function(scheme, host, port)
michael@0 925 {
michael@0 926 this._validate(scheme, host, port);
michael@0 927
michael@0 928 return "x" + host in this._locations &&
michael@0 929 scheme === this._locations["x" + host][port];
michael@0 930 },
michael@0 931
michael@0 932 //
michael@0 933 // see nsIHttpServerIdentity.has
michael@0 934 //
michael@0 935 getScheme: function(host, port)
michael@0 936 {
michael@0 937 this._validate("http", host, port);
michael@0 938
michael@0 939 var entry = this._locations["x" + host];
michael@0 940 if (!entry)
michael@0 941 return "";
michael@0 942
michael@0 943 return entry[port] || "";
michael@0 944 },
michael@0 945
michael@0 946 //
michael@0 947 // see nsIHttpServerIdentity.setPrimary
michael@0 948 //
michael@0 949 setPrimary: function(scheme, host, port)
michael@0 950 {
michael@0 951 this._validate(scheme, host, port);
michael@0 952
michael@0 953 this.add(scheme, host, port);
michael@0 954
michael@0 955 this._primaryScheme = scheme;
michael@0 956 this._primaryHost = host;
michael@0 957 this._primaryPort = port;
michael@0 958 },
michael@0 959
michael@0 960
michael@0 961 // NSISUPPORTS
michael@0 962
michael@0 963 //
michael@0 964 // see nsISupports.QueryInterface
michael@0 965 //
michael@0 966 QueryInterface: function(iid)
michael@0 967 {
michael@0 968 if (iid.equals(Ci.nsIHttpServerIdentity) || iid.equals(Ci.nsISupports))
michael@0 969 return this;
michael@0 970
michael@0 971 throw Cr.NS_ERROR_NO_INTERFACE;
michael@0 972 },
michael@0 973
michael@0 974
michael@0 975 // PRIVATE IMPLEMENTATION
michael@0 976
michael@0 977 /**
michael@0 978 * Initializes the primary name for the corresponding server, based on the
michael@0 979 * provided port number.
michael@0 980 */
michael@0 981 _initialize: function(port, host, addSecondaryDefault)
michael@0 982 {
michael@0 983 this._host = host;
michael@0 984 if (this._primaryPort !== -1)
michael@0 985 this.add("http", host, port);
michael@0 986 else
michael@0 987 this.setPrimary("http", "localhost", port);
michael@0 988 this._defaultPort = port;
michael@0 989
michael@0 990 // Only add this if we're being called at server startup
michael@0 991 if (addSecondaryDefault && host != "127.0.0.1")
michael@0 992 this.add("http", "127.0.0.1", port);
michael@0 993 },
michael@0 994
michael@0 995 /**
michael@0 996 * Called at server shutdown time, unsets the primary location only if it was
michael@0 997 * the default-assigned location and removes the default location from the
michael@0 998 * set of locations used.
michael@0 999 */
michael@0 1000 _teardown: function()
michael@0 1001 {
michael@0 1002 if (this._host != "127.0.0.1") {
michael@0 1003 // Not the default primary location, nothing special to do here
michael@0 1004 this.remove("http", "127.0.0.1", this._defaultPort);
michael@0 1005 }
michael@0 1006
michael@0 1007 // This is a *very* tricky bit of reasoning here; make absolutely sure the
michael@0 1008 // tests for this code pass before you commit changes to it.
michael@0 1009 if (this._primaryScheme == "http" &&
michael@0 1010 this._primaryHost == this._host &&
michael@0 1011 this._primaryPort == this._defaultPort)
michael@0 1012 {
michael@0 1013 // Make sure we don't trigger the readding logic in .remove(), then remove
michael@0 1014 // the default location.
michael@0 1015 var port = this._defaultPort;
michael@0 1016 this._defaultPort = -1;
michael@0 1017 this.remove("http", this._host, port);
michael@0 1018
michael@0 1019 // Ensure a server start triggers the setPrimary() path in ._initialize()
michael@0 1020 this._primaryPort = -1;
michael@0 1021 }
michael@0 1022 else
michael@0 1023 {
michael@0 1024 // No reason not to remove directly as it's not our primary location
michael@0 1025 this.remove("http", this._host, this._defaultPort);
michael@0 1026 }
michael@0 1027 },
michael@0 1028
michael@0 1029 /**
michael@0 1030 * Ensures scheme, host, and port are all valid with respect to RFC 2396.
michael@0 1031 *
michael@0 1032 * @throws NS_ERROR_ILLEGAL_VALUE
michael@0 1033 * if any argument doesn't match the corresponding production
michael@0 1034 */
michael@0 1035 _validate: function(scheme, host, port)
michael@0 1036 {
michael@0 1037 if (scheme !== "http" && scheme !== "https")
michael@0 1038 {
michael@0 1039 dumpn("*** server only supports http/https schemes: '" + scheme + "'");
michael@0 1040 dumpStack();
michael@0 1041 throw Cr.NS_ERROR_ILLEGAL_VALUE;
michael@0 1042 }
michael@0 1043 if (!HOST_REGEX.test(host))
michael@0 1044 {
michael@0 1045 dumpn("*** unexpected host: '" + host + "'");
michael@0 1046 throw Cr.NS_ERROR_ILLEGAL_VALUE;
michael@0 1047 }
michael@0 1048 if (port < 0 || port > 65535)
michael@0 1049 {
michael@0 1050 dumpn("*** unexpected port: '" + port + "'");
michael@0 1051 throw Cr.NS_ERROR_ILLEGAL_VALUE;
michael@0 1052 }
michael@0 1053 }
michael@0 1054 };
michael@0 1055
michael@0 1056
michael@0 1057 /**
michael@0 1058 * Represents a connection to the server (and possibly in the future the thread
michael@0 1059 * on which the connection is processed).
michael@0 1060 *
michael@0 1061 * @param input : nsIInputStream
michael@0 1062 * stream from which incoming data on the connection is read
michael@0 1063 * @param output : nsIOutputStream
michael@0 1064 * stream to write data out the connection
michael@0 1065 * @param server : nsHttpServer
michael@0 1066 * the server handling the connection
michael@0 1067 * @param port : int
michael@0 1068 * the port on which the server is running
michael@0 1069 * @param outgoingPort : int
michael@0 1070 * the outgoing port used by this connection
michael@0 1071 * @param number : uint
michael@0 1072 * a serial number used to uniquely identify this connection
michael@0 1073 */
michael@0 1074 function Connection(input, output, server, port, outgoingPort, number)
michael@0 1075 {
michael@0 1076 dumpn("*** opening new connection " + number + " on port " + outgoingPort);
michael@0 1077
michael@0 1078 /** Stream of incoming data. */
michael@0 1079 this.input = input;
michael@0 1080
michael@0 1081 /** Stream for outgoing data. */
michael@0 1082 this.output = output;
michael@0 1083
michael@0 1084 /** The server associated with this request. */
michael@0 1085 this.server = server;
michael@0 1086
michael@0 1087 /** The port on which the server is running. */
michael@0 1088 this.port = port;
michael@0 1089
michael@0 1090 /** The outgoing poort used by this connection. */
michael@0 1091 this._outgoingPort = outgoingPort;
michael@0 1092
michael@0 1093 /** The serial number of this connection. */
michael@0 1094 this.number = number;
michael@0 1095
michael@0 1096 /**
michael@0 1097 * The request for which a response is being generated, null if the
michael@0 1098 * incoming request has not been fully received or if it had errors.
michael@0 1099 */
michael@0 1100 this.request = null;
michael@0 1101
michael@0 1102 /** State variables for debugging. */
michael@0 1103 this._closed = this._processed = false;
michael@0 1104 }
michael@0 1105 Connection.prototype =
michael@0 1106 {
michael@0 1107 /** Closes this connection's input/output streams. */
michael@0 1108 close: function()
michael@0 1109 {
michael@0 1110 dumpn("*** closing connection " + this.number +
michael@0 1111 " on port " + this._outgoingPort);
michael@0 1112
michael@0 1113 this.input.close();
michael@0 1114 this.output.close();
michael@0 1115 this._closed = true;
michael@0 1116
michael@0 1117 var server = this.server;
michael@0 1118 server._connectionClosed(this);
michael@0 1119
michael@0 1120 // If an error triggered a server shutdown, act on it now
michael@0 1121 if (server._doQuit)
michael@0 1122 server.stop(function() { /* not like we can do anything better */ });
michael@0 1123 },
michael@0 1124
michael@0 1125 /**
michael@0 1126 * Initiates processing of this connection, using the data in the given
michael@0 1127 * request.
michael@0 1128 *
michael@0 1129 * @param request : Request
michael@0 1130 * the request which should be processed
michael@0 1131 */
michael@0 1132 process: function(request)
michael@0 1133 {
michael@0 1134 NS_ASSERT(!this._closed && !this._processed);
michael@0 1135
michael@0 1136 this._processed = true;
michael@0 1137
michael@0 1138 this.request = request;
michael@0 1139 this.server._handler.handleResponse(this);
michael@0 1140 },
michael@0 1141
michael@0 1142 /**
michael@0 1143 * Initiates processing of this connection, generating a response with the
michael@0 1144 * given HTTP error code.
michael@0 1145 *
michael@0 1146 * @param code : uint
michael@0 1147 * an HTTP code, so in the range [0, 1000)
michael@0 1148 * @param request : Request
michael@0 1149 * incomplete data about the incoming request (since there were errors
michael@0 1150 * during its processing
michael@0 1151 */
michael@0 1152 processError: function(code, request)
michael@0 1153 {
michael@0 1154 NS_ASSERT(!this._closed && !this._processed);
michael@0 1155
michael@0 1156 this._processed = true;
michael@0 1157 this.request = request;
michael@0 1158 this.server._handler.handleError(code, this);
michael@0 1159 },
michael@0 1160
michael@0 1161 /** Converts this to a string for debugging purposes. */
michael@0 1162 toString: function()
michael@0 1163 {
michael@0 1164 return "<Connection(" + this.number +
michael@0 1165 (this.request ? ", " + this.request.path : "") +"): " +
michael@0 1166 (this._closed ? "closed" : "open") + ">";
michael@0 1167 }
michael@0 1168 };
michael@0 1169
michael@0 1170
michael@0 1171
michael@0 1172 /** Returns an array of count bytes from the given input stream. */
michael@0 1173 function readBytes(inputStream, count)
michael@0 1174 {
michael@0 1175 return new BinaryInputStream(inputStream).readByteArray(count);
michael@0 1176 }
michael@0 1177
michael@0 1178
michael@0 1179
michael@0 1180 /** Request reader processing states; see RequestReader for details. */
michael@0 1181 const READER_IN_REQUEST_LINE = 0;
michael@0 1182 const READER_IN_HEADERS = 1;
michael@0 1183 const READER_IN_BODY = 2;
michael@0 1184 const READER_FINISHED = 3;
michael@0 1185
michael@0 1186
michael@0 1187 /**
michael@0 1188 * Reads incoming request data asynchronously, does any necessary preprocessing,
michael@0 1189 * and forwards it to the request handler. Processing occurs in three states:
michael@0 1190 *
michael@0 1191 * READER_IN_REQUEST_LINE Reading the request's status line
michael@0 1192 * READER_IN_HEADERS Reading headers in the request
michael@0 1193 * READER_IN_BODY Reading the body of the request
michael@0 1194 * READER_FINISHED Entire request has been read and processed
michael@0 1195 *
michael@0 1196 * During the first two stages, initial metadata about the request is gathered
michael@0 1197 * into a Request object. Once the status line and headers have been processed,
michael@0 1198 * we start processing the body of the request into the Request. Finally, when
michael@0 1199 * the entire body has been read, we create a Response and hand it off to the
michael@0 1200 * ServerHandler to be given to the appropriate request handler.
michael@0 1201 *
michael@0 1202 * @param connection : Connection
michael@0 1203 * the connection for the request being read
michael@0 1204 */
michael@0 1205 function RequestReader(connection)
michael@0 1206 {
michael@0 1207 /** Connection metadata for this request. */
michael@0 1208 this._connection = connection;
michael@0 1209
michael@0 1210 /**
michael@0 1211 * A container providing line-by-line access to the raw bytes that make up the
michael@0 1212 * data which has been read from the connection but has not yet been acted
michael@0 1213 * upon (by passing it to the request handler or by extracting request
michael@0 1214 * metadata from it).
michael@0 1215 */
michael@0 1216 this._data = new LineData();
michael@0 1217
michael@0 1218 /**
michael@0 1219 * The amount of data remaining to be read from the body of this request.
michael@0 1220 * After all headers in the request have been read this is the value in the
michael@0 1221 * Content-Length header, but as the body is read its value decreases to zero.
michael@0 1222 */
michael@0 1223 this._contentLength = 0;
michael@0 1224
michael@0 1225 /** The current state of parsing the incoming request. */
michael@0 1226 this._state = READER_IN_REQUEST_LINE;
michael@0 1227
michael@0 1228 /** Metadata constructed from the incoming request for the request handler. */
michael@0 1229 this._metadata = new Request(connection.port);
michael@0 1230
michael@0 1231 /**
michael@0 1232 * Used to preserve state if we run out of line data midway through a
michael@0 1233 * multi-line header. _lastHeaderName stores the name of the header, while
michael@0 1234 * _lastHeaderValue stores the value we've seen so far for the header.
michael@0 1235 *
michael@0 1236 * These fields are always either both undefined or both strings.
michael@0 1237 */
michael@0 1238 this._lastHeaderName = this._lastHeaderValue = undefined;
michael@0 1239 }
michael@0 1240 RequestReader.prototype =
michael@0 1241 {
michael@0 1242 // NSIINPUTSTREAMCALLBACK
michael@0 1243
michael@0 1244 /**
michael@0 1245 * Called when more data from the incoming request is available. This method
michael@0 1246 * then reads the available data from input and deals with that data as
michael@0 1247 * necessary, depending upon the syntax of already-downloaded data.
michael@0 1248 *
michael@0 1249 * @param input : nsIAsyncInputStream
michael@0 1250 * the stream of incoming data from the connection
michael@0 1251 */
michael@0 1252 onInputStreamReady: function(input)
michael@0 1253 {
michael@0 1254 dumpn("*** onInputStreamReady(input=" + input + ") on thread " +
michael@0 1255 gThreadManager.currentThread + " (main is " +
michael@0 1256 gThreadManager.mainThread + ")");
michael@0 1257 dumpn("*** this._state == " + this._state);
michael@0 1258
michael@0 1259 // Handle cases where we get more data after a request error has been
michael@0 1260 // discovered but *before* we can close the connection.
michael@0 1261 var data = this._data;
michael@0 1262 if (!data)
michael@0 1263 return;
michael@0 1264
michael@0 1265 try
michael@0 1266 {
michael@0 1267 data.appendBytes(readBytes(input, input.available()));
michael@0 1268 }
michael@0 1269 catch (e)
michael@0 1270 {
michael@0 1271 if (streamClosed(e))
michael@0 1272 {
michael@0 1273 dumpn("*** WARNING: unexpected error when reading from socket; will " +
michael@0 1274 "be treated as if the input stream had been closed");
michael@0 1275 dumpn("*** WARNING: actual error was: " + e);
michael@0 1276 }
michael@0 1277
michael@0 1278 // We've lost a race -- input has been closed, but we're still expecting
michael@0 1279 // to read more data. available() will throw in this case, and since
michael@0 1280 // we're dead in the water now, destroy the connection.
michael@0 1281 dumpn("*** onInputStreamReady called on a closed input, destroying " +
michael@0 1282 "connection");
michael@0 1283 this._connection.close();
michael@0 1284 return;
michael@0 1285 }
michael@0 1286
michael@0 1287 switch (this._state)
michael@0 1288 {
michael@0 1289 default:
michael@0 1290 NS_ASSERT(false, "invalid state: " + this._state);
michael@0 1291 break;
michael@0 1292
michael@0 1293 case READER_IN_REQUEST_LINE:
michael@0 1294 if (!this._processRequestLine())
michael@0 1295 break;
michael@0 1296 /* fall through */
michael@0 1297
michael@0 1298 case READER_IN_HEADERS:
michael@0 1299 if (!this._processHeaders())
michael@0 1300 break;
michael@0 1301 /* fall through */
michael@0 1302
michael@0 1303 case READER_IN_BODY:
michael@0 1304 this._processBody();
michael@0 1305 }
michael@0 1306
michael@0 1307 if (this._state != READER_FINISHED)
michael@0 1308 input.asyncWait(this, 0, 0, gThreadManager.currentThread);
michael@0 1309 },
michael@0 1310
michael@0 1311 //
michael@0 1312 // see nsISupports.QueryInterface
michael@0 1313 //
michael@0 1314 QueryInterface: function(aIID)
michael@0 1315 {
michael@0 1316 if (aIID.equals(Ci.nsIInputStreamCallback) ||
michael@0 1317 aIID.equals(Ci.nsISupports))
michael@0 1318 return this;
michael@0 1319
michael@0 1320 throw Cr.NS_ERROR_NO_INTERFACE;
michael@0 1321 },
michael@0 1322
michael@0 1323
michael@0 1324 // PRIVATE API
michael@0 1325
michael@0 1326 /**
michael@0 1327 * Processes unprocessed, downloaded data as a request line.
michael@0 1328 *
michael@0 1329 * @returns boolean
michael@0 1330 * true iff the request line has been fully processed
michael@0 1331 */
michael@0 1332 _processRequestLine: function()
michael@0 1333 {
michael@0 1334 NS_ASSERT(this._state == READER_IN_REQUEST_LINE);
michael@0 1335
michael@0 1336 // Servers SHOULD ignore any empty line(s) received where a Request-Line
michael@0 1337 // is expected (section 4.1).
michael@0 1338 var data = this._data;
michael@0 1339 var line = {};
michael@0 1340 var readSuccess;
michael@0 1341 while ((readSuccess = data.readLine(line)) && line.value == "")
michael@0 1342 dumpn("*** ignoring beginning blank line...");
michael@0 1343
michael@0 1344 // if we don't have a full line, wait until we do
michael@0 1345 if (!readSuccess)
michael@0 1346 return false;
michael@0 1347
michael@0 1348 // we have the first non-blank line
michael@0 1349 try
michael@0 1350 {
michael@0 1351 this._parseRequestLine(line.value);
michael@0 1352 this._state = READER_IN_HEADERS;
michael@0 1353 return true;
michael@0 1354 }
michael@0 1355 catch (e)
michael@0 1356 {
michael@0 1357 this._handleError(e);
michael@0 1358 return false;
michael@0 1359 }
michael@0 1360 },
michael@0 1361
michael@0 1362 /**
michael@0 1363 * Processes stored data, assuming it is either at the beginning or in
michael@0 1364 * the middle of processing request headers.
michael@0 1365 *
michael@0 1366 * @returns boolean
michael@0 1367 * true iff header data in the request has been fully processed
michael@0 1368 */
michael@0 1369 _processHeaders: function()
michael@0 1370 {
michael@0 1371 NS_ASSERT(this._state == READER_IN_HEADERS);
michael@0 1372
michael@0 1373 // XXX things to fix here:
michael@0 1374 //
michael@0 1375 // - need to support RFC 2047-encoded non-US-ASCII characters
michael@0 1376
michael@0 1377 try
michael@0 1378 {
michael@0 1379 var done = this._parseHeaders();
michael@0 1380 if (done)
michael@0 1381 {
michael@0 1382 var request = this._metadata;
michael@0 1383
michael@0 1384 // XXX this is wrong for requests with transfer-encodings applied to
michael@0 1385 // them, particularly chunked (which by its nature can have no
michael@0 1386 // meaningful Content-Length header)!
michael@0 1387 this._contentLength = request.hasHeader("Content-Length")
michael@0 1388 ? parseInt(request.getHeader("Content-Length"), 10)
michael@0 1389 : 0;
michael@0 1390 dumpn("_processHeaders, Content-length=" + this._contentLength);
michael@0 1391
michael@0 1392 this._state = READER_IN_BODY;
michael@0 1393 }
michael@0 1394 return done;
michael@0 1395 }
michael@0 1396 catch (e)
michael@0 1397 {
michael@0 1398 this._handleError(e);
michael@0 1399 return false;
michael@0 1400 }
michael@0 1401 },
michael@0 1402
michael@0 1403 /**
michael@0 1404 * Processes stored data, assuming it is either at the beginning or in
michael@0 1405 * the middle of processing the request body.
michael@0 1406 *
michael@0 1407 * @returns boolean
michael@0 1408 * true iff the request body has been fully processed
michael@0 1409 */
michael@0 1410 _processBody: function()
michael@0 1411 {
michael@0 1412 NS_ASSERT(this._state == READER_IN_BODY);
michael@0 1413
michael@0 1414 // XXX handle chunked transfer-coding request bodies!
michael@0 1415
michael@0 1416 try
michael@0 1417 {
michael@0 1418 if (this._contentLength > 0)
michael@0 1419 {
michael@0 1420 var data = this._data.purge();
michael@0 1421 var count = Math.min(data.length, this._contentLength);
michael@0 1422 dumpn("*** loading data=" + data + " len=" + data.length +
michael@0 1423 " excess=" + (data.length - count));
michael@0 1424
michael@0 1425 var bos = new BinaryOutputStream(this._metadata._bodyOutputStream);
michael@0 1426 bos.writeByteArray(data, count);
michael@0 1427 this._contentLength -= count;
michael@0 1428 }
michael@0 1429
michael@0 1430 dumpn("*** remaining body data len=" + this._contentLength);
michael@0 1431 if (this._contentLength == 0)
michael@0 1432 {
michael@0 1433 this._validateRequest();
michael@0 1434 this._state = READER_FINISHED;
michael@0 1435 this._handleResponse();
michael@0 1436 return true;
michael@0 1437 }
michael@0 1438
michael@0 1439 return false;
michael@0 1440 }
michael@0 1441 catch (e)
michael@0 1442 {
michael@0 1443 this._handleError(e);
michael@0 1444 return false;
michael@0 1445 }
michael@0 1446 },
michael@0 1447
michael@0 1448 /**
michael@0 1449 * Does various post-header checks on the data in this request.
michael@0 1450 *
michael@0 1451 * @throws : HttpError
michael@0 1452 * if the request was malformed in some way
michael@0 1453 */
michael@0 1454 _validateRequest: function()
michael@0 1455 {
michael@0 1456 NS_ASSERT(this._state == READER_IN_BODY);
michael@0 1457
michael@0 1458 dumpn("*** _validateRequest");
michael@0 1459
michael@0 1460 var metadata = this._metadata;
michael@0 1461 var headers = metadata._headers;
michael@0 1462
michael@0 1463 // 19.6.1.1 -- servers MUST report 400 to HTTP/1.1 requests w/o Host header
michael@0 1464 var identity = this._connection.server.identity;
michael@0 1465 if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1))
michael@0 1466 {
michael@0 1467 if (!headers.hasHeader("Host"))
michael@0 1468 {
michael@0 1469 dumpn("*** malformed HTTP/1.1 or greater request with no Host header!");
michael@0 1470 throw HTTP_400;
michael@0 1471 }
michael@0 1472
michael@0 1473 // If the Request-URI wasn't absolute, then we need to determine our host.
michael@0 1474 // We have to determine what scheme was used to access us based on the
michael@0 1475 // server identity data at this point, because the request just doesn't
michael@0 1476 // contain enough data on its own to do this, sadly.
michael@0 1477 if (!metadata._host)
michael@0 1478 {
michael@0 1479 var host, port;
michael@0 1480 var hostPort = headers.getHeader("Host");
michael@0 1481 var colon = hostPort.indexOf(":");
michael@0 1482 if (colon < 0)
michael@0 1483 {
michael@0 1484 host = hostPort;
michael@0 1485 port = "";
michael@0 1486 }
michael@0 1487 else
michael@0 1488 {
michael@0 1489 host = hostPort.substring(0, colon);
michael@0 1490 port = hostPort.substring(colon + 1);
michael@0 1491 }
michael@0 1492
michael@0 1493 // NB: We allow an empty port here because, oddly, a colon may be
michael@0 1494 // present even without a port number, e.g. "example.com:"; in this
michael@0 1495 // case the default port applies.
michael@0 1496 if (!HOST_REGEX.test(host) || !/^\d*$/.test(port))
michael@0 1497 {
michael@0 1498 dumpn("*** malformed hostname (" + hostPort + ") in Host " +
michael@0 1499 "header, 400 time");
michael@0 1500 throw HTTP_400;
michael@0 1501 }
michael@0 1502
michael@0 1503 // If we're not given a port, we're stuck, because we don't know what
michael@0 1504 // scheme to use to look up the correct port here, in general. Since
michael@0 1505 // the HTTPS case requires a tunnel/proxy and thus requires that the
michael@0 1506 // requested URI be absolute (and thus contain the necessary
michael@0 1507 // information), let's assume HTTP will prevail and use that.
michael@0 1508 port = +port || 80;
michael@0 1509
michael@0 1510 var scheme = identity.getScheme(host, port);
michael@0 1511 if (!scheme)
michael@0 1512 {
michael@0 1513 dumpn("*** unrecognized hostname (" + hostPort + ") in Host " +
michael@0 1514 "header, 400 time");
michael@0 1515 throw HTTP_400;
michael@0 1516 }
michael@0 1517
michael@0 1518 metadata._scheme = scheme;
michael@0 1519 metadata._host = host;
michael@0 1520 metadata._port = port;
michael@0 1521 }
michael@0 1522 }
michael@0 1523 else
michael@0 1524 {
michael@0 1525 NS_ASSERT(metadata._host === undefined,
michael@0 1526 "HTTP/1.0 doesn't allow absolute paths in the request line!");
michael@0 1527
michael@0 1528 metadata._scheme = identity.primaryScheme;
michael@0 1529 metadata._host = identity.primaryHost;
michael@0 1530 metadata._port = identity.primaryPort;
michael@0 1531 }
michael@0 1532
michael@0 1533 NS_ASSERT(identity.has(metadata._scheme, metadata._host, metadata._port),
michael@0 1534 "must have a location we recognize by now!");
michael@0 1535 },
michael@0 1536
michael@0 1537 /**
michael@0 1538 * Handles responses in case of error, either in the server or in the request.
michael@0 1539 *
michael@0 1540 * @param e
michael@0 1541 * the specific error encountered, which is an HttpError in the case where
michael@0 1542 * the request is in some way invalid or cannot be fulfilled; if this isn't
michael@0 1543 * an HttpError we're going to be paranoid and shut down, because that
michael@0 1544 * shouldn't happen, ever
michael@0 1545 */
michael@0 1546 _handleError: function(e)
michael@0 1547 {
michael@0 1548 // Don't fall back into normal processing!
michael@0 1549 this._state = READER_FINISHED;
michael@0 1550
michael@0 1551 var server = this._connection.server;
michael@0 1552 if (e instanceof HttpError)
michael@0 1553 {
michael@0 1554 var code = e.code;
michael@0 1555 }
michael@0 1556 else
michael@0 1557 {
michael@0 1558 dumpn("!!! UNEXPECTED ERROR: " + e +
michael@0 1559 (e.lineNumber ? ", line " + e.lineNumber : ""));
michael@0 1560
michael@0 1561 // no idea what happened -- be paranoid and shut down
michael@0 1562 code = 500;
michael@0 1563 server._requestQuit();
michael@0 1564 }
michael@0 1565
michael@0 1566 // make attempted reuse of data an error
michael@0 1567 this._data = null;
michael@0 1568
michael@0 1569 this._connection.processError(code, this._metadata);
michael@0 1570 },
michael@0 1571
michael@0 1572 /**
michael@0 1573 * Now that we've read the request line and headers, we can actually hand off
michael@0 1574 * the request to be handled.
michael@0 1575 *
michael@0 1576 * This method is called once per request, after the request line and all
michael@0 1577 * headers and the body, if any, have been received.
michael@0 1578 */
michael@0 1579 _handleResponse: function()
michael@0 1580 {
michael@0 1581 NS_ASSERT(this._state == READER_FINISHED);
michael@0 1582
michael@0 1583 // We don't need the line-based data any more, so make attempted reuse an
michael@0 1584 // error.
michael@0 1585 this._data = null;
michael@0 1586
michael@0 1587 this._connection.process(this._metadata);
michael@0 1588 },
michael@0 1589
michael@0 1590
michael@0 1591 // PARSING
michael@0 1592
michael@0 1593 /**
michael@0 1594 * Parses the request line for the HTTP request associated with this.
michael@0 1595 *
michael@0 1596 * @param line : string
michael@0 1597 * the request line
michael@0 1598 */
michael@0 1599 _parseRequestLine: function(line)
michael@0 1600 {
michael@0 1601 NS_ASSERT(this._state == READER_IN_REQUEST_LINE);
michael@0 1602
michael@0 1603 dumpn("*** _parseRequestLine('" + line + "')");
michael@0 1604
michael@0 1605 var metadata = this._metadata;
michael@0 1606
michael@0 1607 // clients and servers SHOULD accept any amount of SP or HT characters
michael@0 1608 // between fields, even though only a single SP is required (section 19.3)
michael@0 1609 var request = line.split(/[ \t]+/);
michael@0 1610 if (!request || request.length != 3)
michael@0 1611 throw HTTP_400;
michael@0 1612
michael@0 1613 metadata._method = request[0];
michael@0 1614
michael@0 1615 // get the HTTP version
michael@0 1616 var ver = request[2];
michael@0 1617 var match = ver.match(/^HTTP\/(\d+\.\d+)$/);
michael@0 1618 if (!match)
michael@0 1619 throw HTTP_400;
michael@0 1620
michael@0 1621 // determine HTTP version
michael@0 1622 try
michael@0 1623 {
michael@0 1624 metadata._httpVersion = new nsHttpVersion(match[1]);
michael@0 1625 if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_0))
michael@0 1626 throw "unsupported HTTP version";
michael@0 1627 }
michael@0 1628 catch (e)
michael@0 1629 {
michael@0 1630 // we support HTTP/1.0 and HTTP/1.1 only
michael@0 1631 throw HTTP_501;
michael@0 1632 }
michael@0 1633
michael@0 1634
michael@0 1635 var fullPath = request[1];
michael@0 1636 var serverIdentity = this._connection.server.identity;
michael@0 1637
michael@0 1638 var scheme, host, port;
michael@0 1639
michael@0 1640 if (fullPath.charAt(0) != "/")
michael@0 1641 {
michael@0 1642 // No absolute paths in the request line in HTTP prior to 1.1
michael@0 1643 if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1))
michael@0 1644 throw HTTP_400;
michael@0 1645
michael@0 1646 try
michael@0 1647 {
michael@0 1648 var uri = Cc["@mozilla.org/network/io-service;1"]
michael@0 1649 .getService(Ci.nsIIOService)
michael@0 1650 .newURI(fullPath, null, null);
michael@0 1651 fullPath = uri.path;
michael@0 1652 scheme = uri.scheme;
michael@0 1653 host = metadata._host = uri.asciiHost;
michael@0 1654 port = uri.port;
michael@0 1655 if (port === -1)
michael@0 1656 {
michael@0 1657 if (scheme === "http")
michael@0 1658 port = 80;
michael@0 1659 else if (scheme === "https")
michael@0 1660 port = 443;
michael@0 1661 else
michael@0 1662 throw HTTP_400;
michael@0 1663 }
michael@0 1664 }
michael@0 1665 catch (e)
michael@0 1666 {
michael@0 1667 // If the host is not a valid host on the server, the response MUST be a
michael@0 1668 // 400 (Bad Request) error message (section 5.2). Alternately, the URI
michael@0 1669 // is malformed.
michael@0 1670 throw HTTP_400;
michael@0 1671 }
michael@0 1672
michael@0 1673 if (!serverIdentity.has(scheme, host, port) || fullPath.charAt(0) != "/")
michael@0 1674 throw HTTP_400;
michael@0 1675 }
michael@0 1676
michael@0 1677 var splitter = fullPath.indexOf("?");
michael@0 1678 if (splitter < 0)
michael@0 1679 {
michael@0 1680 // _queryString already set in ctor
michael@0 1681 metadata._path = fullPath;
michael@0 1682 }
michael@0 1683 else
michael@0 1684 {
michael@0 1685 metadata._path = fullPath.substring(0, splitter);
michael@0 1686 metadata._queryString = fullPath.substring(splitter + 1);
michael@0 1687 }
michael@0 1688
michael@0 1689 metadata._scheme = scheme;
michael@0 1690 metadata._host = host;
michael@0 1691 metadata._port = port;
michael@0 1692 },
michael@0 1693
michael@0 1694 /**
michael@0 1695 * Parses all available HTTP headers in this until the header-ending CRLFCRLF,
michael@0 1696 * adding them to the store of headers in the request.
michael@0 1697 *
michael@0 1698 * @throws
michael@0 1699 * HTTP_400 if the headers are malformed
michael@0 1700 * @returns boolean
michael@0 1701 * true if all headers have now been processed, false otherwise
michael@0 1702 */
michael@0 1703 _parseHeaders: function()
michael@0 1704 {
michael@0 1705 NS_ASSERT(this._state == READER_IN_HEADERS);
michael@0 1706
michael@0 1707 dumpn("*** _parseHeaders");
michael@0 1708
michael@0 1709 var data = this._data;
michael@0 1710
michael@0 1711 var headers = this._metadata._headers;
michael@0 1712 var lastName = this._lastHeaderName;
michael@0 1713 var lastVal = this._lastHeaderValue;
michael@0 1714
michael@0 1715 var line = {};
michael@0 1716 while (true)
michael@0 1717 {
michael@0 1718 NS_ASSERT(!((lastVal === undefined) ^ (lastName === undefined)),
michael@0 1719 lastName === undefined ?
michael@0 1720 "lastVal without lastName? lastVal: '" + lastVal + "'" :
michael@0 1721 "lastName without lastVal? lastName: '" + lastName + "'");
michael@0 1722
michael@0 1723 if (!data.readLine(line))
michael@0 1724 {
michael@0 1725 // save any data we have from the header we might still be processing
michael@0 1726 this._lastHeaderName = lastName;
michael@0 1727 this._lastHeaderValue = lastVal;
michael@0 1728 return false;
michael@0 1729 }
michael@0 1730
michael@0 1731 var lineText = line.value;
michael@0 1732 var firstChar = lineText.charAt(0);
michael@0 1733
michael@0 1734 // blank line means end of headers
michael@0 1735 if (lineText == "")
michael@0 1736 {
michael@0 1737 // we're finished with the previous header
michael@0 1738 if (lastName)
michael@0 1739 {
michael@0 1740 try
michael@0 1741 {
michael@0 1742 headers.setHeader(lastName, lastVal, true);
michael@0 1743 }
michael@0 1744 catch (e)
michael@0 1745 {
michael@0 1746 dumpn("*** e == " + e);
michael@0 1747 throw HTTP_400;
michael@0 1748 }
michael@0 1749 }
michael@0 1750 else
michael@0 1751 {
michael@0 1752 // no headers in request -- valid for HTTP/1.0 requests
michael@0 1753 }
michael@0 1754
michael@0 1755 // either way, we're done processing headers
michael@0 1756 this._state = READER_IN_BODY;
michael@0 1757 return true;
michael@0 1758 }
michael@0 1759 else if (firstChar == " " || firstChar == "\t")
michael@0 1760 {
michael@0 1761 // multi-line header if we've already seen a header line
michael@0 1762 if (!lastName)
michael@0 1763 {
michael@0 1764 // we don't have a header to continue!
michael@0 1765 throw HTTP_400;
michael@0 1766 }
michael@0 1767
michael@0 1768 // append this line's text to the value; starts with SP/HT, so no need
michael@0 1769 // for separating whitespace
michael@0 1770 lastVal += lineText;
michael@0 1771 }
michael@0 1772 else
michael@0 1773 {
michael@0 1774 // we have a new header, so set the old one (if one existed)
michael@0 1775 if (lastName)
michael@0 1776 {
michael@0 1777 try
michael@0 1778 {
michael@0 1779 headers.setHeader(lastName, lastVal, true);
michael@0 1780 }
michael@0 1781 catch (e)
michael@0 1782 {
michael@0 1783 dumpn("*** e == " + e);
michael@0 1784 throw HTTP_400;
michael@0 1785 }
michael@0 1786 }
michael@0 1787
michael@0 1788 var colon = lineText.indexOf(":"); // first colon must be splitter
michael@0 1789 if (colon < 1)
michael@0 1790 {
michael@0 1791 // no colon or missing header field-name
michael@0 1792 throw HTTP_400;
michael@0 1793 }
michael@0 1794
michael@0 1795 // set header name, value (to be set in the next loop, usually)
michael@0 1796 lastName = lineText.substring(0, colon);
michael@0 1797 lastVal = lineText.substring(colon + 1);
michael@0 1798 } // empty, continuation, start of header
michael@0 1799 } // while (true)
michael@0 1800 }
michael@0 1801 };
michael@0 1802
michael@0 1803
michael@0 1804 /** The character codes for CR and LF. */
michael@0 1805 const CR = 0x0D, LF = 0x0A;
michael@0 1806
michael@0 1807 /**
michael@0 1808 * Calculates the number of characters before the first CRLF pair in array, or
michael@0 1809 * -1 if the array contains no CRLF pair.
michael@0 1810 *
michael@0 1811 * @param array : Array
michael@0 1812 * an array of numbers in the range [0, 256), each representing a single
michael@0 1813 * character; the first CRLF is the lowest index i where
michael@0 1814 * |array[i] == "\r".charCodeAt(0)| and |array[i+1] == "\n".charCodeAt(0)|,
michael@0 1815 * if such an |i| exists, and -1 otherwise
michael@0 1816 * @returns int
michael@0 1817 * the index of the first CRLF if any were present, -1 otherwise
michael@0 1818 */
michael@0 1819 function findCRLF(array)
michael@0 1820 {
michael@0 1821 for (var i = array.indexOf(CR); i >= 0; i = array.indexOf(CR, i + 1))
michael@0 1822 {
michael@0 1823 if (array[i + 1] == LF)
michael@0 1824 return i;
michael@0 1825 }
michael@0 1826 return -1;
michael@0 1827 }
michael@0 1828
michael@0 1829
michael@0 1830 /**
michael@0 1831 * A container which provides line-by-line access to the arrays of bytes with
michael@0 1832 * which it is seeded.
michael@0 1833 */
michael@0 1834 function LineData()
michael@0 1835 {
michael@0 1836 /** An array of queued bytes from which to get line-based characters. */
michael@0 1837 this._data = [];
michael@0 1838 }
michael@0 1839 LineData.prototype =
michael@0 1840 {
michael@0 1841 /**
michael@0 1842 * Appends the bytes in the given array to the internal data cache maintained
michael@0 1843 * by this.
michael@0 1844 */
michael@0 1845 appendBytes: function(bytes)
michael@0 1846 {
michael@0 1847 Array.prototype.push.apply(this._data, bytes);
michael@0 1848 },
michael@0 1849
michael@0 1850 /**
michael@0 1851 * Removes and returns a line of data, delimited by CRLF, from this.
michael@0 1852 *
michael@0 1853 * @param out
michael@0 1854 * an object whose "value" property will be set to the first line of text
michael@0 1855 * present in this, sans CRLF, if this contains a full CRLF-delimited line
michael@0 1856 * of text; if this doesn't contain enough data, the value of the property
michael@0 1857 * is undefined
michael@0 1858 * @returns boolean
michael@0 1859 * true if a full line of data could be read from the data in this, false
michael@0 1860 * otherwise
michael@0 1861 */
michael@0 1862 readLine: function(out)
michael@0 1863 {
michael@0 1864 var data = this._data;
michael@0 1865 var length = findCRLF(data);
michael@0 1866 if (length < 0)
michael@0 1867 return false;
michael@0 1868
michael@0 1869 //
michael@0 1870 // We have the index of the CR, so remove all the characters, including
michael@0 1871 // CRLF, from the array with splice, and convert the removed array into the
michael@0 1872 // corresponding string, from which we then strip the trailing CRLF.
michael@0 1873 //
michael@0 1874 // Getting the line in this matter acknowledges that substring is an O(1)
michael@0 1875 // operation in SpiderMonkey because strings are immutable, whereas two
michael@0 1876 // splices, both from the beginning of the data, are less likely to be as
michael@0 1877 // cheap as a single splice plus two extra character conversions.
michael@0 1878 //
michael@0 1879 var line = String.fromCharCode.apply(null, data.splice(0, length + 2));
michael@0 1880 out.value = line.substring(0, length);
michael@0 1881
michael@0 1882 return true;
michael@0 1883 },
michael@0 1884
michael@0 1885 /**
michael@0 1886 * Removes the bytes currently within this and returns them in an array.
michael@0 1887 *
michael@0 1888 * @returns Array
michael@0 1889 * the bytes within this when this method is called
michael@0 1890 */
michael@0 1891 purge: function()
michael@0 1892 {
michael@0 1893 var data = this._data;
michael@0 1894 this._data = [];
michael@0 1895 return data;
michael@0 1896 }
michael@0 1897 };
michael@0 1898
michael@0 1899
michael@0 1900
michael@0 1901 /**
michael@0 1902 * Creates a request-handling function for an nsIHttpRequestHandler object.
michael@0 1903 */
michael@0 1904 function createHandlerFunc(handler)
michael@0 1905 {
michael@0 1906 return function(metadata, response) { handler.handle(metadata, response); };
michael@0 1907 }
michael@0 1908
michael@0 1909
michael@0 1910 /**
michael@0 1911 * The default handler for directories; writes an HTML response containing a
michael@0 1912 * slightly-formatted directory listing.
michael@0 1913 */
michael@0 1914 function defaultIndexHandler(metadata, response)
michael@0 1915 {
michael@0 1916 response.setHeader("Content-Type", "text/html", false);
michael@0 1917
michael@0 1918 var path = htmlEscape(decodeURI(metadata.path));
michael@0 1919
michael@0 1920 //
michael@0 1921 // Just do a very basic bit of directory listings -- no need for too much
michael@0 1922 // fanciness, especially since we don't have a style sheet in which we can
michael@0 1923 // stick rules (don't want to pollute the default path-space).
michael@0 1924 //
michael@0 1925
michael@0 1926 var body = '<html>\
michael@0 1927 <head>\
michael@0 1928 <title>' + path + '</title>\
michael@0 1929 </head>\
michael@0 1930 <body>\
michael@0 1931 <h1>' + path + '</h1>\
michael@0 1932 <ol style="list-style-type: none">';
michael@0 1933
michael@0 1934 var directory = metadata.getProperty("directory").QueryInterface(Ci.nsILocalFile);
michael@0 1935 NS_ASSERT(directory && directory.isDirectory());
michael@0 1936
michael@0 1937 var fileList = [];
michael@0 1938 var files = directory.directoryEntries;
michael@0 1939 while (files.hasMoreElements())
michael@0 1940 {
michael@0 1941 var f = files.getNext().QueryInterface(Ci.nsIFile);
michael@0 1942 var name = f.leafName;
michael@0 1943 if (!f.isHidden() &&
michael@0 1944 (name.charAt(name.length - 1) != HIDDEN_CHAR ||
michael@0 1945 name.charAt(name.length - 2) == HIDDEN_CHAR))
michael@0 1946 fileList.push(f);
michael@0 1947 }
michael@0 1948
michael@0 1949 fileList.sort(fileSort);
michael@0 1950
michael@0 1951 for (var i = 0; i < fileList.length; i++)
michael@0 1952 {
michael@0 1953 var file = fileList[i];
michael@0 1954 try
michael@0 1955 {
michael@0 1956 var name = file.leafName;
michael@0 1957 if (name.charAt(name.length - 1) == HIDDEN_CHAR)
michael@0 1958 name = name.substring(0, name.length - 1);
michael@0 1959 var sep = file.isDirectory() ? "/" : "";
michael@0 1960
michael@0 1961 // Note: using " to delimit the attribute here because encodeURIComponent
michael@0 1962 // passes through '.
michael@0 1963 var item = '<li><a href="' + encodeURIComponent(name) + sep + '">' +
michael@0 1964 htmlEscape(name) + sep +
michael@0 1965 '</a></li>';
michael@0 1966
michael@0 1967 body += item;
michael@0 1968 }
michael@0 1969 catch (e) { /* some file system error, ignore the file */ }
michael@0 1970 }
michael@0 1971
michael@0 1972 body += ' </ol>\
michael@0 1973 </body>\
michael@0 1974 </html>';
michael@0 1975
michael@0 1976 response.bodyOutputStream.write(body, body.length);
michael@0 1977 }
michael@0 1978
michael@0 1979 /**
michael@0 1980 * Sorts a and b (nsIFile objects) into an aesthetically pleasing order.
michael@0 1981 */
michael@0 1982 function fileSort(a, b)
michael@0 1983 {
michael@0 1984 var dira = a.isDirectory(), dirb = b.isDirectory();
michael@0 1985
michael@0 1986 if (dira && !dirb)
michael@0 1987 return -1;
michael@0 1988 if (dirb && !dira)
michael@0 1989 return 1;
michael@0 1990
michael@0 1991 var namea = a.leafName.toLowerCase(), nameb = b.leafName.toLowerCase();
michael@0 1992 return nameb > namea ? -1 : 1;
michael@0 1993 }
michael@0 1994
michael@0 1995
michael@0 1996 /**
michael@0 1997 * Converts an externally-provided path into an internal path for use in
michael@0 1998 * determining file mappings.
michael@0 1999 *
michael@0 2000 * @param path
michael@0 2001 * the path to convert
michael@0 2002 * @param encoded
michael@0 2003 * true if the given path should be passed through decodeURI prior to
michael@0 2004 * conversion
michael@0 2005 * @throws URIError
michael@0 2006 * if path is incorrectly encoded
michael@0 2007 */
michael@0 2008 function toInternalPath(path, encoded)
michael@0 2009 {
michael@0 2010 if (encoded)
michael@0 2011 path = decodeURI(path);
michael@0 2012
michael@0 2013 var comps = path.split("/");
michael@0 2014 for (var i = 0, sz = comps.length; i < sz; i++)
michael@0 2015 {
michael@0 2016 var comp = comps[i];
michael@0 2017 if (comp.charAt(comp.length - 1) == HIDDEN_CHAR)
michael@0 2018 comps[i] = comp + HIDDEN_CHAR;
michael@0 2019 }
michael@0 2020 return comps.join("/");
michael@0 2021 }
michael@0 2022
michael@0 2023
michael@0 2024 /**
michael@0 2025 * Adds custom-specified headers for the given file to the given response, if
michael@0 2026 * any such headers are specified.
michael@0 2027 *
michael@0 2028 * @param file
michael@0 2029 * the file on the disk which is to be written
michael@0 2030 * @param metadata
michael@0 2031 * metadata about the incoming request
michael@0 2032 * @param response
michael@0 2033 * the Response to which any specified headers/data should be written
michael@0 2034 * @throws HTTP_500
michael@0 2035 * if an error occurred while processing custom-specified headers
michael@0 2036 */
michael@0 2037 function maybeAddHeaders(file, metadata, response)
michael@0 2038 {
michael@0 2039 var name = file.leafName;
michael@0 2040 if (name.charAt(name.length - 1) == HIDDEN_CHAR)
michael@0 2041 name = name.substring(0, name.length - 1);
michael@0 2042
michael@0 2043 var headerFile = file.parent;
michael@0 2044 headerFile.append(name + HEADERS_SUFFIX);
michael@0 2045
michael@0 2046 if (!headerFile.exists())
michael@0 2047 return;
michael@0 2048
michael@0 2049 const PR_RDONLY = 0x01;
michael@0 2050 var fis = new FileInputStream(headerFile, PR_RDONLY, parseInt("444", 8),
michael@0 2051 Ci.nsIFileInputStream.CLOSE_ON_EOF);
michael@0 2052
michael@0 2053 try
michael@0 2054 {
michael@0 2055 var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0);
michael@0 2056 lis.QueryInterface(Ci.nsIUnicharLineInputStream);
michael@0 2057
michael@0 2058 var line = {value: ""};
michael@0 2059 var more = lis.readLine(line);
michael@0 2060
michael@0 2061 if (!more && line.value == "")
michael@0 2062 return;
michael@0 2063
michael@0 2064
michael@0 2065 // request line
michael@0 2066
michael@0 2067 var status = line.value;
michael@0 2068 if (status.indexOf("HTTP ") == 0)
michael@0 2069 {
michael@0 2070 status = status.substring(5);
michael@0 2071 var space = status.indexOf(" ");
michael@0 2072 var code, description;
michael@0 2073 if (space < 0)
michael@0 2074 {
michael@0 2075 code = status;
michael@0 2076 description = "";
michael@0 2077 }
michael@0 2078 else
michael@0 2079 {
michael@0 2080 code = status.substring(0, space);
michael@0 2081 description = status.substring(space + 1, status.length);
michael@0 2082 }
michael@0 2083
michael@0 2084 response.setStatusLine(metadata.httpVersion, parseInt(code, 10), description);
michael@0 2085
michael@0 2086 line.value = "";
michael@0 2087 more = lis.readLine(line);
michael@0 2088 }
michael@0 2089
michael@0 2090 // headers
michael@0 2091 while (more || line.value != "")
michael@0 2092 {
michael@0 2093 var header = line.value;
michael@0 2094 var colon = header.indexOf(":");
michael@0 2095
michael@0 2096 response.setHeader(header.substring(0, colon),
michael@0 2097 header.substring(colon + 1, header.length),
michael@0 2098 false); // allow overriding server-set headers
michael@0 2099
michael@0 2100 line.value = "";
michael@0 2101 more = lis.readLine(line);
michael@0 2102 }
michael@0 2103 }
michael@0 2104 catch (e)
michael@0 2105 {
michael@0 2106 dumpn("WARNING: error in headers for " + metadata.path + ": " + e);
michael@0 2107 throw HTTP_500;
michael@0 2108 }
michael@0 2109 finally
michael@0 2110 {
michael@0 2111 fis.close();
michael@0 2112 }
michael@0 2113 }
michael@0 2114
michael@0 2115
michael@0 2116 /**
michael@0 2117 * An object which handles requests for a server, executing default and
michael@0 2118 * overridden behaviors as instructed by the code which uses and manipulates it.
michael@0 2119 * Default behavior includes the paths / and /trace (diagnostics), with some
michael@0 2120 * support for HTTP error pages for various codes and fallback to HTTP 500 if
michael@0 2121 * those codes fail for any reason.
michael@0 2122 *
michael@0 2123 * @param server : nsHttpServer
michael@0 2124 * the server in which this handler is being used
michael@0 2125 */
michael@0 2126 function ServerHandler(server)
michael@0 2127 {
michael@0 2128 // FIELDS
michael@0 2129
michael@0 2130 /**
michael@0 2131 * The nsHttpServer instance associated with this handler.
michael@0 2132 */
michael@0 2133 this._server = server;
michael@0 2134
michael@0 2135 /**
michael@0 2136 * A FileMap object containing the set of path->nsILocalFile mappings for
michael@0 2137 * all directory mappings set in the server (e.g., "/" for /var/www/html/,
michael@0 2138 * "/foo/bar/" for /local/path/, and "/foo/bar/baz/" for /local/path2).
michael@0 2139 *
michael@0 2140 * Note carefully: the leading and trailing "/" in each path (not file) are
michael@0 2141 * removed before insertion to simplify the code which uses this. You have
michael@0 2142 * been warned!
michael@0 2143 */
michael@0 2144 this._pathDirectoryMap = new FileMap();
michael@0 2145
michael@0 2146 /**
michael@0 2147 * Custom request handlers for the server in which this resides. Path-handler
michael@0 2148 * pairs are stored as property-value pairs in this property.
michael@0 2149 *
michael@0 2150 * @see ServerHandler.prototype._defaultPaths
michael@0 2151 */
michael@0 2152 this._overridePaths = {};
michael@0 2153
michael@0 2154 /**
michael@0 2155 * Custom request handlers for the server in which this resides. Prefix-handler
michael@0 2156 * pairs are stored as property-value pairs in this property.
michael@0 2157 */
michael@0 2158 this._overridePrefixes = {};
michael@0 2159
michael@0 2160 /**
michael@0 2161 * Custom request handlers for the error handlers in the server in which this
michael@0 2162 * resides. Path-handler pairs are stored as property-value pairs in this
michael@0 2163 * property.
michael@0 2164 *
michael@0 2165 * @see ServerHandler.prototype._defaultErrors
michael@0 2166 */
michael@0 2167 this._overrideErrors = {};
michael@0 2168
michael@0 2169 /**
michael@0 2170 * Maps file extensions to their MIME types in the server, overriding any
michael@0 2171 * mapping that might or might not exist in the MIME service.
michael@0 2172 */
michael@0 2173 this._mimeMappings = {};
michael@0 2174
michael@0 2175 /**
michael@0 2176 * The default handler for requests for directories, used to serve directories
michael@0 2177 * when no index file is present.
michael@0 2178 */
michael@0 2179 this._indexHandler = defaultIndexHandler;
michael@0 2180
michael@0 2181 /** Per-path state storage for the server. */
michael@0 2182 this._state = {};
michael@0 2183
michael@0 2184 /** Entire-server state storage. */
michael@0 2185 this._sharedState = {};
michael@0 2186
michael@0 2187 /** Entire-server state storage for nsISupports values. */
michael@0 2188 this._objectState = {};
michael@0 2189 }
michael@0 2190 ServerHandler.prototype =
michael@0 2191 {
michael@0 2192 // PUBLIC API
michael@0 2193
michael@0 2194 /**
michael@0 2195 * Handles a request to this server, responding to the request appropriately
michael@0 2196 * and initiating server shutdown if necessary.
michael@0 2197 *
michael@0 2198 * This method never throws an exception.
michael@0 2199 *
michael@0 2200 * @param connection : Connection
michael@0 2201 * the connection for this request
michael@0 2202 */
michael@0 2203 handleResponse: function(connection)
michael@0 2204 {
michael@0 2205 var request = connection.request;
michael@0 2206 var response = new Response(connection);
michael@0 2207
michael@0 2208 var path = request.path;
michael@0 2209 dumpn("*** path == " + path);
michael@0 2210
michael@0 2211 try
michael@0 2212 {
michael@0 2213 try
michael@0 2214 {
michael@0 2215 if (path in this._overridePaths)
michael@0 2216 {
michael@0 2217 // explicit paths first, then files based on existing directory mappings,
michael@0 2218 // then (if the file doesn't exist) built-in server default paths
michael@0 2219 dumpn("calling override for " + path);
michael@0 2220 this._overridePaths[path](request, response);
michael@0 2221 }
michael@0 2222 else
michael@0 2223 {
michael@0 2224 let longestPrefix = "";
michael@0 2225 for (let prefix in this._overridePrefixes)
michael@0 2226 {
michael@0 2227 if (prefix.length > longestPrefix.length && path.startsWith(prefix))
michael@0 2228 {
michael@0 2229 longestPrefix = prefix;
michael@0 2230 }
michael@0 2231 }
michael@0 2232 if (longestPrefix.length > 0)
michael@0 2233 {
michael@0 2234 dumpn("calling prefix override for " + longestPrefix);
michael@0 2235 this._overridePrefixes[longestPrefix](request, response);
michael@0 2236 }
michael@0 2237 else
michael@0 2238 {
michael@0 2239 this._handleDefault(request, response);
michael@0 2240 }
michael@0 2241 }
michael@0 2242 }
michael@0 2243 catch (e)
michael@0 2244 {
michael@0 2245 if (response.partiallySent())
michael@0 2246 {
michael@0 2247 response.abort(e);
michael@0 2248 return;
michael@0 2249 }
michael@0 2250
michael@0 2251 if (!(e instanceof HttpError))
michael@0 2252 {
michael@0 2253 dumpn("*** unexpected error: e == " + e);
michael@0 2254 throw HTTP_500;
michael@0 2255 }
michael@0 2256 if (e.code !== 404)
michael@0 2257 throw e;
michael@0 2258
michael@0 2259 dumpn("*** default: " + (path in this._defaultPaths));
michael@0 2260
michael@0 2261 response = new Response(connection);
michael@0 2262 if (path in this._defaultPaths)
michael@0 2263 this._defaultPaths[path](request, response);
michael@0 2264 else
michael@0 2265 throw HTTP_404;
michael@0 2266 }
michael@0 2267 }
michael@0 2268 catch (e)
michael@0 2269 {
michael@0 2270 if (response.partiallySent())
michael@0 2271 {
michael@0 2272 response.abort(e);
michael@0 2273 return;
michael@0 2274 }
michael@0 2275
michael@0 2276 var errorCode = "internal";
michael@0 2277
michael@0 2278 try
michael@0 2279 {
michael@0 2280 if (!(e instanceof HttpError))
michael@0 2281 throw e;
michael@0 2282
michael@0 2283 errorCode = e.code;
michael@0 2284 dumpn("*** errorCode == " + errorCode);
michael@0 2285
michael@0 2286 response = new Response(connection);
michael@0 2287 if (e.customErrorHandling)
michael@0 2288 e.customErrorHandling(response);
michael@0 2289 this._handleError(errorCode, request, response);
michael@0 2290 return;
michael@0 2291 }
michael@0 2292 catch (e2)
michael@0 2293 {
michael@0 2294 dumpn("*** error handling " + errorCode + " error: " +
michael@0 2295 "e2 == " + e2 + ", shutting down server");
michael@0 2296
michael@0 2297 connection.server._requestQuit();
michael@0 2298 response.abort(e2);
michael@0 2299 return;
michael@0 2300 }
michael@0 2301 }
michael@0 2302
michael@0 2303 response.complete();
michael@0 2304 },
michael@0 2305
michael@0 2306 //
michael@0 2307 // see nsIHttpServer.registerFile
michael@0 2308 //
michael@0 2309 registerFile: function(path, file)
michael@0 2310 {
michael@0 2311 if (!file)
michael@0 2312 {
michael@0 2313 dumpn("*** unregistering '" + path + "' mapping");
michael@0 2314 delete this._overridePaths[path];
michael@0 2315 return;
michael@0 2316 }
michael@0 2317
michael@0 2318 dumpn("*** registering '" + path + "' as mapping to " + file.path);
michael@0 2319 file = file.clone();
michael@0 2320
michael@0 2321 var self = this;
michael@0 2322 this._overridePaths[path] =
michael@0 2323 function(request, response)
michael@0 2324 {
michael@0 2325 if (!file.exists())
michael@0 2326 throw HTTP_404;
michael@0 2327
michael@0 2328 response.setStatusLine(request.httpVersion, 200, "OK");
michael@0 2329 self._writeFileResponse(request, file, response, 0, file.fileSize);
michael@0 2330 };
michael@0 2331 },
michael@0 2332
michael@0 2333 //
michael@0 2334 // see nsIHttpServer.registerPathHandler
michael@0 2335 //
michael@0 2336 registerPathHandler: function(path, handler)
michael@0 2337 {
michael@0 2338 // XXX true path validation!
michael@0 2339 if (path.charAt(0) != "/")
michael@0 2340 throw Cr.NS_ERROR_INVALID_ARG;
michael@0 2341
michael@0 2342 this._handlerToField(handler, this._overridePaths, path);
michael@0 2343 },
michael@0 2344
michael@0 2345 //
michael@0 2346 // see nsIHttpServer.registerPrefixHandler
michael@0 2347 //
michael@0 2348 registerPrefixHandler: function(prefix, handler)
michael@0 2349 {
michael@0 2350 // XXX true prefix validation!
michael@0 2351 if (!(prefix.startsWith("/") && prefix.endsWith("/")))
michael@0 2352 throw Cr.NS_ERROR_INVALID_ARG;
michael@0 2353
michael@0 2354 this._handlerToField(handler, this._overridePrefixes, prefix);
michael@0 2355 },
michael@0 2356
michael@0 2357 //
michael@0 2358 // see nsIHttpServer.registerDirectory
michael@0 2359 //
michael@0 2360 registerDirectory: function(path, directory)
michael@0 2361 {
michael@0 2362 // strip off leading and trailing '/' so that we can use lastIndexOf when
michael@0 2363 // determining exactly how a path maps onto a mapped directory --
michael@0 2364 // conditional is required here to deal with "/".substring(1, 0) being
michael@0 2365 // converted to "/".substring(0, 1) per the JS specification
michael@0 2366 var key = path.length == 1 ? "" : path.substring(1, path.length - 1);
michael@0 2367
michael@0 2368 // the path-to-directory mapping code requires that the first character not
michael@0 2369 // be "/", or it will go into an infinite loop
michael@0 2370 if (key.charAt(0) == "/")
michael@0 2371 throw Cr.NS_ERROR_INVALID_ARG;
michael@0 2372
michael@0 2373 key = toInternalPath(key, false);
michael@0 2374
michael@0 2375 if (directory)
michael@0 2376 {
michael@0 2377 dumpn("*** mapping '" + path + "' to the location " + directory.path);
michael@0 2378 this._pathDirectoryMap.put(key, directory);
michael@0 2379 }
michael@0 2380 else
michael@0 2381 {
michael@0 2382 dumpn("*** removing mapping for '" + path + "'");
michael@0 2383 this._pathDirectoryMap.put(key, null);
michael@0 2384 }
michael@0 2385 },
michael@0 2386
michael@0 2387 //
michael@0 2388 // see nsIHttpServer.registerErrorHandler
michael@0 2389 //
michael@0 2390 registerErrorHandler: function(err, handler)
michael@0 2391 {
michael@0 2392 if (!(err in HTTP_ERROR_CODES))
michael@0 2393 dumpn("*** WARNING: registering non-HTTP/1.1 error code " +
michael@0 2394 "(" + err + ") handler -- was this intentional?");
michael@0 2395
michael@0 2396 this._handlerToField(handler, this._overrideErrors, err);
michael@0 2397 },
michael@0 2398
michael@0 2399 //
michael@0 2400 // see nsIHttpServer.setIndexHandler
michael@0 2401 //
michael@0 2402 setIndexHandler: function(handler)
michael@0 2403 {
michael@0 2404 if (!handler)
michael@0 2405 handler = defaultIndexHandler;
michael@0 2406 else if (typeof(handler) != "function")
michael@0 2407 handler = createHandlerFunc(handler);
michael@0 2408
michael@0 2409 this._indexHandler = handler;
michael@0 2410 },
michael@0 2411
michael@0 2412 //
michael@0 2413 // see nsIHttpServer.registerContentType
michael@0 2414 //
michael@0 2415 registerContentType: function(ext, type)
michael@0 2416 {
michael@0 2417 if (!type)
michael@0 2418 delete this._mimeMappings[ext];
michael@0 2419 else
michael@0 2420 this._mimeMappings[ext] = headerUtils.normalizeFieldValue(type);
michael@0 2421 },
michael@0 2422
michael@0 2423 // PRIVATE API
michael@0 2424
michael@0 2425 /**
michael@0 2426 * Sets or remove (if handler is null) a handler in an object with a key.
michael@0 2427 *
michael@0 2428 * @param handler
michael@0 2429 * a handler, either function or an nsIHttpRequestHandler
michael@0 2430 * @param dict
michael@0 2431 * The object to attach the handler to.
michael@0 2432 * @param key
michael@0 2433 * The field name of the handler.
michael@0 2434 */
michael@0 2435 _handlerToField: function(handler, dict, key)
michael@0 2436 {
michael@0 2437 // for convenience, handler can be a function if this is run from xpcshell
michael@0 2438 if (typeof(handler) == "function")
michael@0 2439 dict[key] = handler;
michael@0 2440 else if (handler)
michael@0 2441 dict[key] = createHandlerFunc(handler);
michael@0 2442 else
michael@0 2443 delete dict[key];
michael@0 2444 },
michael@0 2445
michael@0 2446 /**
michael@0 2447 * Handles a request which maps to a file in the local filesystem (if a base
michael@0 2448 * path has already been set; otherwise the 404 error is thrown).
michael@0 2449 *
michael@0 2450 * @param metadata : Request
michael@0 2451 * metadata for the incoming request
michael@0 2452 * @param response : Response
michael@0 2453 * an uninitialized Response to the given request, to be initialized by a
michael@0 2454 * request handler
michael@0 2455 * @throws HTTP_###
michael@0 2456 * if an HTTP error occurred (usually HTTP_404); note that in this case the
michael@0 2457 * calling code must handle post-processing of the response
michael@0 2458 */
michael@0 2459 _handleDefault: function(metadata, response)
michael@0 2460 {
michael@0 2461 dumpn("*** _handleDefault()");
michael@0 2462
michael@0 2463 response.setStatusLine(metadata.httpVersion, 200, "OK");
michael@0 2464
michael@0 2465 var path = metadata.path;
michael@0 2466 NS_ASSERT(path.charAt(0) == "/", "invalid path: <" + path + ">");
michael@0 2467
michael@0 2468 // determine the actual on-disk file; this requires finding the deepest
michael@0 2469 // path-to-directory mapping in the requested URL
michael@0 2470 var file = this._getFileForPath(path);
michael@0 2471
michael@0 2472 // the "file" might be a directory, in which case we either serve the
michael@0 2473 // contained index.html or make the index handler write the response
michael@0 2474 if (file.exists() && file.isDirectory())
michael@0 2475 {
michael@0 2476 file.append("index.html"); // make configurable?
michael@0 2477 if (!file.exists() || file.isDirectory())
michael@0 2478 {
michael@0 2479 metadata._ensurePropertyBag();
michael@0 2480 metadata._bag.setPropertyAsInterface("directory", file.parent);
michael@0 2481 this._indexHandler(metadata, response);
michael@0 2482 return;
michael@0 2483 }
michael@0 2484 }
michael@0 2485
michael@0 2486 // alternately, the file might not exist
michael@0 2487 if (!file.exists())
michael@0 2488 throw HTTP_404;
michael@0 2489
michael@0 2490 var start, end;
michael@0 2491 if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1) &&
michael@0 2492 metadata.hasHeader("Range") &&
michael@0 2493 this._getTypeFromFile(file) !== SJS_TYPE)
michael@0 2494 {
michael@0 2495 var rangeMatch = metadata.getHeader("Range").match(/^bytes=(\d+)?-(\d+)?$/);
michael@0 2496 if (!rangeMatch)
michael@0 2497 throw HTTP_400;
michael@0 2498
michael@0 2499 if (rangeMatch[1] !== undefined)
michael@0 2500 start = parseInt(rangeMatch[1], 10);
michael@0 2501
michael@0 2502 if (rangeMatch[2] !== undefined)
michael@0 2503 end = parseInt(rangeMatch[2], 10);
michael@0 2504
michael@0 2505 if (start === undefined && end === undefined)
michael@0 2506 throw HTTP_400;
michael@0 2507
michael@0 2508 // No start given, so the end is really the count of bytes from the
michael@0 2509 // end of the file.
michael@0 2510 if (start === undefined)
michael@0 2511 {
michael@0 2512 start = Math.max(0, file.fileSize - end);
michael@0 2513 end = file.fileSize - 1;
michael@0 2514 }
michael@0 2515
michael@0 2516 // start and end are inclusive
michael@0 2517 if (end === undefined || end >= file.fileSize)
michael@0 2518 end = file.fileSize - 1;
michael@0 2519
michael@0 2520 if (start !== undefined && start >= file.fileSize) {
michael@0 2521 var HTTP_416 = new HttpError(416, "Requested Range Not Satisfiable");
michael@0 2522 HTTP_416.customErrorHandling = function(errorResponse)
michael@0 2523 {
michael@0 2524 maybeAddHeaders(file, metadata, errorResponse);
michael@0 2525 };
michael@0 2526 throw HTTP_416;
michael@0 2527 }
michael@0 2528
michael@0 2529 if (end < start)
michael@0 2530 {
michael@0 2531 response.setStatusLine(metadata.httpVersion, 200, "OK");
michael@0 2532 start = 0;
michael@0 2533 end = file.fileSize - 1;
michael@0 2534 }
michael@0 2535 else
michael@0 2536 {
michael@0 2537 response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
michael@0 2538 var contentRange = "bytes " + start + "-" + end + "/" + file.fileSize;
michael@0 2539 response.setHeader("Content-Range", contentRange);
michael@0 2540 }
michael@0 2541 }
michael@0 2542 else
michael@0 2543 {
michael@0 2544 start = 0;
michael@0 2545 end = file.fileSize - 1;
michael@0 2546 }
michael@0 2547
michael@0 2548 // finally...
michael@0 2549 dumpn("*** handling '" + path + "' as mapping to " + file.path + " from " +
michael@0 2550 start + " to " + end + " inclusive");
michael@0 2551 this._writeFileResponse(metadata, file, response, start, end - start + 1);
michael@0 2552 },
michael@0 2553
michael@0 2554 /**
michael@0 2555 * Writes an HTTP response for the given file, including setting headers for
michael@0 2556 * file metadata.
michael@0 2557 *
michael@0 2558 * @param metadata : Request
michael@0 2559 * the Request for which a response is being generated
michael@0 2560 * @param file : nsILocalFile
michael@0 2561 * the file which is to be sent in the response
michael@0 2562 * @param response : Response
michael@0 2563 * the response to which the file should be written
michael@0 2564 * @param offset: uint
michael@0 2565 * the byte offset to skip to when writing
michael@0 2566 * @param count: uint
michael@0 2567 * the number of bytes to write
michael@0 2568 */
michael@0 2569 _writeFileResponse: function(metadata, file, response, offset, count)
michael@0 2570 {
michael@0 2571 const PR_RDONLY = 0x01;
michael@0 2572
michael@0 2573 var type = this._getTypeFromFile(file);
michael@0 2574 if (type === SJS_TYPE)
michael@0 2575 {
michael@0 2576 var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8),
michael@0 2577 Ci.nsIFileInputStream.CLOSE_ON_EOF);
michael@0 2578
michael@0 2579 try
michael@0 2580 {
michael@0 2581 var sis = new ScriptableInputStream(fis);
michael@0 2582 var s = Cu.Sandbox(gGlobalObject);
michael@0 2583 s.importFunction(dump, "dump");
michael@0 2584
michael@0 2585 // Define a basic key-value state-preservation API across requests, with
michael@0 2586 // keys initially corresponding to the empty string.
michael@0 2587 var self = this;
michael@0 2588 var path = metadata.path;
michael@0 2589 s.importFunction(function getState(k)
michael@0 2590 {
michael@0 2591 return self._getState(path, k);
michael@0 2592 });
michael@0 2593 s.importFunction(function setState(k, v)
michael@0 2594 {
michael@0 2595 self._setState(path, k, v);
michael@0 2596 });
michael@0 2597 s.importFunction(function getSharedState(k)
michael@0 2598 {
michael@0 2599 return self._getSharedState(k);
michael@0 2600 });
michael@0 2601 s.importFunction(function setSharedState(k, v)
michael@0 2602 {
michael@0 2603 self._setSharedState(k, v);
michael@0 2604 });
michael@0 2605 s.importFunction(function getObjectState(k, callback)
michael@0 2606 {
michael@0 2607 callback(self._getObjectState(k));
michael@0 2608 });
michael@0 2609 s.importFunction(function setObjectState(k, v)
michael@0 2610 {
michael@0 2611 self._setObjectState(k, v);
michael@0 2612 });
michael@0 2613 s.importFunction(function registerPathHandler(p, h)
michael@0 2614 {
michael@0 2615 self.registerPathHandler(p, h);
michael@0 2616 });
michael@0 2617
michael@0 2618 // Make it possible for sjs files to access their location
michael@0 2619 this._setState(path, "__LOCATION__", file.path);
michael@0 2620
michael@0 2621 try
michael@0 2622 {
michael@0 2623 // Alas, the line number in errors dumped to console when calling the
michael@0 2624 // request handler is simply an offset from where we load the SJS file.
michael@0 2625 // Work around this in a reasonably non-fragile way by dynamically
michael@0 2626 // getting the line number where we evaluate the SJS file. Don't
michael@0 2627 // separate these two lines!
michael@0 2628 var line = new Error().lineNumber;
michael@0 2629 Cu.evalInSandbox(sis.read(file.fileSize), s);
michael@0 2630 }
michael@0 2631 catch (e)
michael@0 2632 {
michael@0 2633 dumpn("*** syntax error in SJS at " + file.path + ": " + e);
michael@0 2634 throw HTTP_500;
michael@0 2635 }
michael@0 2636
michael@0 2637 try
michael@0 2638 {
michael@0 2639 s.handleRequest(metadata, response);
michael@0 2640 }
michael@0 2641 catch (e)
michael@0 2642 {
michael@0 2643 dump("*** error running SJS at " + file.path + ": " +
michael@0 2644 e + " on line " +
michael@0 2645 (e instanceof Error
michael@0 2646 ? e.lineNumber + " in httpd.js"
michael@0 2647 : (e.lineNumber - line)) + "\n");
michael@0 2648 throw HTTP_500;
michael@0 2649 }
michael@0 2650 }
michael@0 2651 finally
michael@0 2652 {
michael@0 2653 fis.close();
michael@0 2654 }
michael@0 2655 }
michael@0 2656 else
michael@0 2657 {
michael@0 2658 try
michael@0 2659 {
michael@0 2660 response.setHeader("Last-Modified",
michael@0 2661 toDateString(file.lastModifiedTime),
michael@0 2662 false);
michael@0 2663 }
michael@0 2664 catch (e) { /* lastModifiedTime threw, ignore */ }
michael@0 2665
michael@0 2666 response.setHeader("Content-Type", type, false);
michael@0 2667 maybeAddHeaders(file, metadata, response);
michael@0 2668 response.setHeader("Content-Length", "" + count, false);
michael@0 2669
michael@0 2670 var fis = new FileInputStream(file, PR_RDONLY, parseInt("444", 8),
michael@0 2671 Ci.nsIFileInputStream.CLOSE_ON_EOF);
michael@0 2672
michael@0 2673 offset = offset || 0;
michael@0 2674 count = count || file.fileSize;
michael@0 2675 NS_ASSERT(offset === 0 || offset < file.fileSize, "bad offset");
michael@0 2676 NS_ASSERT(count >= 0, "bad count");
michael@0 2677 NS_ASSERT(offset + count <= file.fileSize, "bad total data size");
michael@0 2678
michael@0 2679 try
michael@0 2680 {
michael@0 2681 if (offset !== 0)
michael@0 2682 {
michael@0 2683 // Seek (or read, if seeking isn't supported) to the correct offset so
michael@0 2684 // the data sent to the client matches the requested range.
michael@0 2685 if (fis instanceof Ci.nsISeekableStream)
michael@0 2686 fis.seek(Ci.nsISeekableStream.NS_SEEK_SET, offset);
michael@0 2687 else
michael@0 2688 new ScriptableInputStream(fis).read(offset);
michael@0 2689 }
michael@0 2690 }
michael@0 2691 catch (e)
michael@0 2692 {
michael@0 2693 fis.close();
michael@0 2694 throw e;
michael@0 2695 }
michael@0 2696
michael@0 2697 let writeMore = function writeMore()
michael@0 2698 {
michael@0 2699 gThreadManager.currentThread
michael@0 2700 .dispatch(writeData, Ci.nsIThread.DISPATCH_NORMAL);
michael@0 2701 }
michael@0 2702
michael@0 2703 var input = new BinaryInputStream(fis);
michael@0 2704 var output = new BinaryOutputStream(response.bodyOutputStream);
michael@0 2705 var writeData =
michael@0 2706 {
michael@0 2707 run: function()
michael@0 2708 {
michael@0 2709 var chunkSize = Math.min(65536, count);
michael@0 2710 count -= chunkSize;
michael@0 2711 NS_ASSERT(count >= 0, "underflow");
michael@0 2712
michael@0 2713 try
michael@0 2714 {
michael@0 2715 var data = input.readByteArray(chunkSize);
michael@0 2716 NS_ASSERT(data.length === chunkSize,
michael@0 2717 "incorrect data returned? got " + data.length +
michael@0 2718 ", expected " + chunkSize);
michael@0 2719 output.writeByteArray(data, data.length);
michael@0 2720 if (count === 0)
michael@0 2721 {
michael@0 2722 fis.close();
michael@0 2723 response.finish();
michael@0 2724 }
michael@0 2725 else
michael@0 2726 {
michael@0 2727 writeMore();
michael@0 2728 }
michael@0 2729 }
michael@0 2730 catch (e)
michael@0 2731 {
michael@0 2732 try
michael@0 2733 {
michael@0 2734 fis.close();
michael@0 2735 }
michael@0 2736 finally
michael@0 2737 {
michael@0 2738 response.finish();
michael@0 2739 }
michael@0 2740 throw e;
michael@0 2741 }
michael@0 2742 }
michael@0 2743 };
michael@0 2744
michael@0 2745 writeMore();
michael@0 2746
michael@0 2747 // Now that we know copying will start, flag the response as async.
michael@0 2748 response.processAsync();
michael@0 2749 }
michael@0 2750 },
michael@0 2751
michael@0 2752 /**
michael@0 2753 * Get the value corresponding to a given key for the given path for SJS state
michael@0 2754 * preservation across requests.
michael@0 2755 *
michael@0 2756 * @param path : string
michael@0 2757 * the path from which the given state is to be retrieved
michael@0 2758 * @param k : string
michael@0 2759 * the key whose corresponding value is to be returned
michael@0 2760 * @returns string
michael@0 2761 * the corresponding value, which is initially the empty string
michael@0 2762 */
michael@0 2763 _getState: function(path, k)
michael@0 2764 {
michael@0 2765 var state = this._state;
michael@0 2766 if (path in state && k in state[path])
michael@0 2767 return state[path][k];
michael@0 2768 return "";
michael@0 2769 },
michael@0 2770
michael@0 2771 /**
michael@0 2772 * Set the value corresponding to a given key for the given path for SJS state
michael@0 2773 * preservation across requests.
michael@0 2774 *
michael@0 2775 * @param path : string
michael@0 2776 * the path from which the given state is to be retrieved
michael@0 2777 * @param k : string
michael@0 2778 * the key whose corresponding value is to be set
michael@0 2779 * @param v : string
michael@0 2780 * the value to be set
michael@0 2781 */
michael@0 2782 _setState: function(path, k, v)
michael@0 2783 {
michael@0 2784 if (typeof v !== "string")
michael@0 2785 throw new Error("non-string value passed");
michael@0 2786 var state = this._state;
michael@0 2787 if (!(path in state))
michael@0 2788 state[path] = {};
michael@0 2789 state[path][k] = v;
michael@0 2790 },
michael@0 2791
michael@0 2792 /**
michael@0 2793 * Get the value corresponding to a given key for SJS state preservation
michael@0 2794 * across requests.
michael@0 2795 *
michael@0 2796 * @param k : string
michael@0 2797 * the key whose corresponding value is to be returned
michael@0 2798 * @returns string
michael@0 2799 * the corresponding value, which is initially the empty string
michael@0 2800 */
michael@0 2801 _getSharedState: function(k)
michael@0 2802 {
michael@0 2803 var state = this._sharedState;
michael@0 2804 if (k in state)
michael@0 2805 return state[k];
michael@0 2806 return "";
michael@0 2807 },
michael@0 2808
michael@0 2809 /**
michael@0 2810 * Set the value corresponding to a given key for SJS state preservation
michael@0 2811 * across requests.
michael@0 2812 *
michael@0 2813 * @param k : string
michael@0 2814 * the key whose corresponding value is to be set
michael@0 2815 * @param v : string
michael@0 2816 * the value to be set
michael@0 2817 */
michael@0 2818 _setSharedState: function(k, v)
michael@0 2819 {
michael@0 2820 if (typeof v !== "string")
michael@0 2821 throw new Error("non-string value passed");
michael@0 2822 this._sharedState[k] = v;
michael@0 2823 },
michael@0 2824
michael@0 2825 /**
michael@0 2826 * Returns the object associated with the given key in the server for SJS
michael@0 2827 * state preservation across requests.
michael@0 2828 *
michael@0 2829 * @param k : string
michael@0 2830 * the key whose corresponding object is to be returned
michael@0 2831 * @returns nsISupports
michael@0 2832 * the corresponding object, or null if none was present
michael@0 2833 */
michael@0 2834 _getObjectState: function(k)
michael@0 2835 {
michael@0 2836 if (typeof k !== "string")
michael@0 2837 throw new Error("non-string key passed");
michael@0 2838 return this._objectState[k] || null;
michael@0 2839 },
michael@0 2840
michael@0 2841 /**
michael@0 2842 * Sets the object associated with the given key in the server for SJS
michael@0 2843 * state preservation across requests.
michael@0 2844 *
michael@0 2845 * @param k : string
michael@0 2846 * the key whose corresponding object is to be set
michael@0 2847 * @param v : nsISupports
michael@0 2848 * the object to be associated with the given key; may be null
michael@0 2849 */
michael@0 2850 _setObjectState: function(k, v)
michael@0 2851 {
michael@0 2852 if (typeof k !== "string")
michael@0 2853 throw new Error("non-string key passed");
michael@0 2854 if (typeof v !== "object")
michael@0 2855 throw new Error("non-object value passed");
michael@0 2856 if (v && !("QueryInterface" in v))
michael@0 2857 {
michael@0 2858 throw new Error("must pass an nsISupports; use wrappedJSObject to ease " +
michael@0 2859 "pain when using the server from JS");
michael@0 2860 }
michael@0 2861
michael@0 2862 this._objectState[k] = v;
michael@0 2863 },
michael@0 2864
michael@0 2865 /**
michael@0 2866 * Gets a content-type for the given file, first by checking for any custom
michael@0 2867 * MIME-types registered with this handler for the file's extension, second by
michael@0 2868 * asking the global MIME service for a content-type, and finally by failing
michael@0 2869 * over to application/octet-stream.
michael@0 2870 *
michael@0 2871 * @param file : nsIFile
michael@0 2872 * the nsIFile for which to get a file type
michael@0 2873 * @returns string
michael@0 2874 * the best content-type which can be determined for the file
michael@0 2875 */
michael@0 2876 _getTypeFromFile: function(file)
michael@0 2877 {
michael@0 2878 try
michael@0 2879 {
michael@0 2880 var name = file.leafName;
michael@0 2881 var dot = name.lastIndexOf(".");
michael@0 2882 if (dot > 0)
michael@0 2883 {
michael@0 2884 var ext = name.slice(dot + 1);
michael@0 2885 if (ext in this._mimeMappings)
michael@0 2886 return this._mimeMappings[ext];
michael@0 2887 }
michael@0 2888 return Cc["@mozilla.org/uriloader/external-helper-app-service;1"]
michael@0 2889 .getService(Ci.nsIMIMEService)
michael@0 2890 .getTypeFromFile(file);
michael@0 2891 }
michael@0 2892 catch (e)
michael@0 2893 {
michael@0 2894 return "application/octet-stream";
michael@0 2895 }
michael@0 2896 },
michael@0 2897
michael@0 2898 /**
michael@0 2899 * Returns the nsILocalFile which corresponds to the path, as determined using
michael@0 2900 * all registered path->directory mappings and any paths which are explicitly
michael@0 2901 * overridden.
michael@0 2902 *
michael@0 2903 * @param path : string
michael@0 2904 * the server path for which a file should be retrieved, e.g. "/foo/bar"
michael@0 2905 * @throws HttpError
michael@0 2906 * when the correct action is the corresponding HTTP error (i.e., because no
michael@0 2907 * mapping was found for a directory in path, the referenced file doesn't
michael@0 2908 * exist, etc.)
michael@0 2909 * @returns nsILocalFile
michael@0 2910 * the file to be sent as the response to a request for the path
michael@0 2911 */
michael@0 2912 _getFileForPath: function(path)
michael@0 2913 {
michael@0 2914 // decode and add underscores as necessary
michael@0 2915 try
michael@0 2916 {
michael@0 2917 path = toInternalPath(path, true);
michael@0 2918 }
michael@0 2919 catch (e)
michael@0 2920 {
michael@0 2921 throw HTTP_400; // malformed path
michael@0 2922 }
michael@0 2923
michael@0 2924 // next, get the directory which contains this path
michael@0 2925 var pathMap = this._pathDirectoryMap;
michael@0 2926
michael@0 2927 // An example progression of tmp for a path "/foo/bar/baz/" might be:
michael@0 2928 // "foo/bar/baz/", "foo/bar/baz", "foo/bar", "foo", ""
michael@0 2929 var tmp = path.substring(1);
michael@0 2930 while (true)
michael@0 2931 {
michael@0 2932 // do we have a match for current head of the path?
michael@0 2933 var file = pathMap.get(tmp);
michael@0 2934 if (file)
michael@0 2935 {
michael@0 2936 // XXX hack; basically disable showing mapping for /foo/bar/ when the
michael@0 2937 // requested path was /foo/bar, because relative links on the page
michael@0 2938 // will all be incorrect -- we really need the ability to easily
michael@0 2939 // redirect here instead
michael@0 2940 if (tmp == path.substring(1) &&
michael@0 2941 tmp.length != 0 &&
michael@0 2942 tmp.charAt(tmp.length - 1) != "/")
michael@0 2943 file = null;
michael@0 2944 else
michael@0 2945 break;
michael@0 2946 }
michael@0 2947
michael@0 2948 // if we've finished trying all prefixes, exit
michael@0 2949 if (tmp == "")
michael@0 2950 break;
michael@0 2951
michael@0 2952 tmp = tmp.substring(0, tmp.lastIndexOf("/"));
michael@0 2953 }
michael@0 2954
michael@0 2955 // no mapping applies, so 404
michael@0 2956 if (!file)
michael@0 2957 throw HTTP_404;
michael@0 2958
michael@0 2959
michael@0 2960 // last, get the file for the path within the determined directory
michael@0 2961 var parentFolder = file.parent;
michael@0 2962 var dirIsRoot = (parentFolder == null);
michael@0 2963
michael@0 2964 // Strategy here is to append components individually, making sure we
michael@0 2965 // never move above the given directory; this allows paths such as
michael@0 2966 // "<file>/foo/../bar" but prevents paths such as "<file>/../base-sibling";
michael@0 2967 // this component-wise approach also means the code works even on platforms
michael@0 2968 // which don't use "/" as the directory separator, such as Windows
michael@0 2969 var leafPath = path.substring(tmp.length + 1);
michael@0 2970 var comps = leafPath.split("/");
michael@0 2971 for (var i = 0, sz = comps.length; i < sz; i++)
michael@0 2972 {
michael@0 2973 var comp = comps[i];
michael@0 2974
michael@0 2975 if (comp == "..")
michael@0 2976 file = file.parent;
michael@0 2977 else if (comp == "." || comp == "")
michael@0 2978 continue;
michael@0 2979 else
michael@0 2980 file.append(comp);
michael@0 2981
michael@0 2982 if (!dirIsRoot && file.equals(parentFolder))
michael@0 2983 throw HTTP_403;
michael@0 2984 }
michael@0 2985
michael@0 2986 return file;
michael@0 2987 },
michael@0 2988
michael@0 2989 /**
michael@0 2990 * Writes the error page for the given HTTP error code over the given
michael@0 2991 * connection.
michael@0 2992 *
michael@0 2993 * @param errorCode : uint
michael@0 2994 * the HTTP error code to be used
michael@0 2995 * @param connection : Connection
michael@0 2996 * the connection on which the error occurred
michael@0 2997 */
michael@0 2998 handleError: function(errorCode, connection)
michael@0 2999 {
michael@0 3000 var response = new Response(connection);
michael@0 3001
michael@0 3002 dumpn("*** error in request: " + errorCode);
michael@0 3003
michael@0 3004 this._handleError(errorCode, new Request(connection.port), response);
michael@0 3005 },
michael@0 3006
michael@0 3007 /**
michael@0 3008 * Handles a request which generates the given error code, using the
michael@0 3009 * user-defined error handler if one has been set, gracefully falling back to
michael@0 3010 * the x00 status code if the code has no handler, and failing to status code
michael@0 3011 * 500 if all else fails.
michael@0 3012 *
michael@0 3013 * @param errorCode : uint
michael@0 3014 * the HTTP error which is to be returned
michael@0 3015 * @param metadata : Request
michael@0 3016 * metadata for the request, which will often be incomplete since this is an
michael@0 3017 * error
michael@0 3018 * @param response : Response
michael@0 3019 * an uninitialized Response should be initialized when this method
michael@0 3020 * completes with information which represents the desired error code in the
michael@0 3021 * ideal case or a fallback code in abnormal circumstances (i.e., 500 is a
michael@0 3022 * fallback for 505, per HTTP specs)
michael@0 3023 */
michael@0 3024 _handleError: function(errorCode, metadata, response)
michael@0 3025 {
michael@0 3026 if (!metadata)
michael@0 3027 throw Cr.NS_ERROR_NULL_POINTER;
michael@0 3028
michael@0 3029 var errorX00 = errorCode - (errorCode % 100);
michael@0 3030
michael@0 3031 try
michael@0 3032 {
michael@0 3033 if (!(errorCode in HTTP_ERROR_CODES))
michael@0 3034 dumpn("*** WARNING: requested invalid error: " + errorCode);
michael@0 3035
michael@0 3036 // RFC 2616 says that we should try to handle an error by its class if we
michael@0 3037 // can't otherwise handle it -- if that fails, we revert to handling it as
michael@0 3038 // a 500 internal server error, and if that fails we throw and shut down
michael@0 3039 // the server
michael@0 3040
michael@0 3041 // actually handle the error
michael@0 3042 try
michael@0 3043 {
michael@0 3044 if (errorCode in this._overrideErrors)
michael@0 3045 this._overrideErrors[errorCode](metadata, response);
michael@0 3046 else
michael@0 3047 this._defaultErrors[errorCode](metadata, response);
michael@0 3048 }
michael@0 3049 catch (e)
michael@0 3050 {
michael@0 3051 if (response.partiallySent())
michael@0 3052 {
michael@0 3053 response.abort(e);
michael@0 3054 return;
michael@0 3055 }
michael@0 3056
michael@0 3057 // don't retry the handler that threw
michael@0 3058 if (errorX00 == errorCode)
michael@0 3059 throw HTTP_500;
michael@0 3060
michael@0 3061 dumpn("*** error in handling for error code " + errorCode + ", " +
michael@0 3062 "falling back to " + errorX00 + "...");
michael@0 3063 response = new Response(response._connection);
michael@0 3064 if (errorX00 in this._overrideErrors)
michael@0 3065 this._overrideErrors[errorX00](metadata, response);
michael@0 3066 else if (errorX00 in this._defaultErrors)
michael@0 3067 this._defaultErrors[errorX00](metadata, response);
michael@0 3068 else
michael@0 3069 throw HTTP_500;
michael@0 3070 }
michael@0 3071 }
michael@0 3072 catch (e)
michael@0 3073 {
michael@0 3074 if (response.partiallySent())
michael@0 3075 {
michael@0 3076 response.abort();
michael@0 3077 return;
michael@0 3078 }
michael@0 3079
michael@0 3080 // we've tried everything possible for a meaningful error -- now try 500
michael@0 3081 dumpn("*** error in handling for error code " + errorX00 + ", falling " +
michael@0 3082 "back to 500...");
michael@0 3083
michael@0 3084 try
michael@0 3085 {
michael@0 3086 response = new Response(response._connection);
michael@0 3087 if (500 in this._overrideErrors)
michael@0 3088 this._overrideErrors[500](metadata, response);
michael@0 3089 else
michael@0 3090 this._defaultErrors[500](metadata, response);
michael@0 3091 }
michael@0 3092 catch (e2)
michael@0 3093 {
michael@0 3094 dumpn("*** multiple errors in default error handlers!");
michael@0 3095 dumpn("*** e == " + e + ", e2 == " + e2);
michael@0 3096 response.abort(e2);
michael@0 3097 return;
michael@0 3098 }
michael@0 3099 }
michael@0 3100
michael@0 3101 response.complete();
michael@0 3102 },
michael@0 3103
michael@0 3104 // FIELDS
michael@0 3105
michael@0 3106 /**
michael@0 3107 * This object contains the default handlers for the various HTTP error codes.
michael@0 3108 */
michael@0 3109 _defaultErrors:
michael@0 3110 {
michael@0 3111 400: function(metadata, response)
michael@0 3112 {
michael@0 3113 // none of the data in metadata is reliable, so hard-code everything here
michael@0 3114 response.setStatusLine("1.1", 400, "Bad Request");
michael@0 3115 response.setHeader("Content-Type", "text/plain", false);
michael@0 3116
michael@0 3117 var body = "Bad request\n";
michael@0 3118 response.bodyOutputStream.write(body, body.length);
michael@0 3119 },
michael@0 3120 403: function(metadata, response)
michael@0 3121 {
michael@0 3122 response.setStatusLine(metadata.httpVersion, 403, "Forbidden");
michael@0 3123 response.setHeader("Content-Type", "text/html", false);
michael@0 3124
michael@0 3125 var body = "<html>\
michael@0 3126 <head><title>403 Forbidden</title></head>\
michael@0 3127 <body>\
michael@0 3128 <h1>403 Forbidden</h1>\
michael@0 3129 </body>\
michael@0 3130 </html>";
michael@0 3131 response.bodyOutputStream.write(body, body.length);
michael@0 3132 },
michael@0 3133 404: function(metadata, response)
michael@0 3134 {
michael@0 3135 response.setStatusLine(metadata.httpVersion, 404, "Not Found");
michael@0 3136 response.setHeader("Content-Type", "text/html", false);
michael@0 3137
michael@0 3138 var body = "<html>\
michael@0 3139 <head><title>404 Not Found</title></head>\
michael@0 3140 <body>\
michael@0 3141 <h1>404 Not Found</h1>\
michael@0 3142 <p>\
michael@0 3143 <span style='font-family: monospace;'>" +
michael@0 3144 htmlEscape(metadata.path) +
michael@0 3145 "</span> was not found.\
michael@0 3146 </p>\
michael@0 3147 </body>\
michael@0 3148 </html>";
michael@0 3149 response.bodyOutputStream.write(body, body.length);
michael@0 3150 },
michael@0 3151 416: function(metadata, response)
michael@0 3152 {
michael@0 3153 response.setStatusLine(metadata.httpVersion,
michael@0 3154 416,
michael@0 3155 "Requested Range Not Satisfiable");
michael@0 3156 response.setHeader("Content-Type", "text/html", false);
michael@0 3157
michael@0 3158 var body = "<html>\
michael@0 3159 <head>\
michael@0 3160 <title>416 Requested Range Not Satisfiable</title></head>\
michael@0 3161 <body>\
michael@0 3162 <h1>416 Requested Range Not Satisfiable</h1>\
michael@0 3163 <p>The byte range was not valid for the\
michael@0 3164 requested resource.\
michael@0 3165 </p>\
michael@0 3166 </body>\
michael@0 3167 </html>";
michael@0 3168 response.bodyOutputStream.write(body, body.length);
michael@0 3169 },
michael@0 3170 500: function(metadata, response)
michael@0 3171 {
michael@0 3172 response.setStatusLine(metadata.httpVersion,
michael@0 3173 500,
michael@0 3174 "Internal Server Error");
michael@0 3175 response.setHeader("Content-Type", "text/html", false);
michael@0 3176
michael@0 3177 var body = "<html>\
michael@0 3178 <head><title>500 Internal Server Error</title></head>\
michael@0 3179 <body>\
michael@0 3180 <h1>500 Internal Server Error</h1>\
michael@0 3181 <p>Something's broken in this server and\
michael@0 3182 needs to be fixed.</p>\
michael@0 3183 </body>\
michael@0 3184 </html>";
michael@0 3185 response.bodyOutputStream.write(body, body.length);
michael@0 3186 },
michael@0 3187 501: function(metadata, response)
michael@0 3188 {
michael@0 3189 response.setStatusLine(metadata.httpVersion, 501, "Not Implemented");
michael@0 3190 response.setHeader("Content-Type", "text/html", false);
michael@0 3191
michael@0 3192 var body = "<html>\
michael@0 3193 <head><title>501 Not Implemented</title></head>\
michael@0 3194 <body>\
michael@0 3195 <h1>501 Not Implemented</h1>\
michael@0 3196 <p>This server is not (yet) Apache.</p>\
michael@0 3197 </body>\
michael@0 3198 </html>";
michael@0 3199 response.bodyOutputStream.write(body, body.length);
michael@0 3200 },
michael@0 3201 505: function(metadata, response)
michael@0 3202 {
michael@0 3203 response.setStatusLine("1.1", 505, "HTTP Version Not Supported");
michael@0 3204 response.setHeader("Content-Type", "text/html", false);
michael@0 3205
michael@0 3206 var body = "<html>\
michael@0 3207 <head><title>505 HTTP Version Not Supported</title></head>\
michael@0 3208 <body>\
michael@0 3209 <h1>505 HTTP Version Not Supported</h1>\
michael@0 3210 <p>This server only supports HTTP/1.0 and HTTP/1.1\
michael@0 3211 connections.</p>\
michael@0 3212 </body>\
michael@0 3213 </html>";
michael@0 3214 response.bodyOutputStream.write(body, body.length);
michael@0 3215 }
michael@0 3216 },
michael@0 3217
michael@0 3218 /**
michael@0 3219 * Contains handlers for the default set of URIs contained in this server.
michael@0 3220 */
michael@0 3221 _defaultPaths:
michael@0 3222 {
michael@0 3223 "/": function(metadata, response)
michael@0 3224 {
michael@0 3225 response.setStatusLine(metadata.httpVersion, 200, "OK");
michael@0 3226 response.setHeader("Content-Type", "text/html", false);
michael@0 3227
michael@0 3228 var body = "<html>\
michael@0 3229 <head><title>httpd.js</title></head>\
michael@0 3230 <body>\
michael@0 3231 <h1>httpd.js</h1>\
michael@0 3232 <p>If you're seeing this page, httpd.js is up and\
michael@0 3233 serving requests! Now set a base path and serve some\
michael@0 3234 files!</p>\
michael@0 3235 </body>\
michael@0 3236 </html>";
michael@0 3237
michael@0 3238 response.bodyOutputStream.write(body, body.length);
michael@0 3239 },
michael@0 3240
michael@0 3241 "/trace": function(metadata, response)
michael@0 3242 {
michael@0 3243 response.setStatusLine(metadata.httpVersion, 200, "OK");
michael@0 3244 response.setHeader("Content-Type", "text/plain", false);
michael@0 3245
michael@0 3246 var body = "Request-URI: " +
michael@0 3247 metadata.scheme + "://" + metadata.host + ":" + metadata.port +
michael@0 3248 metadata.path + "\n\n";
michael@0 3249 body += "Request (semantically equivalent, slightly reformatted):\n\n";
michael@0 3250 body += metadata.method + " " + metadata.path;
michael@0 3251
michael@0 3252 if (metadata.queryString)
michael@0 3253 body += "?" + metadata.queryString;
michael@0 3254
michael@0 3255 body += " HTTP/" + metadata.httpVersion + "\r\n";
michael@0 3256
michael@0 3257 var headEnum = metadata.headers;
michael@0 3258 while (headEnum.hasMoreElements())
michael@0 3259 {
michael@0 3260 var fieldName = headEnum.getNext()
michael@0 3261 .QueryInterface(Ci.nsISupportsString)
michael@0 3262 .data;
michael@0 3263 body += fieldName + ": " + metadata.getHeader(fieldName) + "\r\n";
michael@0 3264 }
michael@0 3265
michael@0 3266 response.bodyOutputStream.write(body, body.length);
michael@0 3267 }
michael@0 3268 }
michael@0 3269 };
michael@0 3270
michael@0 3271
michael@0 3272 /**
michael@0 3273 * Maps absolute paths to files on the local file system (as nsILocalFiles).
michael@0 3274 */
michael@0 3275 function FileMap()
michael@0 3276 {
michael@0 3277 /** Hash which will map paths to nsILocalFiles. */
michael@0 3278 this._map = {};
michael@0 3279 }
michael@0 3280 FileMap.prototype =
michael@0 3281 {
michael@0 3282 // PUBLIC API
michael@0 3283
michael@0 3284 /**
michael@0 3285 * Maps key to a clone of the nsILocalFile value if value is non-null;
michael@0 3286 * otherwise, removes any extant mapping for key.
michael@0 3287 *
michael@0 3288 * @param key : string
michael@0 3289 * string to which a clone of value is mapped
michael@0 3290 * @param value : nsILocalFile
michael@0 3291 * the file to map to key, or null to remove a mapping
michael@0 3292 */
michael@0 3293 put: function(key, value)
michael@0 3294 {
michael@0 3295 if (value)
michael@0 3296 this._map[key] = value.clone();
michael@0 3297 else
michael@0 3298 delete this._map[key];
michael@0 3299 },
michael@0 3300
michael@0 3301 /**
michael@0 3302 * Returns a clone of the nsILocalFile mapped to key, or null if no such
michael@0 3303 * mapping exists.
michael@0 3304 *
michael@0 3305 * @param key : string
michael@0 3306 * key to which the returned file maps
michael@0 3307 * @returns nsILocalFile
michael@0 3308 * a clone of the mapped file, or null if no mapping exists
michael@0 3309 */
michael@0 3310 get: function(key)
michael@0 3311 {
michael@0 3312 var val = this._map[key];
michael@0 3313 return val ? val.clone() : null;
michael@0 3314 }
michael@0 3315 };
michael@0 3316
michael@0 3317
michael@0 3318 // Response CONSTANTS
michael@0 3319
michael@0 3320 // token = *<any CHAR except CTLs or separators>
michael@0 3321 // CHAR = <any US-ASCII character (0-127)>
michael@0 3322 // CTL = <any US-ASCII control character (0-31) and DEL (127)>
michael@0 3323 // separators = "(" | ")" | "<" | ">" | "@"
michael@0 3324 // | "," | ";" | ":" | "\" | <">
michael@0 3325 // | "/" | "[" | "]" | "?" | "="
michael@0 3326 // | "{" | "}" | SP | HT
michael@0 3327 const IS_TOKEN_ARRAY =
michael@0 3328 [0, 0, 0, 0, 0, 0, 0, 0, // 0
michael@0 3329 0, 0, 0, 0, 0, 0, 0, 0, // 8
michael@0 3330 0, 0, 0, 0, 0, 0, 0, 0, // 16
michael@0 3331 0, 0, 0, 0, 0, 0, 0, 0, // 24
michael@0 3332
michael@0 3333 0, 1, 0, 1, 1, 1, 1, 1, // 32
michael@0 3334 0, 0, 1, 1, 0, 1, 1, 0, // 40
michael@0 3335 1, 1, 1, 1, 1, 1, 1, 1, // 48
michael@0 3336 1, 1, 0, 0, 0, 0, 0, 0, // 56
michael@0 3337
michael@0 3338 0, 1, 1, 1, 1, 1, 1, 1, // 64
michael@0 3339 1, 1, 1, 1, 1, 1, 1, 1, // 72
michael@0 3340 1, 1, 1, 1, 1, 1, 1, 1, // 80
michael@0 3341 1, 1, 1, 0, 0, 0, 1, 1, // 88
michael@0 3342
michael@0 3343 1, 1, 1, 1, 1, 1, 1, 1, // 96
michael@0 3344 1, 1, 1, 1, 1, 1, 1, 1, // 104
michael@0 3345 1, 1, 1, 1, 1, 1, 1, 1, // 112
michael@0 3346 1, 1, 1, 0, 1, 0, 1]; // 120
michael@0 3347
michael@0 3348
michael@0 3349 /**
michael@0 3350 * Determines whether the given character code is a CTL.
michael@0 3351 *
michael@0 3352 * @param code : uint
michael@0 3353 * the character code
michael@0 3354 * @returns boolean
michael@0 3355 * true if code is a CTL, false otherwise
michael@0 3356 */
michael@0 3357 function isCTL(code)
michael@0 3358 {
michael@0 3359 return (code >= 0 && code <= 31) || (code == 127);
michael@0 3360 }
michael@0 3361
michael@0 3362 /**
michael@0 3363 * Represents a response to an HTTP request, encapsulating all details of that
michael@0 3364 * response. This includes all headers, the HTTP version, status code and
michael@0 3365 * explanation, and the entity itself.
michael@0 3366 *
michael@0 3367 * @param connection : Connection
michael@0 3368 * the connection over which this response is to be written
michael@0 3369 */
michael@0 3370 function Response(connection)
michael@0 3371 {
michael@0 3372 /** The connection over which this response will be written. */
michael@0 3373 this._connection = connection;
michael@0 3374
michael@0 3375 /**
michael@0 3376 * The HTTP version of this response; defaults to 1.1 if not set by the
michael@0 3377 * handler.
michael@0 3378 */
michael@0 3379 this._httpVersion = nsHttpVersion.HTTP_1_1;
michael@0 3380
michael@0 3381 /**
michael@0 3382 * The HTTP code of this response; defaults to 200.
michael@0 3383 */
michael@0 3384 this._httpCode = 200;
michael@0 3385
michael@0 3386 /**
michael@0 3387 * The description of the HTTP code in this response; defaults to "OK".
michael@0 3388 */
michael@0 3389 this._httpDescription = "OK";
michael@0 3390
michael@0 3391 /**
michael@0 3392 * An nsIHttpHeaders object in which the headers in this response should be
michael@0 3393 * stored. This property is null after the status line and headers have been
michael@0 3394 * written to the network, and it may be modified up until it is cleared,
michael@0 3395 * except if this._finished is set first (in which case headers are written
michael@0 3396 * asynchronously in response to a finish() call not preceded by
michael@0 3397 * flushHeaders()).
michael@0 3398 */
michael@0 3399 this._headers = new nsHttpHeaders();
michael@0 3400
michael@0 3401 /**
michael@0 3402 * Set to true when this response is ended (completely constructed if possible
michael@0 3403 * and the connection closed); further actions on this will then fail.
michael@0 3404 */
michael@0 3405 this._ended = false;
michael@0 3406
michael@0 3407 /**
michael@0 3408 * A stream used to hold data written to the body of this response.
michael@0 3409 */
michael@0 3410 this._bodyOutputStream = null;
michael@0 3411
michael@0 3412 /**
michael@0 3413 * A stream containing all data that has been written to the body of this
michael@0 3414 * response so far. (Async handlers make the data contained in this
michael@0 3415 * unreliable as a way of determining content length in general, but auxiliary
michael@0 3416 * saved information can sometimes be used to guarantee reliability.)
michael@0 3417 */
michael@0 3418 this._bodyInputStream = null;
michael@0 3419
michael@0 3420 /**
michael@0 3421 * A stream copier which copies data to the network. It is initially null
michael@0 3422 * until replaced with a copier for response headers; when headers have been
michael@0 3423 * fully sent it is replaced with a copier for the response body, remaining
michael@0 3424 * so for the duration of response processing.
michael@0 3425 */
michael@0 3426 this._asyncCopier = null;
michael@0 3427
michael@0 3428 /**
michael@0 3429 * True if this response has been designated as being processed
michael@0 3430 * asynchronously rather than for the duration of a single call to
michael@0 3431 * nsIHttpRequestHandler.handle.
michael@0 3432 */
michael@0 3433 this._processAsync = false;
michael@0 3434
michael@0 3435 /**
michael@0 3436 * True iff finish() has been called on this, signaling that no more changes
michael@0 3437 * to this may be made.
michael@0 3438 */
michael@0 3439 this._finished = false;
michael@0 3440
michael@0 3441 /**
michael@0 3442 * True iff powerSeized() has been called on this, signaling that this
michael@0 3443 * response is to be handled manually by the response handler (which may then
michael@0 3444 * send arbitrary data in response, even non-HTTP responses).
michael@0 3445 */
michael@0 3446 this._powerSeized = false;
michael@0 3447 }
michael@0 3448 Response.prototype =
michael@0 3449 {
michael@0 3450 // PUBLIC CONSTRUCTION API
michael@0 3451
michael@0 3452 //
michael@0 3453 // see nsIHttpResponse.bodyOutputStream
michael@0 3454 //
michael@0 3455 get bodyOutputStream()
michael@0 3456 {
michael@0 3457 if (this._finished)
michael@0 3458 throw Cr.NS_ERROR_NOT_AVAILABLE;
michael@0 3459
michael@0 3460 if (!this._bodyOutputStream)
michael@0 3461 {
michael@0 3462 var pipe = new Pipe(true, false, Response.SEGMENT_SIZE, PR_UINT32_MAX,
michael@0 3463 null);
michael@0 3464 this._bodyOutputStream = pipe.outputStream;
michael@0 3465 this._bodyInputStream = pipe.inputStream;
michael@0 3466 if (this._processAsync || this._powerSeized)
michael@0 3467 this._startAsyncProcessor();
michael@0 3468 }
michael@0 3469
michael@0 3470 return this._bodyOutputStream;
michael@0 3471 },
michael@0 3472
michael@0 3473 //
michael@0 3474 // see nsIHttpResponse.write
michael@0 3475 //
michael@0 3476 write: function(data)
michael@0 3477 {
michael@0 3478 if (this._finished)
michael@0 3479 throw Cr.NS_ERROR_NOT_AVAILABLE;
michael@0 3480
michael@0 3481 var dataAsString = String(data);
michael@0 3482 this.bodyOutputStream.write(dataAsString, dataAsString.length);
michael@0 3483 },
michael@0 3484
michael@0 3485 //
michael@0 3486 // see nsIHttpResponse.setStatusLine
michael@0 3487 //
michael@0 3488 setStatusLine: function(httpVersion, code, description)
michael@0 3489 {
michael@0 3490 if (!this._headers || this._finished || this._powerSeized)
michael@0 3491 throw Cr.NS_ERROR_NOT_AVAILABLE;
michael@0 3492 this._ensureAlive();
michael@0 3493
michael@0 3494 if (!(code >= 0 && code < 1000))
michael@0 3495 throw Cr.NS_ERROR_INVALID_ARG;
michael@0 3496
michael@0 3497 try
michael@0 3498 {
michael@0 3499 var httpVer;
michael@0 3500 // avoid version construction for the most common cases
michael@0 3501 if (!httpVersion || httpVersion == "1.1")
michael@0 3502 httpVer = nsHttpVersion.HTTP_1_1;
michael@0 3503 else if (httpVersion == "1.0")
michael@0 3504 httpVer = nsHttpVersion.HTTP_1_0;
michael@0 3505 else
michael@0 3506 httpVer = new nsHttpVersion(httpVersion);
michael@0 3507 }
michael@0 3508 catch (e)
michael@0 3509 {
michael@0 3510 throw Cr.NS_ERROR_INVALID_ARG;
michael@0 3511 }
michael@0 3512
michael@0 3513 // Reason-Phrase = *<TEXT, excluding CR, LF>
michael@0 3514 // TEXT = <any OCTET except CTLs, but including LWS>
michael@0 3515 //
michael@0 3516 // XXX this ends up disallowing octets which aren't Unicode, I think -- not
michael@0 3517 // much to do if description is IDL'd as string
michael@0 3518 if (!description)
michael@0 3519 description = "";
michael@0 3520 for (var i = 0; i < description.length; i++)
michael@0 3521 if (isCTL(description.charCodeAt(i)) && description.charAt(i) != "\t")
michael@0 3522 throw Cr.NS_ERROR_INVALID_ARG;
michael@0 3523
michael@0 3524 // set the values only after validation to preserve atomicity
michael@0 3525 this._httpDescription = description;
michael@0 3526 this._httpCode = code;
michael@0 3527 this._httpVersion = httpVer;
michael@0 3528 },
michael@0 3529
michael@0 3530 //
michael@0 3531 // see nsIHttpResponse.setHeader
michael@0 3532 //
michael@0 3533 setHeader: function(name, value, merge)
michael@0 3534 {
michael@0 3535 if (!this._headers || this._finished || this._powerSeized)
michael@0 3536 throw Cr.NS_ERROR_NOT_AVAILABLE;
michael@0 3537 this._ensureAlive();
michael@0 3538
michael@0 3539 this._headers.setHeader(name, value, merge);
michael@0 3540 },
michael@0 3541
michael@0 3542 //
michael@0 3543 // see nsIHttpResponse.processAsync
michael@0 3544 //
michael@0 3545 processAsync: function()
michael@0 3546 {
michael@0 3547 if (this._finished)
michael@0 3548 throw Cr.NS_ERROR_UNEXPECTED;
michael@0 3549 if (this._powerSeized)
michael@0 3550 throw Cr.NS_ERROR_NOT_AVAILABLE;
michael@0 3551 if (this._processAsync)
michael@0 3552 return;
michael@0 3553 this._ensureAlive();
michael@0 3554
michael@0 3555 dumpn("*** processing connection " + this._connection.number + " async");
michael@0 3556 this._processAsync = true;
michael@0 3557
michael@0 3558 /*
michael@0 3559 * Either the bodyOutputStream getter or this method is responsible for
michael@0 3560 * starting the asynchronous processor and catching writes of data to the
michael@0 3561 * response body of async responses as they happen, for the purpose of
michael@0 3562 * forwarding those writes to the actual connection's output stream.
michael@0 3563 * If bodyOutputStream is accessed first, calling this method will create
michael@0 3564 * the processor (when it first is clear that body data is to be written
michael@0 3565 * immediately, not buffered). If this method is called first, accessing
michael@0 3566 * bodyOutputStream will create the processor. If only this method is
michael@0 3567 * called, we'll write nothing, neither headers nor the nonexistent body,
michael@0 3568 * until finish() is called. Since that delay is easily avoided by simply
michael@0 3569 * getting bodyOutputStream or calling write(""), we don't worry about it.
michael@0 3570 */
michael@0 3571 if (this._bodyOutputStream && !this._asyncCopier)
michael@0 3572 this._startAsyncProcessor();
michael@0 3573 },
michael@0 3574
michael@0 3575 //
michael@0 3576 // see nsIHttpResponse.seizePower
michael@0 3577 //
michael@0 3578 seizePower: function()
michael@0 3579 {
michael@0 3580 if (this._processAsync)
michael@0 3581 throw Cr.NS_ERROR_NOT_AVAILABLE;
michael@0 3582 if (this._finished)
michael@0 3583 throw Cr.NS_ERROR_UNEXPECTED;
michael@0 3584 if (this._powerSeized)
michael@0 3585 return;
michael@0 3586 this._ensureAlive();
michael@0 3587
michael@0 3588 dumpn("*** forcefully seizing power over connection " +
michael@0 3589 this._connection.number + "...");
michael@0 3590
michael@0 3591 // Purge any already-written data without sending it. We could as easily
michael@0 3592 // swap out the streams entirely, but that makes it possible to acquire and
michael@0 3593 // unknowingly use a stale reference, so we require there only be one of
michael@0 3594 // each stream ever for any response to avoid this complication.
michael@0 3595 if (this._asyncCopier)
michael@0 3596 this._asyncCopier.cancel(Cr.NS_BINDING_ABORTED);
michael@0 3597 this._asyncCopier = null;
michael@0 3598 if (this._bodyOutputStream)
michael@0 3599 {
michael@0 3600 var input = new BinaryInputStream(this._bodyInputStream);
michael@0 3601 var avail;
michael@0 3602 while ((avail = input.available()) > 0)
michael@0 3603 input.readByteArray(avail);
michael@0 3604 }
michael@0 3605
michael@0 3606 this._powerSeized = true;
michael@0 3607 if (this._bodyOutputStream)
michael@0 3608 this._startAsyncProcessor();
michael@0 3609 },
michael@0 3610
michael@0 3611 //
michael@0 3612 // see nsIHttpResponse.finish
michael@0 3613 //
michael@0 3614 finish: function()
michael@0 3615 {
michael@0 3616 if (!this._processAsync && !this._powerSeized)
michael@0 3617 throw Cr.NS_ERROR_UNEXPECTED;
michael@0 3618 if (this._finished)
michael@0 3619 return;
michael@0 3620
michael@0 3621 dumpn("*** finishing connection " + this._connection.number);
michael@0 3622 this._startAsyncProcessor(); // in case bodyOutputStream was never accessed
michael@0 3623 if (this._bodyOutputStream)
michael@0 3624 this._bodyOutputStream.close();
michael@0 3625 this._finished = true;
michael@0 3626 },
michael@0 3627
michael@0 3628
michael@0 3629 // NSISUPPORTS
michael@0 3630
michael@0 3631 //
michael@0 3632 // see nsISupports.QueryInterface
michael@0 3633 //
michael@0 3634 QueryInterface: function(iid)
michael@0 3635 {
michael@0 3636 if (iid.equals(Ci.nsIHttpResponse) || iid.equals(Ci.nsISupports))
michael@0 3637 return this;
michael@0 3638
michael@0 3639 throw Cr.NS_ERROR_NO_INTERFACE;
michael@0 3640 },
michael@0 3641
michael@0 3642
michael@0 3643 // POST-CONSTRUCTION API (not exposed externally)
michael@0 3644
michael@0 3645 /**
michael@0 3646 * The HTTP version number of this, as a string (e.g. "1.1").
michael@0 3647 */
michael@0 3648 get httpVersion()
michael@0 3649 {
michael@0 3650 this._ensureAlive();
michael@0 3651 return this._httpVersion.toString();
michael@0 3652 },
michael@0 3653
michael@0 3654 /**
michael@0 3655 * The HTTP status code of this response, as a string of three characters per
michael@0 3656 * RFC 2616.
michael@0 3657 */
michael@0 3658 get httpCode()
michael@0 3659 {
michael@0 3660 this._ensureAlive();
michael@0 3661
michael@0 3662 var codeString = (this._httpCode < 10 ? "0" : "") +
michael@0 3663 (this._httpCode < 100 ? "0" : "") +
michael@0 3664 this._httpCode;
michael@0 3665 return codeString;
michael@0 3666 },
michael@0 3667
michael@0 3668 /**
michael@0 3669 * The description of the HTTP status code of this response, or "" if none is
michael@0 3670 * set.
michael@0 3671 */
michael@0 3672 get httpDescription()
michael@0 3673 {
michael@0 3674 this._ensureAlive();
michael@0 3675
michael@0 3676 return this._httpDescription;
michael@0 3677 },
michael@0 3678
michael@0 3679 /**
michael@0 3680 * The headers in this response, as an nsHttpHeaders object.
michael@0 3681 */
michael@0 3682 get headers()
michael@0 3683 {
michael@0 3684 this._ensureAlive();
michael@0 3685
michael@0 3686 return this._headers;
michael@0 3687 },
michael@0 3688
michael@0 3689 //
michael@0 3690 // see nsHttpHeaders.getHeader
michael@0 3691 //
michael@0 3692 getHeader: function(name)
michael@0 3693 {
michael@0 3694 this._ensureAlive();
michael@0 3695
michael@0 3696 return this._headers.getHeader(name);
michael@0 3697 },
michael@0 3698
michael@0 3699 /**
michael@0 3700 * Determines whether this response may be abandoned in favor of a newly
michael@0 3701 * constructed response. A response may be abandoned only if it is not being
michael@0 3702 * sent asynchronously and if raw control over it has not been taken from the
michael@0 3703 * server.
michael@0 3704 *
michael@0 3705 * @returns boolean
michael@0 3706 * true iff no data has been written to the network
michael@0 3707 */
michael@0 3708 partiallySent: function()
michael@0 3709 {
michael@0 3710 dumpn("*** partiallySent()");
michael@0 3711 return this._processAsync || this._powerSeized;
michael@0 3712 },
michael@0 3713
michael@0 3714 /**
michael@0 3715 * If necessary, kicks off the remaining request processing needed to be done
michael@0 3716 * after a request handler performs its initial work upon this response.
michael@0 3717 */
michael@0 3718 complete: function()
michael@0 3719 {
michael@0 3720 dumpn("*** complete()");
michael@0 3721 if (this._processAsync || this._powerSeized)
michael@0 3722 {
michael@0 3723 NS_ASSERT(this._processAsync ^ this._powerSeized,
michael@0 3724 "can't both send async and relinquish power");
michael@0 3725 return;
michael@0 3726 }
michael@0 3727
michael@0 3728 NS_ASSERT(!this.partiallySent(), "completing a partially-sent response?");
michael@0 3729
michael@0 3730 this._startAsyncProcessor();
michael@0 3731
michael@0 3732 // Now make sure we finish processing this request!
michael@0 3733 if (this._bodyOutputStream)
michael@0 3734 this._bodyOutputStream.close();
michael@0 3735 },
michael@0 3736
michael@0 3737 /**
michael@0 3738 * Abruptly ends processing of this response, usually due to an error in an
michael@0 3739 * incoming request but potentially due to a bad error handler. Since we
michael@0 3740 * cannot handle the error in the usual way (giving an HTTP error page in
michael@0 3741 * response) because data may already have been sent (or because the response
michael@0 3742 * might be expected to have been generated asynchronously or completely from
michael@0 3743 * scratch by the handler), we stop processing this response and abruptly
michael@0 3744 * close the connection.
michael@0 3745 *
michael@0 3746 * @param e : Error
michael@0 3747 * the exception which precipitated this abort, or null if no such exception
michael@0 3748 * was generated
michael@0 3749 */
michael@0 3750 abort: function(e)
michael@0 3751 {
michael@0 3752 dumpn("*** abort(<" + e + ">)");
michael@0 3753
michael@0 3754 // This response will be ended by the processor if one was created.
michael@0 3755 var copier = this._asyncCopier;
michael@0 3756 if (copier)
michael@0 3757 {
michael@0 3758 // We dispatch asynchronously here so that any pending writes of data to
michael@0 3759 // the connection will be deterministically written. This makes it easier
michael@0 3760 // to specify exact behavior, and it makes observable behavior more
michael@0 3761 // predictable for clients. Note that the correctness of this depends on
michael@0 3762 // callbacks in response to _waitToReadData in WriteThroughCopier
michael@0 3763 // happening asynchronously with respect to the actual writing of data to
michael@0 3764 // bodyOutputStream, as they currently do; if they happened synchronously,
michael@0 3765 // an event which ran before this one could write more data to the
michael@0 3766 // response body before we get around to canceling the copier. We have
michael@0 3767 // tests for this in test_seizepower.js, however, and I can't think of a
michael@0 3768 // way to handle both cases without removing bodyOutputStream access and
michael@0 3769 // moving its effective write(data, length) method onto Response, which
michael@0 3770 // would be slower and require more code than this anyway.
michael@0 3771 gThreadManager.currentThread.dispatch({
michael@0 3772 run: function()
michael@0 3773 {
michael@0 3774 dumpn("*** canceling copy asynchronously...");
michael@0 3775 copier.cancel(Cr.NS_ERROR_UNEXPECTED);
michael@0 3776 }
michael@0 3777 }, Ci.nsIThread.DISPATCH_NORMAL);
michael@0 3778 }
michael@0 3779 else
michael@0 3780 {
michael@0 3781 this.end();
michael@0 3782 }
michael@0 3783 },
michael@0 3784
michael@0 3785 /**
michael@0 3786 * Closes this response's network connection, marks the response as finished,
michael@0 3787 * and notifies the server handler that the request is done being processed.
michael@0 3788 */
michael@0 3789 end: function()
michael@0 3790 {
michael@0 3791 NS_ASSERT(!this._ended, "ending this response twice?!?!");
michael@0 3792
michael@0 3793 this._connection.close();
michael@0 3794 if (this._bodyOutputStream)
michael@0 3795 this._bodyOutputStream.close();
michael@0 3796
michael@0 3797 this._finished = true;
michael@0 3798 this._ended = true;
michael@0 3799 },
michael@0 3800
michael@0 3801 // PRIVATE IMPLEMENTATION
michael@0 3802
michael@0 3803 /**
michael@0 3804 * Sends the status line and headers of this response if they haven't been
michael@0 3805 * sent and initiates the process of copying data written to this response's
michael@0 3806 * body to the network.
michael@0 3807 */
michael@0 3808 _startAsyncProcessor: function()
michael@0 3809 {
michael@0 3810 dumpn("*** _startAsyncProcessor()");
michael@0 3811
michael@0 3812 // Handle cases where we're being called a second time. The former case
michael@0 3813 // happens when this is triggered both by complete() and by processAsync(),
michael@0 3814 // while the latter happens when processAsync() in conjunction with sent
michael@0 3815 // data causes abort() to be called.
michael@0 3816 if (this._asyncCopier || this._ended)
michael@0 3817 {
michael@0 3818 dumpn("*** ignoring second call to _startAsyncProcessor");
michael@0 3819 return;
michael@0 3820 }
michael@0 3821
michael@0 3822 // Send headers if they haven't been sent already and should be sent, then
michael@0 3823 // asynchronously continue to send the body.
michael@0 3824 if (this._headers && !this._powerSeized)
michael@0 3825 {
michael@0 3826 this._sendHeaders();
michael@0 3827 return;
michael@0 3828 }
michael@0 3829
michael@0 3830 this._headers = null;
michael@0 3831 this._sendBody();
michael@0 3832 },
michael@0 3833
michael@0 3834 /**
michael@0 3835 * Signals that all modifications to the response status line and headers are
michael@0 3836 * complete and then sends that data over the network to the client. Once
michael@0 3837 * this method completes, a different response to the request that resulted
michael@0 3838 * in this response cannot be sent -- the only possible action in case of
michael@0 3839 * error is to abort the response and close the connection.
michael@0 3840 */
michael@0 3841 _sendHeaders: function()
michael@0 3842 {
michael@0 3843 dumpn("*** _sendHeaders()");
michael@0 3844
michael@0 3845 NS_ASSERT(this._headers);
michael@0 3846 NS_ASSERT(!this._powerSeized);
michael@0 3847
michael@0 3848 // request-line
michael@0 3849 var statusLine = "HTTP/" + this.httpVersion + " " +
michael@0 3850 this.httpCode + " " +
michael@0 3851 this.httpDescription + "\r\n";
michael@0 3852
michael@0 3853 // header post-processing
michael@0 3854
michael@0 3855 var headers = this._headers;
michael@0 3856 headers.setHeader("Connection", "close", false);
michael@0 3857 headers.setHeader("Server", "httpd.js", false);
michael@0 3858 if (!headers.hasHeader("Date"))
michael@0 3859 headers.setHeader("Date", toDateString(Date.now()), false);
michael@0 3860
michael@0 3861 // Any response not being processed asynchronously must have an associated
michael@0 3862 // Content-Length header for reasons of backwards compatibility with the
michael@0 3863 // initial server, which fully buffered every response before sending it.
michael@0 3864 // Beyond that, however, it's good to do this anyway because otherwise it's
michael@0 3865 // impossible to test behaviors that depend on the presence or absence of a
michael@0 3866 // Content-Length header.
michael@0 3867 if (!this._processAsync)
michael@0 3868 {
michael@0 3869 dumpn("*** non-async response, set Content-Length");
michael@0 3870
michael@0 3871 var bodyStream = this._bodyInputStream;
michael@0 3872 var avail = bodyStream ? bodyStream.available() : 0;
michael@0 3873
michael@0 3874 // XXX assumes stream will always report the full amount of data available
michael@0 3875 headers.setHeader("Content-Length", "" + avail, false);
michael@0 3876 }
michael@0 3877
michael@0 3878
michael@0 3879 // construct and send response
michael@0 3880 dumpn("*** header post-processing completed, sending response head...");
michael@0 3881
michael@0 3882 // request-line
michael@0 3883 var preambleData = [statusLine];
michael@0 3884
michael@0 3885 // headers
michael@0 3886 var headEnum = headers.enumerator;
michael@0 3887 while (headEnum.hasMoreElements())
michael@0 3888 {
michael@0 3889 var fieldName = headEnum.getNext()
michael@0 3890 .QueryInterface(Ci.nsISupportsString)
michael@0 3891 .data;
michael@0 3892 var values = headers.getHeaderValues(fieldName);
michael@0 3893 for (var i = 0, sz = values.length; i < sz; i++)
michael@0 3894 preambleData.push(fieldName + ": " + values[i] + "\r\n");
michael@0 3895 }
michael@0 3896
michael@0 3897 // end request-line/headers
michael@0 3898 preambleData.push("\r\n");
michael@0 3899
michael@0 3900 var preamble = preambleData.join("");
michael@0 3901
michael@0 3902 var responseHeadPipe = new Pipe(true, false, 0, PR_UINT32_MAX, null);
michael@0 3903 responseHeadPipe.outputStream.write(preamble, preamble.length);
michael@0 3904
michael@0 3905 var response = this;
michael@0 3906 var copyObserver =
michael@0 3907 {
michael@0 3908 onStartRequest: function(request, cx)
michael@0 3909 {
michael@0 3910 dumpn("*** preamble copying started");
michael@0 3911 },
michael@0 3912
michael@0 3913 onStopRequest: function(request, cx, statusCode)
michael@0 3914 {
michael@0 3915 dumpn("*** preamble copying complete " +
michael@0 3916 "[status=0x" + statusCode.toString(16) + "]");
michael@0 3917
michael@0 3918 if (!components.isSuccessCode(statusCode))
michael@0 3919 {
michael@0 3920 dumpn("!!! header copying problems: non-success statusCode, " +
michael@0 3921 "ending response");
michael@0 3922
michael@0 3923 response.end();
michael@0 3924 }
michael@0 3925 else
michael@0 3926 {
michael@0 3927 response._sendBody();
michael@0 3928 }
michael@0 3929 },
michael@0 3930
michael@0 3931 QueryInterface: function(aIID)
michael@0 3932 {
michael@0 3933 if (aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsISupports))
michael@0 3934 return this;
michael@0 3935
michael@0 3936 throw Cr.NS_ERROR_NO_INTERFACE;
michael@0 3937 }
michael@0 3938 };
michael@0 3939
michael@0 3940 var headerCopier = this._asyncCopier =
michael@0 3941 new WriteThroughCopier(responseHeadPipe.inputStream,
michael@0 3942 this._connection.output,
michael@0 3943 copyObserver, null);
michael@0 3944
michael@0 3945 responseHeadPipe.outputStream.close();
michael@0 3946
michael@0 3947 // Forbid setting any more headers or modifying the request line.
michael@0 3948 this._headers = null;
michael@0 3949 },
michael@0 3950
michael@0 3951 /**
michael@0 3952 * Asynchronously writes the body of the response (or the entire response, if
michael@0 3953 * seizePower() has been called) to the network.
michael@0 3954 */
michael@0 3955 _sendBody: function()
michael@0 3956 {
michael@0 3957 dumpn("*** _sendBody");
michael@0 3958
michael@0 3959 NS_ASSERT(!this._headers, "still have headers around but sending body?");
michael@0 3960
michael@0 3961 // If no body data was written, we're done
michael@0 3962 if (!this._bodyInputStream)
michael@0 3963 {
michael@0 3964 dumpn("*** empty body, response finished");
michael@0 3965 this.end();
michael@0 3966 return;
michael@0 3967 }
michael@0 3968
michael@0 3969 var response = this;
michael@0 3970 var copyObserver =
michael@0 3971 {
michael@0 3972 onStartRequest: function(request, context)
michael@0 3973 {
michael@0 3974 dumpn("*** onStartRequest");
michael@0 3975 },
michael@0 3976
michael@0 3977 onStopRequest: function(request, cx, statusCode)
michael@0 3978 {
michael@0 3979 dumpn("*** onStopRequest [status=0x" + statusCode.toString(16) + "]");
michael@0 3980
michael@0 3981 if (statusCode === Cr.NS_BINDING_ABORTED)
michael@0 3982 {
michael@0 3983 dumpn("*** terminating copy observer without ending the response");
michael@0 3984 }
michael@0 3985 else
michael@0 3986 {
michael@0 3987 if (!components.isSuccessCode(statusCode))
michael@0 3988 dumpn("*** WARNING: non-success statusCode in onStopRequest");
michael@0 3989
michael@0 3990 response.end();
michael@0 3991 }
michael@0 3992 },
michael@0 3993
michael@0 3994 QueryInterface: function(aIID)
michael@0 3995 {
michael@0 3996 if (aIID.equals(Ci.nsIRequestObserver) || aIID.equals(Ci.nsISupports))
michael@0 3997 return this;
michael@0 3998
michael@0 3999 throw Cr.NS_ERROR_NO_INTERFACE;
michael@0 4000 }
michael@0 4001 };
michael@0 4002
michael@0 4003 dumpn("*** starting async copier of body data...");
michael@0 4004 this._asyncCopier =
michael@0 4005 new WriteThroughCopier(this._bodyInputStream, this._connection.output,
michael@0 4006 copyObserver, null);
michael@0 4007 },
michael@0 4008
michael@0 4009 /** Ensures that this hasn't been ended. */
michael@0 4010 _ensureAlive: function()
michael@0 4011 {
michael@0 4012 NS_ASSERT(!this._ended, "not handling response lifetime correctly");
michael@0 4013 }
michael@0 4014 };
michael@0 4015
michael@0 4016 /**
michael@0 4017 * Size of the segments in the buffer used in storing response data and writing
michael@0 4018 * it to the socket.
michael@0 4019 */
michael@0 4020 Response.SEGMENT_SIZE = 8192;
michael@0 4021
michael@0 4022 /** Serves double duty in WriteThroughCopier implementation. */
michael@0 4023 function notImplemented()
michael@0 4024 {
michael@0 4025 throw Cr.NS_ERROR_NOT_IMPLEMENTED;
michael@0 4026 }
michael@0 4027
michael@0 4028 /** Returns true iff the given exception represents stream closure. */
michael@0 4029 function streamClosed(e)
michael@0 4030 {
michael@0 4031 return e === Cr.NS_BASE_STREAM_CLOSED ||
michael@0 4032 (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_CLOSED);
michael@0 4033 }
michael@0 4034
michael@0 4035 /** Returns true iff the given exception represents a blocked stream. */
michael@0 4036 function wouldBlock(e)
michael@0 4037 {
michael@0 4038 return e === Cr.NS_BASE_STREAM_WOULD_BLOCK ||
michael@0 4039 (typeof e === "object" && e.result === Cr.NS_BASE_STREAM_WOULD_BLOCK);
michael@0 4040 }
michael@0 4041
michael@0 4042 /**
michael@0 4043 * Copies data from source to sink as it becomes available, when that data can
michael@0 4044 * be written to sink without blocking.
michael@0 4045 *
michael@0 4046 * @param source : nsIAsyncInputStream
michael@0 4047 * the stream from which data is to be read
michael@0 4048 * @param sink : nsIAsyncOutputStream
michael@0 4049 * the stream to which data is to be copied
michael@0 4050 * @param observer : nsIRequestObserver
michael@0 4051 * an observer which will be notified when the copy starts and finishes
michael@0 4052 * @param context : nsISupports
michael@0 4053 * context passed to observer when notified of start/stop
michael@0 4054 * @throws NS_ERROR_NULL_POINTER
michael@0 4055 * if source, sink, or observer are null
michael@0 4056 */
michael@0 4057 function WriteThroughCopier(source, sink, observer, context)
michael@0 4058 {
michael@0 4059 if (!source || !sink || !observer)
michael@0 4060 throw Cr.NS_ERROR_NULL_POINTER;
michael@0 4061
michael@0 4062 /** Stream from which data is being read. */
michael@0 4063 this._source = source;
michael@0 4064
michael@0 4065 /** Stream to which data is being written. */
michael@0 4066 this._sink = sink;
michael@0 4067
michael@0 4068 /** Observer watching this copy. */
michael@0 4069 this._observer = observer;
michael@0 4070
michael@0 4071 /** Context for the observer watching this. */
michael@0 4072 this._context = context;
michael@0 4073
michael@0 4074 /**
michael@0 4075 * True iff this is currently being canceled (cancel has been called, the
michael@0 4076 * callback may not yet have been made).
michael@0 4077 */
michael@0 4078 this._canceled = false;
michael@0 4079
michael@0 4080 /**
michael@0 4081 * False until all data has been read from input and written to output, at
michael@0 4082 * which point this copy is completed and cancel() is asynchronously called.
michael@0 4083 */
michael@0 4084 this._completed = false;
michael@0 4085
michael@0 4086 /** Required by nsIRequest, meaningless. */
michael@0 4087 this.loadFlags = 0;
michael@0 4088 /** Required by nsIRequest, meaningless. */
michael@0 4089 this.loadGroup = null;
michael@0 4090 /** Required by nsIRequest, meaningless. */
michael@0 4091 this.name = "response-body-copy";
michael@0 4092
michael@0 4093 /** Status of this request. */
michael@0 4094 this.status = Cr.NS_OK;
michael@0 4095
michael@0 4096 /** Arrays of byte strings waiting to be written to output. */
michael@0 4097 this._pendingData = [];
michael@0 4098
michael@0 4099 // start copying
michael@0 4100 try
michael@0 4101 {
michael@0 4102 observer.onStartRequest(this, context);
michael@0 4103 this._waitToReadData();
michael@0 4104 this._waitForSinkClosure();
michael@0 4105 }
michael@0 4106 catch (e)
michael@0 4107 {
michael@0 4108 dumpn("!!! error starting copy: " + e +
michael@0 4109 ("lineNumber" in e ? ", line " + e.lineNumber : ""));
michael@0 4110 dumpn(e.stack);
michael@0 4111 this.cancel(Cr.NS_ERROR_UNEXPECTED);
michael@0 4112 }
michael@0 4113 }
michael@0 4114 WriteThroughCopier.prototype =
michael@0 4115 {
michael@0 4116 /* nsISupports implementation */
michael@0 4117
michael@0 4118 QueryInterface: function(iid)
michael@0 4119 {
michael@0 4120 if (iid.equals(Ci.nsIInputStreamCallback) ||
michael@0 4121 iid.equals(Ci.nsIOutputStreamCallback) ||
michael@0 4122 iid.equals(Ci.nsIRequest) ||
michael@0 4123 iid.equals(Ci.nsISupports))
michael@0 4124 {
michael@0 4125 return this;
michael@0 4126 }
michael@0 4127
michael@0 4128 throw Cr.NS_ERROR_NO_INTERFACE;
michael@0 4129 },
michael@0 4130
michael@0 4131
michael@0 4132 // NSIINPUTSTREAMCALLBACK
michael@0 4133
michael@0 4134 /**
michael@0 4135 * Receives a more-data-in-input notification and writes the corresponding
michael@0 4136 * data to the output.
michael@0 4137 *
michael@0 4138 * @param input : nsIAsyncInputStream
michael@0 4139 * the input stream on whose data we have been waiting
michael@0 4140 */
michael@0 4141 onInputStreamReady: function(input)
michael@0 4142 {
michael@0 4143 if (this._source === null)
michael@0 4144 return;
michael@0 4145
michael@0 4146 dumpn("*** onInputStreamReady");
michael@0 4147
michael@0 4148 //
michael@0 4149 // Ordinarily we'll read a non-zero amount of data from input, queue it up
michael@0 4150 // to be written and then wait for further callbacks. The complications in
michael@0 4151 // this method are the cases where we deviate from that behavior when errors
michael@0 4152 // occur or when copying is drawing to a finish.
michael@0 4153 //
michael@0 4154 // The edge cases when reading data are:
michael@0 4155 //
michael@0 4156 // Zero data is read
michael@0 4157 // If zero data was read, we're at the end of available data, so we can
michael@0 4158 // should stop reading and move on to writing out what we have (or, if
michael@0 4159 // we've already done that, onto notifying of completion).
michael@0 4160 // A stream-closed exception is thrown
michael@0 4161 // This is effectively a less kind version of zero data being read; the
michael@0 4162 // only difference is that we notify of completion with that result
michael@0 4163 // rather than with NS_OK.
michael@0 4164 // Some other exception is thrown
michael@0 4165 // This is the least kind result. We don't know what happened, so we
michael@0 4166 // act as though the stream closed except that we notify of completion
michael@0 4167 // with the result NS_ERROR_UNEXPECTED.
michael@0 4168 //
michael@0 4169
michael@0 4170 var bytesWanted = 0, bytesConsumed = -1;
michael@0 4171 try
michael@0 4172 {
michael@0 4173 input = new BinaryInputStream(input);
michael@0 4174
michael@0 4175 bytesWanted = Math.min(input.available(), Response.SEGMENT_SIZE);
michael@0 4176 dumpn("*** input wanted: " + bytesWanted);
michael@0 4177
michael@0 4178 if (bytesWanted > 0)
michael@0 4179 {
michael@0 4180 var data = input.readByteArray(bytesWanted);
michael@0 4181 bytesConsumed = data.length;
michael@0 4182 this._pendingData.push(String.fromCharCode.apply(String, data));
michael@0 4183 }
michael@0 4184
michael@0 4185 dumpn("*** " + bytesConsumed + " bytes read");
michael@0 4186
michael@0 4187 // Handle the zero-data edge case in the same place as all other edge
michael@0 4188 // cases are handled.
michael@0 4189 if (bytesWanted === 0)
michael@0 4190 throw Cr.NS_BASE_STREAM_CLOSED;
michael@0 4191 }
michael@0 4192 catch (e)
michael@0 4193 {
michael@0 4194 if (streamClosed(e))
michael@0 4195 {
michael@0 4196 dumpn("*** input stream closed");
michael@0 4197 e = bytesWanted === 0 ? Cr.NS_OK : Cr.NS_ERROR_UNEXPECTED;
michael@0 4198 }
michael@0 4199 else
michael@0 4200 {
michael@0 4201 dumpn("!!! unexpected error reading from input, canceling: " + e);
michael@0 4202 e = Cr.NS_ERROR_UNEXPECTED;
michael@0 4203 }
michael@0 4204
michael@0 4205 this._doneReadingSource(e);
michael@0 4206 return;
michael@0 4207 }
michael@0 4208
michael@0 4209 var pendingData = this._pendingData;
michael@0 4210
michael@0 4211 NS_ASSERT(bytesConsumed > 0);
michael@0 4212 NS_ASSERT(pendingData.length > 0, "no pending data somehow?");
michael@0 4213 NS_ASSERT(pendingData[pendingData.length - 1].length > 0,
michael@0 4214 "buffered zero bytes of data?");
michael@0 4215
michael@0 4216 NS_ASSERT(this._source !== null);
michael@0 4217
michael@0 4218 // Reading has gone great, and we've gotten data to write now. What if we
michael@0 4219 // don't have a place to write that data, because output went away just
michael@0 4220 // before this read? Drop everything on the floor, including new data, and
michael@0 4221 // cancel at this point.
michael@0 4222 if (this._sink === null)
michael@0 4223 {
michael@0 4224 pendingData.length = 0;
michael@0 4225 this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED);
michael@0 4226 return;
michael@0 4227 }
michael@0 4228
michael@0 4229 // Okay, we've read the data, and we know we have a place to write it. We
michael@0 4230 // need to queue up the data to be written, but *only* if none is queued
michael@0 4231 // already -- if data's already queued, the code that actually writes the
michael@0 4232 // data will make sure to wait on unconsumed pending data.
michael@0 4233 try
michael@0 4234 {
michael@0 4235 if (pendingData.length === 1)
michael@0 4236 this._waitToWriteData();
michael@0 4237 }
michael@0 4238 catch (e)
michael@0 4239 {
michael@0 4240 dumpn("!!! error waiting to write data just read, swallowing and " +
michael@0 4241 "writing only what we already have: " + e);
michael@0 4242 this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
michael@0 4243 return;
michael@0 4244 }
michael@0 4245
michael@0 4246 // Whee! We successfully read some data, and it's successfully queued up to
michael@0 4247 // be written. All that remains now is to wait for more data to read.
michael@0 4248 try
michael@0 4249 {
michael@0 4250 this._waitToReadData();
michael@0 4251 }
michael@0 4252 catch (e)
michael@0 4253 {
michael@0 4254 dumpn("!!! error waiting to read more data: " + e);
michael@0 4255 this._doneReadingSource(Cr.NS_ERROR_UNEXPECTED);
michael@0 4256 }
michael@0 4257 },
michael@0 4258
michael@0 4259
michael@0 4260 // NSIOUTPUTSTREAMCALLBACK
michael@0 4261
michael@0 4262 /**
michael@0 4263 * Callback when data may be written to the output stream without blocking, or
michael@0 4264 * when the output stream has been closed.
michael@0 4265 *
michael@0 4266 * @param output : nsIAsyncOutputStream
michael@0 4267 * the output stream on whose writability we've been waiting, also known as
michael@0 4268 * this._sink
michael@0 4269 */
michael@0 4270 onOutputStreamReady: function(output)
michael@0 4271 {
michael@0 4272 if (this._sink === null)
michael@0 4273 return;
michael@0 4274
michael@0 4275 dumpn("*** onOutputStreamReady");
michael@0 4276
michael@0 4277 var pendingData = this._pendingData;
michael@0 4278 if (pendingData.length === 0)
michael@0 4279 {
michael@0 4280 // There's no pending data to write. The only way this can happen is if
michael@0 4281 // we're waiting on the output stream's closure, so we can respond to a
michael@0 4282 // copying failure as quickly as possible (rather than waiting for data to
michael@0 4283 // be available to read and then fail to be copied). Therefore, we must
michael@0 4284 // be done now -- don't bother to attempt to write anything and wrap
michael@0 4285 // things up.
michael@0 4286 dumpn("!!! output stream closed prematurely, ending copy");
michael@0 4287
michael@0 4288 this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
michael@0 4289 return;
michael@0 4290 }
michael@0 4291
michael@0 4292
michael@0 4293 NS_ASSERT(pendingData[0].length > 0, "queued up an empty quantum?");
michael@0 4294
michael@0 4295 //
michael@0 4296 // Write out the first pending quantum of data. The possible errors here
michael@0 4297 // are:
michael@0 4298 //
michael@0 4299 // The write might fail because we can't write that much data
michael@0 4300 // Okay, we've written what we can now, so re-queue what's left and
michael@0 4301 // finish writing it out later.
michael@0 4302 // The write failed because the stream was closed
michael@0 4303 // Discard pending data that we can no longer write, stop reading, and
michael@0 4304 // signal that copying finished.
michael@0 4305 // Some other error occurred.
michael@0 4306 // Same as if the stream were closed, but notify with the status
michael@0 4307 // NS_ERROR_UNEXPECTED so the observer knows something was wonky.
michael@0 4308 //
michael@0 4309
michael@0 4310 try
michael@0 4311 {
michael@0 4312 var quantum = pendingData[0];
michael@0 4313
michael@0 4314 // XXX |quantum| isn't guaranteed to be ASCII, so we're relying on
michael@0 4315 // undefined behavior! We're only using this because writeByteArray
michael@0 4316 // is unusably broken for asynchronous output streams; see bug 532834
michael@0 4317 // for details.
michael@0 4318 var bytesWritten = output.write(quantum, quantum.length);
michael@0 4319 if (bytesWritten === quantum.length)
michael@0 4320 pendingData.shift();
michael@0 4321 else
michael@0 4322 pendingData[0] = quantum.substring(bytesWritten);
michael@0 4323
michael@0 4324 dumpn("*** wrote " + bytesWritten + " bytes of data");
michael@0 4325 }
michael@0 4326 catch (e)
michael@0 4327 {
michael@0 4328 if (wouldBlock(e))
michael@0 4329 {
michael@0 4330 NS_ASSERT(pendingData.length > 0,
michael@0 4331 "stream-blocking exception with no data to write?");
michael@0 4332 NS_ASSERT(pendingData[0].length > 0,
michael@0 4333 "stream-blocking exception with empty quantum?");
michael@0 4334 this._waitToWriteData();
michael@0 4335 return;
michael@0 4336 }
michael@0 4337
michael@0 4338 if (streamClosed(e))
michael@0 4339 dumpn("!!! output stream prematurely closed, signaling error...");
michael@0 4340 else
michael@0 4341 dumpn("!!! unknown error: " + e + ", quantum=" + quantum);
michael@0 4342
michael@0 4343 this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
michael@0 4344 return;
michael@0 4345 }
michael@0 4346
michael@0 4347 // The day is ours! Quantum written, now let's see if we have more data
michael@0 4348 // still to write.
michael@0 4349 try
michael@0 4350 {
michael@0 4351 if (pendingData.length > 0)
michael@0 4352 {
michael@0 4353 this._waitToWriteData();
michael@0 4354 return;
michael@0 4355 }
michael@0 4356 }
michael@0 4357 catch (e)
michael@0 4358 {
michael@0 4359 dumpn("!!! unexpected error waiting to write pending data: " + e);
michael@0 4360 this._doneWritingToSink(Cr.NS_ERROR_UNEXPECTED);
michael@0 4361 return;
michael@0 4362 }
michael@0 4363
michael@0 4364 // Okay, we have no more pending data to write -- but might we get more in
michael@0 4365 // the future?
michael@0 4366 if (this._source !== null)
michael@0 4367 {
michael@0 4368 /*
michael@0 4369 * If we might, then wait for the output stream to be closed. (We wait
michael@0 4370 * only for closure because we have no data to write -- and if we waited
michael@0 4371 * for a specific amount of data, we would get repeatedly notified for no
michael@0 4372 * reason if over time the output stream permitted more and more data to
michael@0 4373 * be written to it without blocking.)
michael@0 4374 */
michael@0 4375 this._waitForSinkClosure();
michael@0 4376 }
michael@0 4377 else
michael@0 4378 {
michael@0 4379 /*
michael@0 4380 * On the other hand, if we can't have more data because the input
michael@0 4381 * stream's gone away, then it's time to notify of copy completion.
michael@0 4382 * Victory!
michael@0 4383 */
michael@0 4384 this._sink = null;
michael@0 4385 this._cancelOrDispatchCancelCallback(Cr.NS_OK);
michael@0 4386 }
michael@0 4387 },
michael@0 4388
michael@0 4389
michael@0 4390 // NSIREQUEST
michael@0 4391
michael@0 4392 /** Returns true if the cancel observer hasn't been notified yet. */
michael@0 4393 isPending: function()
michael@0 4394 {
michael@0 4395 return !this._completed;
michael@0 4396 },
michael@0 4397
michael@0 4398 /** Not implemented, don't use! */
michael@0 4399 suspend: notImplemented,
michael@0 4400 /** Not implemented, don't use! */
michael@0 4401 resume: notImplemented,
michael@0 4402
michael@0 4403 /**
michael@0 4404 * Cancels data reading from input, asynchronously writes out any pending
michael@0 4405 * data, and causes the observer to be notified with the given error code when
michael@0 4406 * all writing has finished.
michael@0 4407 *
michael@0 4408 * @param status : nsresult
michael@0 4409 * the status to pass to the observer when data copying has been canceled
michael@0 4410 */
michael@0 4411 cancel: function(status)
michael@0 4412 {
michael@0 4413 dumpn("*** cancel(" + status.toString(16) + ")");
michael@0 4414
michael@0 4415 if (this._canceled)
michael@0 4416 {
michael@0 4417 dumpn("*** suppressing a late cancel");
michael@0 4418 return;
michael@0 4419 }
michael@0 4420
michael@0 4421 this._canceled = true;
michael@0 4422 this.status = status;
michael@0 4423
michael@0 4424 // We could be in the middle of absolutely anything at this point. Both
michael@0 4425 // input and output might still be around, we might have pending data to
michael@0 4426 // write, and in general we know nothing about the state of the world. We
michael@0 4427 // therefore must assume everything's in progress and take everything to its
michael@0 4428 // final steady state (or so far as it can go before we need to finish
michael@0 4429 // writing out remaining data).
michael@0 4430
michael@0 4431 this._doneReadingSource(status);
michael@0 4432 },
michael@0 4433
michael@0 4434
michael@0 4435 // PRIVATE IMPLEMENTATION
michael@0 4436
michael@0 4437 /**
michael@0 4438 * Stop reading input if we haven't already done so, passing e as the status
michael@0 4439 * when closing the stream, and kick off a copy-completion notice if no more
michael@0 4440 * data remains to be written.
michael@0 4441 *
michael@0 4442 * @param e : nsresult
michael@0 4443 * the status to be used when closing the input stream
michael@0 4444 */
michael@0 4445 _doneReadingSource: function(e)
michael@0 4446 {
michael@0 4447 dumpn("*** _doneReadingSource(0x" + e.toString(16) + ")");
michael@0 4448
michael@0 4449 this._finishSource(e);
michael@0 4450 if (this._pendingData.length === 0)
michael@0 4451 this._sink = null;
michael@0 4452 else
michael@0 4453 NS_ASSERT(this._sink !== null, "null output?");
michael@0 4454
michael@0 4455 // If we've written out all data read up to this point, then it's time to
michael@0 4456 // signal completion.
michael@0 4457 if (this._sink === null)
michael@0 4458 {
michael@0 4459 NS_ASSERT(this._pendingData.length === 0, "pending data still?");
michael@0 4460 this._cancelOrDispatchCancelCallback(e);
michael@0 4461 }
michael@0 4462 },
michael@0 4463
michael@0 4464 /**
michael@0 4465 * Stop writing output if we haven't already done so, discard any data that
michael@0 4466 * remained to be sent, close off input if it wasn't already closed, and kick
michael@0 4467 * off a copy-completion notice.
michael@0 4468 *
michael@0 4469 * @param e : nsresult
michael@0 4470 * the status to be used when closing input if it wasn't already closed
michael@0 4471 */
michael@0 4472 _doneWritingToSink: function(e)
michael@0 4473 {
michael@0 4474 dumpn("*** _doneWritingToSink(0x" + e.toString(16) + ")");
michael@0 4475
michael@0 4476 this._pendingData.length = 0;
michael@0 4477 this._sink = null;
michael@0 4478 this._doneReadingSource(e);
michael@0 4479 },
michael@0 4480
michael@0 4481 /**
michael@0 4482 * Completes processing of this copy: either by canceling the copy if it
michael@0 4483 * hasn't already been canceled using the provided status, or by dispatching
michael@0 4484 * the cancel callback event (with the originally provided status, of course)
michael@0 4485 * if it already has been canceled.
michael@0 4486 *
michael@0 4487 * @param status : nsresult
michael@0 4488 * the status code to use to cancel this, if this hasn't already been
michael@0 4489 * canceled
michael@0 4490 */
michael@0 4491 _cancelOrDispatchCancelCallback: function(status)
michael@0 4492 {
michael@0 4493 dumpn("*** _cancelOrDispatchCancelCallback(" + status + ")");
michael@0 4494
michael@0 4495 NS_ASSERT(this._source === null, "should have finished input");
michael@0 4496 NS_ASSERT(this._sink === null, "should have finished output");
michael@0 4497 NS_ASSERT(this._pendingData.length === 0, "should have no pending data");
michael@0 4498
michael@0 4499 if (!this._canceled)
michael@0 4500 {
michael@0 4501 this.cancel(status);
michael@0 4502 return;
michael@0 4503 }
michael@0 4504
michael@0 4505 var self = this;
michael@0 4506 var event =
michael@0 4507 {
michael@0 4508 run: function()
michael@0 4509 {
michael@0 4510 dumpn("*** onStopRequest async callback");
michael@0 4511
michael@0 4512 self._completed = true;
michael@0 4513 try
michael@0 4514 {
michael@0 4515 self._observer.onStopRequest(self, self._context, self.status);
michael@0 4516 }
michael@0 4517 catch (e)
michael@0 4518 {
michael@0 4519 NS_ASSERT(false,
michael@0 4520 "how are we throwing an exception here? we control " +
michael@0 4521 "all the callers! " + e);
michael@0 4522 }
michael@0 4523 }
michael@0 4524 };
michael@0 4525
michael@0 4526 gThreadManager.currentThread.dispatch(event, Ci.nsIThread.DISPATCH_NORMAL);
michael@0 4527 },
michael@0 4528
michael@0 4529 /**
michael@0 4530 * Kicks off another wait for more data to be available from the input stream.
michael@0 4531 */
michael@0 4532 _waitToReadData: function()
michael@0 4533 {
michael@0 4534 dumpn("*** _waitToReadData");
michael@0 4535 this._source.asyncWait(this, 0, Response.SEGMENT_SIZE,
michael@0 4536 gThreadManager.mainThread);
michael@0 4537 },
michael@0 4538
michael@0 4539 /**
michael@0 4540 * Kicks off another wait until data can be written to the output stream.
michael@0 4541 */
michael@0 4542 _waitToWriteData: function()
michael@0 4543 {
michael@0 4544 dumpn("*** _waitToWriteData");
michael@0 4545
michael@0 4546 var pendingData = this._pendingData;
michael@0 4547 NS_ASSERT(pendingData.length > 0, "no pending data to write?");
michael@0 4548 NS_ASSERT(pendingData[0].length > 0, "buffered an empty write?");
michael@0 4549
michael@0 4550 this._sink.asyncWait(this, 0, pendingData[0].length,
michael@0 4551 gThreadManager.mainThread);
michael@0 4552 },
michael@0 4553
michael@0 4554 /**
michael@0 4555 * Kicks off a wait for the sink to which data is being copied to be closed.
michael@0 4556 * We wait for stream closure when we don't have any data to be copied, rather
michael@0 4557 * than waiting to write a specific amount of data. We can't wait to write
michael@0 4558 * data because the sink might be infinitely writable, and if no data appears
michael@0 4559 * in the source for a long time we might have to spin quite a bit waiting to
michael@0 4560 * write, waiting to write again, &c. Waiting on stream closure instead means
michael@0 4561 * we'll get just one notification if the sink dies. Note that when data
michael@0 4562 * starts arriving from the sink we'll resume waiting for data to be written,
michael@0 4563 * dropping this closure-only callback entirely.
michael@0 4564 */
michael@0 4565 _waitForSinkClosure: function()
michael@0 4566 {
michael@0 4567 dumpn("*** _waitForSinkClosure");
michael@0 4568
michael@0 4569 this._sink.asyncWait(this, Ci.nsIAsyncOutputStream.WAIT_CLOSURE_ONLY, 0,
michael@0 4570 gThreadManager.mainThread);
michael@0 4571 },
michael@0 4572
michael@0 4573 /**
michael@0 4574 * Closes input with the given status, if it hasn't already been closed;
michael@0 4575 * otherwise a no-op.
michael@0 4576 *
michael@0 4577 * @param status : nsresult
michael@0 4578 * status code use to close the source stream if necessary
michael@0 4579 */
michael@0 4580 _finishSource: function(status)
michael@0 4581 {
michael@0 4582 dumpn("*** _finishSource(" + status.toString(16) + ")");
michael@0 4583
michael@0 4584 if (this._source !== null)
michael@0 4585 {
michael@0 4586 this._source.closeWithStatus(status);
michael@0 4587 this._source = null;
michael@0 4588 }
michael@0 4589 }
michael@0 4590 };
michael@0 4591
michael@0 4592
michael@0 4593 /**
michael@0 4594 * A container for utility functions used with HTTP headers.
michael@0 4595 */
michael@0 4596 const headerUtils =
michael@0 4597 {
michael@0 4598 /**
michael@0 4599 * Normalizes fieldName (by converting it to lowercase) and ensures it is a
michael@0 4600 * valid header field name (although not necessarily one specified in RFC
michael@0 4601 * 2616).
michael@0 4602 *
michael@0 4603 * @throws NS_ERROR_INVALID_ARG
michael@0 4604 * if fieldName does not match the field-name production in RFC 2616
michael@0 4605 * @returns string
michael@0 4606 * fieldName converted to lowercase if it is a valid header, for characters
michael@0 4607 * where case conversion is possible
michael@0 4608 */
michael@0 4609 normalizeFieldName: function(fieldName)
michael@0 4610 {
michael@0 4611 if (fieldName == "")
michael@0 4612 throw Cr.NS_ERROR_INVALID_ARG;
michael@0 4613
michael@0 4614 for (var i = 0, sz = fieldName.length; i < sz; i++)
michael@0 4615 {
michael@0 4616 if (!IS_TOKEN_ARRAY[fieldName.charCodeAt(i)])
michael@0 4617 {
michael@0 4618 dumpn(fieldName + " is not a valid header field name!");
michael@0 4619 throw Cr.NS_ERROR_INVALID_ARG;
michael@0 4620 }
michael@0 4621 }
michael@0 4622
michael@0 4623 return fieldName.toLowerCase();
michael@0 4624 },
michael@0 4625
michael@0 4626 /**
michael@0 4627 * Ensures that fieldValue is a valid header field value (although not
michael@0 4628 * necessarily as specified in RFC 2616 if the corresponding field name is
michael@0 4629 * part of the HTTP protocol), normalizes the value if it is, and
michael@0 4630 * returns the normalized value.
michael@0 4631 *
michael@0 4632 * @param fieldValue : string
michael@0 4633 * a value to be normalized as an HTTP header field value
michael@0 4634 * @throws NS_ERROR_INVALID_ARG
michael@0 4635 * if fieldValue does not match the field-value production in RFC 2616
michael@0 4636 * @returns string
michael@0 4637 * fieldValue as a normalized HTTP header field value
michael@0 4638 */
michael@0 4639 normalizeFieldValue: function(fieldValue)
michael@0 4640 {
michael@0 4641 // field-value = *( field-content | LWS )
michael@0 4642 // field-content = <the OCTETs making up the field-value
michael@0 4643 // and consisting of either *TEXT or combinations
michael@0 4644 // of token, separators, and quoted-string>
michael@0 4645 // TEXT = <any OCTET except CTLs,
michael@0 4646 // but including LWS>
michael@0 4647 // LWS = [CRLF] 1*( SP | HT )
michael@0 4648 //
michael@0 4649 // quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
michael@0 4650 // qdtext = <any TEXT except <">>
michael@0 4651 // quoted-pair = "\" CHAR
michael@0 4652 // CHAR = <any US-ASCII character (octets 0 - 127)>
michael@0 4653
michael@0 4654 // Any LWS that occurs between field-content MAY be replaced with a single
michael@0 4655 // SP before interpreting the field value or forwarding the message
michael@0 4656 // downstream (section 4.2); we replace 1*LWS with a single SP
michael@0 4657 var val = fieldValue.replace(/(?:(?:\r\n)?[ \t]+)+/g, " ");
michael@0 4658
michael@0 4659 // remove leading/trailing LWS (which has been converted to SP)
michael@0 4660 val = val.replace(/^ +/, "").replace(/ +$/, "");
michael@0 4661
michael@0 4662 // that should have taken care of all CTLs, so val should contain no CTLs
michael@0 4663 for (var i = 0, len = val.length; i < len; i++)
michael@0 4664 if (isCTL(val.charCodeAt(i)))
michael@0 4665 throw Cr.NS_ERROR_INVALID_ARG;
michael@0 4666
michael@0 4667 // XXX disallows quoted-pair where CHAR is a CTL -- will not invalidly
michael@0 4668 // normalize, however, so this can be construed as a tightening of the
michael@0 4669 // spec and not entirely as a bug
michael@0 4670 return val;
michael@0 4671 }
michael@0 4672 };
michael@0 4673
michael@0 4674
michael@0 4675
michael@0 4676 /**
michael@0 4677 * Converts the given string into a string which is safe for use in an HTML
michael@0 4678 * context.
michael@0 4679 *
michael@0 4680 * @param str : string
michael@0 4681 * the string to make HTML-safe
michael@0 4682 * @returns string
michael@0 4683 * an HTML-safe version of str
michael@0 4684 */
michael@0 4685 function htmlEscape(str)
michael@0 4686 {
michael@0 4687 // this is naive, but it'll work
michael@0 4688 var s = "";
michael@0 4689 for (var i = 0; i < str.length; i++)
michael@0 4690 s += "&#" + str.charCodeAt(i) + ";";
michael@0 4691 return s;
michael@0 4692 }
michael@0 4693
michael@0 4694
michael@0 4695 /**
michael@0 4696 * Constructs an object representing an HTTP version (see section 3.1).
michael@0 4697 *
michael@0 4698 * @param versionString
michael@0 4699 * a string of the form "#.#", where # is an non-negative decimal integer with
michael@0 4700 * or without leading zeros
michael@0 4701 * @throws
michael@0 4702 * if versionString does not specify a valid HTTP version number
michael@0 4703 */
michael@0 4704 function nsHttpVersion(versionString)
michael@0 4705 {
michael@0 4706 var matches = /^(\d+)\.(\d+)$/.exec(versionString);
michael@0 4707 if (!matches)
michael@0 4708 throw "Not a valid HTTP version!";
michael@0 4709
michael@0 4710 /** The major version number of this, as a number. */
michael@0 4711 this.major = parseInt(matches[1], 10);
michael@0 4712
michael@0 4713 /** The minor version number of this, as a number. */
michael@0 4714 this.minor = parseInt(matches[2], 10);
michael@0 4715
michael@0 4716 if (isNaN(this.major) || isNaN(this.minor) ||
michael@0 4717 this.major < 0 || this.minor < 0)
michael@0 4718 throw "Not a valid HTTP version!";
michael@0 4719 }
michael@0 4720 nsHttpVersion.prototype =
michael@0 4721 {
michael@0 4722 /**
michael@0 4723 * Returns the standard string representation of the HTTP version represented
michael@0 4724 * by this (e.g., "1.1").
michael@0 4725 */
michael@0 4726 toString: function ()
michael@0 4727 {
michael@0 4728 return this.major + "." + this.minor;
michael@0 4729 },
michael@0 4730
michael@0 4731 /**
michael@0 4732 * Returns true if this represents the same HTTP version as otherVersion,
michael@0 4733 * false otherwise.
michael@0 4734 *
michael@0 4735 * @param otherVersion : nsHttpVersion
michael@0 4736 * the version to compare against this
michael@0 4737 */
michael@0 4738 equals: function (otherVersion)
michael@0 4739 {
michael@0 4740 return this.major == otherVersion.major &&
michael@0 4741 this.minor == otherVersion.minor;
michael@0 4742 },
michael@0 4743
michael@0 4744 /** True if this >= otherVersion, false otherwise. */
michael@0 4745 atLeast: function(otherVersion)
michael@0 4746 {
michael@0 4747 return this.major > otherVersion.major ||
michael@0 4748 (this.major == otherVersion.major &&
michael@0 4749 this.minor >= otherVersion.minor);
michael@0 4750 }
michael@0 4751 };
michael@0 4752
michael@0 4753 nsHttpVersion.HTTP_1_0 = new nsHttpVersion("1.0");
michael@0 4754 nsHttpVersion.HTTP_1_1 = new nsHttpVersion("1.1");
michael@0 4755
michael@0 4756
michael@0 4757 /**
michael@0 4758 * An object which stores HTTP headers for a request or response.
michael@0 4759 *
michael@0 4760 * Note that since headers are case-insensitive, this object converts headers to
michael@0 4761 * lowercase before storing them. This allows the getHeader and hasHeader
michael@0 4762 * methods to work correctly for any case of a header, but it means that the
michael@0 4763 * values returned by .enumerator may not be equal case-sensitively to the
michael@0 4764 * values passed to setHeader when adding headers to this.
michael@0 4765 */
michael@0 4766 function nsHttpHeaders()
michael@0 4767 {
michael@0 4768 /**
michael@0 4769 * A hash of headers, with header field names as the keys and header field
michael@0 4770 * values as the values. Header field names are case-insensitive, but upon
michael@0 4771 * insertion here they are converted to lowercase. Header field values are
michael@0 4772 * normalized upon insertion to contain no leading or trailing whitespace.
michael@0 4773 *
michael@0 4774 * Note also that per RFC 2616, section 4.2, two headers with the same name in
michael@0 4775 * a message may be treated as one header with the same field name and a field
michael@0 4776 * value consisting of the separate field values joined together with a "," in
michael@0 4777 * their original order. This hash stores multiple headers with the same name
michael@0 4778 * in this manner.
michael@0 4779 */
michael@0 4780 this._headers = {};
michael@0 4781 }
michael@0 4782 nsHttpHeaders.prototype =
michael@0 4783 {
michael@0 4784 /**
michael@0 4785 * Sets the header represented by name and value in this.
michael@0 4786 *
michael@0 4787 * @param name : string
michael@0 4788 * the header name
michael@0 4789 * @param value : string
michael@0 4790 * the header value
michael@0 4791 * @throws NS_ERROR_INVALID_ARG
michael@0 4792 * if name or value is not a valid header component
michael@0 4793 */
michael@0 4794 setHeader: function(fieldName, fieldValue, merge)
michael@0 4795 {
michael@0 4796 var name = headerUtils.normalizeFieldName(fieldName);
michael@0 4797 var value = headerUtils.normalizeFieldValue(fieldValue);
michael@0 4798
michael@0 4799 // The following three headers are stored as arrays because their real-world
michael@0 4800 // syntax prevents joining individual headers into a single header using
michael@0 4801 // ",". See also <http://hg.mozilla.org/mozilla-central/diff/9b2a99adc05e/netwerk/protocol/http/src/nsHttpHeaderArray.cpp#l77>
michael@0 4802 if (merge && name in this._headers)
michael@0 4803 {
michael@0 4804 if (name === "www-authenticate" ||
michael@0 4805 name === "proxy-authenticate" ||
michael@0 4806 name === "set-cookie")
michael@0 4807 {
michael@0 4808 this._headers[name].push(value);
michael@0 4809 }
michael@0 4810 else
michael@0 4811 {
michael@0 4812 this._headers[name][0] += "," + value;
michael@0 4813 NS_ASSERT(this._headers[name].length === 1,
michael@0 4814 "how'd a non-special header have multiple values?")
michael@0 4815 }
michael@0 4816 }
michael@0 4817 else
michael@0 4818 {
michael@0 4819 this._headers[name] = [value];
michael@0 4820 }
michael@0 4821 },
michael@0 4822
michael@0 4823 /**
michael@0 4824 * Returns the value for the header specified by this.
michael@0 4825 *
michael@0 4826 * @throws NS_ERROR_INVALID_ARG
michael@0 4827 * if fieldName does not constitute a valid header field name
michael@0 4828 * @throws NS_ERROR_NOT_AVAILABLE
michael@0 4829 * if the given header does not exist in this
michael@0 4830 * @returns string
michael@0 4831 * the field value for the given header, possibly with non-semantic changes
michael@0 4832 * (i.e., leading/trailing whitespace stripped, whitespace runs replaced
michael@0 4833 * with spaces, etc.) at the option of the implementation; multiple
michael@0 4834 * instances of the header will be combined with a comma, except for
michael@0 4835 * the three headers noted in the description of getHeaderValues
michael@0 4836 */
michael@0 4837 getHeader: function(fieldName)
michael@0 4838 {
michael@0 4839 return this.getHeaderValues(fieldName).join("\n");
michael@0 4840 },
michael@0 4841
michael@0 4842 /**
michael@0 4843 * Returns the value for the header specified by fieldName as an array.
michael@0 4844 *
michael@0 4845 * @throws NS_ERROR_INVALID_ARG
michael@0 4846 * if fieldName does not constitute a valid header field name
michael@0 4847 * @throws NS_ERROR_NOT_AVAILABLE
michael@0 4848 * if the given header does not exist in this
michael@0 4849 * @returns [string]
michael@0 4850 * an array of all the header values in this for the given
michael@0 4851 * header name. Header values will generally be collapsed
michael@0 4852 * into a single header by joining all header values together
michael@0 4853 * with commas, but certain headers (Proxy-Authenticate,
michael@0 4854 * WWW-Authenticate, and Set-Cookie) violate the HTTP spec
michael@0 4855 * and cannot be collapsed in this manner. For these headers
michael@0 4856 * only, the returned array may contain multiple elements if
michael@0 4857 * that header has been added more than once.
michael@0 4858 */
michael@0 4859 getHeaderValues: function(fieldName)
michael@0 4860 {
michael@0 4861 var name = headerUtils.normalizeFieldName(fieldName);
michael@0 4862
michael@0 4863 if (name in this._headers)
michael@0 4864 return this._headers[name];
michael@0 4865 else
michael@0 4866 throw Cr.NS_ERROR_NOT_AVAILABLE;
michael@0 4867 },
michael@0 4868
michael@0 4869 /**
michael@0 4870 * Returns true if a header with the given field name exists in this, false
michael@0 4871 * otherwise.
michael@0 4872 *
michael@0 4873 * @param fieldName : string
michael@0 4874 * the field name whose existence is to be determined in this
michael@0 4875 * @throws NS_ERROR_INVALID_ARG
michael@0 4876 * if fieldName does not constitute a valid header field name
michael@0 4877 * @returns boolean
michael@0 4878 * true if the header's present, false otherwise
michael@0 4879 */
michael@0 4880 hasHeader: function(fieldName)
michael@0 4881 {
michael@0 4882 var name = headerUtils.normalizeFieldName(fieldName);
michael@0 4883 return (name in this._headers);
michael@0 4884 },
michael@0 4885
michael@0 4886 /**
michael@0 4887 * Returns a new enumerator over the field names of the headers in this, as
michael@0 4888 * nsISupportsStrings. The names returned will be in lowercase, regardless of
michael@0 4889 * how they were input using setHeader (header names are case-insensitive per
michael@0 4890 * RFC 2616).
michael@0 4891 */
michael@0 4892 get enumerator()
michael@0 4893 {
michael@0 4894 var headers = [];
michael@0 4895 for (var i in this._headers)
michael@0 4896 {
michael@0 4897 var supports = new SupportsString();
michael@0 4898 supports.data = i;
michael@0 4899 headers.push(supports);
michael@0 4900 }
michael@0 4901
michael@0 4902 return new nsSimpleEnumerator(headers);
michael@0 4903 }
michael@0 4904 };
michael@0 4905
michael@0 4906
michael@0 4907 /**
michael@0 4908 * Constructs an nsISimpleEnumerator for the given array of items.
michael@0 4909 *
michael@0 4910 * @param items : Array
michael@0 4911 * the items, which must all implement nsISupports
michael@0 4912 */
michael@0 4913 function nsSimpleEnumerator(items)
michael@0 4914 {
michael@0 4915 this._items = items;
michael@0 4916 this._nextIndex = 0;
michael@0 4917 }
michael@0 4918 nsSimpleEnumerator.prototype =
michael@0 4919 {
michael@0 4920 hasMoreElements: function()
michael@0 4921 {
michael@0 4922 return this._nextIndex < this._items.length;
michael@0 4923 },
michael@0 4924 getNext: function()
michael@0 4925 {
michael@0 4926 if (!this.hasMoreElements())
michael@0 4927 throw Cr.NS_ERROR_NOT_AVAILABLE;
michael@0 4928
michael@0 4929 return this._items[this._nextIndex++];
michael@0 4930 },
michael@0 4931 QueryInterface: function(aIID)
michael@0 4932 {
michael@0 4933 if (Ci.nsISimpleEnumerator.equals(aIID) ||
michael@0 4934 Ci.nsISupports.equals(aIID))
michael@0 4935 return this;
michael@0 4936
michael@0 4937 throw Cr.NS_ERROR_NO_INTERFACE;
michael@0 4938 }
michael@0 4939 };
michael@0 4940
michael@0 4941
michael@0 4942 /**
michael@0 4943 * A representation of the data in an HTTP request.
michael@0 4944 *
michael@0 4945 * @param port : uint
michael@0 4946 * the port on which the server receiving this request runs
michael@0 4947 */
michael@0 4948 function Request(port)
michael@0 4949 {
michael@0 4950 /** Method of this request, e.g. GET or POST. */
michael@0 4951 this._method = "";
michael@0 4952
michael@0 4953 /** Path of the requested resource; empty paths are converted to '/'. */
michael@0 4954 this._path = "";
michael@0 4955
michael@0 4956 /** Query string, if any, associated with this request (not including '?'). */
michael@0 4957 this._queryString = "";
michael@0 4958
michael@0 4959 /** Scheme of requested resource, usually http, always lowercase. */
michael@0 4960 this._scheme = "http";
michael@0 4961
michael@0 4962 /** Hostname on which the requested resource resides. */
michael@0 4963 this._host = undefined;
michael@0 4964
michael@0 4965 /** Port number over which the request was received. */
michael@0 4966 this._port = port;
michael@0 4967
michael@0 4968 var bodyPipe = new Pipe(false, false, 0, PR_UINT32_MAX, null);
michael@0 4969
michael@0 4970 /** Stream from which data in this request's body may be read. */
michael@0 4971 this._bodyInputStream = bodyPipe.inputStream;
michael@0 4972
michael@0 4973 /** Stream to which data in this request's body is written. */
michael@0 4974 this._bodyOutputStream = bodyPipe.outputStream;
michael@0 4975
michael@0 4976 /**
michael@0 4977 * The headers in this request.
michael@0 4978 */
michael@0 4979 this._headers = new nsHttpHeaders();
michael@0 4980
michael@0 4981 /**
michael@0 4982 * For the addition of ad-hoc properties and new functionality without having
michael@0 4983 * to change nsIHttpRequest every time; currently lazily created, as its only
michael@0 4984 * use is in directory listings.
michael@0 4985 */
michael@0 4986 this._bag = null;
michael@0 4987 }
michael@0 4988 Request.prototype =
michael@0 4989 {
michael@0 4990 // SERVER METADATA
michael@0 4991
michael@0 4992 //
michael@0 4993 // see nsIHttpRequest.scheme
michael@0 4994 //
michael@0 4995 get scheme()
michael@0 4996 {
michael@0 4997 return this._scheme;
michael@0 4998 },
michael@0 4999
michael@0 5000 //
michael@0 5001 // see nsIHttpRequest.host
michael@0 5002 //
michael@0 5003 get host()
michael@0 5004 {
michael@0 5005 return this._host;
michael@0 5006 },
michael@0 5007
michael@0 5008 //
michael@0 5009 // see nsIHttpRequest.port
michael@0 5010 //
michael@0 5011 get port()
michael@0 5012 {
michael@0 5013 return this._port;
michael@0 5014 },
michael@0 5015
michael@0 5016 // REQUEST LINE
michael@0 5017
michael@0 5018 //
michael@0 5019 // see nsIHttpRequest.method
michael@0 5020 //
michael@0 5021 get method()
michael@0 5022 {
michael@0 5023 return this._method;
michael@0 5024 },
michael@0 5025
michael@0 5026 //
michael@0 5027 // see nsIHttpRequest.httpVersion
michael@0 5028 //
michael@0 5029 get httpVersion()
michael@0 5030 {
michael@0 5031 return this._httpVersion.toString();
michael@0 5032 },
michael@0 5033
michael@0 5034 //
michael@0 5035 // see nsIHttpRequest.path
michael@0 5036 //
michael@0 5037 get path()
michael@0 5038 {
michael@0 5039 return this._path;
michael@0 5040 },
michael@0 5041
michael@0 5042 //
michael@0 5043 // see nsIHttpRequest.queryString
michael@0 5044 //
michael@0 5045 get queryString()
michael@0 5046 {
michael@0 5047 return this._queryString;
michael@0 5048 },
michael@0 5049
michael@0 5050 // HEADERS
michael@0 5051
michael@0 5052 //
michael@0 5053 // see nsIHttpRequest.getHeader
michael@0 5054 //
michael@0 5055 getHeader: function(name)
michael@0 5056 {
michael@0 5057 return this._headers.getHeader(name);
michael@0 5058 },
michael@0 5059
michael@0 5060 //
michael@0 5061 // see nsIHttpRequest.hasHeader
michael@0 5062 //
michael@0 5063 hasHeader: function(name)
michael@0 5064 {
michael@0 5065 return this._headers.hasHeader(name);
michael@0 5066 },
michael@0 5067
michael@0 5068 //
michael@0 5069 // see nsIHttpRequest.headers
michael@0 5070 //
michael@0 5071 get headers()
michael@0 5072 {
michael@0 5073 return this._headers.enumerator;
michael@0 5074 },
michael@0 5075
michael@0 5076 //
michael@0 5077 // see nsIPropertyBag.enumerator
michael@0 5078 //
michael@0 5079 get enumerator()
michael@0 5080 {
michael@0 5081 this._ensurePropertyBag();
michael@0 5082 return this._bag.enumerator;
michael@0 5083 },
michael@0 5084
michael@0 5085 //
michael@0 5086 // see nsIHttpRequest.headers
michael@0 5087 //
michael@0 5088 get bodyInputStream()
michael@0 5089 {
michael@0 5090 return this._bodyInputStream;
michael@0 5091 },
michael@0 5092
michael@0 5093 //
michael@0 5094 // see nsIPropertyBag.getProperty
michael@0 5095 //
michael@0 5096 getProperty: function(name)
michael@0 5097 {
michael@0 5098 this._ensurePropertyBag();
michael@0 5099 return this._bag.getProperty(name);
michael@0 5100 },
michael@0 5101
michael@0 5102
michael@0 5103 // NSISUPPORTS
michael@0 5104
michael@0 5105 //
michael@0 5106 // see nsISupports.QueryInterface
michael@0 5107 //
michael@0 5108 QueryInterface: function(iid)
michael@0 5109 {
michael@0 5110 if (iid.equals(Ci.nsIHttpRequest) || iid.equals(Ci.nsISupports))
michael@0 5111 return this;
michael@0 5112
michael@0 5113 throw Cr.NS_ERROR_NO_INTERFACE;
michael@0 5114 },
michael@0 5115
michael@0 5116
michael@0 5117 // PRIVATE IMPLEMENTATION
michael@0 5118
michael@0 5119 /** Ensures a property bag has been created for ad-hoc behaviors. */
michael@0 5120 _ensurePropertyBag: function()
michael@0 5121 {
michael@0 5122 if (!this._bag)
michael@0 5123 this._bag = new WritablePropertyBag();
michael@0 5124 }
michael@0 5125 };
michael@0 5126
michael@0 5127
michael@0 5128 // XPCOM trappings
michael@0 5129 if ("XPCOMUtils" in this && // Firefox 3.6 doesn't load XPCOMUtils in this scope for some reason...
michael@0 5130 "generateNSGetFactory" in XPCOMUtils) {
michael@0 5131 var NSGetFactory = XPCOMUtils.generateNSGetFactory([nsHttpServer]);
michael@0 5132 }
michael@0 5133
michael@0 5134
michael@0 5135
michael@0 5136 /**
michael@0 5137 * Creates a new HTTP server listening for loopback traffic on the given port,
michael@0 5138 * starts it, and runs the server until the server processes a shutdown request,
michael@0 5139 * spinning an event loop so that events posted by the server's socket are
michael@0 5140 * processed.
michael@0 5141 *
michael@0 5142 * This method is primarily intended for use in running this script from within
michael@0 5143 * xpcshell and running a functional HTTP server without having to deal with
michael@0 5144 * non-essential details.
michael@0 5145 *
michael@0 5146 * Note that running multiple servers using variants of this method probably
michael@0 5147 * doesn't work, simply due to how the internal event loop is spun and stopped.
michael@0 5148 *
michael@0 5149 * @note
michael@0 5150 * This method only works with Mozilla 1.9 (i.e., Firefox 3 or trunk code);
michael@0 5151 * you should use this server as a component in Mozilla 1.8.
michael@0 5152 * @param port
michael@0 5153 * the port on which the server will run, or -1 if there exists no preference
michael@0 5154 * for a specific port; note that attempting to use some values for this
michael@0 5155 * parameter (particularly those below 1024) may cause this method to throw or
michael@0 5156 * may result in the server being prematurely shut down
michael@0 5157 * @param basePath
michael@0 5158 * a local directory from which requests will be served (i.e., if this is
michael@0 5159 * "/home/jwalden/" then a request to /index.html will load
michael@0 5160 * /home/jwalden/index.html); if this is omitted, only the default URLs in
michael@0 5161 * this server implementation will be functional
michael@0 5162 */
michael@0 5163 function server(port, basePath)
michael@0 5164 {
michael@0 5165 if (basePath)
michael@0 5166 {
michael@0 5167 var lp = Cc["@mozilla.org/file/local;1"]
michael@0 5168 .createInstance(Ci.nsILocalFile);
michael@0 5169 lp.initWithPath(basePath);
michael@0 5170 }
michael@0 5171
michael@0 5172 // if you're running this, you probably want to see debugging info
michael@0 5173 DEBUG = true;
michael@0 5174
michael@0 5175 var srv = new nsHttpServer();
michael@0 5176 if (lp)
michael@0 5177 srv.registerDirectory("/", lp);
michael@0 5178 srv.registerContentType("sjs", SJS_TYPE);
michael@0 5179 srv.start(port);
michael@0 5180
michael@0 5181 var thread = gThreadManager.currentThread;
michael@0 5182 while (!srv.isStopped())
michael@0 5183 thread.processNextEvent(true);
michael@0 5184
michael@0 5185 // get rid of any pending requests
michael@0 5186 while (thread.hasPendingEvents())
michael@0 5187 thread.processNextEvent(true);
michael@0 5188
michael@0 5189 DEBUG = false;
michael@0 5190 }
michael@0 5191
michael@0 5192 function startServerAsync(port, basePath)
michael@0 5193 {
michael@0 5194 if (basePath)
michael@0 5195 {
michael@0 5196 var lp = Cc["@mozilla.org/file/local;1"]
michael@0 5197 .createInstance(Ci.nsILocalFile);
michael@0 5198 lp.initWithPath(basePath);
michael@0 5199 }
michael@0 5200
michael@0 5201 var srv = new nsHttpServer();
michael@0 5202 if (lp)
michael@0 5203 srv.registerDirectory("/", lp);
michael@0 5204 srv.registerContentType("sjs", "sjs");
michael@0 5205 srv.start(port);
michael@0 5206 return srv;
michael@0 5207 }
michael@0 5208
michael@0 5209 exports.nsHttpServer = nsHttpServer;
michael@0 5210 exports.ScriptableInputStream = ScriptableInputStream;
michael@0 5211 exports.server = server;
michael@0 5212 exports.startServerAsync = startServerAsync;

mercurial