      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.0
      5  */
      6
      7 var netlib = require("net"),
      8     utillib = require("util"),
      9     EventEmitter = require('events').EventEmitter,
     10     starttls = require("./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
     24 /**
     25  * <p>Creates instance of RAIServer</p>
     26  *
     27  * <p>Options object has the following properties:</p>
     28  *
     29  * <ul>
     30  *   <li><b>debug</b> - if set to true print traffic to console</li>
     31  *   <li><b>timeout</b> - timeout in milliseconds for disconnecting the client,
     32  *       defaults to 0 (no timeout)</li>
     33  * </ul>
     34  *
     35  * <p><b>Events</b></p>
     36  *
     37  * <ul>
     38  *     <li><b>'connect'</b> - emitted if a client connects to the server, param
     39  *         is a client ({@link RAISocket}) object</li>
     40  * </ul>
     41  *
     42  * @constructor
     43  * @param {Object} [options] Optional options object
     44  */
     45 function RAIServer(options){
     46     EventEmitter.call(this);
     47
     48     this.options = options || {};
     49
     50     this._createServer();
     51 }
     52 utillib.inherits(RAIServer, EventEmitter);
     53
     54 /**
     55  * <p>Starts listening on selected port</p>
     56  *
     57  * @param {Number} port The port to listen
     58  * @param {String} [host] The IP address to listen
     59  * @param {Function} callback The callback function to be run after the server
     60  * is listening, the only param is an error message if the operation failed
     61  */
     62 RAIServer.prototype.listen = function(port, host, callback){
     63     if(!callback && typeof host=="function"){
     64         callback = host;
     65         host = undefined;
     66     }
     67     this._port = port;
     68     this._host = host;
     69
     70     this._server.listen(port, host, function(err){
     71         if(err && !callback){
     72             this.emit("error", err);
     73         }else if(callback){
     74             callback(err || null);
     75         }
     76     });
     77 }
     78
     79 /**
     80  * <p>Creates a server with listener callback</p>
     81  */
     82 RAIServer.prototype._createServer = function(){
     83     this._server = netlib.createServer(this._serverListener.bind(this));
     84 }
     85
     86 /**
     87  * <p>Server listener that is run on client connection</p>
     88  *
     89  * <p>{@link RAISocket} object instance is created based on the client socket
     90  *    and a <code>'connection'</code> event is emitted</p>
     91  *
     92  * @param {Object} socket The socket to the client
     93  */
     94 RAIServer.prototype._serverListener = function(socket){
     95     if(this.options.debug){
     96         console.log("CONNECTION FROM "+socket.remoteAddress);
     97     }
     98
     99     var handler = new RAISocket(socket, this.options);
    100
    101     socket.on("data", handler._onReceiveData.bind(handler));
    102     socket.on("end", handler._onEnd.bind(handler));
    103     socket.on("error", handler._onError.bind(handler));
    104     socket.on("timeout", handler._onTimeout.bind(handler));
    105     socket.on("close", handler._onClose.bind(handler));
    106
    107     this.emit("connection", handler);
    108 }
    109
    110 /**
    111  * <p>Creates a instance for interacting with a client (socket)</p>
    112  *
    113  * <p>Optional options object is the same that is passed to the parent
    114  * {@link RAIServer} object</p>
    115  *
    116  * <p><b>Events</b></p>
    117  *
    118  * <ul>
    119  *     <li><b>'command'</b> - emitted if a client sends a command. Gets two
    120  *         params - command (String) and payload (Buffer)</li>
    121  *     <li><b>'data'</b> - emitted when a chunk is received in data mode, the
    122  *         param being the payload (Buffer)</li>
    123  *     <li><b>'ready'</b> - emitted when data stream ends and normal command
    124  *         flow is recovered</li>
    125  *     <li><b>'tls'</b> - emitted when the connection is secured by TLS</li>
    126  *     <li><b>'error'</b> - emitted when an error occurs. Connection to the
    127  *         client is disconnected automatically. Param is an error object.</l>
    128  *     <li><b>'timeout'</b> - emitted when a timeout occurs. Connection to the
    129  *         client is disconnected automatically.</l>
    130  *     <li><b>'end'</b> - emitted when the client disconnects</l>
    131  * </ul>
    132  *
    133  * @constructor
    134  * @param {Object} socket Socket for the client
    135  * @param {Object} [options] Optional options object
    136  */
    137 function RAISocket(socket, options){
    138     EventEmitter.call(this);
    139
    140     this.socket = socket;
    141     this.options = options || {};
    142
    143     this.remoteAddress = socket.remoteAddress;
    144
    145     this._dataMode = false;
    146     this._endDataModeSequence = /\r\n\.\r\n|^\.\r\n/;
    147
    148     this._secureConnection = false;
    149     this._destroyed = false;
    150     this._remainder = "";
    151
    152     this._ignore_data = false;
    153
    154     if(this.options.timeout){
    155         socket.setTimeout(this.options.timeout);
    156     }
    157 }
    158 utillib.inherits(RAISocket, EventEmitter);
    159
    160 /**
    161  * <p>Sends some data to the client. <CR><LF> is automatically appended to
    162  *    the data</p>
    163  *
    164  * @param {String|Buffer} data Data to be sent to the client
    165  */
    166 RAISocket.prototype.send = function(data){
    167     var buffer;
    168     if(data instanceof Buffer || (typeof SlowBuffer != "undefined" && data instanceof SlowBuffer)){
    169         buffer = new Buffer(data.length+2);
    170         buffer[buffer.length-2] = 0xD;
    171         buffer[buffer.length-1] = 0xA;
    172         data.copy(buffer);
    173     }else{
    174         buffer = new Buffer((data || "").toString()+"\r\n", "binary");
    175     }
    176
    177     if(this.options.debug){
    178         console.log("OUT: \"" +buffer.toString("utf-8").trim()+"\"");
    179     }
    180
    181     this.socket.write(buffer);
    182 }
    183
    184 /**
    185  * <p>Instructs the server to be listening for mixed data instead of line based
    186  *    commands</p>
    187  *
    188  * @param {String|RegExp} [sequence="\r\n.\r\n"] - optional sequence for
    189  *        matching the data end
    190  */
    191 RAISocket.prototype.startDataMode = function(sequence){
    192     this._dataMode = true;
    193     if(sequence){
    194         this._endDataModeSequence = typeof sequence == "string" ? new RegExp(sequence) : sequence;
    195     }
    196 }
    197
    198 /**
    199  * <p>Instructs the server to upgrade the connection to secure TLS connection</p>
    200  *
    201  * <p>Emits <code>'tls'</code> on successful upgrade</p>
    202  *
    203  * @param {Object} [credentials] - An object with PEM encoded key and
    204  *        certificate <code>{key:"---BEGIN...", cert:"---BEGIN..."}</code>,
    205  *        if not set autogenerated values will be used.
    206  */
    207 RAISocket.prototype.startTLS = function(credentials){
    208     if(this._secureConnection){
    209         this._onError(new Error("Secure connection already established"));
    210     }
    211
    212     credentials = credentials || defaultCredentials;
    213
    214     this._ignore_data = true;
    215
    216     var secure_connector = starttls(this.socket, credentials, (function(ssl_socket){
    217
    218         if(this.options.debug && !ssl_socket.authorized){
    219             console.log("WARNING: TLS ERROR ("+ssl_socket.authorizationError+")");
    220         }
    221
    222         this._remainder = "";
    223         this._ignore_data = false;
    224
    225         this._secureConnection = true;
    226
    227         this.socket = ssl_socket;
    228         this.socket.on("data", this._onReceiveData.bind(this));
    229
    230         if(this.options.debug){
    231             console.log("TLS CONNECTION STARTED");
    232         }
    233
    234         this.emit("tls");
    235
    236     }).bind(this));
    237
    238     secure_connector.on("error", (function(err){
    239         this._onError(err);
    240     }).bind(this));
    241 }
    242
    243 /**
    244  * <p>Closes the connection to the client</p>
    245  */
    246 RAISocket.prototype.end = function(){
    247     this.socket.end();
    248 }
    249
    250 /**
    251  * <p>Called when a chunk of data arrives from the client. If currently in data
    252  * mode, transmit the data otherwise send it to <code>_processData</code></p>
    253  *
    254  * @param {Buffer|String} chunk Data sent by the client
    255  */
    256 RAISocket.prototype._onReceiveData = function(chunk){
    257     if(this._ignore_data){ // if currently setting up TLS connection
    258         return;
    259     }
    260
    261     var str = typeof chunk=="string"?chunk:chunk.toString("binary"),
    262         dataEndMatch, dataRemainderMatch, data;
    263
    264     if(this._dataMode){
    265
    266         str = this._remainder + str;
    267
    268         if(dataEndMatch = str.match(/\r\n.*?$/)){
    269             // if theres a line that is not ended, keep it for later
    270             this._remainder = str.substr(dataEndMatch.index);
    271             str = str.substr(0, dataEndMatch.index);
    272         }else{
    273             this._remainder = "";
    274         }
    275
    276         if((dataRemainderMatch = (str+this._remainder).match(this._endDataModeSequence))){
    277             if(dataRemainderMatch.index){
    278                 data = new Buffer((str+this._remainder).substr(0, dataRemainderMatch.index), "binary");
    279                 if(this.options.debug){
    280                     console.log("DATA:", data.toString("utf-8"));
    281                 }
    282                 this.emit("data", data);
    283             }
    284             this._remainder = "";
    285             this.emit("ready");
    286             this._dataMode = false;
    287
    288             // send the remaining data for processing
    289             this._processData(str.substr(dataRemainderMatch.index + dataRemainderMatch[0].length));
    290         }else{
    291             data = new Buffer(str, "binary");
    292             if(this.options.debug){
    293                 console.log("DATA:", data.toString("utf-8"));
    294             }
    295             this.emit("data", data);
    296         }
    297     }else{
    298         this._processData(str);
    299     }
    300
    301 }
    302
    303 /**
    304  * <p>Processed incoming command lines and emits found data as
    305  * <code>'command'</code> with the command name as the first param and the rest
    306  * of the data as second (Buffer)</p>
    307  *
    308  * @param {String} str Binary string to be processed
    309  */
    310 RAISocket.prototype._processData = function(str){
    311     if(!str.length){
    312         return;
    313     }
    314     var lines = (this._remainder+str).split("\r\n"),
    315         match, command;
    316
    317     this._remainder = lines.pop();
    318
    319     for(var i=0, len = lines.length; i<len; i++){
    320         if(!this._dataMode){
    321             if(match = lines[i].match(/\s*[\S]+\s?/)){
    322                 command = (match[0] || "").trim();
    323                 if(this.options.debug){
    324                     console.log("COMMAND:", lines[i]);
    325                 }
    326                 this.emit("command", command, new Buffer(lines[i].substr(match.index + match[0].length), "binary"));
    327             }
    328         }else{
    329             if(this._remainder){
    330                 this._remainder += "\r\n";
    331             }
    332             this._onReceiveData(lines.slice(i).join("\r\n"));
    333             break;
    334         }
    335     }
    336
    337 }
    338
    339 /**
    340  * <p>Called when the connection is or is going to be ended</p>
    341  */
    342 RAISocket.prototype._destroy = function(){
    343     if(this._destroyed)return;
    344     this._destroyed = true;
    345
    346     this.removeAllListeners();
    347 }
    348
    349 /**
    350  * <p>Called when the connection is ended. Emits <code>'end'</code></p>
    351  */
    352 RAISocket.prototype._onEnd = function(){
    353     this.emit("end");
    354     this._destroy();
    355 }
    356
    357 /**
    358  * <p>Called when an error has appeared. Emits <code>'error'</code> with
    359  * the error object as a parameter.</p>
    360  *
    361  * @param {Object} err Error object
    362  */
    363 RAISocket.prototype._onError = function(err){
    364     this.emit("error", err);
    365     this._destroy();
    366 }
    367
    368 /**
    369  * <p>Called when a timeout has occured. Connection will be closed and
    370  * <code>'timeout'</code> is emitted.</p>
    371  */
    372 RAISocket.prototype._onTimeout = function(){
    373     if(this.socket && !this.socket.destroyed){
    374         this.socket.end();
    375     }
    376     this.emit("timeout");
    377     this._destroy();
    378 }
    379
    380 /**
    381  * <p>Called when the connection is closed</p>
    382  *
    383  * @param {Boolean} hadError did the connection end because of an error?
    384  */
    385 RAISocket.prototype._onClose = function(hadError){
    386     this._destroy();
    387 }