diff options
Diffstat (limited to 'comm/mailnews/test/fakeserver/Maild.jsm')
-rw-r--r-- | comm/mailnews/test/fakeserver/Maild.jsm | 566 |
1 files changed, 566 insertions, 0 deletions
diff --git a/comm/mailnews/test/fakeserver/Maild.jsm b/comm/mailnews/test/fakeserver/Maild.jsm new file mode 100644 index 0000000000..30647e1d56 --- /dev/null +++ b/comm/mailnews/test/fakeserver/Maild.jsm @@ -0,0 +1,566 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Much of the original code is taken from netwerk's httpserver implementation + +var EXPORTED_SYMBOLS = [ + "nsMailServer", + "gThreadManager", // TODO: kill this export + "fsDebugNone", + "fsDebugAll", + "fsDebugRecv", + "fsDebugRecvSend", +]; + +var CC = Components.Constructor; + +/** + * The XPCOM thread manager. This declaration is obsolete and exists only + * because deleting it breaks several dozen tests at the moment. + */ +var gThreadManager = Services.tm; + +var fsDebugNone = 0; +var fsDebugRecv = 1; +var fsDebugRecvSend = 2; +var fsDebugAll = 3; + +/** + * JavaScript constructors for commonly-used classes; precreating these is a + * speedup over doing the same from base principles. See the docs at + * http://developer.mozilla.org/en/Components.Constructor for details. + */ +var ServerSocket = CC( + "@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "init" +); +var BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +// Time out after 3 minutes +var TIMEOUT = 3 * 60 * 1000; + +/** + * The main server handling class. A fake server consists of three parts, this + * server implementation (which handles the network communication), the handler + * (which handles the state for a connection), and the daemon (which handles + * the state for the logical server). To make a new server, one needs to pass + * in a function to create handlers--not the handlers themselves--and the + * backend daemon. Since each handler presumably needs access to the logical + * server daemon, that is passed into the handler creation function. A new + * handler will be constructed for every connection made. + * + * As the core code is inherently single-threaded, it is guaranteed that all of + * the calls to the daemon will be made on the same thread, so you do not have + * to worry about reentrancy in daemon calls. + * + * Typical usage: + * + * function createHandler(daemon) { + * return new handler(daemon); + * } + * do_test_pending(); + * var server = new nsMailServer(createHandler, serverDaemon); + * // Port to use. I tend to like using 1024 + default port number myself. + * server.start(port); + * + * // Set up a connection the server... + * server.performTest(); + * transaction = server.playTransaction(); + * // Verify that the transaction is correct... + * + * server.resetTest(); + * // Set up second test... + * server.performTest(); + * transaction = server.playTransaction(); + * + * // Finished with tests + * server.stop(); + * + * var thread = Services.tm.currentThread; + * while (thread.hasPendingEvents()) + * thread.processNextEvent(true); + * + * do_test_finished(); + */ +class nsMailServer { + constructor(handlerCreator, daemon) { + this._debug = fsDebugNone; + + /** The port on which this server listens. */ + this._port = -1; + + /** The socket associated with this. */ + this._socket = null; + + /** + * True if the socket in this is closed (and closure notifications have been + * sent and processed if the socket was ever opened), false otherwise. + */ + this._socketClosed = true; + + /** + * Should we log transactions? This only matters if you want to inspect the + * protocol traffic. Defaults to true because this was written for protocol + * testing. + */ + this._logTransactions = true; + + this._handlerCreator = handlerCreator; + this._daemon = daemon; + this._readers = []; + this._test = false; + this._watchWord = undefined; + + /** + * An array to hold refs to all the input streams below, so that they don't + * get GCed + */ + this._inputStreams = []; + } + + onSocketAccepted(socket, trans) { + if (this._debug != fsDebugNone) { + dump("Received Connection from " + trans.host + ":" + trans.port + "\n"); + } + + const SEGMENT_SIZE = 1024; + const SEGMENT_COUNT = 1024; + var input = trans + .openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT) + .QueryInterface(Ci.nsIAsyncInputStream); + this._inputStreams.push(input); + + var handler = this._handlerCreator(this._daemon); + var reader = new nsMailReader( + this, + handler, + trans, + this._debug, + this._logTransactions + ); + this._readers.push(reader); + + // Note: must use main thread here, or we might get a GC that will cause + // threadsafety assertions. We really need to fix XPConnect so that + // you can actually do things in multi-threaded JS. :-( + input.asyncWait(reader, 0, 0, Services.tm.mainThread); + this._test = true; + } + + onStopListening(socket, status) { + if (this._debug != fsDebugNone) { + dump("Connection Lost " + status + "\n"); + } + + this._socketClosed = true; + // We've been killed or we've stopped, reset the handler to the original + // state (e.g. to require authentication again). + for (var i = 0; i < this._readers.length; i++) { + this._readers[i]._handler.resetTest(); + this._readers[i]._realCloseSocket(); + } + } + + setDebugLevel(debug) { + this._debug = debug; + for (var i = 0; i < this._readers.length; i++) { + this._readers[i].setDebugLevel(debug); + } + } + + start(port = -1) { + if (this._socket) { + throw Components.Exception("", Cr.NS_ERROR_ALREADY_INITIALIZED); + } + + if (port > 0) { + this._port = port; + } + this._socketClosed = false; + + var socket = new ServerSocket( + this._port, + true, // loopback only + -1 + ); // default number of pending connections + + socket.asyncListen(this); + this._socket = socket; + } + + stop() { + if (!this._socket) { + return; + } + + this._socket.close(); + this._socket = null; + + for (let reader of this._readers) { + reader._realCloseSocket(); + } + + if (this._readers.some(e => e.observer.forced)) { + return; + } + + // spin an event loop and wait for the socket-close notification + let thr = Services.tm.currentThread; + while (!this._socketClosed) { + // Don't wait for the next event, just in case there isn't one. + thr.processNextEvent(false); + } + } + stopTest() { + this._test = false; + } + + get port() { + if (this._port == -1) { + this._port = this._socket.port; + } + return this._port; + } + + // NSISUPPORTS + + // + // see nsISupports.QueryInterface + // + QueryInterface = ChromeUtils.generateQI(["nsIServerSocketListener"]); + + // NON-XPCOM PUBLIC API + + /** + * Returns true if this server is not running (and is not in the process of + * serving any requests still to be processed when the server was last + * stopped after being run). + */ + isStopped() { + return this._socketClosed; + } + + /** + * Runs the test. It will not exit until the test has finished. + */ + performTest(watchWord) { + this._watchWord = watchWord; + + let thread = Services.tm.currentThread; + while (!this.isTestFinished()) { + thread.processNextEvent(false); + } + } + + /** + * Returns true if the current processing test has finished. + */ + isTestFinished() { + return this._readers.length > 0 && !this._test; + } + + /** + * Returns the commands run between the server and client. + * The return is an object with two variables (us and them), both of which + * are arrays returning the commands given by each server. + */ + playTransaction() { + if (this._readers.some(e => e.observer.forced)) { + throw new Error("Server timed out!"); + } + if (this._readers.length == 1) { + return this._readers[0].transaction; + } + return this._readers.map(e => e.transaction); + } + + /** + * Prepares for the next test. + */ + resetTest() { + this._readers = this._readers.filter(function (reader) { + return reader._isRunning; + }); + this._test = true; + for (var i = 0; i < this._readers.length; i++) { + this._readers[i]._handler.resetTest(); + } + } +} + +function readTo(input, count, arr) { + var old = new BinaryInputStream(input).readByteArray(count); + Array.prototype.push.apply(arr, old); +} + +/** + * The nsMailReader service, which reads and handles the lines. + * All specific handling is passed off to the handler, which is responsible + * for maintaining its own state. The following commands are required for the + * handler object: + * onError Called when handler[command] does not exist with both the + * command and rest-of-line as arguments + * onStartup Called on initialization with no arguments + * onMultiline Called when in multiline with the entire line as an argument + * postCommand Called after every command with this reader as the argument + * [command] An untranslated command with the rest of the line as the + * argument. Defined as everything to the first space + * + * All functions, except onMultiline and postCommand, treat the + * returned value as the text to be sent to the client; a newline at the end + * may be added if it does not exist, and all lone newlines are converted to + * CRLF sequences. + * + * The return of postCommand is ignored. The return of onMultiline is a bit + * complicated: it may or may not return a response string (returning one is + * necessary to trigger the postCommand handler). + * + * This object has the following supplemental functions for use by handlers: + * closeSocket Performs a server-side socket closing + * setMultiline Sets the multiline mode based on the argument + */ +class nsMailReader { + constructor(server, handler, transport, debug, logTransaction) { + this._debug = debug; + this._server = server; + this._buffer = []; + this._lines = []; + this._handler = handler; + this._transport = transport; + // We don't seem to properly handle large streams when the buffer gets + // exhausted, which causes issues trying to test large messages. So just + // allow a really big buffer. + var output = transport.openOutputStream( + Ci.nsITransport.OPEN_BLOCKING, + 1024, + 4096 + ); + this._output = output; + if (logTransaction) { + this.transaction = { us: [], them: [] }; + } else { + this.transaction = null; + } + + // Send response line + var response = this._handler.onStartup(); + response = response.replace(/([^\r])\n/g, "$1\r\n"); + if (!response.endsWith("\n")) { + response = response + "\r\n"; + } + if (this.transaction) { + this.transaction.us.push(response); + } + this._output.write(response, response.length); + this._output.flush(); + + this._multiline = false; + + this._isRunning = true; + + this.observer = { + server, + forced: false, + notify(timer) { + this.forced = true; + this.server.stopTest(); + this.server.stop(); + }, + QueryInterface: ChromeUtils.generateQI(["nsITimerCallback"]), + }; + this.timer = Cc["@mozilla.org/timer;1"] + .createInstance() + .QueryInterface(Ci.nsITimer); + this.timer.initWithCallback( + this.observer, + TIMEOUT, + Ci.nsITimer.TYPE_ONE_SHOT + ); + } + + _findLines() { + var buf = this._buffer; + for ( + var crlfLoc = buf.indexOf(13); + crlfLoc >= 0; + crlfLoc = buf.indexOf(13, crlfLoc + 1) + ) { + if (buf[crlfLoc + 1] == 10) { + break; + } + } + if (crlfLoc == -1) { + // We failed to find a newline + return; + } + + var line = String.fromCharCode.apply(null, buf.slice(0, crlfLoc)); + this._buffer = buf.slice(crlfLoc + 2); + this._lines.push(line); + this._findLines(); + } + + onInputStreamReady(stream) { + if (this.observer.forced) { + return; + } + + this.timer.cancel(); + try { + var bytes = stream.available(); + } catch (e) { + // Someone, not us, has closed the stream. This means we can't get any + // more data from the stream, so we'll just go and close our socket. + this._realCloseSocket(); + return; + } + readTo(stream, bytes, this._buffer); + this._findLines(); + + while (this._lines.length > 0) { + var line = this._lines.shift(); + + if (this._debug != fsDebugNone) { + dump("RECV: " + line + "\n"); + } + + var response; + try { + let command; + if (this._multiline) { + response = this._handler.onMultiline(line); + + if (response === undefined) { + continue; + } + } else { + // Record the transaction + if (this.transaction) { + this.transaction.them.push(line); + } + + // Find the command and splice it out... + var splitter = line.indexOf(" "); + command = splitter == -1 ? line : line.substring(0, splitter); + let args = splitter == -1 ? "" : line.substring(splitter + 1); + + // By convention, commands are uppercase + command = command.toUpperCase(); + + if (this._debug == fsDebugAll) { + dump("Received command " + command + "\n"); + } + + if (command in this._handler) { + response = this._handler[command](args); + } else { + response = this._handler.onError(command, args); + } + } + + this._preventLFMunge = false; + this._handler.postCommand(this); + + if (this.watchWord && command == this.watchWord) { + this.stopTest(); + } + } catch (e) { + response = this._handler.onServerFault(e); + if (e instanceof Error) { + dump(e.name + ": " + e.message + "\n"); + dump("File: " + e.fileName + " Line: " + e.lineNumber + "\n"); + dump("Stack trace:\n" + e.stack); + } else { + dump("Exception caught: " + e + "\n"); + } + } + + if (!this._preventLFMunge) { + response = response.replaceAll("\r\n", "\n").replaceAll("\n", "\r\n"); + } + + if (!response.endsWith("\n")) { + response = response + "\r\n"; + } + + if (this._debug == fsDebugRecvSend) { + dump("SEND: " + response.split(" ", 1)[0] + "\n"); + } else if (this._debug == fsDebugAll) { + var responses = response.split("\n"); + responses.forEach(function (line) { + dump("SEND: " + line + "\n"); + }); + } + + if (this.transaction) { + this.transaction.us.push(response); + } + + try { + this._output.write(response, response.length); + this._output.flush(); + } catch (ex) { + if (ex.result == Cr.NS_BASE_STREAM_CLOSED) { + dump("Stream closed whilst sending, this may be expected\n"); + this._realCloseSocket(); + } else { + // Some other issue, let the test see it. + throw ex; + } + } + + if (this._signalStop) { + this._realCloseSocket(); + this._signalStop = false; + } + } + + if (this._isRunning) { + stream.asyncWait(this, 0, 0, Services.tm.currentThread); + this.timer.initWithCallback( + this.observer, + TIMEOUT, + Ci.nsITimer.TYPE_ONE_SHOT + ); + } + } + + closeSocket() { + this._signalStop = true; + } + _realCloseSocket() { + this._isRunning = false; + this._output.close(); + this._transport.close(Cr.NS_OK); + this._server.stopTest(); + } + + setMultiline(multi) { + this._multiline = multi; + } + + setDebugLevel(debug) { + this._debug = debug; + } + + preventLFMunge() { + this._preventLFMunge = true; + } + + get watchWord() { + return this._server._watchWord; + } + + stopTest() { + this._server.stopTest(); + } + + QueryInterface = ChromeUtils.generateQI(["nsIInputStreamCallback"]); +} |