You're Invited:Meet the Socket Team at BlackHat and DEF CON in Las Vegas, Aug 7-8.RSVP
Socket
Socket
Sign inDemoInstall

smtp-server

Package Overview
Dependencies
Maintainers
1
Versions
65
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.1.1 to 1.2.0

7

CHANGELOG.md
# Changelog
## v1.2.0 2015-03-11
* Do not allow HTTP requests. If the client tries to send a command that looks like a HTTP request, then disconnect
* Close connection after 10 unrecognized commands
* Close connection after 10 unauthenticated commands
* Close all pending connections after `server.close()` has been called. Default delay to wait is 30 sec. Can be changed with `closeTimeout` option
## v1.1.1 2015-03-11

@@ -4,0 +11,0 @@

81

lib/smtp-connection.js

@@ -65,2 +65,8 @@ 'use strict';

// Error counter - if too many commands in non-authenticated state are used, then disconnect
this._unauthenticatedCommands = 0;
// Error counter - if too many invalid commands are used, then disconnect
this._unrecognizedCommands = 0;
// Server hostname for the greegins

@@ -75,12 +81,18 @@ this.name = this._server.options.name || os.hostname();

// Setup event handlers for the socket
this._setListeners();
// increment connection count
this._server.connections++;
this._closing = false;
this._closed = false;
}
util.inherits(SMTPConnection, EventEmitter);
/**
* Initiates the connection. Checks connection limits and reverse resolves client hostname. The client
* is not allowed to send anything before init has finished otherwise 'You talk too soon' error is returned
*/
SMTPConnection.prototype.init = function() {
// Setup event handlers for the socket
this._setListeners();
// Check that connection limit is not exceeded
if (this._server.options.maxClients && this._server.connections > this._server.options.maxClients) {
if (this._server.options.maxClients && this._server.connections.size > this._server.options.maxClients) {
this.send(421, this.name + ' Too many connected clients, try again in a moment');

@@ -90,18 +102,19 @@ return this.close();

// Resolve hostname for the remote IP
dns.reverse(this.remoteAddress, function(err, hostnames) {
if (this._closing || this._closed) {
return;
}
// Resolve hostname for the remote IP, keep a small delay for detecting early talkers
setTimeout(function() {
dns.reverse(this.remoteAddress, function(err, hostnames) {
if (this._closing || this._closed) {
return;
}
this.clientHostname = hostnames && hostnames.shift() || '[' + this.remoteAddress + ']';
this.clientHostname = hostnames && hostnames.shift() || '[' + this.remoteAddress + ']';
this._startSession();
this._startSession();
this._ready = true; // Start accepting data from input
this._server.logger.info('[%s] Connection from %s', this._id, this.clientHostname);
this.send(220, this.name + ' ESMTP' + (this._server.options.banner ? ' ' + this._server.options.banner : ''));
}.bind(this));
}
util.inherits(SMTPConnection, EventEmitter);
this._ready = true; // Start accepting data from input
this._server.logger.info('[%s] Connection from %s', this._id, this.clientHostname);
this.send(220, this.name + ' ESMTP' + (this._server.options.banner ? ' ' + this._server.options.banner : ''));
}.bind(this));
}.bind(this), 100);
};

@@ -139,5 +152,3 @@ /**

if (!this._closing && !this._closed) {
this._server.connections--;
}
this._server.connections.delete(this);

@@ -171,5 +182,3 @@ this._closing = true;

if (!this._closing && !this._closed) {
this._server.connections--;
}
this._server.connections.delete(this);

@@ -221,2 +230,3 @@ if (this._closed) {

// block spammers that send payloads before server greeting
if (!this._ready) {

@@ -227,2 +237,8 @@ this.send(421, this.name + ' You talk too soon');

// block malicious web pages that try to make SMTP calls from an AJAX request
if (/^(OPTIONS|GET|HEAD|POST|PUT|DELETE|TRACE|CONNECT) \/.* HTTP\/\d\.\d$/i.test(command)) {
this.send(554, 'HTTP requests not allowed');
return this.close();
}
callback = callback || function() {};

@@ -251,2 +267,9 @@

if (!handler) {
// if the user makes more
this._unrecognizedCommands++;
if (this._unrecognizedCommands >= 10) {
this.send(554, 'Error: too many unrecognized commands');
return this.close();
}
this.send(500, 'Error: command not recognized');

@@ -256,2 +279,11 @@ return setImmediate(callback);

// block users that try to fiddle around without logging in
if (!this.session.user && this._isSupported('AUTH') && commandName !== 'AUTH') {
this._unauthenticatedCommands++;
if (this._unauthenticatedCommands >= 10) {
this.send(554, 'Error: too many unauthenticated commands');
return this.close();
}
}
if (!this.hostNameAppearsAs && commandName &&

@@ -639,2 +671,3 @@ ['MAIL', 'RCPT', 'DATA', 'AUTH'].indexOf(commandName) >= 0) {

this._unrecognizedCommands = 0; // reset unrecognized commands counter
this._startSession(); // reset session state

@@ -641,0 +674,0 @@ this._parser.continue();

@@ -10,2 +10,4 @@ 'use strict';

var CLOSE_TIMEOUT = 30 * 1000; // how much to wait until pending connections are terminated
// Expose to the world

@@ -46,3 +48,3 @@ module.exports.SMTPServer = SMTPServer;

if ('logger' in this.options) {
// use provided logger or use vanity logger
// use provided logger or use vanity logger if option is set to false
this.logger = this.options.logger || {

@@ -58,2 +60,3 @@ info: function() {},

// apply shorthand handlers
['onAuth', 'onMailFrom', 'onRcptTo', 'onData'].forEach(function(handler) {

@@ -65,7 +68,18 @@ if (typeof this.options[handler] === 'function') {

this.connections = 0;
/**
* Timeout after close has been called until pending connections are forcibly closed
*/
this._closeTimeout = false;
/**
* A set of all currently open connections
*/
this.connections = new Set();
// setup server listener and connection handler
this.server = (this.options.secure ? tls : net).createServer(this.options, function(socket) {
var connection = new SMTPConnection(this, socket);
this.connections.add(connection);
connection.on('error', this._onError.bind(this));
connection.init();
}.bind(this));

@@ -90,3 +104,27 @@

SMTPServer.prototype.close = function(callback) {
this.server.close(callback);
var connections = this.connections.size;
var timeout = this.options.closeTimeout || CLOSE_TIMEOUT;
// stop accepting new connections
this.server.close(function() {
clearTimeout(this._closeTimeout);
callback();
}.bind(this));
// close active connections
if (connections) {
this.logger.info('Server closing with %s pending connection%s, waiting %s seconds before terminating', connections, connections !== 1 ? 's' : '', timeout / 1000);
}
this._closeTimeout = setTimeout(function() {
connections = this.connections.size;
if (connections) {
this.logger.info('Closing %s pending connection%s to close the server', connections, connections !== 1 ? 's' : '');
this.connections.forEach(function(connection) {
connection.send(421, 'Server shutting down');
connection.close();
}.bind(this));
}
}.bind(this), timeout);
};

@@ -93,0 +131,0 @@

{
"name": "smtp-server",
"version": "1.1.1",
"version": "1.2.0",
"description": "Create custom SMTP servers on the fly",

@@ -5,0 +5,0 @@ "main": "lib/smtp-server.js",

@@ -44,2 +44,3 @@ # smtp-server

* **options.socketTimeout** how many milliseconds of inactivity to allow before disconnecting the client (defaults to 1 minute)
* **options.closeTimeout** how many millisceonds to wait before disconnecting pending connections once server.close() has been called (defaults to 30 seconds)
* **onAuth** is the callback to handle authentications (see details [here](#handling-authentication))

@@ -46,0 +47,0 @@ * **onMailFrom** is the callback to validate MAIL FROM commands (see details [here](#validating-sender-addresses))

@@ -7,2 +7,3 @@ 'use strict';

var SMTPConnection = require('../lib/smtp-connection').SMTPConnection;
var net = require('net');

@@ -21,4 +22,7 @@ var expect = chai.expect;

it('should parse MAIL FROM/RCPT TO', function() {
var conn = new SMTPConnection({
options: {}
}, {});
expect(SMTPConnection.prototype._parseAddressCommand('MAIL FROM', 'MAIL FROM:<test@example.com>')).to.deep.equal({
expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<test@example.com>')).to.deep.equal({
address: 'test@example.com',

@@ -28,3 +32,3 @@ args: false

expect(SMTPConnection.prototype._parseAddressCommand('MAIL FROM', 'MAIL FROM:<sender@example.com> SIZE=12345 RET=HDRS ')).to.deep.equal({
expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<sender@example.com> SIZE=12345 RET=HDRS ')).to.deep.equal({
address: 'sender@example.com',

@@ -37,3 +41,3 @@ args: {

expect(SMTPConnection.prototype._parseAddressCommand('MAIL FROM', 'MAIL FROM : <test@example.com>')).to.deep.equal({
expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM : <test@example.com>')).to.deep.equal({
address: 'test@example.com',

@@ -43,3 +47,3 @@ args: false

expect(SMTPConnection.prototype._parseAddressCommand('MAIL TO', 'MAIL FROM:<test@example.com>')).to.be.false;
expect(conn._parseAddressCommand('MAIL TO', 'MAIL FROM:<test@example.com>')).to.be.false;
});

@@ -241,3 +245,73 @@ });

});
});
describe('Plaintext server with no connection limit', function() {
this.timeout(60 * 1000);
var PORT = 1336;
var server = new SMTPServer({
logger: false,
socketTimeout: 100 * 1000,
closeTimeout: 6 * 1000
});
beforeEach(function(done) {
server.listen(PORT, '127.0.0.1', done);
});
it('open multiple connections and close all at once', function(done) {
var limit = 100;
var cleanClose = 4;
var disconnected = 0;
var connected = 0;
var connections = [];
var createConnection = function(callback) {
var connection = new Client({
port: PORT,
host: '127.0.0.1',
tls: {
rejectUnauthorized: false
}
});
connection.on('error', function(err) {
expect(err.responseCode).to.equal(421); // Server shutting down
});
connection.on('end', function() {
disconnected++;
if (disconnected >= limit) {
done();
}
});
connection.connect(function() {
connected++;
callback(null, connection);
});
};
var connCb = function(err, conn) {
expect(err).to.not.exist;
connections.push(conn);
if (connected >= limit) {
server.close();
setTimeout(function() {
for (var i = 0; i < cleanClose; i++) {
connections[i].quit();
}
}, 1000);
} else {
createConnection(connCb);
}
};
createConnection(connCb);
});
});

@@ -303,3 +377,14 @@

logger: false,
socketTimeout: 2 * 1000
socketTimeout: 2 * 1000,
onAuth: function(auth, session, callback) {
if (auth.username === 'testuser' && auth.password === 'testpass') {
callback(null, {
user: 'userdata'
});
} else {
callback(null, {
message: 'Authentication failed'
});
}
}
});

@@ -356,2 +441,93 @@

});
it('should close after too many unauthenticated commands', function(done) {
var connection = new Client({
port: PORT,
host: '127.0.0.1',
ignoreTLS: true
});
connection.on('error', function(err) {
expect(err).to.exist;
});
connection.on('end', done);
connection.connect(function() {
var looper = function() {
connection._currentAction = function() {
looper();
};
connection._sendCommand('NOOP');
};
looper();
});
});
it('should close after too many unrecognized commands', function(done) {
var connection = new Client({
port: PORT,
host: '127.0.0.1',
ignoreTLS: true
});
connection.on('error', function(err) {
expect(err).to.exist;
});
connection.on('end', done);
connection.connect(function() {
connection.login({
user: 'testuser',
pass: 'testpass'
}, function(err) {
expect(err).to.not.exist;
var looper = function() {
connection._currentAction = function() {
looper();
};
connection._sendCommand('ZOOP');
};
looper();
});
});
});
it('should reject early talker', function(done) {
var socket = net.connect(PORT, '127.0.0.1', function() {
var buffers = [];
socket.on('data', function(chunk) {
buffers.push(chunk);
});
socket.on('end', function() {
var data = Buffer.concat(buffers).toString();
expect(/^421 /.test(data)).to.be.true;
done();
});
socket.write('EHLO FOO\r\n');
});
});
it('should reject HTTP requests', function(done) {
var socket = net.connect(PORT, '127.0.0.1', function() {
var buffers = [];
var started = false;
socket.on('data', function(chunk) {
buffers.push(chunk);
if (!started) {
started = true;
socket.write('GET /path/file.html HTTP/1.0\r\nHost: www.example.com\r\n\r\n');
}
});
socket.on('end', function() {
var data = Buffer.concat(buffers).toString();
expect(/^554 /m.test(data)).to.be.true;
done();
});
});
});
});

@@ -401,8 +577,19 @@

logger: false,
authMethods: ['PLAIN', 'LOGIN', 'XOAUTH2']
});
server.onAuth = function(auth, session, callback) {
if (auth.method === 'XOAUTH2') {
if (auth.username === 'testuser' && auth.accessToken === 'testtoken') {
authMethods: ['PLAIN', 'LOGIN', 'XOAUTH2'],
onAuth: function(auth, session, callback) {
if (auth.method === 'XOAUTH2') {
if (auth.username === 'testuser' && auth.accessToken === 'testtoken') {
callback(null, {
user: 'userdata'
});
} else {
callback(null, {
data: {
status: '401',
schemes: 'bearer mac',
scope: 'https://mail.google.com/'
}
});
}
} else if (auth.username === 'testuser' && auth.password === 'testpass') {
callback(null, {

@@ -413,19 +600,7 @@ user: 'userdata'

callback(null, {
data: {
status: '401',
schemes: 'bearer mac',
scope: 'https://mail.google.com/'
}
message: 'Authentication failed'
});
}
} else if (auth.username === 'testuser' && auth.password === 'testpass') {
callback(null, {
user: 'userdata'
});
} else {
callback(null, {
message: 'Authentication failed'
});
}
};
});

@@ -432,0 +607,0 @@ beforeEach(function(done) {

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc