pax_global_header00006660000000000000000000000064124663522720014523gustar00rootroot0000000000000052 comment=86b8a7231597c2307f35424c5646e663391ae301 rai-0.1.12/000077500000000000000000000000001246635227200123575ustar00rootroot00000000000000rai-0.1.12/.gitignore000066400000000000000000000000441246635227200143450ustar00rootroot00000000000000node_modules .DS_Store npm-debug.lograi-0.1.12/.npmignore000066400000000000000000000000151246635227200143520ustar00rootroot00000000000000test examplesrai-0.1.12/.travis.yml000066400000000000000000000002531246635227200144700ustar00rootroot00000000000000language: node_js node_js: - "0.8" - "0.10" - "0.11" notifications: email: recipients: - andris@kreata.ee on_success: change on_failure: change rai-0.1.12/LICENSE000066400000000000000000000016411246635227200133660ustar00rootroot00000000000000Copyright (c) 2012 Andris Reinman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.rai-0.1.12/README.md000066400000000000000000000121161246635227200136370ustar00rootroot00000000000000# RAI - Request-Answer-Interface **rai** is a node.js module to easily generate text based command line servers. When a client sends something to the server, the first word of the line is treated as a command and the rest of the line as binary payload. [![Build Status](https://secure.travis-ci.org/andris9/rai.png)](http://travis-ci.org/andris9/rai) In addition to line based commands, there's also a data mode, to transmit everygting received. And there's also an option to switch to TLS mode for secure connections. This way it is trivial to create SMTP, POP3 or similar servers. ## Installation npm install rai ## Usage ### Simple server var RAIServer = require("rai").RAIServer; // create a RAIServer on port 1234 var server = new RAIServer(); server.listen(1234); // Start listening for client connections server.on("connect", function(client){ // Greet the client client.send("Hello!"); // Wait for a command client.on("command", function(command, payload){ if(command == "STATUS"){ client.send("Status is OK!"); }else if(command == "QUIT"){ client.send("Goodbye"); client.end(); }else{ client.send("Unknown command"); } }); }); Server only emits `'connect'` and `'error'` events, while the client objects emit `'timeout'`, `'error'` and `'end'` in addition to data related events. ### Starting a server Server can be started with `new RAIServer([options])` where options is an optional parameters object with the following properties: * **debug** - if set to true print traffic to console * **disconnectOnTimeout** - if set to true close the connection on disconnect * **secureConnection** - if set to true close the connection on disconnect * **credentials** - credentials for secureConnection and STARTTLS * **timeout** - timeout in milliseconds for disconnecting the client, defaults to 0 (no timeout) Once the server has been set up, it can start listening for client connections with `server.listen(port[, hostname][, callback])`. Callback function gets an error object as a parameter if the listening failed. var server = new RAIServer(); server.listen(25); // start listening for port 25 on "localhost" ### Closing server Server can be closed with `server.end([callback])` where callback is run when the server is finally closed. ### Sending data Data can be sent with `client.send(data)` where `data` is either a String or a Buffer. `"\r\n"` is automatically appended to the data. client.send("Greetings!"); ### Forcing connection close Connections can be ended with `client.end()` if(command == "QUIT"){ client.send("Good bye!"); client.end(); } ### TLS mode TLS can be switched on with `client.startTLS([credentials][, callback])` and the status can be listened with `'tls'` (emitted when secure connection is established) `credentials` is an object with strings of pem encoded `key`, `cert` and optionally an array `ca`. If `credentials` is not supplied, an autogenerated value is used. if(command == "STARTTLS"){ client.startTLS(); } client.on("tls", function(){ console.log("Switched to secure connection"); }); If `callback` is not set `'tls'` will be emitted on connection upgrade. ### Data mode Data mode can be turned on with `client.startDataMode([endSequence])` and incoming chunks can be received with `'data'`. The end of data mode can be detected by `'ready'`. `endSequence` is a String for matching the end (entire line) of the data stream. By default it's `"."` which is suitable for SMTP and POP3. if(command == "DATA"){ client.send("End data with ."); client.startDataMode(); } client.on("data", function(chunk){ console.log("Data from client:", chunk); }); client.on("ready", function(){ client.send("Data received"); }); ## Testing There is a possibility to set up a mockup client which sends a batch of commands one by one to the server and returns the last response and an array of all responses(except the TLS negotiation). var runClientMockup = require("rai").runClientMockup; var cmds = ["EHLO FOOBAR", "STARTTLS", "QUIT"]; runClientMockup(25, "mail.hot.ee", cmds, function(lastResponse, allResponses){ console.log("Final:", lastResponse.toString("utf-8").trim()); console.log("All:", allResponses.map(function(e){ return e.toString("utf-8").trim() }).join(', ')); }); `runClientMockup` has he following parameters in the following order: * **port** - Port number * **host** - Hostname to connect to * **commands** - Command list (an array) to be sent to server * **callback** - Callback function to run on completion * **debug** - if set to true log all input/output Response from the callback function is a Buffer and contains the last data received from the server and an array of Buffers with all data received from the server. ## License **MIT**rai-0.1.12/cert/000077500000000000000000000000001246635227200133145ustar00rootroot00000000000000rai-0.1.12/cert/server.crt000066400000000000000000000015671246635227200153450ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIICYTCCAcoCCQDl53qKS6iIgDANBgkqhkiG9w0BAQUFADB1MQswCQYDVQQGEwJF RTEOMAwGA1UECBMFSGFyanUxEDAOBgNVBAcTB1RhbGxpbm4xDzANBgNVBAoTBkty ZWF0YTESMBAGA1UEAxMJbG9jYWxob3N0MR8wHQYJKoZIhvcNAQkBFhBhbmRyaXNA a3JlYXRhLmVlMB4XDTEzMDMxOTA5NTcxNVoXDTE0MDMxOTA5NTcxNVowdTELMAkG A1UEBhMCRUUxDjAMBgNVBAgTBUhhcmp1MRAwDgYDVQQHEwdUYWxsaW5uMQ8wDQYD VQQKEwZLcmVhdGExEjAQBgNVBAMTCWxvY2FsaG9zdDEfMB0GCSqGSIb3DQEJARYQ YW5kcmlzQGtyZWF0YS5lZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwke9 RYIa5uOTqOSwJHO3lQyT6p9v7g/NI6FckuvqerGThS+irvP4Nd9xuqGjRB2HBEgM QqSlPqYQ+pI5zcI3V3r1/A9OxSQoR9ar6obKsAfHiWP1u96mpiAZJudYLPud69RR 1/BoihM6t2FSvwGXO+q38wOLM9tBWgt5Ng68fM0CAwEAATANBgkqhkiG9w0BAQUF AAOBgQBZJBkf/piXM2Kl725w1ZESlt0m1DbpP55K3ZLLJEQ2IZxQ1wtWChl2duAe s9Hv5YGm1U44wDbzNXfqqgUIDJVwDzJlq8xtTbfUCJ8HtDKLqH7rGIgDArdtwACZ bOW7J6ei0ZDhtyDnc9eHB5CT8bgTR1VkMlx3v/bPSCEmTiRJNA== -----END CERTIFICATE----- rai-0.1.12/cert/server.key000066400000000000000000000015731246635227200153420ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIICXgIBAAKBgQDCR71Fghrm45Oo5LAkc7eVDJPqn2/uD80joVyS6+p6sZOFL6Ku 8/g133G6oaNEHYcESAxCpKU+phD6kjnNwjdXevX8D07FJChH1qvqhsqwB8eJY/W7 3qamIBkm51gs+53r1FHX8GiKEzq3YVK/AZc76rfzA4sz20FaC3k2Drx8zQIDAQAB AoGARhUM4LsLK0ji5iUAqVWY3sp3vUYgYVcP4A+ATnuNzQ6rsXq6i7P0ULK22uUd +R9Rqii3S38LIOtU6p6+/UtXHLUxnNqGKx/6mSamKhv01UgqN65Laq1pCX40Wjgj k5R1wdkwQG+DOj+L6mPxnp92Stn+PYPCUqYpK1qLvPu7X2ECQQDvRToiJ0XRLLxa pz4eAUeN4KJWBJRIT+GZzx27qB3aAr6UT0ccXLmhYBl3AUNYcWyDdAp+ZoX+CQuZ yEc1Xob1AkEAz904LHgZSMNrKIPhU93Og7fXypY3Smlci5jPQ7rCxzGz0XWUslne pL8wRtwa8DpTufDj8Ihfw0E8BxUG+yYHeQJBAIL/quFSESaB0KntUNQKrUtfRmHD 5g9lNMYKIGRCmf1nbUIz2WIM3lEdFTQTi/SbPOcHnEsyBIBeIWzTuzDcDRUCQQDC 2cKgnOxGszkuP4Hn1hKSkrFsLKgjzuR7z4DrIpUXmNXRUYFUNr5ofPhKVGXEL0jx Eoj5nzz1kZ8tnF5w61MxAkEAmJYV3PN/V4pTqX/bnNwQWUAQfrDBdkjw2RAAUtbY MnunBSguCrxmQEKo/zomkZZ/R7/WpJyGZxN90SHa8mZJJA== -----END RSA PRIVATE KEY----- rai-0.1.12/examples/000077500000000000000000000000001246635227200141755ustar00rootroot00000000000000rai-0.1.12/examples/smtp.js000066400000000000000000000036441246635227200155250ustar00rootroot00000000000000var RAIServer = require("../lib/rai").RAIServer; var server = new RAIServer({debug: true, timeout:25*1000}); server.listen(1234, function(err){ console.log(err || "listening on port 1234...") }); server.on("connection", function(socket){ socket.send("220 foo.bar"); // send banner greeting socket.on("command", function(command, payload){ command = (command || "").toString().toUpperCase().trim(); switch(command){ case "EHLO": socket.send("250-foo.bar at your service\r\n"+ "250-PIPELINING\r\n" + "250-8BITMIME\r\n"+ "250 STARTTLS"); break; case "STARTTLS": socket.send("220 Ready to start TLS"); socket.startTLS(); break; case "MAIL": socket.send("250 Ok"); break; case "RCPT": socket.send("250 Ok"); break; case "DATA": socket.send("354 End with ."); socket.startDataMode(); break; case "QUIT": socket.send("221 Good bye"); socket.end(); break; default: socket.send("500 Unknown command"); } }); socket.on("tls", function(data){ console.log("TLS STARTED"); }); socket.on("data", function(data){ console.log("MAIL DATA", data); }); socket.on("ready", function(data){ console.log("DATA READY"); socket.send("250 Ok: queued as FOOBAR"); }); socket.on("timeout", function(data){ console.log("TIMEOUT"); }); socket.on("error", function(err){ console.log("ERROR:", err.message || err); }); socket.on("end", function(){ console.log("Connection closed"); }); });rai-0.1.12/lib/000077500000000000000000000000001246635227200131255ustar00rootroot00000000000000rai-0.1.12/lib/mockup.js000066400000000000000000000075331246635227200147710ustar00rootroot00000000000000"use strict"; var net = require("net"), crypto = require("crypto"), tlslib = require("tls"); // monkey patch net and tls to support nodejs 0.4 if(!net.connect && net.createConnection){ net.connect = net.createConnection; } if(!tlslib.connect && tlslib.createConnection){ tlslib.connect = tlslib.createConnection; } /** * @namespace Mockup module * @name mockup */ module.exports = runClientMockup; /** *

Runs a batch of commands against a server

* *
 * var cmds = ["EHLO FOOBAR", "STARTTLS", "QUIT"];
 * runClientMockup(25, "mail.hot.ee", cmds, function(resp){
 *     console.log("Final:", resp.toString("utf-8").trim());
 * });
 * 
* * @memberOf mockup * @param {Number} port Port number * @param {String} host Hostname to connect to * @param {Array} commands Command list to be sent to server * @param {Function} callback Callback function to run on completion, * has the last response from the server as a param * @param {Boolean} [debug] if set to true log all input/output */ function runClientMockup(port, host, commands, callback, debug){ host = host || "localhost"; port = port || 25; commands = Array.isArray(commands) ? commands : []; var command, ignore_data = false, responses = [], sslcontext, pair; var socket = net.connect(port, host); socket.on("connect", function(){ socket.on("data", function(chunk){ if(ignore_data){ return; } if(debug){ console.log("S: "+chunk.toString("utf-8").trim()); } if(!commands.length){ socket.end(); if(typeof callback == "function"){ responses.push(chunk); callback(chunk, responses); } return; }else{ responses.push(chunk); } if(["STARTTLS", "STLS"].indexOf((command || "").trim().toUpperCase())>=0){ ignore_data = true; if(debug){ console.log("Initiated TLS connection"); } sslcontext = crypto.createCredentials(); pair = tlslib.createSecurePair(sslcontext, false); pair.encrypted.pipe(socket); socket.pipe(pair.encrypted); pair.fd = socket.fd; pair.on("secure", function(){ if(debug){ console.log("TLS connection secured"); } command = commands.shift(); if(debug){ console.log("C: "+command); } pair.cleartext.write(command+"\r\n"); pair.cleartext.on("data", function(chunk){ if(debug){ console.log("S: "+chunk.toString("utf-8").trim()); } if(!commands.length){ pair.cleartext.end(); if(typeof callback == "function"){ responses.push(chunk); callback(chunk, responses); } return; }else{ responses.push(chunk); } command = commands.shift(); pair.cleartext.write(command+"\r\n"); if(debug){ console.log("C: "+command); } }); }); }else{ command = commands.shift(); socket.write(command+"\r\n"); if(debug){ console.log("C: "+command); } } }); }); } rai-0.1.12/lib/rai.js000066400000000000000000000344271246635227200142500ustar00rootroot00000000000000"use strict"; /** * @fileOverview This is the main file for the RAI library to create text based servers * @author Andris Reinman */ var netlib = require("net"), utillib = require("util"), EventEmitter = require('events').EventEmitter, starttls = require("./starttls").starttls, tlslib = require("tls"), fs = require("fs"), SlowBuffer = require("buffer").SlowBuffer; // Default credentials for starting TLS server var defaultCredentials = { key: fs.readFileSync(__dirname+"/../cert/server.key"), cert: fs.readFileSync(__dirname+"/../cert/server.crt") }; // Expose to the world module.exports.RAIServer = RAIServer; module.exports.runClientMockup = require("./mockup"); /** *

Creates instance of RAIServer

* *

Options object has the following properties:

* *
    *
  • debug - if set to true print traffic to console
  • *
  • disconnectOnTimeout - if set to true close the connection on disconnect
  • *
  • secureConnection - if set to true close the connection on disconnect
  • *
  • credentials - credentials for secureConnection and STARTTLS
  • *
  • timeout - timeout in milliseconds for disconnecting the client, * defaults to 0 (no timeout)
  • *
* *

Events

* *
    *
  • 'connect' - emitted if a client connects to the server, param * is a client ({@link RAISocket}) object
  • *
  • 'error' - emitted on error, has an error object as a param
  • *
* * @constructor * @param {Object} [options] Optional options object */ function RAIServer(options){ EventEmitter.call(this); this.options = options || {}; if (this.options.debug) { this._logger = this.options.debug === true ? console.log : this.options.debug; } else { this._logger = function(){}; } this._createServer(); } utillib.inherits(RAIServer, EventEmitter); /** *

Starts listening on selected port

* * @param {Number} port The port to listen * @param {String} [host] The IP address to listen * @param {Function} callback The callback function to be run after the server * is listening, the only param is an error message if the operation failed */ RAIServer.prototype.listen = function(port, host, callback){ if(!callback && typeof host=="function"){ callback = host; host = undefined; } this._port = port; this._host = host; this._connected = false; if(callback){ this._server.on("listening", (function(){ this._connected = true; callback(null); }).bind(this)); this._server.on("error", (function(err){ if(!this._connected){ callback(err); } }).bind(this)); } this._server.listen(this._port, this._host); }; /** *

Stops the server

* * @param {Function} callback Is run when the server is closed */ RAIServer.prototype.end = function(callback){ this._server.on("close", callback); this._server.close(); }; /** *

Creates a server with listener callback

*/ RAIServer.prototype._createServer = function(){ if(this.options.secureConnection){ this._server = tlslib.createServer( this.options.credentials || defaultCredentials, this._serverListener.bind(this)); }else{ this._server = netlib.createServer(this._serverListener.bind(this)); } this._server.on("error", this._onError.bind(this)); }; /** *

Listens for errors

* * @event * @param {Object} err Error object */ RAIServer.prototype._onError = function(err){ if(this._connected){ this.emit("error", err); } }; /** *

Server listener that is run on client connection

* *

{@link RAISocket} object instance is created based on the client socket * and a 'connection' event is emitted

* * @param {Object} socket The socket to the client */ RAIServer.prototype._serverListener = function(socket){ this._logger("CONNECTION FROM "+socket.remoteAddress); var handler = new RAISocket(socket, this.options); socket.on("data", handler._onReceiveData.bind(handler)); socket.on("end", handler._onEnd.bind(handler)); socket.on("error", handler._onError.bind(handler)); socket.on("timeout", handler._onTimeout.bind(handler)); socket.on("close", handler._onClose.bind(handler)); if("setKeepAlive" in socket){ socket.setKeepAlive(true); // plaintext server }else if(socket.encrypted && "setKeepAlive" in socket.encrypted){ socket.encrypted.setKeepAlive(true); // secure server } this.emit("connect", handler); }; /** *

Creates a instance for interacting with a client (socket)

* *

Optional options object is the same that is passed to the parent * {@link RAIServer} object

* *

Events

* *
    *
  • 'command' - emitted if a client sends a command. Gets two * params - command (String) and payload (Buffer)
  • *
  • 'data' - emitted when a chunk is received in data mode, the * param being the payload (Buffer)
  • *
  • 'ready' - emitted when data stream ends and normal command * flow is recovered
  • *
  • 'tls' - emitted when the connection is secured by TLS
  • *
  • 'error' - emitted when an error occurs. Connection to the * client is disconnected automatically. Param is an error object. *
  • 'timeout' - emitted when a timeout occurs. Connection to the * client is disconnected automatically if disconnectOnTimeout option * is set to true. *
  • 'end' - emitted when the client disconnects *
* * @constructor * @param {Object} socket Socket for the client * @param {Object} [options] Optional options object */ function RAISocket(socket, options){ EventEmitter.call(this); this.socket = socket; this.options = options || {}; if (this.options.debug) { this._logger = this.options.debug === true ? console.log : this.options.debug; } else { this._logger = function(){}; } this.remoteAddress = socket.remoteAddress; this._dataMode = false; this._endDataModeSequence = "\r\n.\r\n"; this._endDataModeSequenceRegEx = /\r\n\.\r\n/; this.secureConnection = !!this.options.secureConnection; this._destroyed = false; this._remainder = ""; this._ignore_data = false; if(this.options.timeout){ socket.setTimeout(this.options.timeout); } } utillib.inherits(RAISocket, EventEmitter); /** *

Sends some data to the client. <CR><LF> is automatically appended to * the data

* * @param {String|Buffer} data Data to be sent to the client */ RAISocket.prototype.send = function(data){ var buffer; if(data instanceof Buffer || data instanceof SlowBuffer){ buffer = new Buffer(data.length+2); buffer[buffer.length-2] = 0xD; buffer[buffer.length-1] = 0xA; data.copy(buffer); }else{ buffer = new Buffer((data || "").toString()+"\r\n", "binary"); } this._logger("OUT: \"" +buffer.toString("utf-8").trim()+"\""); if(this.socket && this.socket.writable){ this.socket.write(buffer); }else{ this.socket.end(); } }; /** *

Instructs the server to be listening for mixed data instead of line based * commands

* * @param {String} [sequence="."] - optional sequence on separate line for * matching the data end */ RAISocket.prototype.startDataMode = function(sequence){ this._dataMode = true; if(sequence){ sequence = sequence.replace(/([\.\=\(\)\-\?\*\\\[\]\^\+\:\|\,])/g, "\\$1"); this._endDataModeSequence = "\r\n"+sequence+"\r\n"; this._endDataModeSequenceRegEx = new RegExp("\\r\\n" + sequence + "\\r\\n"); } }; /** *

Instructs the server to upgrade the connection to secure TLS connection

* *

Fires callback on successful connection upgrade if set, * otherwise emits 'tls'

* * @param {Object} [credentials] An object with PEM encoded key and * certificate {key:"---BEGIN...", cert:"---BEGIN..."}, * if not set autogenerated values will be used. * @param {Function} [callback] If calback is set fire it after successful connection * upgrade, otherwise 'tls' is emitted */ RAISocket.prototype.startTLS = function(credentials, callback){ if(this.secureConnection){ return this._onError(new Error("Secure connection already established")); } if(!callback && typeof credentials == "function"){ callback = credentials; credentials = undefined; } credentials = credentials || this.options.credentials || defaultCredentials; this._ignore_data = true; var secure_connector = starttls(this.socket, credentials, (function(ssl_socket){ if(!ssl_socket.authorized){ this._logger("WARNING: TLS ERROR ("+ssl_socket.authorizationError+")"); } this._remainder = ""; this._ignore_data = false; this.secureConnection = true; this.socket = ssl_socket; this.socket.on("data", this._onReceiveData.bind(this)); this.socket.on("error", this._onError.bind(this)); this._logger("TLS CONNECTION STARTED"); if(callback){ callback(); }else{ this.emit("tls"); } }).bind(this)); secure_connector.on("error", (function(err){ this._onError(err); }).bind(this)); }; /** *

Closes the connection to the client

*/ RAISocket.prototype.end = function(){ this.socket.end(); }; /** *

Called when a chunk of data arrives from the client. If currently in data * mode, transmit the data otherwise send it to _processData

* * @event * @param {Buffer|String} chunk Data sent by the client */ RAISocket.prototype._onReceiveData = function(chunk){ if(this._ignore_data){ // if currently setting up TLS connection return; } var str = typeof chunk=="string"?chunk:chunk.toString("binary"), dataRemainderMatch, data; if(this._dataMode){ // prefix the incoming chunk with the remainder of the previous chunk str = this._remainder + str; this._remainder = ""; // check if a data end sequence is found from the data if((dataRemainderMatch = str.match(this._endDataModeSequenceRegEx))){ // if the sequence is not on byte 0 emit remaining data if(dataRemainderMatch.index){ data = new Buffer(str.substr(0, dataRemainderMatch.index), "binary"); this._logger("DATA:", data.toString("utf-8")); this.emit("data", data); } // emit data ready this.emit("ready"); this._dataMode = false; // send the remaining data for processing this._processData(str.substr(dataRemainderMatch.index + dataRemainderMatch[0].length)+"\r\n"); }else{ // check if there's not something in the end of the data that resembles // end sequence - if so, cut it off and save it to the remainder for(var i = Math.min(this._endDataModeSequence.length-1, str.length); i>0; i--){ if(str.substr(-i) == this._endDataModeSequence.substr(0, i)){ this._remainder = str.substr(-i); str = str.substr(0, str.length - i); } } // if there's some data left, emit it if(str.length){ data = new Buffer(str, "binary"); this._logger("DATA:", data.toString("utf-8")); this.emit("data", data); } } }else{ // Not in data mode, process as command this._processData(str); } }; /** *

Processed incoming command lines and emits found data as * 'command' with the command name as the first param and the rest * of the data as second (Buffer)

* * @param {String} str Binary string to be processed */ RAISocket.prototype._processData = function(str){ var lines = (this._remainder+str).split("\r\n"), match, command; this._remainder = lines.pop(); for(var i=0, len = lines.length; iCalled when the connection is or is going to be ended

*/ RAISocket.prototype._destroy = function(){ if(this._destroyed){ return; } this._destroyed = true; this.removeAllListeners(); }; /** *

Called when the connection is ended. Emits 'end'

* * @event */ RAISocket.prototype._onEnd = function(){ this.emit("end"); this._destroy(); }; /** *

Called when an error has appeared. Emits 'error' with * the error object as a parameter.

* * @event * @param {Object} err Error object */ RAISocket.prototype._onError = function(err){ this.emit("error", err); this._destroy(); }; /** *

Called when a timeout has occured. Connection will be closed and * 'timeout' is emitted.

* * @event */ RAISocket.prototype._onTimeout = function(){ if(this.options.disconnectOnTimeout){ if(this.socket && !this.socket.destroyed){ this.socket.end(); } this.emit("timeout"); this._destroy(); }else{ this.emit("timeout"); } }; /** *

Called when the connection is closed

* * @event * @param {Boolean} hadError did the connection end because of an error? */ RAISocket.prototype._onClose = function(/* hadError */){ this._destroy(); }; rai-0.1.12/lib/starttls.js000066400000000000000000000051351246635227200153470ustar00rootroot00000000000000"use strict"; // SOURCE: https://gist.github.com/848444 // Target API: // // var s = require('net').createStream(25, 'smtp.example.com'); // s.on('connect', function() { // require('starttls')(s, options, function() { // if (!s.authorized) { // s.destroy(); // return; // } // // s.end("hello world\n"); // }); // }); // // /** * @namespace STARTTLS module * @name starttls */ module.exports.starttls = starttls; /** *

Upgrades a socket to a secure TLS connection

* * @memberOf starttls * @param {Object} socket Plaintext socket to be upgraded * @param {Object} options Certificate data for the server * @param {Function} callback Callback function to be run after upgrade */ function starttls(socket, options, callback) { var sslcontext, pair, cleartext; socket.removeAllListeners("data"); sslcontext = require('crypto').createCredentials(options); pair = require('tls').createSecurePair(sslcontext, true); cleartext = pipe(pair, socket); pair.on('secure', function() { var verifyError = (pair._ssl || pair.ssl).verifyError(); if (verifyError) { cleartext.authorized = false; cleartext.authorizationError = verifyError; } else { cleartext.authorized = true; } callback(cleartext); }); cleartext._controlReleased = true; return pair; } function forwardEvents(events, emitterSource, emitterDestination) { var map = [], handler; events.forEach(function(name){ handler = function(){ this.emit.apply(this, arguments); }.bind(emitterDestination, name); map.push(name); emitterSource.on(name, handler); }); return map; } function removeEvents(map, emitterSource) { for(var i = 0, len = map.length; i < len; i++){ emitterSource.removeAllListeners(map[i]); } } function pipe(pair, socket) { pair.encrypted.pipe(socket); socket.pipe(pair.encrypted); pair.fd = socket.fd; var cleartext = pair.cleartext; cleartext.socket = socket; cleartext.encrypted = pair.encrypted; cleartext.authorized = false; function onerror(e) { if (cleartext._controlReleased) { cleartext.emit('error', e); } } var map = forwardEvents(["timeout", "end", "close", "drain", "error"], socket, cleartext); function onclose() { socket.removeListener('error', onerror); socket.removeListener('close', onclose); removeEvents(map,socket); } socket.on('error', onerror); socket.on('close', onclose); return cleartext; }rai-0.1.12/package.json000066400000000000000000000013671246635227200146540ustar00rootroot00000000000000{ "name": "rai", "description": "Request-Answer-Interface for generating text based command servers (SMTP, POP etc)", "version": "0.1.12", "author" : "Andris Reinman", "maintainers":[ { "name":"andris", "email":"andris@node.ee" } ], "repository" : { "type" : "git", "url" : "http://github.com/andris9/rai.git" }, "scripts":{ "test": "nodeunit test" }, "main" : "./lib/rai", "licenses" : [ { "type": "MIT", "url": "http://github.com/andris9/rai/blob/master/LICENSE" } ], "devDependencies": { "nodeunit": "*" }, "engines": ["node >=0.4.0"], "keywords": ["servers", "text-based"] } rai-0.1.12/test/000077500000000000000000000000001246635227200133365ustar00rootroot00000000000000rai-0.1.12/test/rai.js000066400000000000000000000612621246635227200144560ustar00rootroot00000000000000process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; var RAIServer = require("../lib/rai").RAIServer, runClientMockup = require("../lib/rai").runClientMockup, testCase = require('nodeunit').testCase, utillib = require("util"), netlib = require("net"), crypto = require("crypto"), tlslib = require("tls"); var PORT_NUMBER = 8397; // monkey patch net and tls to support nodejs 0.4 if(!netlib.connect && netlib.createConnection){ netlib.connect = netlib.createConnection; } if(!tlslib.connect && tlslib.createConnection){ tlslib.connect = tlslib.createConnection; } exports["General tests"] = { "Create and close a server": function(test){ var server = new RAIServer(); server.listen(PORT_NUMBER, function(err){ test.ifError(err); server.end(function(){ test.ok(1, "Server closed"); test.done(); }); }); }, "Create a secure server": function(test){ var server = new RAIServer({secureConnection: true}); server.listen(PORT_NUMBER, function(err){ test.ifError(err); server.end(function(){ test.ok(1, "Server closed"); test.done(); }); }); }, "Duplicate server fails": function(test){ var server = new RAIServer(); server.listen(PORT_NUMBER, function(err){ test.ifError(err); var duplicate = new RAIServer(); duplicate.listen(PORT_NUMBER, function(err){ test.ok(err, "Responds with error"); server.end(function(){ test.ok(1, "Server closed"); test.done(); }); }); }); }, "Connection event": function(test){ var server = new RAIServer(); test.expect(3); server.listen(PORT_NUMBER, function(err){ server.on("connect", function(socket){ test.ok(socket, "Client connected"); socket.on("end", function(){ test.ok(1, "Connection closed"); server.end(function(){ test.done(); }); }); socket.on("error", function(err){ test.isError(err); }); }); var client = netlib.connect(PORT_NUMBER, function(){ test.ok(1, "Connected to server"); client.end(); }); }); }, "Close client socket": function(test){ var server = new RAIServer(); test.expect(4); server.listen(PORT_NUMBER, function(err){ server.on("connect", function(socket){ test.ok(socket, "Client connected"); socket.on("end", function(){ test.ok(1, "Connection closed"); server.end(function(){ test.done(); }); }); socket.on("error", function(err){ test.isError(err); }); socket.end(); }); var client = netlib.connect(PORT_NUMBER, function(){ test.ok(1, "Connected to server"); }); client.on("end", function(){ test.ok(1, "Connection closed by host"); }); }); }, "Send data to client": function(test){ var server = new RAIServer(); server.listen(PORT_NUMBER, function(err){ server.on("connect", function(socket){ socket.send("HELLO"); socket.on("end", function(){ server.end(function(){ test.done(); }); }); socket.on("error", function(err){ test.isError(err); }); }); var client = netlib.connect(PORT_NUMBER, function(){ client.on("data", function(chunk){ test.equal(chunk.toString(), "HELLO\r\n"); client.end(); }); }); }); } }; exports["Secure connection"] = { "STARTTLS with event": function(test){ var server = new RAIServer(); server.listen(PORT_NUMBER, function(err){ test.expect(2); server.on("connect", function(socket){ socket.startTLS(); socket.on("tls", function(){ test.ok(1, "Secure connection opened"); socket.send("TEST"); }); socket.on("end", function(){ server.end(function(){ test.done(); }); }); socket.on("error", function(err){ test.isError(err); }); }); var client = netlib.connect(PORT_NUMBER, function(){ var sslcontext = crypto.createCredentials(); var pair = tlslib.createSecurePair(sslcontext, false); pair.encrypted.pipe(client); client.pipe(pair.encrypted); pair.fd = client.fd; pair.on("secure", function(){ pair.cleartext.on("data", function(chunk){ test.equal(chunk.toString(), "TEST\r\n"); pair.cleartext.end(); }); }); }); }); }, "STARTTLS Callback": function(test){ var server = new RAIServer(); server.listen(PORT_NUMBER, function(err){ test.expect(2); server.on("connect", function(socket){ socket.startTLS(function(){ test.ok(1, "Secure connection opened"); socket.send("TEST"); }); socket.on("tls", function(){ test.ok(0, "Should not occur"); }); socket.on("end", function(){ server.end(function(){ test.done(); }); }); socket.on("error", function(err){ test.isError(err); }); }); var client = netlib.connect(PORT_NUMBER, function(){ var sslcontext = crypto.createCredentials(); var pair = tlslib.createSecurePair(sslcontext, false); pair.encrypted.pipe(client); client.pipe(pair.encrypted); pair.fd = client.fd; pair.on("secure", function(){ pair.cleartext.on("data", function(chunk){ test.equal(chunk.toString(), "TEST\r\n"); pair.cleartext.end(); }); }); }); }); }, "STARTTLS clears command buffer": function(test){ var server = new RAIServer(); server.listen(PORT_NUMBER, function(err){ test.expect(2); server.on("connect", function(socket){ socket.on("command", function(command){ if(command == "STARTTLS"){ socket.startTLS(); socket.send("OK"); }else if(command == "KILL"){ test.ok(0, "Should not occur"); }else if(command == "OK"){ test.ok(1, "OK"); } }); socket.on("tls", function(){ test.ok(1, "Secure connection opened"); socket.send("TEST"); }); socket.on("end", function(){ server.end(function(){ test.done(); }); }); socket.on("error", function(err){ test.isError(err); }); }); var client = netlib.connect(PORT_NUMBER, function(){ client.write("STARTTLS\r\nKILL\r\n"); client.on("data", function(chunk){ if(chunk.toString("utf-8").trim() == "OK"){ var sslcontext = crypto.createCredentials(); var pair = tlslib.createSecurePair(sslcontext, false); pair.encrypted.pipe(client); client.pipe(pair.encrypted); pair.fd = client.fd; pair.on("secure", function(){ pair.cleartext.write("OK\r\n"); pair.cleartext.end(); }); } }); }); }); }, "STARTTLS on secure server fails": function(test){ var server = new RAIServer({secureConnection: true}); server.listen(PORT_NUMBER, function(err){ test.expect(2); server.on("connect", function(socket){ socket.on("error", function(err){ test.ok(err); socket.end(); server.end(function(){ test.done(); }); }); socket.on("command", (function(command){ process.nextTick(socket.startTLS.bind(socket, function(){ test.ok(false, "Secure connection opened"); // should not occur server.end(function(){ test.done(); }); })); }).bind(this)); socket.on("tls", function(){ test.ok(0, "Should not occur"); }); }); var client = tlslib.connect(PORT_NUMBER, function(){ test.ok(true); client.write("HELLO!\r\n"); }); }); } }; exports["Client commands"] = { "Receive Simple Command": function(test){ var server = new RAIServer(); server.listen(PORT_NUMBER, function(err){ server.on("connect", function(socket){ socket.on("command", function(command, payload){ test.equal(command, "STATUS"); test.equal(payload.toString(), ""); socket.end(); server.end(function(){ test.done(); }); }); socket.on("error", function(err){ test.isError(err); }); }); var client = netlib.connect(PORT_NUMBER, function(){ client.write("STATUS\r\n"); }); }); }, "Receive Command with payload": function(test){ var server = new RAIServer(); server.listen(PORT_NUMBER, function(err){ server.on("connect", function(socket){ socket.on("command", function(command, payload){ test.equal(command, "MAIL"); test.equal(payload.toString(), "TO:"); socket.end(); server.end(function(){ test.done(); }); }); socket.on("error", function(err){ test.isError(err); }); }); var client = netlib.connect(PORT_NUMBER, function(){ client.write("MAIL TO:\r\n"); }); }); } }; exports["Data mode"] = { "DATA mode": function(test){ var server = new RAIServer(), datapayload = "tere\r\nvana kere"; server.listen(PORT_NUMBER, function(err){ server.on("connect", function(socket){ socket.startDataMode(); test.expect(2); socket.on("data", function(chunk){ test.equal(datapayload, chunk.toString()); }); socket.on("ready", function(){ test.ok(1,"Data ready"); server.end(function(){ test.done(); }); }); socket.on("error", function(err){ test.isError(err); }); }); var client = netlib.connect(PORT_NUMBER, function(){ client.write(datapayload+"\r\n.\r\n"); client.end(); }); }); }, "Small chunks DATA mode": function(test){ var server = new RAIServer(), datapayload = "tere\r\nvana kere õäöü\r\n.\r", databytes = [], fullpayload = datapayload+"\r\n.\r\n"; server.listen(PORT_NUMBER, function(err){ server.on("connect", function(socket){ socket.startDataMode(); test.expect(1); socket.on("data", function(chunk){ databytes = databytes.concat(Array.prototype.slice.call(chunk)); }); socket.on("ready", function(){ test.equal(new Buffer(databytes).toString("utf-8"), datapayload); server.end(function(){ test.done(); }); }); socket.on("error", function(err){ test.isError(err); }); for(var i=0, len = fullpayload.length; i