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 };