httpd.js
Go to the documentation of this file.
1 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:set ts=2 sw=2 sts=2 et: */
3 /* ***** BEGIN LICENSE BLOCK *****
4  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
5  *
6  * The contents of this file are subject to the Mozilla Public License Version
7  * 1.1 (the "License"); you may not use this file except in compliance with
8  * the License. You may obtain a copy of the License at
9  * http://www.mozilla.org/MPL/
10  *
11  * Software distributed under the License is distributed on an "AS IS" basis,
12  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
13  * for the specific language governing rights and limitations under the
14  * License.
15  *
16  * The Original Code is the httpd.js server.
17  *
18  * The Initial Developer of the Original Code is
19  * Mozilla Corporation.
20  * Portions created by the Initial Developer are Copyright (C) 2006
21  * the Initial Developer. All Rights Reserved.
22  *
23  * Contributor(s):
24  * Darin Fisher (v1, netwerk/test/TestServ.js)
25  * Christian Biesinger (v2, netwerk/test/unit/head_http_server.js)
26  * Jeff Walden <jwalden+code@mit.edu> (v3, netwerk/test/httpserver/httpd.js)
27  * Robert Sayre <sayrer@gmail.com>
28  *
29  * Alternatively, the contents of this file may be used under the terms of
30  * either the GNU General Public License Version 2 or later (the "GPL"), or
31  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
32  * in which case the provisions of the GPL or the LGPL are applicable instead
33  * of those above. If you wish to allow use of your version of this file only
34  * under the terms of either the GPL or the LGPL, and not to allow others to
35  * use your version of this file under the terms of the MPL, indicate your
36  * decision by deleting the provisions above and replace them with the notice
37  * and other provisions required by the GPL or the LGPL. If you do not delete
38  * the provisions above, a recipient may use your version of this file under
39  * the terms of any one of the MPL, the GPL or the LGPL.
40  *
41  * ***** END LICENSE BLOCK ***** */
42 
43 /*
44  * An implementation of an HTTP server both as a loadable script and as an XPCOM
45  * component. See the accompanying README file for user documentation on
46  * httpd.js.
47  */
48 
49 const Cc = Components.classes;
50 const Ci = Components.interfaces;
51 const Cr = Components.results;
52 const Cu = Components.utils;
53 const CC = Components.Constructor;
54 
55 const PR_UINT32_MAX = Math.pow(2, 32) - 1;
56 
57 const EXPORTED_SYMBOLS = ["server", "nsHttpServer", "HttpError"];
58 
60 var DEBUG = false; // non-const *only* so tweakable in server tests
61 
62 var gGlobalObject = this;
63 
70 function NS_ASSERT(cond, msg)
71 {
72  if (DEBUG && !cond)
73  {
74  dumpn("###!!!");
75  dumpn("###!!! ASSERTION" + (msg ? ": " + msg : "!"));
76  dumpn("###!!! Stack follows:");
77 
78  var stack = new Error().stack.split(/\n/);
79  dumpn(stack.map(function(val) { return "###!!! " + val; }).join("\n"));
80 
81  throw Cr.NS_ERROR_ABORT;
82  }
83 }
84 
86 function HttpError(code, description)
87 {
88  this.code = code;
89  this.description = description;
90 }
91 HttpError.prototype =
92 {
93  toString: function()
94  {
95  return this.code + " " + this.description;
96  }
97 };
98 
102 const HTTP_400 = new HttpError(400, "Bad Request");
103 const HTTP_401 = new HttpError(401, "Unauthorized");
104 const HTTP_402 = new HttpError(402, "Payment Required");
105 const HTTP_403 = new HttpError(403, "Forbidden");
106 const HTTP_404 = new HttpError(404, "Not Found");
107 const HTTP_405 = new HttpError(405, "Method Not Allowed");
108 const HTTP_406 = new HttpError(406, "Not Acceptable");
109 const HTTP_407 = new HttpError(407, "Proxy Authentication Required");
110 const HTTP_408 = new HttpError(408, "Request Timeout");
111 const HTTP_409 = new HttpError(409, "Conflict");
112 const HTTP_410 = new HttpError(410, "Gone");
113 const HTTP_411 = new HttpError(411, "Length Required");
114 const HTTP_412 = new HttpError(412, "Precondition Failed");
115 const HTTP_413 = new HttpError(413, "Request Entity Too Large");
116 const HTTP_414 = new HttpError(414, "Request-URI Too Long");
117 const HTTP_415 = new HttpError(415, "Unsupported Media Type");
118 const HTTP_416 = new HttpError(416, "Requested Range Not Satisfiable");
119 const HTTP_417 = new HttpError(417, "Expectation Failed");
120 
121 const HTTP_500 = new HttpError(500, "Internal Server Error");
122 const HTTP_501 = new HttpError(501, "Not Implemented");
123 const HTTP_502 = new HttpError(502, "Bad Gateway");
124 const HTTP_503 = new HttpError(503, "Service Unavailable");
125 const HTTP_504 = new HttpError(504, "Gateway Timeout");
126 const HTTP_505 = new HttpError(505, "HTTP Version Not Supported");
127 
129 function array2obj(arr)
130 {
131  var obj = {};
132  for (var i = 0; i < arr.length; i++)
133  obj[arr[i]] = arr[i];
134  return obj;
135 }
136 
138 function range(x, y)
139 {
140  var arr = [];
141  for (var i = x; i <= y; i++)
142  arr.push(i);
143  return arr;
144 }
145 
147 const HTTP_ERROR_CODES = array2obj(range(400, 417).concat(range(500, 505)));
148 
149 
159 const HIDDEN_CHAR = "^";
160 
165 const HEADERS_SUFFIX = HIDDEN_CHAR + "headers" + HIDDEN_CHAR;
166 
168 const SJS_TYPE = "sjs";
169 
170 
172 function dumpn(str)
173 {
174  if (DEBUG)
175  dump(str + "\n");
176 }
177 
179 function dumpStack()
180 {
181  // peel off the frames for dumpStack() and Error()
182  var stack = new Error().stack.split(/\n/).slice(2);
183  stack.forEach(dumpn);
184 }
185 
186 
189 
193 {
194  if (!gRootPrefBranch)
195  {
196  gRootPrefBranch = Cc["@mozilla.org/preferences-service;1"]
197  .getService(Ci.nsIPrefBranch);
198  }
199  return gRootPrefBranch;
200 }
201 
207 const ServerSocket = CC("@mozilla.org/network/server-socket;1",
208  "nsIServerSocket",
209  "init");
210 const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
211  "nsIBinaryInputStream",
212  "setInputStream");
213 const BinaryOutputStream = CC("@mozilla.org/binaryoutputstream;1",
214  "nsIBinaryOutputStream",
215  "setOutputStream");
216 const ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1",
217  "nsIScriptableInputStream",
218  "init");
219 const Pipe = CC("@mozilla.org/pipe;1",
220  "nsIPipe",
221  "init");
222 const FileInputStream = CC("@mozilla.org/network/file-input-stream;1",
223  "nsIFileInputStream",
224  "init");
225 const ConverterInputStream = CC("@mozilla.org/intl/converter-input-stream;1",
226  "nsIConverterInputStream",
227  "init");
228 const WritablePropertyBag = CC("@mozilla.org/hash-property-bag;1",
229  "nsIWritablePropertyBag2");
230 const SupportsString = CC("@mozilla.org/supports-string;1",
231  "nsISupportsString");
232 
233 
243 {
244  //
245  // rfc1123-date = wkday "," SP date1 SP time SP "GMT"
246  // date1 = 2DIGIT SP month SP 4DIGIT
247  // ; day month year (e.g., 02 Jun 1982)
248  // time = 2DIGIT ":" 2DIGIT ":" 2DIGIT
249  // ; 00:00:00 - 23:59:59
250  // wkday = "Mon" | "Tue" | "Wed"
251  // | "Thu" | "Fri" | "Sat" | "Sun"
252  // month = "Jan" | "Feb" | "Mar" | "Apr"
253  // | "May" | "Jun" | "Jul" | "Aug"
254  // | "Sep" | "Oct" | "Nov" | "Dec"
255  //
256 
257  const wkdayStrings = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
258  const monthStrings = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
259  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
260 
270  function toTime(date)
271  {
272  var hrs = date.getUTCHours();
273  var rv = (hrs < 10) ? "0" + hrs : hrs;
274 
275  var mins = date.getUTCMinutes();
276  rv += ":";
277  rv += (mins < 10) ? "0" + mins : mins;
278 
279  var secs = date.getUTCSeconds();
280  rv += ":";
281  rv += (secs < 10) ? "0" + secs : secs;
282 
283  return rv;
284  }
285 
295  function toDate1(date)
296  {
297  var day = date.getUTCDate();
298  var month = date.getUTCMonth();
299  var year = date.getUTCFullYear();
300 
301  var rv = (day < 10) ? "0" + day : day;
302  rv += " " + monthStrings[month];
303  rv += " " + year;
304 
305  return rv;
306  }
307 
308  date = new Date(date);
309 
310  const fmtString = "%wkday%, %date1% %time% GMT";
311  var rv = fmtString.replace("%wkday%", wkdayStrings[date.getUTCDay()]);
312  rv = rv.replace("%time%", toTime(date));
313  return rv.replace("%date1%", toDate1(date));
314 }
315 
321 function printObj(o, showMembers)
322 {
323  var s = "******************************\n";
324  s += "o = {\n";
325  for (var i in o)
326  {
327  if (typeof(i) != "string" ||
328  (showMembers || (i.length > 0 && i[0] != "_")))
329  s+= " " + i + ": " + o[i] + ",\n";
330  }
331  s += " };\n";
332  s += "******************************";
333  dumpn(s);
334 }
335 
339 function nsHttpServer()
340 {
341  if (!gThreadManager)
342  gThreadManager = Cc["@mozilla.org/thread-manager;1"].getService();
343 
345  this._port = undefined;
346 
348  this._socket = null;
349 
351  this._handler = new ServerHandler(this);
352 
354  this._identity = new ServerIdentity();
355 
359  this._doQuit = false;
360 
365  this._socketClosed = true;
366 
372  this._connectionGen = 0;
373 
378  this._connections = {};
379 }
380 nsHttpServer.prototype =
381 {
382  // NSISERVERSOCKETLISTENER
383 
394  onSocketAccepted: function(socket, trans)
395  {
396  dumpn("*** onSocketAccepted(socket=" + socket + ", trans=" + trans + ")");
397 
398  dumpn(">>> new connection on " + trans.host + ":" + trans.port);
399 
400  const SEGMENT_SIZE = 8192;
401  const SEGMENT_COUNT = 1024;
402  try
403  {
404  var input = trans.openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT)
405  .QueryInterface(Ci.nsIAsyncInputStream);
406  var output = trans.openOutputStream(Ci.nsITransport.OPEN_BLOCKING, 0, 0);
407  }
408  catch (e)
409  {
410  dumpn("*** error opening transport streams: " + e);
411  trans.close(Cr.NS_BINDING_ABORTED);
412  return;
413  }
414 
415  var connectionNumber = ++this._connectionGen;
416 
417  try
418  {
419  var conn = new Connection(input, output, this, socket.port, trans.port,
420  connectionNumber);
421  var reader = new RequestReader(conn);
422 
423  // XXX add request timeout functionality here!
424 
425  // Note: must use main thread here, or we might get a GC that will cause
426  // threadsafety assertions. We really need to fix XPConnect so that
427  // you can actually do things in multi-threaded JS. :-(
428  input.asyncWait(reader, 0, 0, gThreadManager.mainThread);
429  }
430  catch (e)
431  {
432  // Assume this connection can't be salvaged and bail on it completely;
433  // don't attempt to close it so that we can assert that any connection
434  // being closed is in this._connections.
435  dumpn("*** error in initial request-processing stages: " + e);
436  trans.close(Cr.NS_BINDING_ABORTED);
437  return;
438  }
439 
440  this._connections[connectionNumber] = conn;
441  dumpn("*** starting connection " + connectionNumber);
442  },
443 
454  onStopListening: function(socket, status)
455  {
456  dumpn(">>> shutting down server on port " + socket.port);
457  this._socketClosed = true;
458  if (!this._hasOpenConnections())
459  {
460  dumpn("*** no open connections, notifying async from onStopListening");
461 
462  // Notify asynchronously so that any pending teardown in stop() has a
463  // chance to run first.
464  var self = this;
465  var stopEvent =
466  {
467  run: function()
468  {
469  dumpn("*** _notifyStopped async callback");
470  self._notifyStopped();
471  }
472  };
473  gThreadManager.currentThread
474  .dispatch(stopEvent, Ci.nsIThread.DISPATCH_NORMAL);
475  }
476  },
477 
478  // NSIHTTPSERVER
479 
480  //
481  // see nsIHttpServer.start
482  //
483  start: function(port)
484  {
485  if (this._socket)
486  throw Cr.NS_ERROR_ALREADY_INITIALIZED;
487 
488  this._port = port;
489  this._doQuit = this._socketClosed = false;
490 
491  // The listen queue needs to be long enough to handle
492  // network.http.max-connections-per-server concurrent connections,
493  // plus a safety margin in case some other process is talking to
494  // the server as well.
495  var prefs = getRootPrefBranch();
496  var maxConnections =
497  prefs.getIntPref("network.http.max-connections-per-server") + 5;
498 
499  try
500  {
501  var socket = new ServerSocket(this._port,
502  true, // loopback only
503  maxConnections);
504  dumpn(">>> listening on port " + socket.port + ", " + maxConnections +
505  " pending connections");
506  socket.asyncListen(this);
507  this._identity._initialize(port, true);
508  this._socket = socket;
509  }
510  catch (e)
511  {
512  dumpn("!!! could not start server on port " + port + ": " + e);
513  throw Cr.NS_ERROR_NOT_AVAILABLE;
514  }
515  },
516 
517  //
518  // see nsIHttpServer.stop
519  //
520  stop: function(callback)
521  {
522  if (!callback)
523  throw Cr.NS_ERROR_NULL_POINTER;
524  if (!this._socket)
525  throw Cr.NS_ERROR_UNEXPECTED;
526 
527  this._stopCallback = typeof callback === "function"
528  ? callback
529  : function() { callback.onStopped(); };
530 
531  dumpn(">>> stopping listening on port " + this._socket.port);
532  this._socket.close();
533  this._socket = null;
534 
535  // We can't have this identity any more, and the port on which we're running
536  // this server now could be meaningless the next time around.
537  this._identity._teardown();
538 
539  this._doQuit = false;
540 
541  // socket-close notification and pending request completion happen async
542  },
543 
544  //
545  // see nsIHttpServer.registerFile
546  //
547  registerFile: function(path, file)
548  {
549  if (file && (!file.exists() || file.isDirectory()))
550  throw Cr.NS_ERROR_INVALID_ARG;
551 
552  this._handler.registerFile(path, file);
553  },
554 
555  //
556  // see nsIHttpServer.registerDirectory
557  //
558  registerDirectory: function(path, directory)
559  {
560  // XXX true path validation!
561  if (path.charAt(0) != "/" ||
562  path.charAt(path.length - 1) != "/" ||
563  (directory &&
564  (!directory.exists() || !directory.isDirectory())))
565  throw Cr.NS_ERROR_INVALID_ARG;
566 
567  // XXX determine behavior of non-existent /foo/bar when a /foo/bar/ mapping
568  // exists!
569 
570  this._handler.registerDirectory(path, directory);
571  },
572 
573  //
574  // see nsIHttpServer.registerPathHandler
575  //
576  registerPathHandler: function(path, handler)
577  {
578  this._handler.registerPathHandler(path, handler);
579  },
580 
581  //
582  // see nsIHttpServer.registerPrefixHandler
583  //
584  registerPrefixHandler: function(prefix, handler)
585  {
586  this._handler.registerPrefixHandler(prefix, handler);
587  },
588 
589  //
590  // see nsIHttpServer.registerErrorHandler
591  //
592  registerErrorHandler: function(code, handler)
593  {
594  this._handler.registerErrorHandler(code, handler);
595  },
596 
597  //
598  // see nsIHttpServer.setIndexHandler
599  //
600  setIndexHandler: function(handler)
601  {
602  this._handler.setIndexHandler(handler);
603  },
604 
605  //
606  // see nsIHttpServer.registerContentType
607  //
608  registerContentType: function(ext, type)
609  {
610  this._handler.registerContentType(ext, type);
611  },
612 
613  //
614  // see nsIHttpServer.serverIdentity
615  //
616  get identity()
617  {
618  return this._identity;
619  },
620 
621  //
622  // see nsIHttpServer.getState
623  //
624  getState: function(path, k)
625  {
626  return this._handler._getState(path, k);
627  },
628 
629  //
630  // see nsIHttpServer.setState
631  //
632  setState: function(path, k, v)
633  {
634  return this._handler._setState(path, k, v);
635  },
636 
637  //
638  // see nsIHttpServer.getSharedState
639  //
640  getSharedState: function(k)
641  {
642  return this._handler._getSharedState(k);
643  },
644 
645  //
646  // see nsIHttpServer.setSharedState
647  //
648  setSharedState: function(k, v)
649  {
650  return this._handler._setSharedState(k, v);
651  },
652 
653  //
654  // see nsIHttpServer.getObjectState
655  //
656  getObjectState: function(k)
657  {
658  return this._handler._getObjectState(k);
659  },
660 
661  //
662  // see nsIHttpServer.setObjectState
663  //
664  setObjectState: function(k, v)
665  {
666  return this._handler._setObjectState(k, v);
667  },
668 
669 
670  // NSISUPPORTS
671 
672  //
673  // see nsISupports.QueryInterface
674  //
675  QueryInterface: function(iid)
676  {
677  if (iid.equals(Ci.nsIHttpServer) ||
678  iid.equals(Ci.nsIServerSocketListener) ||
679  iid.equals(Ci.nsISupports))
680  return this;
681 
682  throw Cr.NS_ERROR_NO_INTERFACE;
683  },
684 
685 
686  // NON-XPCOM PUBLIC API
687 
693  isStopped: function()
694  {
695  return this._socketClosed && !this._hasOpenConnections();
696  },
697 
698  // PRIVATE IMPLEMENTATION
699 
701  _hasOpenConnections: function()
702  {
703  //
704  // If we have any open connections, they're tracked as numeric properties on
705  // |this._connections|. The non-standard __count__ property could be used
706  // to check whether there are any properties, but standard-wise, even
707  // looking forward to ES5, there's no less ugly yet still O(1) way to do
708  // this.
709  //
710  for (var n in this._connections)
711  return true;
712  return false;
713  },
714 
716  _notifyStopped: function()
717  {
718  NS_ASSERT(this._stopCallback !== null, "double-notifying?");
719  NS_ASSERT(!this._hasOpenConnections(), "should be done serving by now");
720 
721  //
722  // NB: We have to grab this now, null out the member, *then* call the
723  // callback here, or otherwise the callback could (indirectly) futz with
724  // this._stopCallback by starting and immediately stopping this, at
725  // which point we'd be nulling out a field we no longer have a right to
726  // modify.
727  //
728  var callback = this._stopCallback;
729  this._stopCallback = null;
730  try
731  {
732  callback();
733  }
734  catch (e)
735  {
736  // not throwing because this is specified as being usually (but not
737  // always) asynchronous
738  dump("!!! error running onStopped callback: " + e + "\n");
739  }
740  },
741 
748  _connectionClosed: function(connection)
749  {
750  NS_ASSERT(connection.number in this._connections,
751  "closing a connection " + this + " that we never added to the " +
752  "set of open connections?");
753  NS_ASSERT(this._connections[connection.number] === connection,
754  "connection number mismatch? " +
755  this._connections[connection.number]);
756  delete this._connections[connection.number];
757 
758  // Fire a pending server-stopped notification if it's our responsibility.
759  if (!this._hasOpenConnections() && this._socketClosed)
760  this._notifyStopped();
761  },
762 
766  _requestQuit: function()
767  {
768  dumpn(">>> requesting a quit");
769  dumpStack();
770  this._doQuit = true;
771  }
772 };
773 
774 
775 //
776 // RFC 2396 section 3.2.2:
777 //
778 // host = hostname | IPv4address
779 // hostname = *( domainlabel "." ) toplabel [ "." ]
780 // domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
781 // toplabel = alpha | alpha *( alphanum | "-" ) alphanum
782 // IPv4address = 1*digit "." 1*digit "." 1*digit "." 1*digit
783 //
784 
785 const HOST_REGEX =
786  new RegExp("^(?:" +
787  // *( domainlabel "." )
788  "(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)*" +
789  // toplabel
790  "[a-z](?:[a-z0-9-]*[a-z0-9])?" +
791  "|" +
792  // IPv4 address
793  "\\d+\\.\\d+\\.\\d+\\.\\d+" +
794  ")$",
795  "i");
796 
797 
811 function ServerIdentity()
812 {
814  this._primaryScheme = "http";
815 
817  this._primaryHost = "127.0.0.1"
818 
820  this._primaryPort = -1;
821 
826  this._defaultPort = -1;
827 
840  this._locations = { "xlocalhost": {} };
841 }
842 ServerIdentity.prototype =
843 {
848  _initialize: function(port, addSecondaryDefault)
849  {
850  if (this._primaryPort !== -1)
851  this.add("http", "localhost", port);
852  else
853  this.setPrimary("http", "localhost", port);
854  this._defaultPort = port;
855 
856  // Only add this if we're being called at server startup
857  if (addSecondaryDefault)
858  this.add("http", "127.0.0.1", port);
859  },
860 
866  _teardown: function()
867  {
868  // Not the default primary location, nothing special to do here
869  this.remove("http", "127.0.0.1", this._defaultPort);
870 
871  // This is a *very* tricky bit of reasoning here; make absolutely sure the
872  // tests for this code pass before you commit changes to it.
873  if (this._primaryScheme == "http" &&
874  this._primaryHost == "localhost" &&
875  this._primaryPort == this._defaultPort)
876  {
877  // Make sure we don't trigger the readding logic in .remove(), then remove
878  // the default location.
879  var port = this._defaultPort;
880  this._defaultPort = -1;
881  this.remove("http", "localhost", port);
882 
883  // Ensure a server start triggers the setPrimary() path in ._initialize()
884  this._primaryPort = -1;
885  }
886  else
887  {
888  // No reason not to remove directly as it's not our primary location
889  this.remove("http", "localhost", this._defaultPort);
890  }
891  },
892 
893  //
894  // see nsIHttpServerIdentity.primaryScheme
895  //
896  get primaryScheme()
897  {
898  if (this._primaryPort === -1)
899  throw Cr.NS_ERROR_NOT_INITIALIZED;
900  return this._primaryScheme;
901  },
902 
903  //
904  // see nsIHttpServerIdentity.primaryHost
905  //
906  get primaryHost()
907  {
908  if (this._primaryPort === -1)
909  throw Cr.NS_ERROR_NOT_INITIALIZED;
910  return this._primaryHost;
911  },
912 
913  //
914  // see nsIHttpServerIdentity.primaryPort
915  //
916  get primaryPort()
917  {
918  if (this._primaryPort === -1)
919  throw Cr.NS_ERROR_NOT_INITIALIZED;
920  return this._primaryPort;
921  },
922 
923  //
924  // see nsIHttpServerIdentity.add
925  //
926  add: function(scheme, host, port)
927  {
928  this._validate(scheme, host, port);
929 
930  var entry = this._locations["x" + host];
931  if (!entry)
932  this._locations["x" + host] = entry = {};
933 
934  entry[port] = scheme;
935  },
936 
937  //
938  // see nsIHttpServerIdentity.remove
939  //
940  remove: function(scheme, host, port)
941  {
942  this._validate(scheme, host, port);
943 
944  var entry = this._locations["x" + host];
945  if (!entry)
946  return false;
947 
948  var present = port in entry;
949  delete entry[port];
950 
951  if (this._primaryScheme == scheme &&
952  this._primaryHost == host &&
953  this._primaryPort == port &&
954  this._defaultPort !== -1)
955  {
956  // Always keep at least one identity in existence at any time, unless
957  // we're in the process of shutting down (the last condition above).
958  this._primaryPort = -1;
959  this._initialize(this._defaultPort, false);
960  }
961 
962  return present;
963  },
964 
965  //
966  // see nsIHttpServerIdentity.has
967  //
968  has: function(scheme, host, port)
969  {
970  this._validate(scheme, host, port);
971 
972  return "x" + host in this._locations &&
973  scheme === this._locations["x" + host][port];
974  },
975 
976  //
977  // see nsIHttpServerIdentity.has
978  //
979  getScheme: function(host, port)
980  {
981  this._validate("http", host, port);
982 
983  var entry = this._locations["x" + host];
984  if (!entry)
985  return "";
986 
987  return entry[port] || "";
988  },
989 
990  //
991  // see nsIHttpServerIdentity.setPrimary
992  //
993  setPrimary: function(scheme, host, port)
994  {
995  this._validate(scheme, host, port);
996 
997  this.add(scheme, host, port);
998 
999  this._primaryScheme = scheme;
1000  this._primaryHost = host;
1001  this._primaryPort = port;
1002  },
1003 
1010  _validate: function(scheme, host, port)
1011  {
1012  if (scheme !== "http" && scheme !== "https")
1013  {
1014  dumpn("*** server only supports http/https schemes: '" + scheme + "'");
1015  dumpStack();
1016  throw Cr.NS_ERROR_ILLEGAL_VALUE;
1017  }
1018  if (!HOST_REGEX.test(host))
1019  {
1020  dumpn("*** unexpected host: '" + host + "'");
1021  throw Cr.NS_ERROR_ILLEGAL_VALUE;
1022  }
1023  if (port < 0 || port > 65535)
1024  {
1025  dumpn("*** unexpected port: '" + port + "'");
1026  throw Cr.NS_ERROR_ILLEGAL_VALUE;
1027  }
1028  }
1029 };
1030 
1031 
1049 function Connection(input, output, server, port, outgoingPort, number)
1050 {
1051  dumpn("*** opening new connection " + number + " on port " + outgoingPort);
1052 
1054  this.input = input;
1055 
1057  this.output = output;
1058 
1060  this.server = server;
1061 
1063  this.port = port;
1064 
1066  this._outgoingPort = outgoingPort;
1067 
1069  this.number = number;
1070 
1075  this.request = null;
1076 
1078  this._closed = this._processed = false;
1079 }
1080 Connection.prototype =
1081 {
1083  close: function()
1084  {
1085  dumpn("*** closing connection " + this.number +
1086  " on port " + this._outgoingPort);
1087 
1088  this.input.close();
1089  this.output.close();
1090  this._closed = true;
1091 
1092  var server = this.server;
1093  server._connectionClosed(this);
1094 
1095  // If an error triggered a server shutdown, act on it now
1096  if (server._doQuit)
1097  server.stop(function() { /* not like we can do anything better */ });
1098  },
1099 
1107  process: function(request)
1108  {
1109  NS_ASSERT(!this._closed && !this._processed);
1110 
1111  this._processed = true;
1112 
1113  this.request = request;
1114  this.server._handler.handleResponse(this);
1115  },
1116 
1127  processError: function(code, request)
1128  {
1129  NS_ASSERT(!this._closed && !this._processed);
1130 
1131  this._processed = true;
1132  this.request = request;
1133  this.server._handler.handleError(code, this);
1134  },
1135 
1137  toString: function()
1138  {
1139  return "<Connection(" + this.number +
1140  (this.request ? ", " + this.request.path : "") +"): " +
1141  (this._closed ? "closed" : "open") + ">";
1142  }
1143 };
1144 
1145 
1146 
1148 function readBytes(inputStream, count)
1149 {
1150  return new BinaryInputStream(inputStream).readByteArray(count);
1151 }
1152 
1153 
1154 
1158 const READER_IN_BODY = 2;
1160 
1161 
1180 function RequestReader(connection)
1181 {
1183  this._connection = connection;
1184 
1191  this._data = new LineData();
1192 
1198  this._contentLength = 0;
1199 
1201  this._state = READER_IN_REQUEST_LINE;
1202 
1204  this._metadata = new Request(connection.port);
1205 
1213  this._lastHeaderName = this._lastHeaderValue = undefined;
1214 }
1215 RequestReader.prototype =
1216 {
1217  // NSIINPUTSTREAMCALLBACK
1218 
1227  onInputStreamReady: function(input)
1228  {
1229  dumpn("*** onInputStreamReady(input=" + input + ") on thread " +
1230  gThreadManager.currentThread + " (main is " +
1231  gThreadManager.mainThread + ")");
1232  dumpn("*** this._state == " + this._state);
1233 
1234  // Handle cases where we get more data after a request error has been
1235  // discovered but *before* we can close the connection.
1236  var data = this._data;
1237  if (!data)
1238  return;
1239 
1240  try
1241  {
1242  data.appendBytes(readBytes(input, input.available()));
1243  }
1244  catch (e)
1245  {
1246  if (e.result !== Cr.NS_BASE_STREAM_CLOSED)
1247  {
1248  dumpn("*** WARNING: unexpected error when reading from socket; will " +
1249  "be treated as if the input stream had been closed");
1250  dumpn("*** WARNING: actual error was: " + e);
1251  }
1252 
1253  // We've lost a race -- input has been closed, but we're still expecting
1254  // to read more data. available() will throw in this case, and since
1255  // we're dead in the water now, destroy the connection.
1256  dumpn("*** onInputStreamReady called on a closed input, destroying " +
1257  "connection");
1258  this._connection.close();
1259  return;
1260  }
1261 
1262  switch (this._state)
1263  {
1264  default:
1265  NS_ASSERT(false, "invalid state: " + this._state);
1266  break;
1267 
1269  if (!this._processRequestLine())
1270  break;
1271  /* fall through */
1272 
1273  case READER_IN_HEADERS:
1274  if (!this._processHeaders())
1275  break;
1276  /* fall through */
1277 
1278  case READER_IN_BODY:
1279  this._processBody();
1280  }
1281 
1282  if (this._state != READER_FINISHED)
1283  input.asyncWait(this, 0, 0, gThreadManager.currentThread);
1284  },
1285 
1286  //
1287  // see nsISupports.QueryInterface
1288  //
1289  QueryInterface: function(aIID)
1290  {
1291  if (aIID.equals(Ci.nsIInputStreamCallback) ||
1292  aIID.equals(Ci.nsISupports))
1293  return this;
1294 
1295  throw Cr.NS_ERROR_NO_INTERFACE;
1296  },
1297 
1298 
1299  // PRIVATE API
1300 
1307  _processRequestLine: function()
1308  {
1309  NS_ASSERT(this._state == READER_IN_REQUEST_LINE);
1310 
1311  // Servers SHOULD ignore any empty line(s) received where a Request-Line
1312  // is expected (section 4.1).
1313  var data = this._data;
1314  var line = {};
1315  var readSuccess;
1316  while ((readSuccess = data.readLine(line)) && line.value == "")
1317  dumpn("*** ignoring beginning blank line...");
1318 
1319  // if we don't have a full line, wait until we do
1320  if (!readSuccess)
1321  return false;
1322 
1323  // we have the first non-blank line
1324  try
1325  {
1326  this._parseRequestLine(line.value);
1327  this._state = READER_IN_HEADERS;
1328  return true;
1329  }
1330  catch (e)
1331  {
1332  this._handleError(e);
1333  return false;
1334  }
1335  },
1336 
1344  _processHeaders: function()
1345  {
1346  NS_ASSERT(this._state == READER_IN_HEADERS);
1347 
1348  // XXX things to fix here:
1349  //
1350  // - need to support RFC 2047-encoded non-US-ASCII characters
1351 
1352  try
1353  {
1354  var done = this._parseHeaders();
1355  if (done)
1356  {
1357  var request = this._metadata;
1358 
1359  // XXX this is wrong for requests with transfer-encodings applied to
1360  // them, particularly chunked (which by its nature can have no
1361  // meaningful Content-Length header)!
1362  this._contentLength = request.hasHeader("Content-Length")
1363  ? parseInt(request.getHeader("Content-Length"), 10)
1364  : 0;
1365  dumpn("_processHeaders, Content-length=" + this._contentLength);
1366 
1367  this._state = READER_IN_BODY;
1368  }
1369  return done;
1370  }
1371  catch (e)
1372  {
1373  this._handleError(e);
1374  return false;
1375  }
1376  },
1377 
1385  _processBody: function()
1386  {
1387  NS_ASSERT(this._state == READER_IN_BODY);
1388 
1389  // XXX handle chunked transfer-coding request bodies!
1390 
1391  try
1392  {
1393  if (this._contentLength > 0)
1394  {
1395  var data = this._data.purge();
1396  var count = Math.min(data.length, this._contentLength);
1397  dumpn("*** loading data=" + data + " len=" + data.length +
1398  " excess=" + (data.length - count));
1399 
1400  var bos = new BinaryOutputStream(this._metadata._bodyOutputStream);
1401  bos.writeByteArray(data, count);
1402  this._contentLength -= count;
1403  }
1404 
1405  dumpn("*** remaining body data len=" + this._contentLength);
1406  if (this._contentLength == 0)
1407  {
1408  this._validateRequest();
1409  this._state = READER_FINISHED;
1410  this._handleResponse();
1411  return true;
1412  }
1413 
1414  return false;
1415  }
1416  catch (e)
1417  {
1418  this._handleError(e);
1419  return false;
1420  }
1421  },
1422 
1429  _validateRequest: function()
1430  {
1431  NS_ASSERT(this._state == READER_IN_BODY);
1432 
1433  dumpn("*** _validateRequest");
1434 
1435  var metadata = this._metadata;
1436  var headers = metadata._headers;
1437 
1438  // 19.6.1.1 -- servers MUST report 400 to HTTP/1.1 requests w/o Host header
1439  var identity = this._connection.server.identity;
1440  if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1))
1441  {
1442  if (!headers.hasHeader("Host"))
1443  {
1444  dumpn("*** malformed HTTP/1.1 or greater request with no Host header!");
1445  throw HTTP_400;
1446  }
1447 
1448  // If the Request-URI wasn't absolute, then we need to determine our host.
1449  // We have to determine what scheme was used to access us based on the
1450  // server identity data at this point, because the request just doesn't
1451  // contain enough data on its own to do this, sadly.
1452  if (!metadata._host)
1453  {
1454  var host, port;
1455  var hostPort = headers.getHeader("Host");
1456  var colon = hostPort.indexOf(":");
1457  if (colon < 0)
1458  {
1459  host = hostPort;
1460  port = "";
1461  }
1462  else
1463  {
1464  host = hostPort.substring(0, colon);
1465  port = hostPort.substring(colon + 1);
1466  }
1467 
1468  // NB: We allow an empty port here because, oddly, a colon may be
1469  // present even without a port number, e.g. "example.com:"; in this
1470  // case the default port applies.
1471  if (!HOST_REGEX.test(host) || !/^\d*$/.test(port))
1472  {
1473  dumpn("*** malformed hostname (" + hostPort + ") in Host " +
1474  "header, 400 time");
1475  throw HTTP_400;
1476  }
1477 
1478  // If we're not given a port, we're stuck, because we don't know what
1479  // scheme to use to look up the correct port here, in general. Since
1480  // the HTTPS case requires a tunnel/proxy and thus requires that the
1481  // requested URI be absolute (and thus contain the necessary
1482  // information), let's assume HTTP will prevail and use that.
1483  port = +port || 80;
1484 
1485  var scheme = identity.getScheme(host, port);
1486  if (!scheme)
1487  {
1488  dumpn("*** unrecognized hostname (" + hostPort + ") in Host " +
1489  "header, 400 time");
1490  throw HTTP_400;
1491  }
1492 
1493  metadata._scheme = scheme;
1494  metadata._host = host;
1495  metadata._port = port;
1496  }
1497  }
1498  else
1499  {
1500  NS_ASSERT(metadata._host === undefined,
1501  "HTTP/1.0 doesn't allow absolute paths in the request line!");
1502 
1503  metadata._scheme = identity.primaryScheme;
1504  metadata._host = identity.primaryHost;
1505  metadata._port = identity.primaryPort;
1506  }
1507 
1508  NS_ASSERT(identity.has(metadata._scheme, metadata._host, metadata._port),
1509  "must have a location we recognize by now!");
1510  },
1511 
1521  _handleError: function(e)
1522  {
1523  // Don't fall back into normal processing!
1524  this._state = READER_FINISHED;
1525 
1526  var server = this._connection.server;
1527  if (e instanceof HttpError)
1528  {
1529  var code = e.code;
1530  }
1531  else
1532  {
1533  dumpn("!!! UNEXPECTED ERROR: " + e +
1534  (e.lineNumber ? ", line " + e.lineNumber : ""));
1535 
1536  // no idea what happened -- be paranoid and shut down
1537  code = 500;
1538  server._requestQuit();
1539  }
1540 
1541  // make attempted reuse of data an error
1542  this._data = null;
1543 
1544  this._connection.processError(code, this._metadata);
1545  },
1546 
1554  _handleResponse: function()
1555  {
1556  NS_ASSERT(this._state == READER_FINISHED);
1557 
1558  // We don't need the line-based data any more, so make attempted reuse an
1559  // error.
1560  this._data = null;
1561 
1562  this._connection.process(this._metadata);
1563  },
1564 
1565 
1566  // PARSING
1567 
1574  _parseRequestLine: function(line)
1575  {
1576  NS_ASSERT(this._state == READER_IN_REQUEST_LINE);
1577 
1578  dumpn("*** _parseRequestLine('" + line + "')");
1579 
1580  var metadata = this._metadata;
1581 
1582  // clients and servers SHOULD accept any amount of SP or HT characters
1583  // between fields, even though only a single SP is required (section 19.3)
1584  var request = line.split(/[ \t]+/);
1585  if (!request || request.length != 3)
1586  throw HTTP_400;
1587 
1588  metadata._method = request[0];
1589 
1590  // get the HTTP version
1591  var ver = request[2];
1592  var match = ver.match(/^HTTP\/(\d+\.\d+)$/);
1593  if (!match)
1594  throw HTTP_400;
1595 
1596  // determine HTTP version
1597  try
1598  {
1599  metadata._httpVersion = new nsHttpVersion(match[1]);
1600  if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_0))
1601  throw "unsupported HTTP version";
1602  }
1603  catch (e)
1604  {
1605  // we support HTTP/1.0 and HTTP/1.1 only
1606  throw HTTP_501;
1607  }
1608 
1609 
1610  var fullPath = request[1];
1611  var serverIdentity = this._connection.server.identity;
1612 
1613  var scheme, host, port;
1614 
1615  if (fullPath.charAt(0) != "/")
1616  {
1617  // No absolute paths in the request line in HTTP prior to 1.1
1618  if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1))
1619  throw HTTP_400;
1620 
1621  try
1622  {
1623  var uri = Cc["@mozilla.org/network/io-service;1"]
1624  .getService(Ci.nsIIOService)
1625  .newURI(fullPath, null, null);
1626  fullPath = uri.path;
1627  scheme = uri.scheme;
1628  host = metadata._host = uri.asciiHost;
1629  port = uri.port;
1630  if (port === -1)
1631  {
1632  if (scheme === "http")
1633  port = 80;
1634  else if (scheme === "https")
1635  port = 443;
1636  else
1637  throw HTTP_400;
1638  }
1639  }
1640  catch (e)
1641  {
1642  // If the host is not a valid host on the server, the response MUST be a
1643  // 400 (Bad Request) error message (section 5.2). Alternately, the URI
1644  // is malformed.
1645  throw HTTP_400;
1646  }
1647 
1648  if (!serverIdentity.has(scheme, host, port) || fullPath.charAt(0) != "/")
1649  throw HTTP_400;
1650  }
1651 
1652  var splitter = fullPath.indexOf("?");
1653  if (splitter < 0)
1654  {
1655  // _queryString already set in ctor
1656  metadata._path = fullPath;
1657  }
1658  else
1659  {
1660  metadata._path = fullPath.substring(0, splitter);
1661  metadata._queryString = fullPath.substring(splitter + 1);
1662  }
1663 
1664  metadata._scheme = scheme;
1665  metadata._host = host;
1666  metadata._port = port;
1667  },
1668 
1678  _parseHeaders: function()
1679  {
1680  NS_ASSERT(this._state == READER_IN_HEADERS);
1681 
1682  dumpn("*** _parseHeaders");
1683 
1684  var data = this._data;
1685 
1686  var headers = this._metadata._headers;
1687  var lastName = this._lastHeaderName;
1688  var lastVal = this._lastHeaderValue;
1689 
1690  var line = {};
1691  while (true)
1692  {
1693  NS_ASSERT(!((lastVal === undefined) ^ (lastName === undefined)),
1694  lastName === undefined ?
1695  "lastVal without lastName? lastVal: '" + lastVal + "'" :
1696  "lastName without lastVal? lastName: '" + lastName + "'");
1697 
1698  if (!data.readLine(line))
1699  {
1700  // save any data we have from the header we might still be processing
1701  this._lastHeaderName = lastName;
1702  this._lastHeaderValue = lastVal;
1703  return false;
1704  }
1705 
1706  var lineText = line.value;
1707  var firstChar = lineText.charAt(0);
1708 
1709  // blank line means end of headers
1710  if (lineText == "")
1711  {
1712  // we're finished with the previous header
1713  if (lastName)
1714  {
1715  try
1716  {
1717  headers.setHeader(lastName, lastVal, true);
1718  }
1719  catch (e)
1720  {
1721  dumpn("*** e == " + e);
1722  throw HTTP_400;
1723  }
1724  }
1725  else
1726  {
1727  // no headers in request -- valid for HTTP/1.0 requests
1728  }
1729 
1730  // either way, we're done processing headers
1731  this._state = READER_IN_BODY;
1732  return true;
1733  }
1734  else if (firstChar == " " || firstChar == "\t")
1735  {
1736  // multi-line header if we've already seen a header line
1737  if (!lastName)
1738  {
1739  // we don't have a header to continue!
1740  throw HTTP_400;
1741  }
1742 
1743  // append this line's text to the value; starts with SP/HT, so no need
1744  // for separating whitespace
1745  lastVal += lineText;
1746  }
1747  else
1748  {
1749  // we have a new header, so set the old one (if one existed)
1750  if (lastName)
1751  {
1752  try
1753  {
1754  headers.setHeader(lastName, lastVal, true);
1755  }
1756  catch (e)
1757  {
1758  dumpn("*** e == " + e);
1759  throw HTTP_400;
1760  }
1761  }
1762 
1763  var colon = lineText.indexOf(":"); // first colon must be splitter
1764  if (colon < 1)
1765  {
1766  // no colon or missing header field-name
1767  throw HTTP_400;
1768  }
1769 
1770  // set header name, value (to be set in the next loop, usually)
1771  lastName = lineText.substring(0, colon);
1772  lastVal = lineText.substring(colon + 1);
1773  } // empty, continuation, start of header
1774  } // while (true)
1775  }
1776 };
1777 
1778 
1780 const CR = 0x0D, LF = 0x0A;
1781 
1794 function findCRLF(array)
1795 {
1796  for (var i = array.indexOf(CR); i >= 0; i = array.indexOf(CR, i + 1))
1797  {
1798  if (array[i + 1] == LF)
1799  return i;
1800  }
1801  return -1;
1802 }
1803 
1804 
1809 function LineData()
1810 {
1812  this._data = [];
1813 }
1814 LineData.prototype =
1815 {
1820  appendBytes: function(bytes)
1821  {
1822  Array.prototype.push.apply(this._data, bytes);
1823  },
1824 
1837  readLine: function(out)
1838  {
1839  var data = this._data;
1840  var length = findCRLF(data);
1841  if (length < 0)
1842  return false;
1843 
1844  //
1845  // We have the index of the CR, so remove all the characters, including
1846  // CRLF, from the array with splice, and convert the removed array into the
1847  // corresponding string, from which we then strip the trailing CRLF.
1848  //
1849  // Getting the line in this matter acknowledges that substring is an O(1)
1850  // operation in SpiderMonkey because strings are immutable, whereas two
1851  // splices, both from the beginning of the data, are less likely to be as
1852  // cheap as a single splice plus two extra character conversions.
1853  //
1854  var line = String.fromCharCode.apply(null, data.splice(0, length + 2));
1855  out.value = line.substring(0, length);
1856 
1857  return true;
1858  },
1859 
1866  purge: function()
1867  {
1868  var data = this._data;
1869  this._data = [];
1870  return data;
1871  }
1872 };
1873 
1874 
1875 
1880 {
1881  return function(metadata, response) { handler.handle(metadata, response); };
1882 }
1883 
1884 
1889 function defaultIndexHandler(metadata, response)
1890 {
1891  response.setHeader("Content-Type", "text/html", false);
1892 
1893  var path = htmlEscape(decodeURI(metadata.path));
1894 
1895  //
1896  // Just do a very basic bit of directory listings -- no need for too much
1897  // fanciness, especially since we don't have a style sheet in which we can
1898  // stick rules (don't want to pollute the default path-space).
1899  //
1900 
1901  var body = '<html>\
1902  <head>\
1903  <title>' + path + '</title>\
1904  </head>\
1905  <body>\
1906  <h1>' + path + '</h1>\
1907  <ol style="list-style-type: none">';
1908 
1909  var directory = metadata.getProperty("directory");
1910  NS_ASSERT(directory && directory.isDirectory());
1911 
1912  var fileList = [];
1913  var files = directory.directoryEntries;
1914  while (files.hasMoreElements())
1915  {
1916  var f = files.getNext().QueryInterface(Ci.nsIFile);
1917  var name = f.leafName;
1918  if (!f.isHidden() &&
1919  (name.charAt(name.length - 1) != HIDDEN_CHAR ||
1920  name.charAt(name.length - 2) == HIDDEN_CHAR))
1921  fileList.push(f);
1922  }
1923 
1924  fileList.sort(fileSort);
1925 
1926  for (var i = 0; i < fileList.length; i++)
1927  {
1928  var file = fileList[i];
1929  try
1930  {
1931  var name = file.leafName;
1932  if (name.charAt(name.length - 1) == HIDDEN_CHAR)
1933  name = name.substring(0, name.length - 1);
1934  var sep = file.isDirectory() ? "/" : "";
1935 
1936  // Note: using " to delimit the attribute here because encodeURIComponent
1937  // passes through '.
1938  var item = '<li><a href="' + encodeURIComponent(name) + sep + '">' +
1939  htmlEscape(name) + sep +
1940  '</a></li>';
1941 
1942  body += item;
1943  }
1944  catch (e) { /* some file system error, ignore the file */ }
1945  }
1946 
1947  body += ' </ol>\
1948  </body>\
1949  </html>';
1950 
1951  response.bodyOutputStream.write(body, body.length);
1952 }
1953 
1957 function fileSort(a, b)
1958 {
1959  var dira = a.isDirectory(), dirb = b.isDirectory();
1960 
1961  if (dira && !dirb)
1962  return -1;
1963  if (dirb && !dira)
1964  return 1;
1965 
1966  var namea = a.leafName.toLowerCase(), nameb = b.leafName.toLowerCase();
1967  return nameb > namea ? -1 : 1;
1968 }
1969 
1970 
1983 function toInternalPath(path, encoded)
1984 {
1985  if (encoded)
1986  path = decodeURI(path);
1987 
1988  var comps = path.split("/");
1989  for (var i = 0, sz = comps.length; i < sz; i++)
1990  {
1991  var comp = comps[i];
1992  if (comp.charAt(comp.length - 1) == HIDDEN_CHAR)
1993  comps[i] = comp + HIDDEN_CHAR;
1994  }
1995  return comps.join("/");
1996 }
1997 
1998 
2012 function maybeAddHeaders(file, metadata, response)
2013 {
2014  var name = file.leafName;
2015  if (name.charAt(name.length - 1) == HIDDEN_CHAR)
2016  name = name.substring(0, name.length - 1);
2017 
2018  var headerFile = file.parent;
2019  headerFile.append(name + HEADERS_SUFFIX);
2020 
2021  if (!headerFile.exists())
2022  return;
2023 
2024  const PR_RDONLY = 0x01;
2025  var fis = new FileInputStream(headerFile, PR_RDONLY, 0444,
2026  Ci.nsIFileInputStream.CLOSE_ON_EOF);
2027 
2028  try
2029  {
2030  var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0);
2031  lis.QueryInterface(Ci.nsIUnicharLineInputStream);
2032 
2033  var line = {value: ""};
2034  var more = lis.readLine(line);
2035 
2036  if (!more && line.value == "")
2037  return;
2038 
2039 
2040  // request line
2041 
2042  var status = line.value;
2043  if (status.indexOf("HTTP ") == 0)
2044  {
2045  status = status.substring(5);
2046  var space = status.indexOf(" ");
2047  var code, description;
2048  if (space < 0)
2049  {
2050  code = status;
2051  description = "";
2052  }
2053  else
2054  {
2055  code = status.substring(0, space);
2056  description = status.substring(space + 1, status.length);
2057  }
2058 
2059  response.setStatusLine(metadata.httpVersion, parseInt(code, 10), description);
2060 
2061  line.value = "";
2062  more = lis.readLine(line);
2063  }
2064 
2065  // headers
2066  while (more || line.value != "")
2067  {
2068  var header = line.value;
2069  var colon = header.indexOf(":");
2070 
2071  response.setHeader(header.substring(0, colon),
2072  header.substring(colon + 1, header.length),
2073  false); // allow overriding server-set headers
2074 
2075  line.value = "";
2076  more = lis.readLine(line);
2077  }
2078  }
2079  catch (e)
2080  {
2081  dumpn("WARNING: error in headers for " + metadata.path + ": " + e);
2082  throw HTTP_500;
2083  }
2084  finally
2085  {
2086  fis.close();
2087  }
2088 }
2089 
2090 
2101 function ServerHandler(server)
2102 {
2103  // FIELDS
2104 
2108  this._server = server;
2109 
2119  this._pathDirectoryMap = new FileMap();
2120 
2127  this._overridePaths = {};
2128 
2136  this._overridePrefixes = {};
2137 
2145  this._overrideErrors = {};
2146 
2151  this._mimeMappings = {};
2152 
2157  this._indexHandler = defaultIndexHandler;
2158 
2160  this._state = {};
2161 
2163  this._sharedState = {};
2164 
2166  this._objectState = {};
2167 }
2168 ServerHandler.prototype =
2169 {
2170  // PUBLIC API
2171 
2181  handleResponse: function(connection)
2182  {
2183  var request = connection.request;
2184  var response = new Response(connection);
2185 
2186  var path = request.path;
2187  dumpn("*** path == " + path);
2188 
2189  try
2190  {
2191  try
2192  {
2193  if (path in this._overridePaths)
2194  {
2195  // explicit paths first, then files based on existing directory mappings,
2196  // then (if the file doesn't exist) built-in server default paths
2197  dumpn("calling override for " + path);
2198  this._overridePaths[path](request, response);
2199  }
2200  else
2201  {
2202  let longestPrefix = "";
2203  for (let prefix in this._overridePrefixes)
2204  {
2205  if (prefix.length > longestPrefix.length &&
2206  path.substr(0, prefix.length) == prefix)
2207  {
2208  longestPrefix = prefix;
2209  }
2210  }
2211  if (longestPrefix.length > 0)
2212  {
2213  dumpn("calling prefix override for " + longestPrefix);
2214  this._overridePrefixes[longestPrefix](request, response);
2215  }
2216  else
2217  {
2218  this._handleDefault(request, response);
2219  }
2220  }
2221  }
2222  catch (e)
2223  {
2224  if (response.partiallySent())
2225  {
2226  response.abort(e);
2227  return;
2228  }
2229 
2230  if (!(e instanceof HttpError))
2231  {
2232  dumpn("*** unexpected error: e == " + e);
2233  throw HTTP_500;
2234  }
2235  if (e.code !== 404)
2236  throw e;
2237 
2238  dumpn("*** default: " + (path in this._defaultPaths));
2239 
2240  response = new Response(connection);
2241  if (path in this._defaultPaths)
2242  this._defaultPaths[path](request, response);
2243  else
2244  throw HTTP_404;
2245  }
2246  }
2247  catch (e)
2248  {
2249  if (response.partiallySent())
2250  {
2251  response.abort(e);
2252  return;
2253  }
2254 
2255  var errorCode = "internal";
2256 
2257  try
2258  {
2259  if (!(e instanceof HttpError))
2260  throw e;
2261 
2262  errorCode = e.code;
2263  dumpn("*** errorCode == " + errorCode);
2264 
2265  response = new Response(connection);
2266  this._handleError(errorCode, request, response);
2267  return;
2268  }
2269  catch (e2)
2270  {
2271  dumpn("*** error handling " + errorCode + " error: " +
2272  "e2 == " + e2 + ", shutting down server");
2273 
2274  connection.server._requestQuit();
2275  response.abort(e2);
2276  return;
2277  }
2278  }
2279 
2280  response.complete();
2281  },
2282 
2283  //
2284  // see nsIHttpServer.registerFile
2285  //
2286  registerFile: function(path, file)
2287  {
2288  if (!file)
2289  {
2290  dumpn("*** unregistering '" + path + "' mapping");
2291  delete this._overridePaths[path];
2292  return;
2293  }
2294 
2295  dumpn("*** registering '" + path + "' as mapping to " + file.path);
2296  file = file.clone();
2297 
2298  var self = this;
2299  this._overridePaths[path] =
2300  function(request, response)
2301  {
2302  if (!file.exists())
2303  throw HTTP_404;
2304 
2305  response.setStatusLine(request.httpVersion, 200, "OK");
2306  self._writeFileResponse(request, file, response, 0, file.fileSize);
2307  };
2308  },
2309 
2310  //
2311  // see nsIHttpServer.registerPathHandler
2312  //
2313  registerPathHandler: function(path, handler)
2314  {
2315  // XXX true path validation!
2316  if (path.charAt(0) != "/")
2317  throw Cr.NS_ERROR_INVALID_ARG;
2318 
2319  this._handlerToField(handler, this._overridePaths, path);
2320  },
2321 
2322  //
2323  // see nsIHttpServer.registerPrefixHandler
2324  //
2325  registerPrefixHandler: function(path, handler)
2326  {
2327  // XXX true path validation!
2328  if (path.charAt(0) != "/" || path.charAt(path.length - 1) != "/")
2329  throw Cr.NS_ERROR_INVALID_ARG;
2330 
2331  this._handlerToField(handler, this._overridePrefixes, path);
2332  },
2333 
2334  //
2335  // see nsIHttpServer.registerDirectory
2336  //
2337  registerDirectory: function(path, directory)
2338  {
2339  // strip off leading and trailing '/' so that we can use lastIndexOf when
2340  // determining exactly how a path maps onto a mapped directory --
2341  // conditional is required here to deal with "/".substring(1, 0) being
2342  // converted to "/".substring(0, 1) per the JS specification
2343  var key = path.length == 1 ? "" : path.substring(1, path.length - 1);
2344 
2345  // the path-to-directory mapping code requires that the first character not
2346  // be "/", or it will go into an infinite loop
2347  if (key.charAt(0) == "/")
2348  throw Cr.NS_ERROR_INVALID_ARG;
2349 
2350  key = toInternalPath(key, false);
2351 
2352  if (directory)
2353  {
2354  dumpn("*** mapping '" + path + "' to the location " + directory.path);
2355  this._pathDirectoryMap.put(key, directory);
2356  }
2357  else
2358  {
2359  dumpn("*** removing mapping for '" + path + "'");
2360  this._pathDirectoryMap.put(key, null);
2361  }
2362  },
2363 
2364  //
2365  // see nsIHttpServer.registerErrorHandler
2366  //
2367  registerErrorHandler: function(err, handler)
2368  {
2369  if (!(err in HTTP_ERROR_CODES))
2370  dumpn("*** WARNING: registering non-HTTP/1.1 error code " +
2371  "(" + err + ") handler -- was this intentional?");
2372 
2373  this._handlerToField(handler, this._overrideErrors, err);
2374  },
2375 
2376  //
2377  // see nsIHttpServer.setIndexHandler
2378  //
2379  setIndexHandler: function(handler)
2380  {
2381  if (!handler)
2383  else if (typeof(handler) != "function")
2385 
2386  this._indexHandler = handler;
2387  },
2388 
2389  //
2390  // see nsIHttpServer.registerContentType
2391  //
2392  registerContentType: function(ext, type)
2393  {
2394  if (!type)
2395  delete this._mimeMappings[ext];
2396  else
2397  this._mimeMappings[ext] = headerUtils.normalizeFieldValue(type);
2398  },
2399 
2400  // PRIVATE API
2401 
2412  _handlerToField: function(handler, dict, key)
2413  {
2414  // for convenience, handler can be a function if this is run from xpcshell
2415  if (typeof(handler) == "function")
2416  dict[key] = handler;
2417  else if (handler)
2418  dict[key] = createHandlerFunc(handler);
2419  else
2420  delete dict[key];
2421  },
2422 
2436  _handleDefault: function(metadata, response)
2437  {
2438  dumpn("*** _handleDefault()");
2439 
2440  response.setStatusLine(metadata.httpVersion, 200, "OK");
2441 
2442  var path = metadata.path;
2443  NS_ASSERT(path.charAt(0) == "/", "invalid path: <" + path + ">");
2444 
2445  // determine the actual on-disk file; this requires finding the deepest
2446  // path-to-directory mapping in the requested URL
2447  var file = this._getFileForPath(path);
2448 
2449  // the "file" might be a directory, in which case we either serve the
2450  // contained index.html or make the index handler write the response
2451  if (file.exists() && file.isDirectory())
2452  {
2453  file.append("index.html"); // make configurable?
2454  if (!file.exists() || file.isDirectory())
2455  {
2456  metadata._ensurePropertyBag();
2457  metadata._bag.setPropertyAsInterface("directory", file.parent);
2458  this._indexHandler(metadata, response);
2459  return;
2460  }
2461  }
2462 
2463  // alternately, the file might not exist
2464  if (!file.exists())
2465  throw HTTP_404;
2466 
2467  var start, end;
2468  if (metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_1) &&
2469  metadata.hasHeader("Range") &&
2470  this._getTypeFromFile(file) !== SJS_TYPE)
2471  {
2472  var rangeMatch = metadata.getHeader("Range").match(/^bytes=(\d+)?-(\d+)?$/);
2473  if (!rangeMatch)
2474  throw HTTP_400;
2475 
2476  if (rangeMatch[1] !== undefined)
2477  start = parseInt(rangeMatch[1], 10);
2478 
2479  if (rangeMatch[2] !== undefined)
2480  end = parseInt(rangeMatch[2], 10);
2481 
2482  if (start === undefined && end === undefined)
2483  throw HTTP_400;
2484 
2485  // No start given, so the end is really the count of bytes from the
2486  // end of the file.
2487  if (start === undefined)
2488  {
2489  start = Math.max(0, file.fileSize - end);
2490  end = file.fileSize - 1;
2491  }
2492 
2493  // start and end are inclusive
2494  if (end === undefined || end >= file.fileSize)
2495  end = file.fileSize - 1;
2496 
2497  if (start !== undefined && start >= file.fileSize)
2498  throw HTTP_416;
2499 
2500  if (end < start)
2501  {
2502  response.setStatusLine(metadata.httpVersion, 200, "OK");
2503  start = 0;
2504  end = file.fileSize - 1;
2505  }
2506  else
2507  {
2508  response.setStatusLine(metadata.httpVersion, 206, "Partial Content");
2509  var contentRange = "bytes " + start + "-" + end + "/" + file.fileSize;
2510  response.setHeader("Content-Range", contentRange);
2511  }
2512  }
2513  else
2514  {
2515  start = 0;
2516  end = file.fileSize - 1;
2517  }
2518 
2519  // finally...
2520  dumpn("*** handling '" + path + "' as mapping to " + file.path + " from " +
2521  start + " to " + end + " inclusive");
2522  this._writeFileResponse(metadata, file, response, start, end - start + 1);
2523  },
2524 
2540  _writeFileResponse: function(metadata, file, response, offset, count)
2541  {
2542  const PR_RDONLY = 0x01;
2543 
2544  var type = this._getTypeFromFile(file);
2545  if (type === SJS_TYPE)
2546  {
2547  var fis = new FileInputStream(file, PR_RDONLY, 0444,
2548  Ci.nsIFileInputStream.CLOSE_ON_EOF);
2549 
2550  try
2551  {
2552  var sis = new ScriptableInputStream(fis);
2553  var s = Cu.Sandbox(gGlobalObject);
2554  s.importFunction(dump, "dump");
2555 
2556  // Define a basic key-value state-preservation API across requests, with
2557  // keys initially corresponding to the empty string.
2558  var self = this;
2559  var path = metadata.path;
2560  s.importFunction(function getState(k)
2561  {
2562  return self._getState(path, k);
2563  });
2564  s.importFunction(function setState(k, v)
2565  {
2566  self._setState(path, k, v);
2567  });
2568  s.importFunction(function getSharedState(k)
2569  {
2570  return self._getSharedState(k);
2571  });
2572  s.importFunction(function setSharedState(k, v)
2573  {
2574  self._setSharedState(k, v);
2575  });
2576  s.importFunction(function getObjectState(k, callback)
2577  {
2578  callback(self._getObjectState(k));
2579  });
2580  s.importFunction(function setObjectState(k, v)
2581  {
2582  self._setObjectState(k, v);
2583  });
2584 
2585  try
2586  {
2587  // Alas, the line number in errors dumped to console when calling the
2588  // request handler is simply an offset from where we load the SJS file.
2589  // Work around this in a reasonably non-fragile way by dynamically
2590  // getting the line number where we evaluate the SJS file. Don't
2591  // separate these two lines!
2592  var line = new Error().lineNumber;
2593  Cu.evalInSandbox(sis.read(file.fileSize), s);
2594  }
2595  catch (e)
2596  {
2597  dumpn("*** syntax error in SJS at " + file.path + ": " + e);
2598  throw HTTP_500;
2599  }
2600 
2601  try
2602  {
2603  s.handleRequest(metadata, response);
2604  }
2605  catch (e)
2606  {
2607  dump("*** error running SJS at " + file.path + ": " +
2608  e + " on line " +
2609  (e instanceof Error
2610  ? e.lineNumber + " in httpd.js"
2611  : (e.lineNumber - line)) + "\n");
2612  throw HTTP_500;
2613  }
2614  }
2615  finally
2616  {
2617  fis.close();
2618  }
2619  }
2620  else
2621  {
2622  try
2623  {
2624  response.setHeader("Last-Modified",
2625  toDateString(file.lastModifiedTime),
2626  false);
2627  }
2628  catch (e) { /* lastModifiedTime threw, ignore */ }
2629 
2630  response.setHeader("Content-Type", type, false);
2631  maybeAddHeaders(file, metadata, response);
2632  response.setHeader("Content-Length", "" + count, false);
2633 
2634  var fis = new FileInputStream(file, PR_RDONLY, 0444,
2635  Ci.nsIFileInputStream.CLOSE_ON_EOF);
2636 
2637  offset = offset || 0;
2638  count = count || file.fileSize;
2639  NS_ASSERT(offset === 0 || offset < file.fileSize, "bad offset");
2640  NS_ASSERT(count >= 0, "bad count");
2641  NS_ASSERT(offset + count <= file.fileSize, "bad total data size");
2642 
2643  try
2644  {
2645  if (offset !== 0)
2646  {
2647  // Seek (or read, if seeking isn't supported) to the correct offset so
2648  // the data sent to the client matches the requested range.
2649  if (fis instanceof Ci.nsISeekableStream)
2650  fis.seek(Ci.nsISeekableStream.NS_SEEK_SET, offset);
2651  else
2652  new ScriptableInputStream(fis).read(offset);
2653  }
2654  }
2655  catch (e)
2656  {
2657  fis.close();
2658  throw e;
2659  }
2660 
2661  function writeMore()
2662  {
2663  gThreadManager.currentThread
2664  .dispatch(writeData, Ci.nsIThread.DISPATCH_NORMAL);
2665  }
2666 
2667  var input = new BinaryInputStream(fis);
2668  var output = new BinaryOutputStream(response.bodyOutputStream);
2669  var writeData =
2670  {
2671  run: function()
2672  {
2673  var chunkSize = Math.min(65536, count);
2674  count -= chunkSize;
2675  NS_ASSERT(count >= 0, "underflow");
2676 
2677  try
2678  {
2679  var data = input.readByteArray(chunkSize);
2680  NS_ASSERT(data.length === chunkSize,
2681  "incorrect data returned? got " + data.length +
2682  ", expected " + chunkSize);
2683  output.writeByteArray(data, data.length);
2684  if (count === 0)
2685  {
2686  fis.close();
2687  response.finish();
2688  }
2689  else
2690  {
2691  writeMore();
2692  }
2693  }
2694  catch (e)
2695  {
2696  try
2697  {
2698  fis.close();
2699  }
2700  finally
2701  {
2702  response.finish();
2703  }
2704  throw e;
2705  }
2706  }
2707  };
2708 
2709  writeMore();
2710 
2711  // Now that we know copying will start, flag the response as async.
2712  response.processAsync();
2713  }
2714  },
2715 
2727  _getState: function(path, k)
2728  {
2729  var state = this._state;
2730  if (path in state && k in state[path])
2731  return state[path][k];
2732  return "";
2733  },
2734 
2746  _setState: function(path, k, v)
2747  {
2748  if (typeof v !== "string")
2749  throw new Error("non-string value passed");
2750  var state = this._state;
2751  if (!(path in state))
2752  state[path] = {};
2753  state[path][k] = v;
2754  },
2755 
2765  _getSharedState: function(k)
2766  {
2767  var state = this._sharedState;
2768  if (k in state)
2769  return state[k];
2770  return "";
2771  },
2772 
2782  _setSharedState: function(k, v)
2783  {
2784  if (typeof v !== "string")
2785  throw new Error("non-string value passed");
2786  this._sharedState[k] = v;
2787  },
2788 
2798  _getObjectState: function(k)
2799  {
2800  if (typeof k !== "string")
2801  throw new Error("non-string key passed");
2802  return this._objectState[k] || null;
2803  },
2804 
2814  _setObjectState: function(k, v)
2815  {
2816  if (typeof k !== "string")
2817  throw new Error("non-string key passed");
2818  if (typeof v !== "object")
2819  throw new Error("non-object value passed");
2820  if (v && !("QueryInterface" in v))
2821  {
2822  throw new Error("must pass an nsISupports; use wrappedJSObject to ease " +
2823  "pain when using the server from JS");
2824  }
2825 
2826  this._objectState[k] = v;
2827  },
2828 
2840  _getTypeFromFile: function(file)
2841  {
2842  try
2843  {
2844  var name = file.leafName;
2845  var dot = name.lastIndexOf(".");
2846  if (dot > 0)
2847  {
2848  var ext = name.slice(dot + 1);
2849  if (ext in this._mimeMappings)
2850  return this._mimeMappings[ext];
2851  }
2852  return Cc["@mozilla.org/uriloader/external-helper-app-service;1"]
2853  .getService(Ci.nsIMIMEService)
2854  .getTypeFromFile(file);
2855  }
2856  catch (e)
2857  {
2858  return "application/octet-stream";
2859  }
2860  },
2861 
2876  _getFileForPath: function(path)
2877  {
2878  // decode and add underscores as necessary
2879  try
2880  {
2881  path = toInternalPath(path, true);
2882  }
2883  catch (e)
2884  {
2885  throw HTTP_400; // malformed path
2886  }
2887 
2888  // next, get the directory which contains this path
2889  var pathMap = this._pathDirectoryMap;
2890 
2891  // An example progression of tmp for a path "/foo/bar/baz/" might be:
2892  // "foo/bar/baz/", "foo/bar/baz", "foo/bar", "foo", ""
2893  var tmp = path.substring(1);
2894  while (true)
2895  {
2896  // do we have a match for current head of the path?
2897  var file = pathMap.get(tmp);
2898  if (file)
2899  {
2900  // XXX hack; basically disable showing mapping for /foo/bar/ when the
2901  // requested path was /foo/bar, because relative links on the page
2902  // will all be incorrect -- we really need the ability to easily
2903  // redirect here instead
2904  if (tmp == path.substring(1) &&
2905  tmp.length != 0 &&
2906  tmp.charAt(tmp.length - 1) != "/")
2907  file = null;
2908  else
2909  break;
2910  }
2911 
2912  // if we've finished trying all prefixes, exit
2913  if (tmp == "")
2914  break;
2915 
2916  tmp = tmp.substring(0, tmp.lastIndexOf("/"));
2917  }
2918 
2919  // no mapping applies, so 404
2920  if (!file)
2921  throw HTTP_404;
2922 
2923 
2924  // last, get the file for the path within the determined directory
2925  var parentFolder = file.parent;
2926  var dirIsRoot = (parentFolder == null);
2927 
2928  // Strategy here is to append components individually, making sure we
2929  // never move above the given directory; this allows paths such as
2930  // "<file>/foo/../bar" but prevents paths such as "<file>/../base-sibling";
2931  // this component-wise approach also means the code works even on platforms
2932  // which don't use "/" as the directory separator, such as Windows
2933  var leafPath = path.substring(tmp.length + 1);
2934  var comps = leafPath.split("/");
2935  for (var i = 0, sz = comps.length; i < sz; i++)
2936  {
2937  var comp = comps[i];
2938 
2939  if (comp == "..")
2940  file = file.parent;
2941  else if (comp == "." || comp == "")
2942  continue;
2943  else
2944  file.append(comp);
2945 
2946  if (!dirIsRoot && file.equals(parentFolder))
2947  throw HTTP_403;
2948  }
2949 
2950  return file;
2951  },
2952 
2962  handleError: function(errorCode, connection)
2963  {
2964  var response = new Response(connection);
2965 
2966  dumpn("*** error in request: " + errorCode);
2967 
2968  this._handleError(errorCode, new Request(connection.port), response);
2969  },
2970 
2988  _handleError: function(errorCode, metadata, response)
2989  {
2990  if (!metadata)
2991  throw Cr.NS_ERROR_NULL_POINTER;
2992 
2993  var errorX00 = errorCode - (errorCode % 100);
2994 
2995  try
2996  {
2997  if (!(errorCode in HTTP_ERROR_CODES))
2998  dumpn("*** WARNING: requested invalid error: " + errorCode);
2999 
3000  // RFC 2616 says that we should try to handle an error by its class if we
3001  // can't otherwise handle it -- if that fails, we revert to handling it as
3002  // a 500 internal server error, and if that fails we throw and shut down
3003  // the server
3004 
3005  // actually handle the error
3006  try
3007  {
3008  if (errorCode in this._overrideErrors)
3009  this._overrideErrors[errorCode](metadata, response);
3010  else
3011  this._defaultErrors[errorCode](metadata, response);
3012  }
3013  catch (e)
3014  {
3015  if (response.partiallySent())
3016  {
3017  response.abort(e);
3018  return;
3019  }
3020 
3021  // don't retry the handler that threw
3022  if (errorX00 == errorCode)
3023  throw HTTP_500;
3024 
3025  dumpn("*** error in handling for error code " + errorCode + ", " +
3026  "falling back to " + errorX00 + "...");
3027  response = new Response(response._connection);
3028  if (errorX00 in this._overrideErrors)
3029  this._overrideErrors[errorX00](metadata, response);
3030  else if (errorX00 in this._defaultErrors)
3031  this._defaultErrors[errorX00](metadata, response);
3032  else
3033  throw HTTP_500;
3034  }
3035  }
3036  catch (e)
3037  {
3038  if (response.partiallySent())
3039  {
3040  response.abort();
3041  return;
3042  }
3043 
3044  // we've tried everything possible for a meaningful error -- now try 500
3045  dumpn("*** error in handling for error code " + errorX00 + ", falling " +
3046  "back to 500...");
3047 
3048  try
3049  {
3050  response = new Response(response._connection);
3051  if (500 in this._overrideErrors)
3052  this._overrideErrors[500](metadata, response);
3053  else
3054  this._defaultErrors[500](metadata, response);
3055  }
3056  catch (e2)
3057  {
3058  dumpn("*** multiple errors in default error handlers!");
3059  dumpn("*** e == " + e + ", e2 == " + e2);
3060  response.abort(e2);
3061  return;
3062  }
3063  }
3064 
3065  response.complete();
3066  },
3067 
3068  // FIELDS
3069 
3073  _defaultErrors:
3074  {
3075  400: function(metadata, response)
3076  {
3077  // none of the data in metadata is reliable, so hard-code everything here
3078  response.setStatusLine("1.1", 400, "Bad Request");
3079  response.setHeader("Content-Type", "text/plain", false);
3080 
3081  var body = "Bad request\n";
3082  response.bodyOutputStream.write(body, body.length);
3083  },
3084  403: function(metadata, response)
3085  {
3086  response.setStatusLine(metadata.httpVersion, 403, "Forbidden");
3087  response.setHeader("Content-Type", "text/html", false);
3088 
3089  var body = "<html>\
3090  <head><title>403 Forbidden</title></head>\
3091  <body>\
3092  <h1>403 Forbidden</h1>\
3093  </body>\
3094  </html>";
3095  response.bodyOutputStream.write(body, body.length);
3096  },
3097  404: function(metadata, response)
3098  {
3099  response.setStatusLine(metadata.httpVersion, 404, "Not Found");
3100  response.setHeader("Content-Type", "text/html", false);
3101 
3102  var body = "<html>\
3103  <head><title>404 Not Found</title></head>\
3104  <body>\
3105  <h1>404 Not Found</h1>\
3106  <p>\
3107  <span style='font-family: monospace;'>" +
3108  htmlEscape(metadata.path) +
3109  "</span> was not found.\
3110  </p>\
3111  </body>\
3112  </html>";
3113  response.bodyOutputStream.write(body, body.length);
3114  },
3115  416: function(metadata, response)
3116  {
3117  response.setStatusLine(metadata.httpVersion,
3118  416,
3119  "Requested Range Not Satisfiable");
3120  response.setHeader("Content-Type", "text/html", false);
3121 
3122  var body = "<html>\
3123  <head>\
3124  <title>416 Requested Range Not Satisfiable</title></head>\
3125  <body>\
3126  <h1>416 Requested Range Not Satisfiable</h1>\
3127  <p>The byte range was not valid for the\
3128  requested resource.\
3129  </p>\
3130  </body>\
3131  </html>";
3132  response.bodyOutputStream.write(body, body.length);
3133  },
3134  500: function(metadata, response)
3135  {
3136  response.setStatusLine(metadata.httpVersion,
3137  500,
3138  "Internal Server Error");
3139  response.setHeader("Content-Type", "text/html", false);
3140 
3141  var body = "<html>\
3142  <head><title>500 Internal Server Error</title></head>\
3143  <body>\
3144  <h1>500 Internal Server Error</h1>\
3145  <p>Something's broken in this server and\
3146  needs to be fixed.</p>\
3147  </body>\
3148  </html>";
3149  response.bodyOutputStream.write(body, body.length);
3150  },
3151  501: function(metadata, response)
3152  {
3153  response.setStatusLine(metadata.httpVersion, 501, "Not Implemented");
3154  response.setHeader("Content-Type", "text/html", false);
3155 
3156  var body = "<html>\
3157  <head><title>501 Not Implemented</title></head>\
3158  <body>\
3159  <h1>501 Not Implemented</h1>\
3160  <p>This server is not (yet) Apache.</p>\
3161  </body>\
3162  </html>";
3163  response.bodyOutputStream.write(body, body.length);
3164  },
3165  505: function(metadata, response)
3166  {
3167  response.setStatusLine("1.1", 505, "HTTP Version Not Supported");
3168  response.setHeader("Content-Type", "text/html", false);
3169 
3170  var body = "<html>\
3171  <head><title>505 HTTP Version Not Supported</title></head>\
3172  <body>\
3173  <h1>505 HTTP Version Not Supported</h1>\
3174  <p>This server only supports HTTP/1.0 and HTTP/1.1\
3175  connections.</p>\
3176  </body>\
3177  </html>";
3178  response.bodyOutputStream.write(body, body.length);
3179  }
3180  },
3181 
3185  _defaultPaths:
3186  {
3187  "/": function(metadata, response)
3188  {
3189  response.setStatusLine(metadata.httpVersion, 200, "OK");
3190  response.setHeader("Content-Type", "text/html", false);
3191 
3192  var body = "<html>\
3193  <head><title>httpd.js</title></head>\
3194  <body>\
3195  <h1>httpd.js</h1>\
3196  <p>If you're seeing this page, httpd.js is up and\
3197  serving requests! Now set a base path and serve some\
3198  files!</p>\
3199  </body>\
3200  </html>";
3201 
3202  response.bodyOutputStream.write(body, body.length);
3203  },
3204 
3205  "/trace": function(metadata, response)
3206  {
3207  response.setStatusLine(metadata.httpVersion, 200, "OK");
3208  response.setHeader("Content-Type", "text/plain", false);
3209 
3210  var body = "Request-URI: " +
3211  metadata.scheme + "://" + metadata.host + ":" + metadata.port +
3212  metadata.path + "\n\n";
3213  body += "Request (semantically equivalent, slightly reformatted):\n\n";
3214  body += metadata.method + " " + metadata.path;
3215 
3216  if (metadata.queryString)
3217  body += "?" + metadata.queryString;
3218 
3219  body += " HTTP/" + metadata.httpVersion + "\r\n";
3220 
3221  var headEnum = metadata.headers;
3222  while (headEnum.hasMoreElements())
3223  {
3224  var fieldName = headEnum.getNext()
3225  .QueryInterface(Ci.nsISupportsString)
3226  .data;
3227  body += fieldName + ": " + metadata.getHeader(fieldName) + "\r\n";
3228  }
3229 
3230  response.bodyOutputStream.write(body, body.length);
3231  }
3232  }
3233 };
3234 
3235 
3239 function FileMap()
3240 {
3242  this._map = {};
3243 }
3244 FileMap.prototype =
3245 {
3246  // PUBLIC API
3247 
3257  put: function(key, value)
3258  {
3259  if (value)
3260  this._map[key] = value.clone();
3261  else
3262  delete this._map[key];
3263  },
3264 
3274  get: function(key)
3275  {
3276  var val = this._map[key];
3277  return val ? val.clone() : null;
3278  }
3279 };
3280 
3281 
3282 // Response CONSTANTS
3283 
3284 // token = *<any CHAR except CTLs or separators>
3285 // CHAR = <any US-ASCII character (0-127)>
3286 // CTL = <any US-ASCII control character (0-31) and DEL (127)>
3287 // separators = "(" | ")" | "<" | ">" | "@"
3288 // | "," | ";" | ":" | "\" | <">
3289 // | "/" | "[" | "]" | "?" | "="
3290 // | "{" | "}" | SP | HT
3292  [0, 0, 0, 0, 0, 0, 0, 0, // 0
3293  0, 0, 0, 0, 0, 0, 0, 0, // 8
3294  0, 0, 0, 0, 0, 0, 0, 0, // 16
3295  0, 0, 0, 0, 0, 0, 0, 0, // 24
3296 
3297  0, 1, 0, 1, 1, 1, 1, 1, // 32
3298  0, 0, 1, 1, 0, 1, 1, 0, // 40
3299  1, 1, 1, 1, 1, 1, 1, 1, // 48
3300  1, 1, 0, 0, 0, 0, 0, 0, // 56
3301 
3302  0, 1, 1, 1, 1, 1, 1, 1, // 64
3303  1, 1, 1, 1, 1, 1, 1, 1, // 72
3304  1, 1, 1, 1, 1, 1, 1, 1, // 80
3305  1, 1, 1, 0, 0, 0, 1, 1, // 88
3306 
3307  1, 1, 1, 1, 1, 1, 1, 1, // 96
3308  1, 1, 1, 1, 1, 1, 1, 1, // 104
3309  1, 1, 1, 1, 1, 1, 1, 1, // 112
3310  1, 1, 1, 0, 1, 0, 1]; // 120
3311 
3312 
3321 function isCTL(code)
3322 {
3323  return (code >= 0 && code <= 31) || (code == 127);
3324 }
3325 
3334 function Response(connection)
3335 {
3337  this._connection = connection;
3338 
3343  this._httpVersion = nsHttpVersion.HTTP_1_1;
3344 
3348  this._httpCode = 200;
3349 
3353  this._httpDescription = "OK";
3354 
3363  this._headers = new nsHttpHeaders();
3364 
3369  this._ended = false;
3370 
3374  this._bodyOutputStream = null;
3375 
3382  this._bodyInputStream = null;
3383 
3388  this._asyncCopier = null;
3389 
3395  this._processAsync = false;
3396 
3401  this._finished = false;
3402 
3408  this._powerSeized = false;
3409 }
3410 Response.prototype =
3411 {
3412  // PUBLIC CONSTRUCTION API
3413 
3414  //
3415  // see nsIHttpResponse.bodyOutputStream
3416  //
3417  get bodyOutputStream()
3418  {
3419  if (this._finished)
3420  throw Cr.NS_ERROR_NOT_AVAILABLE;
3421 
3422  if (!this._bodyOutputStream)
3423  {
3424  var pipe = new Pipe(false, false, Response.SEGMENT_SIZE, PR_UINT32_MAX,
3425  null);
3426  this._bodyOutputStream = pipe.outputStream;
3427  this._bodyInputStream = pipe.inputStream;
3428  if (this._processAsync || this._powerSeized)
3429  this._startAsyncProcessor();
3430  }
3431 
3432  return this._bodyOutputStream;
3433  },
3434 
3435  //
3436  // see nsIHttpResponse.write
3437  //
3438  write: function(data)
3439  {
3440  if (this._finished)
3441  throw Cr.NS_ERROR_NOT_AVAILABLE;
3442 
3443  var dataAsString = String(data);
3444  this.bodyOutputStream.write(dataAsString, dataAsString.length);
3445  },
3446 
3447  //
3448  // see nsIHttpResponse.setStatusLine
3449  //
3450  setStatusLine: function(httpVersion, code, description)
3451  {
3452  if (!this._headers || this._finished || this._powerSeized)
3453  throw Cr.NS_ERROR_NOT_AVAILABLE;
3454  this._ensureAlive();
3455 
3456  if (!(code >= 0 && code < 1000))
3457  throw Cr.NS_ERROR_INVALID_ARG;
3458 
3459  try
3460  {
3461  var httpVer;
3462  // avoid version construction for the most common cases
3463  if (!httpVersion || httpVersion == "1.1")
3464  httpVer = nsHttpVersion.HTTP_1_1;
3465  else if (httpVersion == "1.0")
3466  httpVer = nsHttpVersion.HTTP_1_0;
3467  else
3468  httpVer = new nsHttpVersion(httpVersion);
3469  }
3470  catch (e)
3471  {
3472  throw Cr.NS_ERROR_INVALID_ARG;
3473  }
3474 
3475  // Reason-Phrase = *<TEXT, excluding CR, LF>
3476  // TEXT = <any OCTET except CTLs, but including LWS>
3477  //
3478  // XXX this ends up disallowing octets which aren't Unicode, I think -- not
3479  // much to do if description is IDL'd as string
3480  if (!description)
3481  description = "";
3482  for (var i = 0; i < description.length; i++)
3483  if (isCTL(description.charCodeAt(i)) && description.charAt(i) != "\t")
3484  throw Cr.NS_ERROR_INVALID_ARG;
3485 
3486  // set the values only after validation to preserve atomicity
3487  this._httpDescription = description;
3488  this._httpCode = code;
3489  this._httpVersion = httpVer;
3490  },
3491 
3492  //
3493  // see nsIHttpResponse.setHeader
3494  //
3495  setHeader: function(name, value, merge)
3496  {
3497  if (!this._headers || this._finished || this._powerSeized)
3498  throw Cr.NS_ERROR_NOT_AVAILABLE;
3499  this._ensureAlive();
3500 
3501  this._headers.setHeader(name, value, merge);
3502  },
3503 
3504  //
3505  // see nsIHttpResponse.processAsync
3506  //
3507  processAsync: function()
3508  {
3509  if (this._finished)
3510  throw Cr.NS_ERROR_UNEXPECTED;
3511  if (this._powerSeized)
3512  throw Cr.NS_ERROR_NOT_AVAILABLE;
3513  if (this._processAsync)
3514  return;
3515  this._ensureAlive();
3516 
3517  dumpn("*** processing connection " + this._connection.number + " async");
3518  this._processAsync = true;
3519 
3520  /*
3521  * Either the bodyOutputStream getter or this method is responsible for
3522  * starting the asynchronous processor and catching writes of data to the
3523  * response body of async responses as they happen, for the purpose of
3524  * forwarding those writes to the actual connection's output stream.
3525  * If bodyOutputStream is accessed first, calling this method will create
3526  * the processor (when it first is clear that body data is to be written
3527  * immediately, not buffered). If this method is called first, accessing
3528  * bodyOutputStream will create the processor. If only this method is
3529  * called, we'll write nothing, neither headers nor the non-existent body,
3530  * until finish() is called. Since that delay is easily avoided by simply
3531  * getting bodyOutputStream or calling write(""), we don't worry about it.
3532  */
3533  if (this._bodyOutputStream && !this._asyncCopier)
3534  this._startAsyncProcessor();
3535  },
3536 
3537  //
3538  // see nsIHttpResponse.seizePower
3539  //
3540  seizePower: function()
3541  {
3542  if (this._processAsync)
3543  throw Cr.NS_ERROR_NOT_AVAILABLE;
3544  if (this._finished)
3545  throw Cr.NS_ERROR_UNEXPECTED;
3546  if (this._powerSeized)
3547  return;
3548  this._ensureAlive();
3549 
3550  dumpn("*** forcefully seizing power over connection " +
3551  this._connection.number + "...");
3552 
3553  // Purge any already-written data without sending it. We could as easily
3554  // swap out the streams entirely, but that makes it possible to acquire and
3555  // unknowingly use a stale reference, so we require there only be one of
3556  // each stream ever for any response to avoid this complication.
3557  if (this._asyncCopier)
3558  this._asyncCopier.cancel(Cr.NS_BINDING_ABORTED);
3559  this._asyncCopier = null;
3560  if (this._bodyOutputStream)
3561  {
3562  var input = new BinaryInputStream(this._bodyInputStream);
3563  var avail;
3564  while ((avail = input.available()) > 0)
3565  input.readByteArray(avail);
3566  }
3567 
3568  this._powerSeized = true;
3569  if (this._bodyOutputStream)
3570  this._startAsyncProcessor();
3571  },
3572 
3573  //
3574  // see nsIHttpResponse.finish
3575  //
3576  finish: function()
3577  {
3578  if (!this._processAsync && !this._powerSeized)
3579  throw Cr.NS_ERROR_UNEXPECTED;
3580  if (this._finished)
3581  return;
3582 
3583  dumpn("*** finishing connection " + this._connection.number);
3584  this._startAsyncProcessor(); // in case bodyOutputStream was never accessed
3585  if (this._bodyOutputStream)
3586  this._bodyOutputStream.close();
3587  this._finished = true;
3588  },
3589 
3590 
3591  // POST-CONSTRUCTION API (not exposed externally)
3592 
3596  get httpVersion()
3597  {
3598  this._ensureAlive();
3599  return this._httpVersion.toString();
3600  },
3601 
3606  get httpCode()
3607  {
3608  this._ensureAlive();
3609 
3610  var codeString = (this._httpCode < 10 ? "0" : "") +
3611  (this._httpCode < 100 ? "0" : "") +
3612  this._httpCode;
3613  return codeString;
3614  },
3615 
3620  get httpDescription()
3621  {
3622  this._ensureAlive();
3623 
3624  return this._httpDescription;
3625  },
3626 
3630  get headers()
3631  {
3632  this._ensureAlive();
3633 
3634  return this._headers;
3635  },
3636 
3637  //
3638  // see nsHttpHeaders.getHeader
3639  //
3640  getHeader: function(name)
3641  {
3642  this._ensureAlive();
3643 
3644  return this._headers.getHeader(name);
3645  },
3646 
3656  partiallySent: function()
3657  {
3658  dumpn("*** partiallySent()");
3659  return this._processAsync || this._powerSeized;
3660  },
3661 
3666  complete: function()
3667  {
3668  dumpn("*** complete()");
3669  if (this._processAsync || this._powerSeized)
3670  {
3671  NS_ASSERT(this._processAsync ^ this._powerSeized,
3672  "can't both send async and relinquish power");
3673  return;
3674  }
3675 
3676  NS_ASSERT(!this.partiallySent(), "completing a partially-sent response?");
3677 
3678  this._startAsyncProcessor();
3679 
3680  // Now make sure we finish processing this request!
3681  if (this._bodyOutputStream)
3682  this._bodyOutputStream.close();
3683  },
3684 
3698  abort: function(e)
3699  {
3700  dumpn("*** abort(<" + e + ">)");
3701 
3702  // This response will be ended by the processor if one was created.
3703  var copier = this._asyncCopier;
3704  if (copier)
3705  {
3706  // We dispatch asynchronously here so that any pending writes of data to
3707  // the connection will be deterministically written. This makes it easier
3708  // to specify exact behavior, and it makes observable behavior more
3709  // predictable for clients. Note that the correctness of this depends on
3710  // callbacks in response to _waitForData in WriteThroughCopier happening
3711  // asynchronously with respect to the actual writing of data to
3712  // bodyOutputStream, as they currently do; if they happened synchronously,
3713  // an event which ran before this one could write more data to the
3714  // response body before we get around to canceling the copier. We have
3715  // tests for this in test_seizepower.js, however, and I can't think of a
3716  // way to handle both cases without removing bodyOutputStream access and
3717  // moving its effective write(data, length) method onto Response, which
3718  // would be slower and require more code than this anyway.
3719  gThreadManager.currentThread.dispatch({
3720  run: function()
3721  {
3722  dumpn("*** canceling copy asynchronously...");
3723  copier.cancel(Cr.NS_ERROR_UNEXPECTED);
3724  }
3725  }, Ci.nsIThread.DISPATCH_NORMAL);
3726  }
3727  else
3728  {
3729  this.end();
3730  }
3731  },
3732 
3737  end: function()
3738  {
3739  NS_ASSERT(!this._ended, "ending this response twice?!?!");
3740 
3741  this._connection.close();
3742  if (this._bodyOutputStream)
3743  this._bodyOutputStream.close();
3744 
3745  this._finished = true;
3746  this._ended = true;
3747  },
3748 
3749  // PRIVATE IMPLEMENTATION
3750 
3758  _sendHeaders: function()
3759  {
3760  dumpn("*** _sendHeaders()");
3761 
3762  NS_ASSERT(this._headers);
3763  NS_ASSERT(!this._powerSeized);
3764 
3765  // request-line
3766  var statusLine = "HTTP/" + this.httpVersion + " " +
3767  this.httpCode + " " +
3768  this.httpDescription + "\r\n";
3769 
3770  // header post-processing
3771 
3772  var headers = this._headers;
3773  headers.setHeader("Connection", "close", false);
3774  headers.setHeader("Server", "httpd.js", false);
3775  if (!headers.hasHeader("Date"))
3776  headers.setHeader("Date", toDateString(Date.now()), false);
3777 
3778  // Any response not being processed asynchronously must have an associated
3779  // Content-Length header for reasons of backwards compatibility with the
3780  // initial server, which fully buffered every response before sending it.
3781  // Beyond that, however, it's good to do this anyway because otherwise it's
3782  // impossible to test behaviors that depend on the presence or absence of a
3783  // Content-Length header.
3784  if (!this._processAsync)
3785  {
3786  dumpn("*** non-async response, set Content-Length");
3787 
3788  var bodyStream = this._bodyInputStream;
3789  var avail = bodyStream ? bodyStream.available() : 0;
3790 
3791  // XXX assumes stream will always report the full amount of data available
3792  headers.setHeader("Content-Length", "" + avail, false);
3793  }
3794 
3795 
3796  // construct and send response
3797  dumpn("*** header post-processing completed, sending response head...");
3798 
3799  // request-line
3800  var preamble = statusLine;
3801 
3802  // headers
3803  var headEnum = headers.enumerator;
3804  while (headEnum.hasMoreElements())
3805  {
3806  var fieldName = headEnum.getNext()
3807  .QueryInterface(Ci.nsISupportsString)
3808  .data;
3809  var values = headers.getHeaderValues(fieldName);
3810  for (var i = 0, sz = values.length; i < sz; i++)
3811  preamble += fieldName + ": " + values[i] + "\r\n";
3812  }
3813 
3814  // end request-line/headers
3815  preamble += "\r\n";
3816 
3817  var connection = this._connection;
3818  try
3819  {
3820  connection.output.write(preamble, preamble.length);
3821  }
3822  catch (e)
3823  {
3824  // Connection closed already? Even if not, failure to write the response
3825  // means we probably will fail later anyway, so in the interests of
3826  // avoiding exceptions we'll (possibly) close the connection and return.
3827  dumpn("*** error writing headers to socket: " + e);
3828  response.end();
3829  return;
3830  }
3831 
3832  // Forbid setting any more headers or modifying the request line.
3833  this._headers = null;
3834  },
3835 
3841  _startAsyncProcessor: function()
3842  {
3843  dumpn("*** _startAsyncProcessor()");
3844 
3845  // Handle cases where we're being called a second time. The former case
3846  // happens when this is triggered both by complete() and by processAsync(),
3847  // while the latter happens when processAsync() in conjunction with sent
3848  // data causes abort() to be called.
3849  if (this._asyncCopier || this._ended)
3850  {
3851  dumpn("*** ignoring second call to _startAsyncProcessor");
3852  return;
3853  }
3854 
3855  // Send headers if they haven't been sent already.
3856  if (this._headers)
3857  {
3858  if (this._powerSeized)
3859  this._headers = null;
3860  else
3861  this._sendHeaders();
3862  NS_ASSERT(this._headers === null, "_sendHeaders() failed?");
3863  }
3864 
3865  var response = this;
3866  var connection = this._connection;
3867 
3868  // If no body data was written, we're done
3869  if (!this._bodyInputStream)
3870  {
3871  dumpn("*** empty body, response finished");
3872  response.end();
3873  return;
3874  }
3875 
3876  var copyObserver =
3877  {
3878  onStartRequest: function(request, context)
3879  {
3880  dumpn("*** onStartRequest");
3881  },
3882 
3883  onStopRequest: function(request, cx, statusCode)
3884  {
3885  dumpn("*** onStopRequest [status=0x" + statusCode.toString(16) + "]");
3886 
3887  if (statusCode === Cr.NS_BINDING_ABORTED)
3888  {
3889  dumpn("*** terminating copy observer without ending the response");
3890  }
3891  else
3892  {
3893  if (!Components.isSuccessCode(statusCode))
3894  dumpn("*** WARNING: non-success statusCode in onStopRequest");
3895 
3896  response.end();
3897  }
3898  },
3899 
3900  QueryInterface: function(aIID)
3901  {
3902  if (aIID.equals(Ci.nsIRequestObserver) ||
3903  aIID.equals(Ci.nsISupports))
3904  return this;
3905 
3906  throw Cr.NS_ERROR_NO_INTERFACE;
3907  }
3908  };
3909 
3910  dumpn("*** starting async copier of body data...");
3911  var copier = this._asyncCopier =
3912  new WriteThroughCopier(this._bodyInputStream, this._connection.output,
3913  copyObserver, null);
3914  },
3915 
3917  _ensureAlive: function()
3918  {
3919  NS_ASSERT(!this._ended, "not handling response lifetime correctly");
3920  }
3921 };
3922 
3927 Response.SEGMENT_SIZE = 8192;
3928 
3930 function notImplemented()
3931 {
3932  throw Cr.NS_ERROR_NOT_IMPLEMENTED;
3933 }
3934 
3949 function WriteThroughCopier(input, output, observer, context)
3950 {
3951  if (!input || !output || !observer)
3952  throw Cr.NS_ERROR_NULL_POINTER;
3953 
3955  this._input = input;
3956 
3958  this._output = new BinaryOutputStream(output);
3959 
3961  this._observer = observer;
3962 
3964  this._context = context;
3965 
3967  this._completed = false;
3968 
3970  this.loadFlags = 0;
3972  this.loadGroup = null;
3974  this.name = "response-body-copy";
3975 
3977  this.status = Cr.NS_OK;
3978 
3979  // start copying
3980  try
3981  {
3982  observer.onStartRequest(this, context);
3983  this._waitForData();
3984  }
3985  catch (e)
3986  {
3987  dumpn("!!! error starting copy: " + e);
3988  this.cancel(Cr.NS_ERROR_UNEXPECTED);
3989  }
3990 }
3991 WriteThroughCopier.prototype =
3992 {
4000  cancel: function(status)
4001  {
4002  dumpn("*** cancel(" + status.toString(16) + ")");
4003 
4004  if (this._completed)
4005  {
4006  dumpn("*** ignoring cancel on already-canceled copier...");
4007  return;
4008  }
4009 
4010  this._completed = true;
4011  this.status = status;
4012 
4013  var self = this;
4014  var cancelEvent =
4015  {
4016  run: function()
4017  {
4018  dumpn("*** onStopRequest async callback");
4019  try
4020  {
4021  self._observer.onStopRequest(self, self._context, self.status);
4022  }
4023  catch (e)
4024  {
4025  NS_ASSERT(false, "how are we throwing an exception here? " + e);
4026  }
4027  }
4028  };
4029  gThreadManager.currentThread
4030  .dispatch(cancelEvent, Ci.nsIThread.DISPATCH_NORMAL);
4031  },
4032 
4037  isPending: function()
4038  {
4039  return !this._completed;
4040  },
4041 
4043  suspend: notImplemented,
4045  resume: notImplemented,
4046 
4051  onInputStreamReady: function(input)
4052  {
4053  dumpn("*** onInputStreamReady");
4054  if (this._completed)
4055  {
4056  dumpn("*** ignoring stream-ready callback on a canceled copier...");
4057  return;
4058  }
4059 
4060  input = new BinaryInputStream(input);
4061  try
4062  {
4063  var avail = input.available();
4064  var data = input.readByteArray(avail);
4065  this._output.writeByteArray(data, data.length);
4066  }
4067  catch (e)
4068  {
4069  if (e === Cr.NS_BASE_STREAM_CLOSED ||
4070  e.result === Cr.NS_BASE_STREAM_CLOSED)
4071  {
4072  this.cancel(Cr.NS_OK);
4073  }
4074  else
4075  {
4076  dumpn("!!! error copying from input to output: " + e);
4077  this.cancel(Cr.NS_ERROR_UNEXPECTED);
4078  }
4079  return;
4080  }
4081 
4082  if (avail === 0)
4083  this.cancel(Cr.NS_OK);
4084  else
4085  this._waitForData();
4086  },
4087 
4091  _waitForData: function()
4092  {
4093  dumpn("*** _waitForData");
4094  this._input.asyncWait(this, 0, 1, gThreadManager.mainThread);
4095  },
4096 
4098  QueryInterface: function(iid)
4099  {
4100  if (iid.equals(Ci.nsIRequest) ||
4101  iid.equals(Ci.nsISupports) ||
4102  iid.equals(Ci.nsIInputStreamCallback))
4103  {
4104  return this;
4105  }
4106 
4107  throw Cr.NS_ERROR_NO_INTERFACE;
4108  }
4109 };
4110 
4111 
4116 {
4128  normalizeFieldName: function(fieldName)
4129  {
4130  if (fieldName == "")
4131  throw Cr.NS_ERROR_INVALID_ARG;
4132 
4133  for (var i = 0, sz = fieldName.length; i < sz; i++)
4134  {
4135  if (!IS_TOKEN_ARRAY[fieldName.charCodeAt(i)])
4136  {
4137  dumpn(fieldName + " is not a valid header field name!");
4138  throw Cr.NS_ERROR_INVALID_ARG;
4139  }
4140  }
4141 
4142  return fieldName.toLowerCase();
4143  },
4144 
4158  normalizeFieldValue: function(fieldValue)
4159  {
4160  // field-value = *( field-content | LWS )
4161  // field-content = <the OCTETs making up the field-value
4162  // and consisting of either *TEXT or combinations
4163  // of token, separators, and quoted-string>
4164  // TEXT = <any OCTET except CTLs,
4165  // but including LWS>
4166  // LWS = [CRLF] 1*( SP | HT )
4167  //
4168  // quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
4169  // qdtext = <any TEXT except <">>
4170  // quoted-pair = "\" CHAR
4171  // CHAR = <any US-ASCII character (octets 0 - 127)>
4172 
4173  // Any LWS that occurs between field-content MAY be replaced with a single
4174  // SP before interpreting the field value or forwarding the message
4175  // downstream (section 4.2); we replace 1*LWS with a single SP
4176  var val = fieldValue.replace(/(?:(?:\r\n)?[ \t]+)+/g, " ");
4177 
4178  // remove leading/trailing LWS (which has been converted to SP)
4179  val = val.replace(/^ +/, "").replace(/ +$/, "");
4180 
4181  // that should have taken care of all CTLs, so val should contain no CTLs
4182  for (var i = 0, len = val.length; i < len; i++)
4183  if (isCTL(val.charCodeAt(i)))
4184  throw Cr.NS_ERROR_INVALID_ARG;
4185 
4186  // XXX disallows quoted-pair where CHAR is a CTL -- will not invalidly
4187  // normalize, however, so this can be construed as a tightening of the
4188  // spec and not entirely as a bug
4189  return val;
4190  }
4191 };
4192 
4193 
4194 
4204 function htmlEscape(str)
4205 {
4206  // this is naive, but it'll work
4207  var s = "";
4208  for (var i = 0; i < str.length; i++)
4209  s += "&#" + str.charCodeAt(i) + ";";
4210  return s;
4211 }
4212 
4213 
4223 function nsHttpVersion(versionString)
4224 {
4225  var matches = /^(\d+)\.(\d+)$/.exec(versionString);
4226  if (!matches)
4227  throw "Not a valid HTTP version!";
4228 
4230  this.major = parseInt(matches[1], 10);
4231 
4233  this.minor = parseInt(matches[2], 10);
4234 
4235  if (isNaN(this.major) || isNaN(this.minor) ||
4236  this.major < 0 || this.minor < 0)
4237  throw "Not a valid HTTP version!";
4238 }
4239 nsHttpVersion.prototype =
4240 {
4245  toString: function ()
4246  {
4247  return this.major + "." + this.minor;
4248  },
4249 
4257  equals: function (otherVersion)
4258  {
4259  return this.major == otherVersion.major &&
4260  this.minor == otherVersion.minor;
4261  },
4262 
4264  atLeast: function(otherVersion)
4265  {
4266  return this.major > otherVersion.major ||
4267  (this.major == otherVersion.major &&
4268  this.minor >= otherVersion.minor);
4269  }
4270 };
4271 
4272 nsHttpVersion.HTTP_1_0 = new nsHttpVersion("1.0");
4273 nsHttpVersion.HTTP_1_1 = new nsHttpVersion("1.1");
4274 
4275 
4285 function nsHttpHeaders()
4286 {
4299  this._headers = {};
4300 }
4301 nsHttpHeaders.prototype =
4302 {
4313  setHeader: function(fieldName, fieldValue, merge)
4314  {
4315  var name = headerUtils.normalizeFieldName(fieldName);
4316  var value = headerUtils.normalizeFieldValue(fieldValue);
4317 
4318  // The following three headers are stored as arrays because their real-world
4319  // syntax prevents joining individual headers into a single header using
4320  // ",". See also <http://hg.mozilla.org/mozilla-central/diff/9b2a99adc05e/netwerk/protocol/http/src/nsHttpHeaderArray.cpp#l77>
4321  if (merge && name in this._headers)
4322  {
4323  if (name === "www-authenticate" ||
4324  name === "proxy-authenticate" ||
4325  name === "set-cookie")
4326  {
4327  this._headers[name].push(value);
4328  }
4329  else
4330  {
4331  this._headers[name][0] += "," + value;
4332  NS_ASSERT(this._headers[name].length === 1,
4333  "how'd a non-special header have multiple values?")
4334  }
4335  }
4336  else
4337  {
4338  this._headers[name] = [value];
4339  }
4340  },
4341 
4356  getHeader: function(fieldName)
4357  {
4358  return this.getHeaderValues(fieldName).join("\n");
4359  },
4360 
4378  getHeaderValues: function(fieldName)
4379  {
4380  var name = headerUtils.normalizeFieldName(fieldName);
4381 
4382  if (name in this._headers)
4383  return this._headers[name];
4384  else
4385  throw Cr.NS_ERROR_NOT_AVAILABLE;
4386  },
4387 
4399  hasHeader: function(fieldName)
4400  {
4401  var name = headerUtils.normalizeFieldName(fieldName);
4402  return (name in this._headers);
4403  },
4404 
4411  get enumerator()
4412  {
4413  var headers = [];
4414  for (var i in this._headers)
4415  {
4416  var supports = new SupportsString();
4417  supports.data = i;
4418  headers.push(supports);
4419  }
4420 
4421  return new nsSimpleEnumerator(headers);
4422  }
4423 };
4424 
4425 
4432 function nsSimpleEnumerator(items)
4433 {
4434  this._items = items;
4435  this._nextIndex = 0;
4436 }
4437 nsSimpleEnumerator.prototype =
4438 {
4439  hasMoreElements: function()
4440  {
4441  return this._nextIndex < this._items.length;
4442  },
4443  getNext: function()
4444  {
4445  if (!this.hasMoreElements())
4446  throw Cr.NS_ERROR_NOT_AVAILABLE;
4447 
4448  return this._items[this._nextIndex++];
4449  },
4450  QueryInterface: function(aIID)
4451  {
4452  if (Ci.nsISimpleEnumerator.equals(aIID) ||
4453  Ci.nsISupports.equals(aIID))
4454  return this;
4455 
4456  throw Cr.NS_ERROR_NO_INTERFACE;
4457  }
4458 };
4459 
4460 
4467 function Request(port)
4468 {
4470  this._method = "";
4471 
4473  this._path = "";
4474 
4476  this._queryString = "";
4477 
4479  this._scheme = "http";
4480 
4482  this._host = undefined;
4483 
4485  this._port = port;
4486 
4487  var bodyPipe = new Pipe(false, false, 0, PR_UINT32_MAX, null);
4488 
4490  this._bodyInputStream = bodyPipe.inputStream;
4491 
4493  this._bodyOutputStream = bodyPipe.outputStream;
4494 
4498  this._headers = new nsHttpHeaders();
4499 
4505  this._bag = null;
4506 }
4507 Request.prototype =
4508 {
4509  // SERVER METADATA
4510 
4511  //
4512  // see nsIHttpRequestMetadata.scheme
4513  //
4514  get scheme()
4515  {
4516  return this._scheme;
4517  },
4518 
4519  //
4520  // see nsIHttpRequestMetadata.host
4521  //
4522  get host()
4523  {
4524  return this._host;
4525  },
4526 
4527  //
4528  // see nsIHttpRequestMetadata.port
4529  //
4530  get port()
4531  {
4532  return this._port;
4533  },
4534 
4535  // REQUEST LINE
4536 
4537  //
4538  // see nsIHttpRequestMetadata.method
4539  //
4540  get method()
4541  {
4542  return this._method;
4543  },
4544 
4545  //
4546  // see nsIHttpRequestMetadata.httpVersion
4547  //
4548  get httpVersion()
4549  {
4550  return this._httpVersion.toString();
4551  },
4552 
4553  //
4554  // see nsIHttpRequestMetadata.path
4555  //
4556  get path()
4557  {
4558  return this._path;
4559  },
4560 
4561  //
4562  // see nsIHttpRequestMetadata.queryString
4563  //
4564  get queryString()
4565  {
4566  return this._queryString;
4567  },
4568 
4569  // HEADERS
4570 
4571  //
4572  // see nsIHttpRequestMetadata.getHeader
4573  //
4574  getHeader: function(name)
4575  {
4576  return this._headers.getHeader(name);
4577  },
4578 
4579  //
4580  // see nsIHttpRequestMetadata.hasHeader
4581  //
4582  hasHeader: function(name)
4583  {
4584  return this._headers.hasHeader(name);
4585  },
4586 
4587  //
4588  // see nsIHttpRequestMetadata.headers
4589  //
4590  get headers()
4591  {
4592  return this._headers.enumerator;
4593  },
4594 
4595  //
4596  // see nsIPropertyBag.enumerator
4597  //
4598  get enumerator()
4599  {
4600  this._ensurePropertyBag();
4601  return this._bag.enumerator;
4602  },
4603 
4604  //
4605  // see nsIHttpRequestMetadata.headers
4606  //
4607  get bodyInputStream()
4608  {
4609  return this._bodyInputStream;
4610  },
4611 
4612  //
4613  // see nsIPropertyBag.getProperty
4614  //
4615  getProperty: function(name)
4616  {
4617  this._ensurePropertyBag();
4618  return this._bag.getProperty(name);
4619  },
4620 
4622  _ensurePropertyBag: function()
4623  {
4624  if (!this._bag)
4625  this._bag = new WritablePropertyBag();
4626  }
4627 };
4628 
4629 
4630 // XPCOM trappings
4631 
4636 function makeFactory(ctor)
4637 {
4638  function ci(outer, iid)
4639  {
4640  if (outer != null)
4641  throw Components.results.NS_ERROR_NO_AGGREGATION;
4642  return (new ctor()).QueryInterface(iid);
4643  }
4644 
4645  return {
4646  createInstance: ci,
4647  lockFactory: function(lock) { },
4648  QueryInterface: function(aIID)
4649  {
4650  if (Ci.nsIFactory.equals(aIID) ||
4651  Ci.nsISupports.equals(aIID))
4652  return this;
4653  throw Cr.NS_ERROR_NO_INTERFACE;
4654  }
4655  };
4656 }
4657 
4659 const module =
4660 {
4661  // nsISupports
4662  QueryInterface: function(aIID)
4663  {
4664  if (Ci.nsIModule.equals(aIID) ||
4665  Ci.nsISupports.equals(aIID))
4666  return this;
4667  throw Cr.NS_ERROR_NO_INTERFACE;
4668  },
4669 
4670  // nsIModule
4671  registerSelf: function(compMgr, fileSpec, location, type)
4672  {
4673  compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar);
4674 
4675  for (var key in this._objects)
4676  {
4677  var obj = this._objects[key];
4678  compMgr.registerFactoryLocation(obj.CID, obj.className, obj.contractID,
4679  fileSpec, location, type);
4680  }
4681  },
4682  unregisterSelf: function (compMgr, location, type)
4683  {
4684  compMgr = compMgr.QueryInterface(Ci.nsIComponentRegistrar);
4685 
4686  for (var key in this._objects)
4687  {
4688  var obj = this._objects[key];
4689  compMgr.unregisterFactoryLocation(obj.CID, location);
4690  }
4691  },
4692  getClassObject: function(compMgr, cid, iid)
4693  {
4694  if (!iid.equals(Ci.nsIFactory))
4695  throw Cr.NS_ERROR_NOT_IMPLEMENTED;
4696 
4697  for (var key in this._objects)
4698  {
4699  if (cid.equals(this._objects[key].CID))
4700  return this._objects[key].factory;
4701  }
4702 
4703  throw Cr.NS_ERROR_NO_INTERFACE;
4704  },
4705  canUnload: function(compMgr)
4706  {
4707  return true;
4708  },
4709 
4710  // private implementation
4711  _objects:
4712  {
4713  server:
4714  {
4715  CID: Components.ID("{54ef6f81-30af-4b1d-ac55-8ba811293e41}"),
4716  contractID: "@mozilla.org/server/jshttp;1",
4717  className: "httpd.js server",
4718  factory: makeFactory(nsHttpServer)
4719  }
4720  }
4721 };
4722 
4723 
4725 function NSGetModule(compMgr, fileSpec)
4726 {
4727  return module;
4728 }
4729 
4730 
4758 function server(port, basePath)
4759 {
4760  if (basePath)
4761  {
4762  var lp = Cc["@mozilla.org/file/local;1"]
4763  .createInstance(Ci.nsILocalFile);
4764  lp.initWithPath(basePath);
4765  }
4766 
4767  // if you're running this, you probably want to see debugging info
4768  DEBUG = true;
4769 
4770  var srv = new nsHttpServer();
4771  if (lp)
4772  srv.registerDirectory("/", lp);
4773  srv.registerContentType("sjs", SJS_TYPE);
4774  srv.identity.setPrimary("http", "localhost", port);
4775  srv.start(port);
4776 
4777  var thread = gThreadManager.currentThread;
4778  while (!srv.isStopped())
4779  thread.processNextEvent(true);
4780 
4781  // get rid of any pending requests
4782  while (thread.hasPendingEvents())
4783  thread.processNextEvent(true);
4784 
4785  DEBUG = false;
4786 }
function start(ch)
const HTTP_505
Definition: httpd.js:126
const CC
Definition: httpd.js:53
classDescription entry
Definition: FeedWriter.js:1427
const BinaryInputStream
Definition: httpd.js:210
function readBytes(inputStream, count)
Definition: httpd.js:1148
function LineData()
Definition: httpd.js:1809
const READER_FINISHED
Definition: httpd.js:1159
function stop(ch, cx, status, data)
const HTTP_400
Definition: httpd.js:102
const LF
Definition: httpd.js:1780
nsString encodeURIComponent(const nsString &c)
function makeFactory(ctor)
Definition: httpd.js:4636
function defaultIndexHandler(metadata, response)
Definition: httpd.js:1889
const HTTP_402
Definition: httpd.js:104
function ServerIdentity()
Definition: httpd.js:811
function findCRLF(array)
Definition: httpd.js:1794
const HTTP_404
Definition: httpd.js:106
const HOST_REGEX
Definition: httpd.js:785
var gRootPrefBranch
Definition: httpd.js:191
function Connection(input, output, server, port, outgoingPort, number)
Definition: httpd.js:1049
_setDateDatepicker date
const PR_RDONLY
sbOSDControlService prototype className
function WriteThroughCopier(input, output, observer, context)
Definition: httpd.js:3949
const Cc
Definition: httpd.js:49
SafebrowsingApplicationMod prototype registerSelf
const HTTP_407
Definition: httpd.js:109
inArray array
const IS_TOKEN_ARRAY
Definition: httpd.js:3291
function nsHttpVersion(versionString)
Definition: httpd.js:4223
const HTTP_409
Definition: httpd.js:111
function Request(port)
Definition: httpd.js:4467
function ServerHandler(server)
Definition: httpd.js:2101
for(let i=0;i< aHistory.count;i++)
const Cu
Definition: httpd.js:52
const HTTP_415
Definition: httpd.js:117
const WritablePropertyBag
Definition: httpd.js:228
function array2obj(arr)
Definition: httpd.js:129
sbDeviceFirmwareAutoCheckForUpdate prototype contractID
_changeFirstDay day
const HTTP_503
Definition: httpd.js:124
const HTTP_504
Definition: httpd.js:125
sidebarFactory createInstance
Definition: nsSidebar.js:351
sbOSDControlService prototype QueryInterface
function nsHttpHeaders()
Definition: httpd.js:4285
const BinaryOutputStream
Definition: httpd.js:213
var header
Definition: FeedWriter.js:953
function RequestReader(connection)
Definition: httpd.js:1180
ui plugin add("draggable","cursor",{start:function(e, ui){var t=$('body');if(t.css("cursor")) ui.options._cursor=t.css("cursor");t.css("cursor", ui.options.cursor);}, stop:function(e, ui){if(ui.options._cursor)$('body').css("cursor", ui.options._cursor);}})
const READER_IN_HEADERS
Definition: httpd.js:1157
function server(port, basePath)
Definition: httpd.js:4758
function dumpn(str)
Definition: httpd.js:172
function toDateString(date)
Definition: httpd.js:242
function fileSort(a, b)
Definition: httpd.js:1957
const HEADERS_SUFFIX
Definition: httpd.js:165
const CID
function NSGetModule(compMgr, fileSpec)
Definition: httpd.js:4725
const HTTP_500
Definition: httpd.js:121
function d(s)
PRUint32 & offset
SafebrowsingApplicationMod prototype getClassObject
function dumpStack()
Definition: httpd.js:179
var t
const module
Definition: httpd.js:4659
var count
Definition: test_bug7406.js:32
const FileInputStream
Definition: httpd.js:222
var gThreadManager
Definition: httpd.js:188
const HTTP_502
Definition: httpd.js:123
const Pipe
Definition: httpd.js:219
this _dialogInput val(dateText)
BogusChannel prototype loadGroup
function FileMap()
Definition: httpd.js:3239
function maybeAddHeaders(file, metadata, response)
Definition: httpd.js:2012
const HTTP_411
Definition: httpd.js:113
function getRootPrefBranch()
Definition: httpd.js:192
unique done
grep callback
const HTTP_413
Definition: httpd.js:115
const SupportsString
Definition: httpd.js:230
function printObj(o, showMembers)
Definition: httpd.js:321
ExtensionSchemeMatcher prototype match
const ConverterInputStream
Definition: httpd.js:225
const HTTP_403
Definition: httpd.js:105
return null
Definition: FeedWriter.js:1143
const HTTP_417
Definition: httpd.js:119
const HTTP_410
Definition: httpd.js:112
const PR_UINT32_MAX
Definition: httpd.js:55
function NS_ASSERT(cond, msg)
Definition: httpd.js:70
var gGlobalObject
Definition: httpd.js:62
const HTTP_414
Definition: httpd.js:116
function nsSimpleEnumerator(items)
Definition: httpd.js:4432
SimpleArrayEnumerator prototype hasMoreElements
const HTTP_412
Definition: httpd.js:114
function Response(connection)
Definition: httpd.js:3334
var uri
Definition: FeedWriter.js:1135
var prefs
Definition: FeedWriter.js:1169
countRef value
Definition: FeedWriter.js:1423
const HTTP_408
Definition: httpd.js:110
const CR
Definition: httpd.js:1780
const ServerSocket
Definition: httpd.js:207
const HTTP_501
Definition: httpd.js:122
const headerUtils
Definition: httpd.js:4115
const HTTP_405
Definition: httpd.js:107
var DEBUG
Definition: httpd.js:60
const ScriptableInputStream
Definition: httpd.js:216
function notImplemented()
Definition: httpd.js:3930
function msg
const SJS_TYPE
Definition: httpd.js:168
const READER_IN_BODY
Definition: httpd.js:1158
const Cr
Definition: httpd.js:51
function createHandlerFunc(handler)
Definition: httpd.js:1879
function HttpError(code, description)
Definition: httpd.js:86
function htmlEscape(str)
Definition: httpd.js:4204
observe data
Definition: FeedWriter.js:1329
const HTTP_ERROR_CODES
Definition: httpd.js:147
function isCTL(code)
Definition: httpd.js:3321
Response SEGMENT_SIZE
Definition: httpd.js:3927
_getSelectedPageStyle s i
function toInternalPath(path, encoded)
Definition: httpd.js:1983
const HTTP_416
Definition: httpd.js:118
const HTTP_401
Definition: httpd.js:103
GstMessage gpointer data sbGStreamerMessageHandler * handler
const EXPORTED_SYMBOLS
Definition: httpd.js:57
const Ci
Definition: httpd.js:50
const READER_IN_REQUEST_LINE
Definition: httpd.js:1156
const HTTP_406
Definition: httpd.js:108
function nsHttpServer()
Definition: httpd.js:339
let observer
const HIDDEN_CHAR
Definition: httpd.js:159
var file
var srv
function range(x, y)
Definition: httpd.js:138