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