      1 /**
      2  * @fileOverview This is the main file for the RAI library to create text based servers
      3  * @author <a href="mailto:andris@node.ee">Andris Reinman</a>
      4  * @version 0.1.3
      5  */
      6
      7 var netlib = require("net"),
      8     utillib = require("util"),
      9     EventEmitter = require('events').EventEmitter,
     10     starttls = require("./starttls").starttls,
     11     tlslib = require("tls"),
     12     crypto = require("crypto"),
     13     fs = require("fs");
     14
     15 // Default credentials for starting TLS server
     16 var defaultCredentials = {
     17     key: fs.readFileSync(__dirname+"/../cert/key.pem"),
     18     cert: fs.readFileSync(__dirname+"/../cert/cert.pem")
     19 };
     20
     21 // Expose to the world
     22 module.exports.RAIServer = RAIServer;
     23 module.exports.runClientMockup = require("./mockup");
     24
     25 /**
     26  * <p>Creates instance of RAIServer</p>
     27  *
     28  * <p>Options object has the following properties:</p>
     29  *
     30  * <ul>
     31  *   <li><b>debug</b> - if set to true print traffic to console</li>
     32  *   <li><b>disconnectOnTimeout</b> - if set to true close the connection on disconnect</li>
     33  *   <li><b>timeout</b> - timeout in milliseconds for disconnecting the client,
     34  *       defaults to 0 (no timeout)</li>
     35  * </ul>
     36  *
     37  * <p><b>Events</b></p>
     38  *
     39  * <ul>
     40  *     <li><b>'connect'</b> - emitted if a client connects to the server, param
     41  *         is a client ({@link RAISocket}) object</li>
     42  *     <li><b>'error'</b> - emitted on error, has an error object as a param</li>
     43  * </ul>
     44  *
     45  * @constructor
     46  * @param {Object} [options] Optional options object
     47  */
     48 function RAIServer(options){
     49     EventEmitter.call(this);
     50
     51     this.options = options || {};
     52
     53     this._createServer();
     54 }
     55 utillib.inherits(RAIServer, EventEmitter);
     56
     57 /**
     58  * <p>Starts listening on selected port</p>
     59  *
     60  * @param {Number} port The port to listen
     61  * @param {String} [host] The IP address to listen
     62  * @param {Function} callback The callback function to be run after the server
     63  * is listening, the only param is an error message if the operation failed
     64  */
     65 RAIServer.prototype.listen = function(port, host, callback){
     66     if(!callback && typeof host=="function"){
     67         callback = host;
     68         host = undefined;
     69     }
     70     this._port = port;
     71     this._host = host;
     72
     73     this._connected = false;
     74
     75     if(callback){
     76
     77         this._server.on("listening", (function(){
     78             this._connected = true;
     79             callback(null);
     80         }).bind(this));
     81
     82         this._server.on("error", (function(err){
     83             if(!this._connected){
     84                 callback(err);
     85             }
     86         }).bind(this));
     87
     88     }
     89
     90     this._server.listen(this._port, this._host);
     91 };
     92
     93 /**
     94  * <p>Stops the server</p>
     95  *
     96  * @param {Function} callback Is run when the server is closed
     97  */
     98 RAIServer.prototype.end = function(callback){
     99     this._server.on("close", callback);
    100     this._server.close();
    101 };
    102
    103 /**
    104  * <p>Creates a server with listener callback</p>
    105  */
    106 RAIServer.prototype._createServer = function(){
    107     this._server = netlib.createServer(this._serverListener.bind(this));
    108     this._server.on("error", this._onError.bind(this));
    109 };
    110
    111 /**
    112  * <p>Listens for errors</p>
    113  *
    114  * @event
    115  * @param {Object} err Error object
    116  */
    117 RAIServer.prototype._onError = function(err){
    118     if(this._connected){
    119         this.emit("error", err);
    120     }
    121 };
    122
    123 /**
    124  * <p>Server listener that is run on client connection</p>
    125  *
    126  * <p>{@link RAISocket} object instance is created based on the client socket
    127  *    and a <code>'connection'</code> event is emitted</p>
    128  *
    129  * @param {Object} socket The socket to the client
    130  */
    131 RAIServer.prototype._serverListener = function(socket){
    132     if(this.options.debug){
    133         console.log("CONNECTION FROM "+socket.remoteAddress);
    134     }
    135
    136     var handler = new RAISocket(socket, this.options);
    137
    138     socket.on("data", handler._onReceiveData.bind(handler));
    139     socket.on("end", handler._onEnd.bind(handler));
    140     socket.on("error", handler._onError.bind(handler));
    141     socket.on("timeout", handler._onTimeout.bind(handler));
    142     socket.on("close", handler._onClose.bind(handler));
    143
    144     this.emit("connection", handler);
    145 };
    146
    147 /**
    148  * <p>Creates a instance for interacting with a client (socket)</p>
    149  *
    150  * <p>Optional options object is the same that is passed to the parent
    151  * {@link RAIServer} object</p>
    152  *
    153  * <p><b>Events</b></p>
    154  *
    155  * <ul>
    156  *     <li><b>'command'</b> - emitted if a client sends a command. Gets two
    157  *         params - command (String) and payload (Buffer)</li>
    158  *     <li><b>'data'</b> - emitted when a chunk is received in data mode, the
    159  *         param being the payload (Buffer)</li>
    160  *     <li><b>'ready'</b> - emitted when data stream ends and normal command
    161  *         flow is recovered</li>
    162  *     <li><b>'tls'</b> - emitted when the connection is secured by TLS</li>
    163  *     <li><b>'error'</b> - emitted when an error occurs. Connection to the
    164  *         client is disconnected automatically. Param is an error object.</l>
    165  *     <li><b>'timeout'</b> - emitted when a timeout occurs. Connection to the
    166  *         client is disconnected automatically if disconnectOnTimeout option
    167  *         is set to true.</l>
    168  *     <li><b>'end'</b> - emitted when the client disconnects</l>
    169  * </ul>
    170  *
    171  * @constructor
    172  * @param {Object} socket Socket for the client
    173  * @param {Object} [options] Optional options object
    174  */
    175 function RAISocket(socket, options){
    176     EventEmitter.call(this);
    177
    178     this.socket = socket;
    179     this.options = options || {};
    180
    181     this.remoteAddress = socket.remoteAddress;
    182
    183     this._dataMode = false;
    184     this._endDataModeSequence = "\r\n.\r\n";
    185     this._endDataModeSequenceRegEx = /\r\n\.\r\n|^\.\r\n/;
    186
    187     this.secureConnection = false;
    188     this._destroyed = false;
    189     this._remainder = "";
    190
    191     this._ignore_data = false;
    192
    193     if(this.options.timeout){
    194         socket.setTimeout(this.options.timeout);
    195     }
    196 }
    197 utillib.inherits(RAISocket, EventEmitter);
    198
    199 /**
    200  * <p>Sends some data to the client. <code><CR><LF></code> is automatically appended to
    201  *    the data</p>
    202  *
    203  * @param {String|Buffer} data Data to be sent to the client
    204  */
    205 RAISocket.prototype.send = function(data){
    206     var buffer;
    207     if(data instanceof Buffer || (typeof SlowBuffer != "undefined" && data instanceof SlowBuffer)){
    208         buffer = new Buffer(data.length+2);
    209         buffer[buffer.length-2] = 0xD;
    210         buffer[buffer.length-1] = 0xA;
    211         data.copy(buffer);
    212     }else{
    213         buffer = new Buffer((data || "").toString()+"\r\n", "binary");
    214     }
    215
    216     if(this.options.debug){
    217         console.log("OUT: \"" +buffer.toString("utf-8").trim()+"\"");
    218     }
    219
    220     if(this.socket && this.socket.writable){
    221         this.socket.write(buffer);
    222     }else{
    223         this.socket.end();
    224     }
    225 };
    226
    227 /**
    228  * <p>Instructs the server to be listening for mixed data instead of line based
    229  *    commands</p>
    230  *
    231  * @param {String} [sequence="."] - optional sequence on separate line for
    232  *        matching the data end
    233  */
    234 RAISocket.prototype.startDataMode = function(sequence){
    235     this._dataMode = true;
    236     if(sequence){
    237         sequence = sequence.replace(/\.\=\(\)\-\?\*\\\[\]\^\+\:\|\,/g, "\\$1");
    238         this._endDataModeSequence = "\r\n"+sequence+"\r\n";
    239         this._endDataModeSequenceRegEx = new RegExp("/\r\n"+sequence+"\r\n|^"+sequence+"\r\n/");
    240     }
    241 };
    242
    243 /**
    244  * <p>Instructs the server to upgrade the connection to secure TLS connection</p>
    245  *
    246  * <p>Fires <code>callback</code> on successful connection upgrade if set,
    247  * otherwise emits <code>'tls'</code></p>
    248  *
    249  * @param {Object} [credentials] An object with PEM encoded key and
    250  *        certificate <code>{key:"---BEGIN...", cert:"---BEGIN..."}</code>,
    251  *        if not set autogenerated values will be used.
    252  * @param {Function} [callback] If calback is set fire it after successful connection
    253  *        upgrade, otherwise <code>'tls'</code> is emitted
    254  */
    255 RAISocket.prototype.startTLS = function(credentials, callback){
    256     if(this.secureConnection){
    257         this._onError(new Error("Secure connection already established"));
    258     }
    259
    260     if(!callback && typeof credentials == "function"){
    261         callback = credentials;
    262         credentials = undefined;
    263     }
    264
    265     credentials = credentials || defaultCredentials;
    266
    267     this._ignore_data = true;
    268
    269     var secure_connector = starttls(this.socket, credentials, (function(ssl_socket){
    270
    271         if(this.options.debug && !ssl_socket.authorized){
    272             console.log("WARNING: TLS ERROR ("+ssl_socket.authorizationError+")");
    273         }
    274
    275         this._remainder = "";
    276         this._ignore_data = false;
    277
    278         this.secureConnection = true;
    279
    280         this.socket = ssl_socket;
    281         this.socket.on("data", this._onReceiveData.bind(this));
    282
    283         if(this.options.debug){
    284             console.log("TLS CONNECTION STARTED");
    285         }
    286
    287         if(callback){
    288             callback();
    289         }else{
    290             this.emit("tls");
    291         }
    292
    293     }).bind(this));
    294
    295     secure_connector.on("error", (function(err){
    296         this._onError(err);
    297     }).bind(this));
    298 };
    299
    300 /**
    301  * <p>Closes the connection to the client</p>
    302  */
    303 RAISocket.prototype.end = function(){
    304     this.socket.end();
    305 };
    306
    307 /**
    308  * <p>Called when a chunk of data arrives from the client. If currently in data
    309  * mode, transmit the data otherwise send it to <code>_processData</code></p>
    310  *
    311  * @event
    312  * @param {Buffer|String} chunk Data sent by the client
    313  */
    314 RAISocket.prototype._onReceiveData = function(chunk){
    315
    316     if(this._ignore_data){ // if currently setting up TLS connection
    317         return;
    318     }
    319
    320     var str = typeof chunk=="string"?chunk:chunk.toString("binary"),
    321         dataEndMatch, dataRemainderMatch, data, match;
    322
    323     if(this._dataMode){
    324
    325         str = this._remainder + str;
    326         if((dataEndMatch = str.match(/\r\n.*?$/))){
    327             // if theres a line that is not ended, keep it for later
    328             this._remainder = str.substr(dataEndMatch.index);
    329             str = str.substr(0, dataEndMatch.index);
    330         }else{
    331             this._remainder = "";
    332         }
    333
    334         // check if a data end sequence is found from the data
    335         if((dataRemainderMatch = (str+this._remainder).match(this._endDataModeSequenceRegEx))){
    336             str = str + this._remainder;
    337             // if the sequence is not on byte 0 emit remaining data
    338             if(dataRemainderMatch.index){
    339                 data = new Buffer(str.substr(0, dataRemainderMatch.index), "binary");
    340                 if(this.options.debug){
    341                     console.log("DATA:", data.toString("utf-8"));
    342                 }
    343                 this.emit("data", data);
    344             }
    345             // emit data ready
    346             this._remainder = "";
    347             this.emit("ready");
    348             this._dataMode = false;
    349             // send the remaining data for processing
    350             this._processData(str.substr(dataRemainderMatch.index + dataRemainderMatch[0].length)+"\r\n");
    351         }else{
    352             // check if there's not something in the end of the data that resembles
    353             // end sequence - if so, cut it off and save it to the remainder
    354             str = str + this._remainder;
    355             this._remainder=  "";
    356             for(var i = Math.min(this._endDataModeSequence.length-1, str.length); i>0; i--){
    357                 match = this._endDataModeSequence.substr(0, i);
    358                 if(str.substr(-match.length) == match){
    359                     this._remainder = str.substr(-match.length);
    360                     str = str.substr(0, str.length - match.length);
    361                 }
    362             }
    363
    364             // if there's some data leht, emit it
    365             if(str.length){
    366                 data = new Buffer(str, "binary");
    367                 if(this.options.debug){
    368                     console.log("DATA:", data.toString("utf-8"));
    369                 }
    370                 this.emit("data", data);
    371             }
    372         }
    373     }else{
    374         // Not in data mode, process as command
    375         this._processData(str);
    376     }
    377 };
    378
    379 /**
    380  * <p>Processed incoming command lines and emits found data as
    381  * <code>'command'</code> with the command name as the first param and the rest
    382  * of the data as second (Buffer)</p>
    383  *
    384  * @param {String} str Binary string to be processed
    385  */
    386 RAISocket.prototype._processData = function(str){
    387     if(!str.length){
    388         return;
    389     }
    390     var lines = (this._remainder+str).split("\r\n"),
    391         match, command;
    392
    393     this._remainder = lines.pop();
    394
    395     for(var i=0, len = lines.length; i<len; i++){
    396         if(this._ignore_data){
    397             // If TLS upgrade is initiated do not process current buffer
    398             this._remainder = "";
    399             break;
    400         }
    401         if(!this._dataMode){
    402             if((match = lines[i].match(/\s*[\S]+\s?/))){
    403                 command = (match[0] || "").trim();
    404                 if(this.options.debug){
    405                     console.log("COMMAND:", lines[i]);
    406                 }
    407                 this.emit("command", command, new Buffer(lines[i].substr(match.index + match[0].length), "binary"));
    408             }
    409         }else{
    410             if(this._remainder){
    411                 this._remainder += "\r\n";
    412             }
    413             this._onReceiveData(lines.slice(i).join("\r\n"));
    414             break;
    415         }
    416     }
    417 };
    418
    419 /**
    420  * <p>Called when the connection is or is going to be ended</p>
    421  */
    422 RAISocket.prototype._destroy = function(){
    423     if(this._destroyed)return;
    424     this._destroyed = true;
    425
    426     this.removeAllListeners();
    427 };
    428
    429 /**
    430  * <p>Called when the connection is ended. Emits <code>'end'</code></p>
    431  *
    432  * @event
    433  */
    434 RAISocket.prototype._onEnd = function(){
    435     this.emit("end");
    436     this._destroy();
    437 };
    438
    439 /**
    440  * <p>Called when an error has appeared. Emits <code>'error'</code> with
    441  * the error object as a parameter.</p>
    442  *
    443  * @event
    444  * @param {Object} err Error object
    445  */
    446 RAISocket.prototype._onError = function(err){
    447     this.emit("error", err);
    448     this._destroy();
    449 };
    450
    451 /**
    452  * <p>Called when a timeout has occured. Connection will be closed and
    453  * <code>'timeout'</code> is emitted.</p>
    454  *
    455  * @event
    456  */
    457 RAISocket.prototype._onTimeout = function(){
    458     if(this.options.disconnectOnTimeout){
    459         if(this.socket && !this.socket.destroyed){
    460             this.socket.end();
    461         }
    462         this.emit("timeout");
    463         this._destroy();
    464     }else{
    465         this.emit("timeout");
    466     }
    467 };
    468
    469 /**
    470  * <p>Called when the connection is closed</p>
    471  *
    472  * @event
    473  * @param {Boolean} hadError did the connection end because of an error?
    474  */
    475 RAISocket.prototype._onClose = function(hadError){
    476     this._destroy();
    477 };