Added PROXY support for IMAP, POP3

This commit is contained in:
Andris Reinman 2017-10-11 22:43:10 +03:00
parent 2d2e6abaa0
commit 6e838f4ac7
7 changed files with 199 additions and 21 deletions

View file

@ -1,8 +1,7 @@
# If enabled then Wild Duck exposes an IMAP interface for listing and fetching emails
enabled=true
port=9993
# by default bind to localhost only
host="127.0.0.1"
host="0.0.0.0"
# Use `true` for port 993 and `false` for 143. If connection is not secured
# on connection then Wild Duck enables STARTTLS extension
@ -20,6 +19,9 @@ disableRetention=false
# If true, then disables STARTTLS support
disableSTARTTLS=false
# If true, then expect HAProxy PROXY header as the first line of data
useProxy=false
[id]
#name="Wild Duck IMAP"
#version="1.0.0"

View file

@ -2,7 +2,7 @@
enabled=true
port=9995
# by default bind to localhost only
host="127.0.0.1"
host="0.0.0.0"
# Use `true` for port 995 and `false` for 110. Try to always use `true` as the included
# POP3 server is limited and does not support the STLS command
@ -15,6 +15,9 @@ disableVersionString=false
# POP3 server never lists all messages but only a limited length list
maxMessages=250
# If true, then expect HAProxy PROXY header as the first line of data
useProxy=false
[tls]
# If certificate path is not defined, use global or built-in self-signed certs
#key="/path/to/server/key.pem"

View file

@ -22,11 +22,13 @@ const SOCKET_TIMEOUT = 10 * 60 * 1000;
* @param {Object} socket Socket instance
*/
class IMAPConnection 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 || crypto.randomBytes(9).toString('base64');
this.compression = false;
this._deflate = false;
@ -63,7 +65,7 @@ class IMAPConnection extends EventEmitter {
this.secure = !!this._server.options.secure;
// Store remote address for later usage
this.remoteAddress = this._socket.remoteAddress;
this.remoteAddress = options.remoteAddress || this._socket.remoteAddress;
// Server hostname for the greegins
this.name = (this._server.options.name || os.hostname()).toLowerCase();
@ -129,6 +131,7 @@ class IMAPConnection extends EventEmitter {
this.id,
this.clientHostname
);
this.send('* OK ' + ((this._server.options.id && this._server.options.id.name) || packageInfo.name) + ' ready');
});
}

View file

@ -2,6 +2,7 @@
const net = require('net');
const tls = require('tls');
const crypto = require('crypto');
const IMAPConnection = require('./imap-connection').IMAPConnection;
const tlsOptions = require('./tls-options');
const EventEmitter = require('events').EventEmitter;
@ -45,22 +46,34 @@ class IMAPServer extends EventEmitter {
// setup server listener and connection handler
if (this.options.secure && !this.options.needsUpgrade) {
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);
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);
})
);
}
this._setListeners();
}
connect(socket) {
let connection = new IMAPConnection(this, socket);
connect(socket, socketOptions) {
let connection = new IMAPConnection(this, socket, socketOptions);
this.connections.add(connection);
connection.on('error', this._onError.bind(this));
connection.init();
@ -180,6 +193,75 @@ class IMAPServer extends EventEmitter {
this.emit('error', err);
}
_handleProxy(socket, callback) {
if (!this.options.useProxy) {
return setImmediate(callback);
}
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 socketOptions = {
id: crypto.randomBytes(9).toString('base64')
};
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]) {
this.logger.info(
{
tnx: 'proxy',
cid: socketOptions.id,
proxy: params[1].trim().toLowerCase(),
destination: socket.remoteAddress
},
'[%s] PROXY from %s through %s',
socketOptions.id,
params[1].trim().toLowerCase(),
socket.remoteAddress
);
socketOptions.remoteAddress = params[1].trim().toLowerCase();
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);
}
_upgrade(socket, callback) {
let socketOptions = {
secureContext: this.secureContext.get('*'),

View file

@ -44,6 +44,8 @@ const serverOptions = {
disableSTARTTLS: config.imap.disableSTARTTLS,
ignoreSTARTTLS: config.imap.ignoreSTARTTLS,
useProxy: !!config.imap.useProxy,
id: {
name: config.imap.name || 'Wild Duck IMAP Server',
version: config.imap.version || packageData.version,

View file

@ -8,16 +8,20 @@ const DataStream = require('nodemailer/lib/smtp-connection/data-stream');
const SOCKET_TIMEOUT = 60 * 1000;
class POP3Connection extends EventEmitter {
constructor(server, socket) {
constructor(server, socket, options) {
super();
options = options || {};
this._server = server;
this._socket = socket;
this._closed = false;
this._closing = false;
this.remoteAddress = this._socket.remoteAddress;
this._id = crypto.randomBytes(9).toString('base64');
// Store remote address for later usage
this.remoteAddress = options.remoteAddress || this._socket.remoteAddress;
this._id = options.id || crypto.randomBytes(9).toString('base64');
this.processing = false;
this.queue = [];

View file

@ -3,6 +3,7 @@
const EventEmitter = require('events');
const net = require('net');
const tls = require('tls');
const crypto = require('crypto');
const tlsOptions = require('../../imap-core/lib/tls-options');
const shared = require('nodemailer/lib/shared');
const POP3Connection = require('./connection');
@ -42,15 +43,27 @@ class POP3Server extends EventEmitter {
if (this.options.secure && !this.options.needsUpgrade) {
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);
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);
});
});
}
this._setListeners();
@ -233,8 +246,77 @@ class POP3Server extends EventEmitter {
this.emit('error', err);
}
connect(socket) {
let connection = new POP3Connection(this, socket);
_handleProxy(socket, callback) {
if (!this.options.useProxy) {
return setImmediate(callback);
}
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 socketOptions = {
id: crypto.randomBytes(9).toString('base64')
};
let header = Buffer.concat(chunks, chunklen)
.toString()
.trim();
let params = (header || '').toString().split(' ');
let commandName = params.shift().toUpperCase();
if (commandName !== 'PROXY') {
try {
socket.end('-ERR Invalid PROXY header\r\n');
} catch (E) {
// ignore
}
return;
}
if (params[1]) {
this.logger.info(
{
tnx: 'proxy',
cid: socketOptions.id,
proxy: params[1].trim().toLowerCase(),
destination: socket.remoteAddress
},
'[%s] PROXY from %s through %s',
socketOptions.id,
params[1].trim().toLowerCase(),
socket.remoteAddress
);
socketOptions.remoteAddress = params[1].trim().toLowerCase();
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);
}
connect(socket, socketOptions) {
let connection = new POP3Connection(this, socket, socketOptions);
this.connections.add(connection);
connection.once('error', err => {
this.connections.delete(connection);