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