pax_global_header 0000666 0000000 0000000 00000000064 12466352272 0014523 g ustar 00root root 0000000 0000000 52 comment=86b8a7231597c2307f35424c5646e663391ae301
rai-0.1.12/ 0000775 0000000 0000000 00000000000 12466352272 0012357 5 ustar 00root root 0000000 0000000 rai-0.1.12/.gitignore 0000664 0000000 0000000 00000000044 12466352272 0014345 0 ustar 00root root 0000000 0000000 node_modules
.DS_Store
npm-debug.log rai-0.1.12/.npmignore 0000664 0000000 0000000 00000000015 12466352272 0014352 0 ustar 00root root 0000000 0000000 test
examples rai-0.1.12/.travis.yml 0000664 0000000 0000000 00000000253 12466352272 0014470 0 ustar 00root root 0000000 0000000 language: node_js
node_js:
- "0.8"
- "0.10"
- "0.11"
notifications:
email:
recipients:
- andris@kreata.ee
on_success: change
on_failure: change
rai-0.1.12/LICENSE 0000664 0000000 0000000 00000001641 12466352272 0013366 0 ustar 00root root 0000000 0000000 Copyright (c) 2012 Andris Reinman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. rai-0.1.12/README.md 0000664 0000000 0000000 00000012116 12466352272 0013637 0 ustar 00root root 0000000 0000000 # RAI - Request-Answer-Interface
**rai** is a node.js module to easily generate text based command line servers.
When a client sends something to the server, the first word of the line is
treated as a command and the rest of the line as binary payload.
[](http://travis-ci.org/andris9/rai)
In addition to line based commands, there's also a data mode, to transmit
everygting received. And there's also an option to switch to TLS mode for
secure connections.
This way it is trivial to create SMTP, POP3 or similar servers.
## Installation
npm install rai
## Usage
### Simple server
var RAIServer = require("rai").RAIServer;
// create a RAIServer on port 1234
var server = new RAIServer();
server.listen(1234);
// Start listening for client connections
server.on("connect", function(client){
// Greet the client
client.send("Hello!");
// Wait for a command
client.on("command", function(command, payload){
if(command == "STATUS"){
client.send("Status is OK!");
}else if(command == "QUIT"){
client.send("Goodbye");
client.end();
}else{
client.send("Unknown command");
}
});
});
Server only emits `'connect'` and `'error'` events, while the client
objects emit `'timeout'`, `'error'` and `'end'` in addition to data
related events.
### Starting a server
Server can be started with `new RAIServer([options])` where options is an optional
parameters object with the following properties:
* **debug** - if set to true print traffic to console
* **disconnectOnTimeout** - if set to true close the connection on disconnect
* **secureConnection** - if set to true close the connection on disconnect
* **credentials** - credentials for secureConnection and STARTTLS
* **timeout** - timeout in milliseconds for disconnecting the client, defaults to 0 (no timeout)
Once the server has been set up, it can start listening for client connections
with `server.listen(port[, hostname][, callback])`. Callback function gets an error
object as a parameter if the listening failed.
var server = new RAIServer();
server.listen(25); // start listening for port 25 on "localhost"
### Closing server
Server can be closed with `server.end([callback])` where callback is run when
the server is finally closed.
### Sending data
Data can be sent with `client.send(data)` where `data` is either a String or
a Buffer. `"\r\n"` is automatically appended to the data.
client.send("Greetings!");
### Forcing connection close
Connections can be ended with `client.end()`
if(command == "QUIT"){
client.send("Good bye!");
client.end();
}
### TLS mode
TLS can be switched on with `client.startTLS([credentials][, callback])` and the status can
be listened with `'tls'` (emitted when secure connection is established)
`credentials` is an object with strings of pem encoded `key`, `cert` and optionally an
array `ca`. If `credentials` is not supplied, an autogenerated value is used.
if(command == "STARTTLS"){
client.startTLS();
}
client.on("tls", function(){
console.log("Switched to secure connection");
});
If `callback` is not set `'tls'` will be emitted on connection upgrade.
### Data mode
Data mode can be turned on with `client.startDataMode([endSequence])` and incoming
chunks can be received with `'data'`. The end of data mode can be detected by
`'ready'`.
`endSequence` is a String for matching the end (entire line) of the data stream.
By default it's `"."` which is suitable for SMTP and POP3.
if(command == "DATA"){
client.send("End data with Runs a batch of commands against a server Creates instance of RAIServer Options object has the following properties: Events Starts listening on selected port Stops the server Creates a server with listener callback Listens for errors Server listener that is run on client connection {@link RAISocket} object instance is created based on the client socket
* and a Creates a instance for interacting with a client (socket) Optional options object is the same that is passed to the parent
* {@link RAIServer} object Events Sends some data to the client. Instructs the server to be listening for mixed data instead of line based
* commands Instructs the server to upgrade the connection to secure TLS connection Fires Closes the connection to the client Called when a chunk of data arrives from the client. If currently in data
* mode, transmit the data otherwise send it to Processed incoming command lines and emits found data as
*
* var cmds = ["EHLO FOOBAR", "STARTTLS", "QUIT"];
* runClientMockup(25, "mail.hot.ee", cmds, function(resp){
* console.log("Final:", resp.toString("utf-8").trim());
* });
*
*
* @memberOf mockup
* @param {Number} port Port number
* @param {String} host Hostname to connect to
* @param {Array} commands Command list to be sent to server
* @param {Function} callback Callback function to run on completion,
* has the last response from the server as a param
* @param {Boolean} [debug] if set to true log all input/output
*/
function runClientMockup(port, host, commands, callback, debug){
host = host || "localhost";
port = port || 25;
commands = Array.isArray(commands) ? commands : [];
var command, ignore_data = false, responses = [], sslcontext, pair;
var socket = net.connect(port, host);
socket.on("connect", function(){
socket.on("data", function(chunk){
if(ignore_data){
return;
}
if(debug){
console.log("S: "+chunk.toString("utf-8").trim());
}
if(!commands.length){
socket.end();
if(typeof callback == "function"){
responses.push(chunk);
callback(chunk, responses);
}
return;
}else{
responses.push(chunk);
}
if(["STARTTLS", "STLS"].indexOf((command || "").trim().toUpperCase())>=0){
ignore_data = true;
if(debug){
console.log("Initiated TLS connection");
}
sslcontext = crypto.createCredentials();
pair = tlslib.createSecurePair(sslcontext, false);
pair.encrypted.pipe(socket);
socket.pipe(pair.encrypted);
pair.fd = socket.fd;
pair.on("secure", function(){
if(debug){
console.log("TLS connection secured");
}
command = commands.shift();
if(debug){
console.log("C: "+command);
}
pair.cleartext.write(command+"\r\n");
pair.cleartext.on("data", function(chunk){
if(debug){
console.log("S: "+chunk.toString("utf-8").trim());
}
if(!commands.length){
pair.cleartext.end();
if(typeof callback == "function"){
responses.push(chunk);
callback(chunk, responses);
}
return;
}else{
responses.push(chunk);
}
command = commands.shift();
pair.cleartext.write(command+"\r\n");
if(debug){
console.log("C: "+command);
}
});
});
}else{
command = commands.shift();
socket.write(command+"\r\n");
if(debug){
console.log("C: "+command);
}
}
});
});
}
rai-0.1.12/lib/rai.js 0000664 0000000 0000000 00000034427 12466352272 0014250 0 ustar 00root root 0000000 0000000 "use strict";
/**
* @fileOverview This is the main file for the RAI library to create text based servers
* @author Andris Reinman
*/
var netlib = require("net"),
utillib = require("util"),
EventEmitter = require('events').EventEmitter,
starttls = require("./starttls").starttls,
tlslib = require("tls"),
fs = require("fs"),
SlowBuffer = require("buffer").SlowBuffer;
// Default credentials for starting TLS server
var defaultCredentials = {
key: fs.readFileSync(__dirname+"/../cert/server.key"),
cert: fs.readFileSync(__dirname+"/../cert/server.crt")
};
// Expose to the world
module.exports.RAIServer = RAIServer;
module.exports.runClientMockup = require("./mockup");
/**
*
*
*
*
*
*
* @constructor
* @param {Object} [options] Optional options object
*/
function RAIServer(options){
EventEmitter.call(this);
this.options = options || {};
if (this.options.debug) {
this._logger = this.options.debug === true ? console.log : this.options.debug;
} else {
this._logger = function(){};
}
this._createServer();
}
utillib.inherits(RAIServer, EventEmitter);
/**
* 'connection'
event is emitted
*
*
* @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 || {};
if (this.options.debug) {
this._logger = this.options.debug === true ? console.log : this.options.debug;
} else {
this._logger = function(){};
}
this.remoteAddress = socket.remoteAddress;
this._dataMode = false;
this._endDataModeSequence = "\r\n.\r\n";
this._endDataModeSequenceRegEx = /\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);
/**
* <CR><LF>
is automatically appended to
* the datacallback
on successful connection upgrade if set,
* otherwise emits 'tls'
{key:"---BEGIN...", cert:"---BEGIN..."}
,
* if not set autogenerated values will be used.
* @param {Function} [callback] If calback is set fire it after successful connection
* upgrade, otherwise 'tls'
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(!ssl_socket.authorized){
this._logger("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));
this.socket.on("error", this._onError.bind(this));
this._logger("TLS CONNECTION STARTED");
if(callback){
callback();
}else{
this.emit("tls");
}
}).bind(this));
secure_connector.on("error", (function(err){
this._onError(err);
}).bind(this));
};
/**
* _processData
'command'
with the command name as the first param and the rest
* of the data as second (Buffer)
Called when the connection is ended. Emits 'end'
Called when an error has appeared. Emits 'error'
with
* the error object as a parameter.
Called when a timeout has occured. Connection will be closed and
* 'timeout'
is emitted.
Called when the connection is closed
* * @event * @param {Boolean} hadError did the connection end because of an error? */ RAISocket.prototype._onClose = function(/* hadError */){ this._destroy(); }; rai-0.1.12/lib/starttls.js 0000664 0000000 0000000 00000005135 12466352272 0015347 0 ustar 00root root 0000000 0000000 "use strict"; // SOURCE: https://gist.github.com/848444 // Target API: // // var s = require('net').createStream(25, 'smtp.example.com'); // s.on('connect', function() { // require('starttls')(s, options, function() { // if (!s.authorized) { // s.destroy(); // return; // } // // s.end("hello world\n"); // }); // }); // // /** * @namespace STARTTLS module * @name starttls */ module.exports.starttls = starttls; /** *Upgrades a socket to a secure TLS connection
* * @memberOf starttls * @param {Object} socket Plaintext socket to be upgraded * @param {Object} options Certificate data for the server * @param {Function} callback Callback function to be run after upgrade */ function starttls(socket, options, callback) { var sslcontext, pair, cleartext; socket.removeAllListeners("data"); sslcontext = require('crypto').createCredentials(options); pair = require('tls').createSecurePair(sslcontext, true); cleartext = pipe(pair, socket); pair.on('secure', function() { var verifyError = (pair._ssl || pair.ssl).verifyError(); if (verifyError) { cleartext.authorized = false; cleartext.authorizationError = verifyError; } else { cleartext.authorized = true; } callback(cleartext); }); cleartext._controlReleased = true; return pair; } function forwardEvents(events, emitterSource, emitterDestination) { var map = [], handler; events.forEach(function(name){ handler = function(){ this.emit.apply(this, arguments); }.bind(emitterDestination, name); map.push(name); emitterSource.on(name, handler); }); return map; } function removeEvents(map, emitterSource) { for(var i = 0, len = map.length; i < len; i++){ emitterSource.removeAllListeners(map[i]); } } function pipe(pair, socket) { pair.encrypted.pipe(socket); socket.pipe(pair.encrypted); pair.fd = socket.fd; var cleartext = pair.cleartext; cleartext.socket = socket; cleartext.encrypted = pair.encrypted; cleartext.authorized = false; function onerror(e) { if (cleartext._controlReleased) { cleartext.emit('error', e); } } var map = forwardEvents(["timeout", "end", "close", "drain", "error"], socket, cleartext); function onclose() { socket.removeListener('error', onerror); socket.removeListener('close', onclose); removeEvents(map,socket); } socket.on('error', onerror); socket.on('close', onclose); return cleartext; } rai-0.1.12/package.json 0000664 0000000 0000000 00000001367 12466352272 0014654 0 ustar 00root root 0000000 0000000 { "name": "rai", "description": "Request-Answer-Interface for generating text based command servers (SMTP, POP etc)", "version": "0.1.12", "author" : "Andris Reinman", "maintainers":[ { "name":"andris", "email":"andris@node.ee" } ], "repository" : { "type" : "git", "url" : "http://github.com/andris9/rai.git" }, "scripts":{ "test": "nodeunit test" }, "main" : "./lib/rai", "licenses" : [ { "type": "MIT", "url": "http://github.com/andris9/rai/blob/master/LICENSE" } ], "devDependencies": { "nodeunit": "*" }, "engines": ["node >=0.4.0"], "keywords": ["servers", "text-based"] } rai-0.1.12/test/ 0000775 0000000 0000000 00000000000 12466352272 0013336 5 ustar 00root root 0000000 0000000 rai-0.1.12/test/rai.js 0000664 0000000 0000000 00000061262 12466352272 0014456 0 ustar 00root root 0000000 0000000 process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; var RAIServer = require("../lib/rai").RAIServer, runClientMockup = require("../lib/rai").runClientMockup, testCase = require('nodeunit').testCase, utillib = require("util"), netlib = require("net"), crypto = require("crypto"), tlslib = require("tls"); var PORT_NUMBER = 8397; // monkey patch net and tls to support nodejs 0.4 if(!netlib.connect && netlib.createConnection){ netlib.connect = netlib.createConnection; } if(!tlslib.connect && tlslib.createConnection){ tlslib.connect = tlslib.createConnection; } exports["General tests"] = { "Create and close a server": function(test){ var server = new RAIServer(); server.listen(PORT_NUMBER, function(err){ test.ifError(err); server.end(function(){ test.ok(1, "Server closed"); test.done(); }); }); }, "Create a secure server": function(test){ var server = new RAIServer({secureConnection: true}); server.listen(PORT_NUMBER, function(err){ test.ifError(err); server.end(function(){ test.ok(1, "Server closed"); test.done(); }); }); }, "Duplicate server fails": function(test){ var server = new RAIServer(); server.listen(PORT_NUMBER, function(err){ test.ifError(err); var duplicate = new RAIServer(); duplicate.listen(PORT_NUMBER, function(err){ test.ok(err, "Responds with error"); server.end(function(){ test.ok(1, "Server closed"); test.done(); }); }); }); }, "Connection event": function(test){ var server = new RAIServer(); test.expect(3); server.listen(PORT_NUMBER, function(err){ server.on("connect", function(socket){ test.ok(socket, "Client connected"); socket.on("end", function(){ test.ok(1, "Connection closed"); server.end(function(){ test.done(); }); }); socket.on("error", function(err){ test.isError(err); }); }); var client = netlib.connect(PORT_NUMBER, function(){ test.ok(1, "Connected to server"); client.end(); }); }); }, "Close client socket": function(test){ var server = new RAIServer(); test.expect(4); server.listen(PORT_NUMBER, function(err){ server.on("connect", function(socket){ test.ok(socket, "Client connected"); socket.on("end", function(){ test.ok(1, "Connection closed"); server.end(function(){ test.done(); }); }); socket.on("error", function(err){ test.isError(err); }); socket.end(); }); var client = netlib.connect(PORT_NUMBER, function(){ test.ok(1, "Connected to server"); }); client.on("end", function(){ test.ok(1, "Connection closed by host"); }); }); }, "Send data to client": function(test){ var server = new RAIServer(); server.listen(PORT_NUMBER, function(err){ server.on("connect", function(socket){ socket.send("HELLO"); socket.on("end", function(){ server.end(function(){ test.done(); }); }); socket.on("error", function(err){ test.isError(err); }); }); var client = netlib.connect(PORT_NUMBER, function(){ client.on("data", function(chunk){ test.equal(chunk.toString(), "HELLO\r\n"); client.end(); }); }); }); } }; exports["Secure connection"] = { "STARTTLS with event": function(test){ var server = new RAIServer(); server.listen(PORT_NUMBER, function(err){ test.expect(2); server.on("connect", function(socket){ socket.startTLS(); socket.on("tls", function(){ test.ok(1, "Secure connection opened"); socket.send("TEST"); }); socket.on("end", function(){ server.end(function(){ test.done(); }); }); socket.on("error", function(err){ test.isError(err); }); }); var client = netlib.connect(PORT_NUMBER, function(){ var sslcontext = crypto.createCredentials(); var pair = tlslib.createSecurePair(sslcontext, false); pair.encrypted.pipe(client); client.pipe(pair.encrypted); pair.fd = client.fd; pair.on("secure", function(){ pair.cleartext.on("data", function(chunk){ test.equal(chunk.toString(), "TEST\r\n"); pair.cleartext.end(); }); }); }); }); }, "STARTTLS Callback": function(test){ var server = new RAIServer(); server.listen(PORT_NUMBER, function(err){ test.expect(2); server.on("connect", function(socket){ socket.startTLS(function(){ test.ok(1, "Secure connection opened"); socket.send("TEST"); }); socket.on("tls", function(){ test.ok(0, "Should not occur"); }); socket.on("end", function(){ server.end(function(){ test.done(); }); }); socket.on("error", function(err){ test.isError(err); }); }); var client = netlib.connect(PORT_NUMBER, function(){ var sslcontext = crypto.createCredentials(); var pair = tlslib.createSecurePair(sslcontext, false); pair.encrypted.pipe(client); client.pipe(pair.encrypted); pair.fd = client.fd; pair.on("secure", function(){ pair.cleartext.on("data", function(chunk){ test.equal(chunk.toString(), "TEST\r\n"); pair.cleartext.end(); }); }); }); }); }, "STARTTLS clears command buffer": function(test){ var server = new RAIServer(); server.listen(PORT_NUMBER, function(err){ test.expect(2); server.on("connect", function(socket){ socket.on("command", function(command){ if(command == "STARTTLS"){ socket.startTLS(); socket.send("OK"); }else if(command == "KILL"){ test.ok(0, "Should not occur"); }else if(command == "OK"){ test.ok(1, "OK"); } }); socket.on("tls", function(){ test.ok(1, "Secure connection opened"); socket.send("TEST"); }); socket.on("end", function(){ server.end(function(){ test.done(); }); }); socket.on("error", function(err){ test.isError(err); }); }); var client = netlib.connect(PORT_NUMBER, function(){ client.write("STARTTLS\r\nKILL\r\n"); client.on("data", function(chunk){ if(chunk.toString("utf-8").trim() == "OK"){ var sslcontext = crypto.createCredentials(); var pair = tlslib.createSecurePair(sslcontext, false); pair.encrypted.pipe(client); client.pipe(pair.encrypted); pair.fd = client.fd; pair.on("secure", function(){ pair.cleartext.write("OK\r\n"); pair.cleartext.end(); }); } }); }); }); }, "STARTTLS on secure server fails": function(test){ var server = new RAIServer({secureConnection: true}); server.listen(PORT_NUMBER, function(err){ test.expect(2); server.on("connect", function(socket){ socket.on("error", function(err){ test.ok(err); socket.end(); server.end(function(){ test.done(); }); }); socket.on("command", (function(command){ process.nextTick(socket.startTLS.bind(socket, function(){ test.ok(false, "Secure connection opened"); // should not occur server.end(function(){ test.done(); }); })); }).bind(this)); socket.on("tls", function(){ test.ok(0, "Should not occur"); }); }); var client = tlslib.connect(PORT_NUMBER, function(){ test.ok(true); client.write("HELLO!\r\n"); }); }); } }; exports["Client commands"] = { "Receive Simple Command": function(test){ var server = new RAIServer(); server.listen(PORT_NUMBER, function(err){ server.on("connect", function(socket){ socket.on("command", function(command, payload){ test.equal(command, "STATUS"); test.equal(payload.toString(), ""); socket.end(); server.end(function(){ test.done(); }); }); socket.on("error", function(err){ test.isError(err); }); }); var client = netlib.connect(PORT_NUMBER, function(){ client.write("STATUS\r\n"); }); }); }, "Receive Command with payload": function(test){ var server = new RAIServer(); server.listen(PORT_NUMBER, function(err){ server.on("connect", function(socket){ socket.on("command", function(command, payload){ test.equal(command, "MAIL"); test.equal(payload.toString(), "TO:"); socket.end(); server.end(function(){ test.done(); }); }); socket.on("error", function(err){ test.isError(err); }); }); var client = netlib.connect(PORT_NUMBER, function(){ client.write("MAIL TO:\r\n"); }); }); } }; exports["Data mode"] = { "DATA mode": function(test){ var server = new RAIServer(), datapayload = "tere\r\nvana kere"; server.listen(PORT_NUMBER, function(err){ server.on("connect", function(socket){ socket.startDataMode(); test.expect(2); socket.on("data", function(chunk){ test.equal(datapayload, chunk.toString()); }); socket.on("ready", function(){ test.ok(1,"Data ready"); server.end(function(){ test.done(); }); }); socket.on("error", function(err){ test.isError(err); }); }); var client = netlib.connect(PORT_NUMBER, function(){ client.write(datapayload+"\r\n.\r\n"); client.end(); }); }); }, "Small chunks DATA mode": function(test){ var server = new RAIServer(), datapayload = "tere\r\nvana kere õäöü\r\n.\r", databytes = [], fullpayload = datapayload+"\r\n.\r\n"; server.listen(PORT_NUMBER, function(err){ server.on("connect", function(socket){ socket.startDataMode(); test.expect(1); socket.on("data", function(chunk){ databytes = databytes.concat(Array.prototype.slice.call(chunk)); }); socket.on("ready", function(){ test.equal(new Buffer(databytes).toString("utf-8"), datapayload); server.end(function(){ test.done(); }); }); socket.on("error", function(err){ test.isError(err); }); for(var i=0, len = fullpayload.length; i