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