netwerk/test/httpserver/httpd.js

Wed, 31 Dec 2014 06:55:46 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:46 +0100
changeset 1
ca08bd8f51b2
permissions
-rw-r--r--

Added tag TORBROWSER_REPLICA for changeset 6474c204b198

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

mercurial