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 }