smtp-server
Advanced tools
Comparing version 3.3.1 to 3.4.0
# Changelog | ||
## v3.4.0 2017-12-01 | ||
* Added new property `secured` to indicate an TLS server where TLS is handled upstream | ||
* Allow handling TLS after PROXY header | ||
## v3.3.1 2017-11-28 | ||
@@ -4,0 +9,0 @@ |
@@ -13,2 +13,3 @@ 'use strict'; | ||
const EventEmitter = require('events'); | ||
const base32 = require('base32.js'); | ||
@@ -25,7 +26,11 @@ const SOCKET_TIMEOUT = 60 * 1000; | ||
class SMTPConnection extends EventEmitter { | ||
constructor(server, socket) { | ||
constructor(server, socket, options) { | ||
super(); | ||
options = options || {}; | ||
// Random session ID, used for logging | ||
this._id = crypto.randomBytes(9).toString('base64'); | ||
this.id = options.id || base32.encode(crypto.randomBytes(10)).toLowerCase(); | ||
this.ignore = options.ignore; | ||
this._server = server; | ||
@@ -65,7 +70,7 @@ this._socket = socket; | ||
this.tlsOptions = this.secure && !this.needsUpgrade ? this._socket.getCipher() : false; | ||
this.tlsOptions = this.secure && !this.needsUpgrade && this._socket.getCipher ? this._socket.getCipher() : false; | ||
// Store remote address for later usage | ||
this.remoteAddress = (this._socket.remoteAddress || '').replace(/^::ffff:/, ''); | ||
this.remotePort = Number(this._socket.remotePort) || 0; | ||
this.remoteAddress = (options.remoteAddress || this._socket.remoteAddress || '').replace(/^::ffff:/, ''); | ||
this.remotePort = Number(options.remotePort || this._socket.remotePort) || 0; | ||
@@ -124,6 +129,4 @@ // normalize IPv6 addresses | ||
if (!this._server.options.useProxy) { | ||
// Keep a small delay for detecting early talkers | ||
setTimeout(() => this.connectionReady(), 100); | ||
} | ||
// Keep a small delay for detecting early talkers | ||
setTimeout(() => this.connectionReady(), 100); | ||
}); | ||
@@ -373,38 +376,6 @@ } | ||
let handler; | ||
let params; | ||
if (!this._ready) { | ||
if (this._server.options.useProxy) { | ||
params = (command || '').toString().split(' '); | ||
commandName = params.shift().toUpperCase(); | ||
if (commandName !== 'PROXY') { | ||
this.send(500, 'Invalid PROXY header'); | ||
return this.close(); | ||
} | ||
if (params[1]) { | ||
this._server.logger.info( | ||
{ | ||
tnx: 'proxy', | ||
cid: this._id, | ||
proxy: params[1].trim().toLowerCase(), | ||
destination: this.remoteAddress, | ||
user: this.session.user && this.session.user.username | ||
}, | ||
'PROXY from %s through %s', | ||
params[1].trim().toLowerCase(), | ||
this.remoteAddress | ||
); | ||
this.remoteAddress = params[1].trim().toLowerCase(); | ||
if (params[3]) { | ||
this.remotePort = Number(params[3].trim()) || this.remotePort; | ||
} | ||
this.emitConnection(); | ||
} | ||
return this.connectionReady(callback); | ||
} else { | ||
// block spammers that send payloads before server greeting | ||
return this.send(421, this.name + ' You talk too soon'); | ||
} | ||
// block spammers that send payloads before server greeting | ||
return this.send(421, this.name + ' You talk too soon'); | ||
} | ||
@@ -411,0 +382,0 @@ |
@@ -10,2 +10,4 @@ 'use strict'; | ||
const punycode = require('punycode'); | ||
const crypto = require('crypto'); | ||
const base32 = require('base32.js'); | ||
@@ -72,11 +74,26 @@ const CLOSE_TIMEOUT = 30 * 1000; // how much to wait until pending connections are terminated | ||
this.server = net.createServer(this.options, socket => { | ||
this._upgrade(socket, (err, tlsSocket) => { | ||
this._handleProxy(socket, (err, socketOptions) => { | ||
if (err) { | ||
return this._onError(err); | ||
// ignore, should not happen | ||
} | ||
this.connect(tlsSocket); | ||
if (this.options.secured) { | ||
return this.connect(socket, socketOptions); | ||
} | ||
this._upgrade(socket, (err, tlsSocket) => { | ||
if (err) { | ||
return this._onError(err); | ||
} | ||
this.connect(tlsSocket, socketOptions); | ||
}); | ||
}); | ||
}); | ||
} else { | ||
this.server = net.createServer(this.options, socket => this.connect(socket)); | ||
this.server = net.createServer(this.options, socket => | ||
this._handleProxy(socket, (err, socketOptions) => { | ||
if (err) { | ||
// ignore, should not happen | ||
} | ||
this.connect(socket, socketOptions); | ||
}) | ||
); | ||
} | ||
@@ -87,4 +104,4 @@ | ||
connect(socket) { | ||
let connection = new SMTPConnection(this, socket); | ||
connect(socket, socketOptions) { | ||
let connection = new SMTPConnection(this, socket, socketOptions); | ||
this.connections.add(connection); | ||
@@ -316,2 +333,81 @@ connection.on('error', err => this._onError(err)); | ||
_handleProxy(socket, callback) { | ||
let socketOptions = { | ||
id: base32.encode(crypto.randomBytes(10)).toLowerCase() | ||
}; | ||
if ( | ||
!this.options.useProxy || | ||
(Array.isArray(this.options.useProxy) && !this.options.useProxy.includes(socket.remoteAddress) && !this.options.useProxy.includes('*')) | ||
) { | ||
socketOptions.ignore = this.options.ignoredHosts && this.options.ignoredHosts.includes(socket.remoteAddress); | ||
return setImmediate(() => callback(null, socketOptions)); | ||
} | ||
let chunks = []; | ||
let chunklen = 0; | ||
let socketReader = () => { | ||
let chunk; | ||
while ((chunk = socket.read()) !== null) { | ||
for (let i = 0, len = chunk.length; i < len; i++) { | ||
let chr = chunk[i]; | ||
if (chr === 0x0a) { | ||
socket.removeListener('readable', socketReader); | ||
chunks.push(chunk.slice(0, i + 1)); | ||
chunklen += i + 1; | ||
let remainder = chunk.slice(i + 1); | ||
if (remainder.length) { | ||
socket.unshift(remainder); | ||
} | ||
let header = Buffer.concat(chunks, chunklen) | ||
.toString() | ||
.trim(); | ||
let params = (header || '').toString().split(' '); | ||
let commandName = params.shift().toUpperCase(); | ||
if (commandName !== 'PROXY') { | ||
try { | ||
socket.end('* BAD Invalid PROXY header\r\n'); | ||
} catch (E) { | ||
// ignore | ||
} | ||
return; | ||
} | ||
if (params[1]) { | ||
socketOptions.remoteAddress = params[1].trim().toLowerCase(); | ||
socketOptions.ignore = this.options.ignoredHosts && this.options.ignoredHosts.includes(socketOptions.remoteAddress); | ||
if (!socketOptions.ignore) { | ||
this.logger.info( | ||
{ | ||
tnx: 'proxy', | ||
cid: socketOptions.id, | ||
proxy: params[1].trim().toLowerCase() | ||
}, | ||
'[%s] PROXY from %s through %s (%s)', | ||
socketOptions.id, | ||
params[1].trim().toLowerCase(), | ||
params[2].trim().toLowerCase(), | ||
JSON.stringify(params) | ||
); | ||
} | ||
if (params[3]) { | ||
socketOptions.remotePort = Number(params[3].trim()) || socketOptions.remotePort; | ||
} | ||
} | ||
return callback(null, socketOptions); | ||
} | ||
} | ||
chunks.push(chunk); | ||
chunklen += chunk.length; | ||
} | ||
}; | ||
socket.on('readable', socketReader); | ||
} | ||
/** | ||
@@ -318,0 +414,0 @@ * Called when a new connection is established. This might not be the same time the socket is opened |
{ | ||
"name": "smtp-server", | ||
"version": "3.3.1", | ||
"version": "3.4.0", | ||
"description": "Create custom SMTP servers on the fly", | ||
@@ -12,4 +12,5 @@ "main": "lib/smtp-server.js", | ||
"dependencies": { | ||
"base32.js": "^0.1.0", | ||
"ipv6-normalize": "^1.0.1", | ||
"nodemailer": "^4.1.2" | ||
"nodemailer": "^4.4.0" | ||
}, | ||
@@ -23,4 +24,4 @@ "devDependencies": { | ||
"grunt-mocha-test": "^0.13.3", | ||
"mocha": "^4.0.0", | ||
"pem": "^1.12.0" | ||
"mocha": "^4.0.1", | ||
"pem": "^1.12.3" | ||
}, | ||
@@ -34,3 +35,5 @@ "repository": { | ||
}, | ||
"keywords": ["SMTP"], | ||
"keywords": [ | ||
"SMTP" | ||
], | ||
"engines": { | ||
@@ -37,0 +40,0 @@ "node": ">=6.0.0" |
@@ -1387,2 +1387,61 @@ /* eslint no-unused-expressions:0, prefer-arrow-callback: 0 */ | ||
describe('Secure PROXY server', function() { | ||
let PORT = 1336; | ||
let server = new SMTPServer({ | ||
maxClients: 5, | ||
logger: false, | ||
useProxy: true, | ||
secure: true, | ||
onConnect(session, callback) { | ||
if (session.remoteAddress === '1.2.3.4') { | ||
let err = new Error('Blacklisted IP'); | ||
err.responseCode = 421; | ||
return callback(err); | ||
} | ||
callback(); | ||
} | ||
}); | ||
beforeEach(function(done) { | ||
server.listen(PORT, '127.0.0.1', done); | ||
}); | ||
afterEach(function(done) { | ||
server.close(done); | ||
}); | ||
it('should rewrite remote address value', function(done) { | ||
let connection = new Client({ | ||
port: PORT, | ||
host: '127.0.0.1', | ||
tls: { | ||
rejectUnauthorized: false | ||
} | ||
}); | ||
connection.on('end', done); | ||
connection.connect(function() { | ||
let conn; | ||
// get first connection | ||
server.connections.forEach(function(val) { | ||
if (!conn) { | ||
conn = val; | ||
} | ||
}); | ||
// default remote address should be overriden by the value from the PROXY header | ||
expect(conn.remoteAddress).to.equal('198.51.100.22'); | ||
expect(conn.remotePort).to.equal(35646); | ||
connection.quit(); | ||
}); | ||
connection._socket.write('PROXY TCP4 198.51.100.22 203.0.113.7 35646 80\r\n'); | ||
connection._upgradeConnection(err => { | ||
expect(err).to.not.exist; | ||
// server should respond with greeting after this point | ||
}); | ||
}); | ||
}); | ||
describe('onClose handler', function() { | ||
@@ -1389,0 +1448,0 @@ let PORT = 1336; |
154432
3772
3
+ Addedbase32.js@^0.1.0
+ Addedbase32.js@0.1.0(transitive)
Updatednodemailer@^4.4.0