%PDF- %PDF-
Direktori : /home/opti/workflow/node_modules/rai/lib/ |
Current File : /home/opti/workflow/node_modules/rai/lib/rai.js |
"use strict"; /** * @fileOverview This is the main file for the RAI library to create text based servers * @author <a href="mailto:andris@node.ee">Andris Reinman</a> */ 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"); /** * <p>Creates instance of RAIServer</p> * * <p>Options object has the following properties:</p> * * <ul> * <li><b>debug</b> - if set to true print traffic to console</li> * <li><b>disconnectOnTimeout</b> - if set to true close the connection on disconnect</li> * <li><b>secureConnection</b> - if set to true close the connection on disconnect</li> * <li><b>credentials</b> - credentials for secureConnection and STARTTLS</li> * <li><b>timeout</b> - timeout in milliseconds for disconnecting the client, * defaults to 0 (no timeout)</li> * </ul> * * <p><b>Events</b></p> * * <ul> * <li><b>'connect'</b> - emitted if a client connects to the server, param * is a client ({@link RAISocket}) object</li> * <li><b>'error'</b> - emitted on error, has an error object as a param</li> * </ul> * * @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); /** * <p>Starts listening on selected port</p> * * @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); }; /** * <p>Stops the server</p> * * @param {Function} callback Is run when the server is closed */ RAIServer.prototype.end = function(callback){ this._server.on("close", callback); this._server.close(); }; /** * <p>Creates a server with listener callback</p> */ 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)); }; /** * <p>Listens for errors</p> * * @event * @param {Object} err Error object */ RAIServer.prototype._onError = function(err){ if(this._connected){ this.emit("error", err); } }; /** * <p>Server listener that is run on client connection</p> * * <p>{@link RAISocket} object instance is created based on the client socket * and a <code>'connection'</code> event is emitted</p> * * @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); }; /** * <p>Creates a instance for interacting with a client (socket)</p> * * <p>Optional options object is the same that is passed to the parent * {@link RAIServer} object</p> * * <p><b>Events</b></p> * * <ul> * <li><b>'command'</b> - emitted if a client sends a command. Gets two * params - command (String) and payload (Buffer)</li> * <li><b>'data'</b> - emitted when a chunk is received in data mode, the * param being the payload (Buffer)</li> * <li><b>'ready'</b> - emitted when data stream ends and normal command * flow is recovered</li> * <li><b>'tls'</b> - emitted when the connection is secured by TLS</li> * <li><b>'error'</b> - emitted when an error occurs. Connection to the * client is disconnected automatically. Param is an error object.</l> * <li><b>'timeout'</b> - emitted when a timeout occurs. Connection to the * client is disconnected automatically if disconnectOnTimeout option * is set to true.</l> * <li><b>'end'</b> - emitted when the client disconnects</l> * </ul> * * @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); /** * <p>Sends some data to the client. <code><CR><LF></code> is automatically appended to * the data</p> * * @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(); } }; /** * <p>Instructs the server to be listening for mixed data instead of line based * commands</p> * * @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"); } }; /** * <p>Instructs the server to upgrade the connection to secure TLS connection</p> * * <p>Fires <code>callback</code> on successful connection upgrade if set, * otherwise emits <code>'tls'</code></p> * * @param {Object} [credentials] An object with PEM encoded key and * certificate <code>{key:"---BEGIN...", cert:"---BEGIN..."}</code>, * if not set autogenerated values will be used. * @param {Function} [callback] If calback is set fire it after successful connection * upgrade, otherwise <code>'tls'</code> 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)); }; /** * <p>Closes the connection to the client</p> */ RAISocket.prototype.end = function(){ this.socket.end(); }; /** * <p>Called when a chunk of data arrives from the client. If currently in data * mode, transmit the data otherwise send it to <code>_processData</code></p> * * @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); } }; /** * <p>Processed incoming command lines and emits found data as * <code>'command'</code> with the command name as the first param and the rest * of the data as second (Buffer)</p> * * @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; i<len; i++){ if(this._ignore_data){ // If TLS upgrade is initiated do not process current buffer this._remainder = ""; break; } if(!this._dataMode){ if((match = lines[i].match(/\s*[\S]+\s?/)) || (match = lines[i].match(/^$/))){ command = (match[0] || "").trim(); this._logger("COMMAND:", lines[i]); this.emit("command", command, new Buffer(lines[i].substr(match.index + match[0].length), "binary")); } }else{ if(this._remainder){ this._remainder += "\r\n"; } this._onReceiveData(lines.slice(i).join("\r\n")); break; } } }; /** * <p>Called when the connection is or is going to be ended</p> */ RAISocket.prototype._destroy = function(){ if(this._destroyed){ return; } this._destroyed = true; this.removeAllListeners(); }; /** * <p>Called when the connection is ended. Emits <code>'end'</code></p> * * @event */ RAISocket.prototype._onEnd = function(){ this.emit("end"); this._destroy(); }; /** * <p>Called when an error has appeared. Emits <code>'error'</code> with * the error object as a parameter.</p> * * @event * @param {Object} err Error object */ RAISocket.prototype._onError = function(err){ this.emit("error", err); this._destroy(); }; /** * <p>Called when a timeout has occured. Connection will be closed and * <code>'timeout'</code> is emitted.</p> * * @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"); } }; /** * <p>Called when the connection is closed</p> * * @event * @param {Boolean} hadError did the connection end because of an error? */ RAISocket.prototype._onClose = function(/* hadError */){ this._destroy(); };