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