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