aboutsummaryrefslogtreecommitdiffstats
path: root/tools/node_modules/nodemailer/node_modules/simplesmtp/node_modules/rai/lib/rai.js
blob: d4720639d35be795a02debd37819d1ce1c8b8907 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
/**
 * @fileOverview This is the main file for the RAI library to create text based servers
 * @author <a href="mailto:andris@node.ee">Andris Reinman</a>
 * @version 0.1.3
 */

var netlib = require("net"),
    utillib = require("util"),
    EventEmitter = require('events').EventEmitter,
    starttls = require("./starttls").starttls,
    tlslib = require("tls"),
    crypto = require("crypto"),
    fs = require("fs");

// Default credentials for starting TLS server
var defaultCredentials = {
    key: fs.readFileSync(__dirname+"/../cert/key.pem"),
    cert: fs.readFileSync(__dirname+"/../cert/cert.pem")
};

// Expose to the world
module.exports.RAIServer = RAIServer;
module.exports.runClientMockup = require("./mockup");

/**
 * <p>Creates instance of RAIServer</p>
 * 
 * <p>Options object has the following properties:</p>
 * 
 * <ul>
 *   <li><b>debug</b> - if set to true print traffic to console</li>
 *   <li><b>disconnectOnTimeout</b> - if set to true close the connection on disconnect</li>
 *   <li><b>secureConnection</b> - if set to true close the connection on disconnect</li>
 *   <li><b>credentials</b> - credentials for secureConnection and STARTTLS</li>
 *   <li><b>timeout</b> - timeout in milliseconds for disconnecting the client,
 *       defaults to 0 (no timeout)</li>
 * </ul>
 * 
 * <p><b>Events</b></p>
 * 
 * <ul>
 *     <li><b>'connect'</b> - emitted if a client connects to the server, param
 *         is a client ({@link RAISocket}) object</li>
 *     <li><b>'error'</b> - emitted on error, has an error object as a param</li>
 * </ul> 
 * 
 * @constructor
 * @param {Object} [options] Optional options object
 */
function RAIServer(options){
    EventEmitter.call(this);
    
    this.options = options || {};
    
    this._createServer();
}
utillib.inherits(RAIServer, EventEmitter);

/**
 * <p>Starts listening on selected port</p>
 * 
 * @param {Number} port The port to listen
 * @param {String} [host] The IP address to listen
 * @param {Function} callback The callback function to be run after the server
 * is listening, the only param is an error message if the operation failed 
 */
RAIServer.prototype.listen = function(port, host, callback){
    if(!callback && typeof host=="function"){
        callback = host;
        host = undefined;
    }
    this._port = port;
    this._host = host;
    
    this._connected = false;
    if(callback){    
        this._server.on("listening", (function(){
            this._connected = true;
            callback(null);
        }).bind(this));
        
        this._server.on("error", (function(err){
            if(!this._connected){
                callback(err);
            }
        }).bind(this));
    }
    
    this._server.listen(this._port, this._host);
};

/**
 * <p>Stops the server</p>
 * 
 * @param {Function} callback Is run when the server is closed 
 */
RAIServer.prototype.end = function(callback){
    this._server.on("close", callback);
    this._server.close();
};

/**
 * <p>Creates a server with listener callback</p> 
 */
RAIServer.prototype._createServer = function(){
    if(this.options.secureConnection){
        this._server = tlslib.createServer(
            this.options.credentials || defaultCredentials,
            this._serverListener.bind(this));
    }else{
        this._server = netlib.createServer(this._serverListener.bind(this));
    }
    this._server.on("error", this._onError.bind(this));
};

/**
 * <p>Listens for errors</p>
 * 
 * @event
 * @param {Object} err Error object
 */
RAIServer.prototype._onError = function(err){
    if(this._connected){
        this.emit("error", err);
    }
};

/**
 * <p>Server listener that is run on client connection</p>
 * 
 * <p>{@link RAISocket} object instance is created based on the client socket
 *    and a <code>'connection'</code> event is emitted</p>
 * 
 * @param {Object} socket The socket to the client 
 */
RAIServer.prototype._serverListener = function(socket){
    if(this.options.debug){
        console.log("CONNECTION FROM "+socket.remoteAddress);
    }
    
    var handler = new RAISocket(socket, this.options);
    
    socket.on("data", handler._onReceiveData.bind(handler));
    socket.on("end", handler._onEnd.bind(handler));
    socket.on("error", handler._onError.bind(handler));
    socket.on("timeout", handler._onTimeout.bind(handler));
    socket.on("close", handler._onClose.bind(handler));

    if("setKeepAlive" in socket){
        socket.setKeepAlive(true); // plaintext server
    }else if(socket.encrypted && "setKeepAlive" in socket.encrypted){
        socket.encrypted.setKeepAlive(true); // secure server
    }
    
    this.emit("connect", handler);
};

/**
 * <p>Creates a instance for interacting with a client (socket)</p>
 * 
 * <p>Optional options object is the same that is passed to the parent
 * {@link RAIServer} object</p>
 * 
 * <p><b>Events</b></p>
 * 
 * <ul>
 *     <li><b>'command'</b> - emitted if a client sends a command. Gets two
 *         params - command (String) and payload (Buffer)</li>
 *     <li><b>'data'</b> - emitted when a chunk is received in data mode, the
 *         param being the payload (Buffer)</li>
 *     <li><b>'ready'</b> - emitted when data stream ends and normal command
 *         flow is recovered</li>
 *     <li><b>'tls'</b> - emitted when the connection is secured by TLS</li>
 *     <li><b>'error'</b> - emitted when an error occurs. Connection to the
 *         client is disconnected automatically. Param is an error object.</l>
 *     <li><b>'timeout'</b> - emitted when a timeout occurs. Connection to the
 *         client is disconnected automatically if disconnectOnTimeout option 
 *         is set to true.</l>
 *     <li><b>'end'</b> - emitted when the client disconnects</l>
 * </ul>
 * 
 * @constructor
 * @param {Object} socket Socket for the client
 * @param {Object} [options] Optional options object
 */
function RAISocket(socket, options){
    EventEmitter.call(this);
    
    this.socket = socket;
    this.options = options || {};
    
    this.remoteAddress = socket.remoteAddress;
    
    this._dataMode = false;
    this._endDataModeSequence = "\r\n.\r\n";
    this._endDataModeSequenceRegEx = /\r\n\.\r\n|^\.\r\n/;
    
    this.secureConnection = !!this.options.secureConnection;
    this._destroyed = false;
    this._remainder = "";
    
    this._ignore_data = false;
    
    if(this.options.timeout){
        socket.setTimeout(this.options.timeout);
    }
}
utillib.inherits(RAISocket, EventEmitter);

/**
 * <p>Sends some data to the client. <code>&lt;CR&gt;&lt;LF&gt;</code> is automatically appended to
 *    the data</p>
 * 
 * @param {String|Buffer} data Data to be sent to the client
 */
RAISocket.prototype.send = function(data){
    var buffer;
    if(data instanceof Buffer || (typeof SlowBuffer != "undefined" && data instanceof SlowBuffer)){
        buffer = new Buffer(data.length+2);
        buffer[buffer.length-2] = 0xD;
        buffer[buffer.length-1] = 0xA;
        data.copy(buffer);
    }else{
        buffer = new Buffer((data || "").toString()+"\r\n", "binary");
    }
    
    if(this.options.debug){
        console.log("OUT: \"" +buffer.toString("utf-8").trim()+"\"");
    }
    
    if(this.socket && this.socket.writable){
        this.socket.write(buffer);
    }else{
        this.socket.end();
    }
};

/**
 * <p>Instructs the server to be listening for mixed data instead of line based
 *    commands</p>
 * 
 * @param {String} [sequence="."] - optional sequence on separate line for
 *        matching the data end
 */
RAISocket.prototype.startDataMode = function(sequence){
    this._dataMode = true;
    if(sequence){
        sequence = sequence.replace(/\.\=\(\)\-\?\*\\\[\]\^\+\:\|\,/g, "\\$1");
        this._endDataModeSequence = "\r\n"+sequence+"\r\n";
        this._endDataModeSequenceRegEx = new RegExp("/\r\n"+sequence+"\r\n|^"+sequence+"\r\n/");
    }
};

/**
 * <p>Instructs the server to upgrade the connection to secure TLS connection</p>
 * 
 * <p>Fires <code>callback</code> on successful connection upgrade if set, 
 * otherwise emits <code>'tls'</code></p>
 * 
 * @param {Object} [credentials] An object with PEM encoded key and 
 *        certificate <code>{key:"---BEGIN...", cert:"---BEGIN..."}</code>,
 *        if not set autogenerated values will be used.
 * @param {Function} [callback] If calback is set fire it after successful connection
 *        upgrade, otherwise <code>'tls'</code> is emitted
 */
RAISocket.prototype.startTLS = function(credentials, callback){

    if(this.secureConnection){
        return this._onError(new Error("Secure connection already established"));
    }
    
    if(!callback && typeof credentials == "function"){
        callback = credentials;
        credentials = undefined;
    }
    
    credentials = credentials || this.options.credentials || defaultCredentials;
    
    this._ignore_data = true;
    
    var secure_connector = starttls(this.socket, credentials, (function(ssl_socket){

        if(this.options.debug && !ssl_socket.authorized){
            console.log("WARNING: TLS ERROR ("+ssl_socket.authorizationError+")");
        }
        
        this._remainder = "";
        this._ignore_data = false;
        
        this.secureConnection = true;
    
        this.socket = ssl_socket;
        this.socket.on("data", this._onReceiveData.bind(this));
        
        if(this.options.debug){
            console.log("TLS CONNECTION STARTED");
        }
        
        if(callback){
            callback();
        }else{
            this.emit("tls");
        }
        
    }).bind(this));
    
    secure_connector.on("error", (function(err){
        this._onError(err);
    }).bind(this));
};

/**
 * <p>Closes the connection to the client</p>
 */
RAISocket.prototype.end = function(){
    this.socket.end();
};

/**
 * <p>Called when a chunk of data arrives from the client. If currently in data
 * mode, transmit the data otherwise send it to <code>_processData</code></p>
 * 
 * @event
 * @param {Buffer|String} chunk Data sent by the client
 */
RAISocket.prototype._onReceiveData = function(chunk){

    if(this._ignore_data){ // if currently setting up TLS connection
        return;
    }
    
    var str = typeof chunk=="string"?chunk:chunk.toString("binary"),
        dataEndMatch, dataRemainderMatch, data, match;
    
    if(this._dataMode){
        
        str = this._remainder + str;
        if((dataEndMatch = str.match(/\r\n.*?$/))){
            // if ther's a line that is not ended, keep it for later
            this._remainder = str.substr(dataEndMatch.index);
            str = str.substr(0, dataEndMatch.index);
        }else{
            this._remainder = "";
        }

        // check if a data end sequence is found from the data
        if((dataRemainderMatch = (str+this._remainder).match(this._endDataModeSequenceRegEx))){
            str = str + this._remainder;
            // if the sequence is not on byte 0 emit remaining data
            if(dataRemainderMatch.index){
                data = new Buffer(str.substr(0, dataRemainderMatch.index), "binary");
                if(this.options.debug){
                    console.log("DATA:", data.toString("utf-8"));
                }
                this.emit("data", data);
            }
            // emit data ready
            this._remainder = "";
            this.emit("ready");
            this._dataMode = false;
            // send the remaining data for processing
            this._processData(str.substr(dataRemainderMatch.index + dataRemainderMatch[0].length)+"\r\n");
        }else{
            // check if there's not something in the end of the data that resembles
            // end sequence - if so, cut it off and save it to the remainder
            str = str + this._remainder;
            this._remainder=  "";
            for(var i = Math.min(this._endDataModeSequence.length-1, str.length); i>0; i--){
                match = this._endDataModeSequence.substr(0, i);
                if(str.substr(-match.length) == match){
                    this._remainder = str.substr(-match.length);
                    str = str.substr(0, str.length - match.length);
                }
            }

            // if there's some data leht, emit it
            if(str.length){
                data = new Buffer(str, "binary");
                if(this.options.debug){
                    console.log("DATA:", data.toString("utf-8"));
                }
                this.emit("data", data);
            }
        }
    }else{
        // Not in data mode, process as command
        this._processData(str);
    }
};

/**
 * <p>Processed incoming command lines and emits found data as 
 * <code>'command'</code> with the command name as the first param and the rest
 * of the data as second (Buffer)</p>
 * 
 * @param {String} str Binary string to be processed
 */
RAISocket.prototype._processData = function(str){
    if(!str.length){
        return;
    }
    var lines = (this._remainder+str).split("\r\n"),
        match, command;
        
    this._remainder = lines.pop();
    
    for(var i=0, len = lines.length; i<len; i++){
        if(this._ignore_data){
            // If TLS upgrade is initiated do not process current buffer
            this._remainder = "";
            break;
        }
        if(!this._dataMode){
            if((match = lines[i].match(/\s*[\S]+\s?/))){
                command = (match[0] || "").trim();
                if(this.options.debug){
                    console.log("COMMAND:", lines[i]);
                }
                this.emit("command", command, new Buffer(lines[i].substr(match.index + match[0].length), "binary"));
            }
        }else{
            if(this._remainder){
                this._remainder += "\r\n";
            }
            this._onReceiveData(lines.slice(i).join("\r\n"));
            break;
        }
    }  
};

/**
 * <p>Called when the connection is or is going to be ended</p> 
 */
RAISocket.prototype._destroy = function(){
    if(this._destroyed)return;
    this._destroyed = true;
    
    this.removeAllListeners();
};

/**
 * <p>Called when the connection is ended. Emits <code>'end'</code></p>
 * 
 * @event
 */
RAISocket.prototype._onEnd = function(){
    this.emit("end");
    this._destroy();
};

/**
 * <p>Called when an error has appeared. Emits <code>'error'</code> with
 * the error object as a parameter.</p>
 * 
 * @event
 * @param {Object} err Error object
 */
RAISocket.prototype._onError = function(err){
    this.emit("error", err);
    this._destroy();
};

/**
 * <p>Called when a timeout has occured. Connection will be closed and
 * <code>'timeout'</code> is emitted.</p>
 * 
 * @event
 */
RAISocket.prototype._onTimeout = function(){
    if(this.options.disconnectOnTimeout){
        if(this.socket && !this.socket.destroyed){
            this.socket.end();
        }
        this.emit("timeout");
        this._destroy();
    }else{
        this.emit("timeout");
    }
};

/**
 * <p>Called when the connection is closed</p>
 * 
 * @event
 * @param {Boolean} hadError did the connection end because of an error?
 */
RAISocket.prototype._onClose = function(hadError){
    this._destroy();
};