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

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

mercurial