Allow TLS to be handled upstream

This commit is contained in:
Andris Reinman 2017-12-01 10:02:40 +02:00
parent 2a4fa1b35f
commit ebf05cda58
9 changed files with 235 additions and 177 deletions

View file

@ -360,7 +360,20 @@ be noticeable.
### HAProxy
When using Haproxy you can enable PROXY protocol to get correct remote addresses in logs
When using HAProxy you can enable PROXY protocol to get correct remote addresses in server logs. You can use the most basic round-robin based balancing as no
persistent sessions against specific hosts are needed. Use TCP load balancing with no extra settings both for plaintext and TLS connections.
If TLS is handled by HAProxy then use the following server config to indicate that WildDuck assumes to be a TLS server but TLS is handled upstream
```toml
[imap]
secure=true # this is a TLS server
secured=true # TLS is handled upstream
[pop3]
secure=true # this is a TLS server
secured=true # TLS is handled upstream
```
### Certificates

View file

@ -30,6 +30,9 @@ disableSTARTTLS=false
# If true, then expect HAProxy PROXY header as the first line of data
useProxy=false
# useProxy=true # expect PROXY from all conections
# useProxy=['*'] # expect PROXY from all conections
# useProxy=['1.2.3.4', '1.2.3.5'] # expect PROXY only from connections from listed IP addresses
# an array of IP addresses to ignore (not logged)
ignoredHosts=[]

View file

@ -67,7 +67,7 @@ class IMAPConnection extends EventEmitter {
this.secure = !!this._server.options.secure;
// Store remote address for later usage
this.remoteAddress = options.remoteAddress || this._socket.remoteAddress;
this.remoteAddress = (options.remoteAddress || this._socket.remoteAddress || '').replace(/^::ffff:/, '');
// Server hostname for the greegins
this.name = (this._server.options.name || os.hostname()).toLowerCase();

View file

@ -51,6 +51,9 @@ class IMAPServer extends EventEmitter {
if (err) {
// ignore, should not happen
}
if (this.options.secured) {
return this.connect(socket, socketOptions);
}
this._upgrade(socket, (err, tlsSocket) => {
if (err) {
return this._onError(err);
@ -199,7 +202,10 @@ class IMAPServer extends EventEmitter {
id: base32.encode(crypto.randomBytes(10)).toLowerCase()
};
if (!this.options.useProxy) {
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));
}

View file

@ -355,6 +355,8 @@ let createInterface = (ifaceOptions, callback) => {
// Setup server
const serverOptions = {
secure: ifaceOptions.secure,
secured: ifaceOptions.secured,
disableSTARTTLS: ifaceOptions.disableSTARTTLS,
ignoreSTARTTLS: ifaceOptions.ignoreSTARTTLS,

View file

@ -48,6 +48,9 @@ class POP3Server extends EventEmitter {
if (err) {
// ignore, should not happen
}
if (this.options.secured) {
return this.connect(socket, socketOptions);
}
this._upgrade(socket, (err, tlsSocket) => {
if (err) {
return this._onError(err);
@ -252,7 +255,10 @@ class POP3Server extends EventEmitter {
id: base32.encode(crypto.randomBytes(10)).toLowerCase()
};
if (!this.options.useProxy) {
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));
}

View file

@ -31,6 +31,9 @@ config.on('reload', () => {
const serverOptions = {
lmtp: true,
secure: config.lmtp.secure,
secured: config.lmtp.secured,
// log to console
logger: {
info(...args) {

View file

@ -8,7 +8,10 @@
"test": "mongo --eval 'db.dropDatabase()' wildduck-test && redis-cli -n 13 flushdb && NODE_ENV=test grunt",
"apidoc": "apidoc -i lib/api/ -o docs/"
},
"keywords": ["imap", "mail server"],
"keywords": [
"imap",
"mail server"
],
"author": "Andris Reinman",
"license": "EUPL-1.1",
"devDependencies": {
@ -39,7 +42,7 @@
"humanparser": "1.5.0",
"iconv-lite": "0.4.19",
"ioredfour": "1.0.2-ioredis",
"ioredis": "3.2.1",
"ioredis": "3.2.2",
"joi": "13.0.2",
"js-yaml": "3.10.0",
"libbase64": "1.0.1",
@ -53,11 +56,11 @@
"mongodb-extended-json": "^1.10.0",
"nodemailer": "4.4.0",
"npmlog": "4.1.2",
"openpgp": "2.5.13",
"openpgp": "2.5.14",
"qrcode": "1.0.0",
"restify": "6.3.4",
"seq-index": "1.1.0",
"smtp-server": "3.3.1",
"smtp-server": "3.4.0",
"speakeasy": "2.0.0",
"tlds": "1.199.0",
"u2f": "0.1.3",

360
pop3.js
View file

@ -21,6 +21,8 @@ const serverOptions = {
host: config.pop3.host,
secure: config.pop3.secure,
secured: config.pop3.secured,
disableSTARTTLS: config.pop3.disableSTARTTLS,
ignoreSTARTTLS: config.pop3.ignoreSTARTTLS,
@ -86,58 +88,61 @@ const serverOptions = {
onListMessages(session, callback) {
// only list messages in INBOX
db.database.collection('mailboxes').findOne({
user: session.user.id,
path: 'INBOX'
}, (err, mailbox) => {
if (err) {
return callback(err);
}
db.database.collection('mailboxes').findOne(
{
user: session.user.id,
path: 'INBOX'
},
(err, mailbox) => {
if (err) {
return callback(err);
}
if (!mailbox) {
return callback(new Error('Mailbox not found for user'));
}
if (!mailbox) {
return callback(new Error('Mailbox not found for user'));
}
session.user.mailbox = mailbox._id;
session.user.mailbox = mailbox._id;
db.database
.collection('messages')
.find({
mailbox: mailbox._id
})
.project({
uid: true,
size: true,
mailbox: true,
// required to decide if we need to update flags after RETR
flags: true,
unseen: true
})
.sort([['uid', -1]])
.limit(config.pop3.maxMessages || MAX_MESSAGES)
.toArray((err, messages) => {
if (err) {
return callback(err);
}
db.database
.collection('messages')
.find({
mailbox: mailbox._id
})
.project({
uid: true,
size: true,
mailbox: true,
// required to decide if we need to update flags after RETR
flags: true,
unseen: true
})
.sort([['uid', -1]])
.limit(config.pop3.maxMessages || MAX_MESSAGES)
.toArray((err, messages) => {
if (err) {
return callback(err);
}
return callback(null, {
messages: messages
// showolder first
.reverse()
// compose message objects
.map(message => ({
id: message._id.toString(),
uid: message.uid,
mailbox: message.mailbox,
size: message.size,
flags: message.flags,
seen: !message.unseen
})),
count: messages.length,
size: messages.reduce((acc, message) => acc + message.size, 0)
return callback(null, {
messages: messages
// showolder first
.reverse()
// compose message objects
.map(message => ({
id: message._id.toString(),
uid: message.uid,
mailbox: message.mailbox,
size: message.size,
flags: message.flags,
seen: !message.unseen
})),
count: messages.length,
size: messages.reduce((acc, message) => acc + message.size, 0)
});
});
});
});
}
);
},
onFetchMessage(message, session, callback) {
@ -149,38 +154,42 @@ const serverOptions = {
let err = new Error('Download was rate limited. Check again in ' + res.ttl + ' seconds');
return callback(err);
}
db.database.collection('messages').findOne({
_id: new ObjectID(message.id),
// shard key
mailbox: message.mailbox,
uid: message.uid
}, {
mimeTree: true,
size: true
}, (err, message) => {
if (err) {
return callback(err);
db.database.collection('messages').findOne(
{
_id: new ObjectID(message.id),
// shard key
mailbox: message.mailbox,
uid: message.uid
},
{
mimeTree: true,
size: true
},
(err, message) => {
if (err) {
return callback(err);
}
if (!message) {
return callback(new Error('Message does not exist or is already deleted'));
}
let response = messageHandler.indexer.rebuild(message.mimeTree);
if (!response || response.type !== 'stream' || !response.value) {
return callback(new Error('Can not fetch message'));
}
let limiter = new LimitedFetch({
key: 'pdw:' + session.user.id,
ttlcounter: messageHandler.counters.ttlcounter,
maxBytes: config.pop3.maxDownloadMB * 1024 * 1024
});
response.value.pipe(limiter);
response.value.once('error', err => limiter.emit('error', err));
callback(null, limiter);
}
if (!message) {
return callback(new Error('Message does not exist or is already deleted'));
}
let response = messageHandler.indexer.rebuild(message.mimeTree);
if (!response || response.type !== 'stream' || !response.value) {
return callback(new Error('Can not fetch message'));
}
let limiter = new LimitedFetch({
key: 'pdw:' + session.user.id,
ttlcounter: messageHandler.counters.ttlcounter,
maxBytes: config.pop3.maxDownloadMB * 1024 * 1024
});
response.value.pipe(limiter);
response.value.once('error', err => limiter.emit('error', err));
callback(null, limiter);
});
);
});
},
@ -225,110 +234,123 @@ certs.registerReload(server, 'pop3');
// move messages to trash
function trashMessages(session, messages, callback) {
// find Trash folder
db.database.collection('mailboxes').findOne({
user: session.user.id,
specialUse: '\\Trash'
}, (err, trashMailbox) => {
if (err) {
return callback(err);
}
if (!trashMailbox) {
return callback(new Error('Trash mailbox not found for user'));
}
messageHandler.move(
{
user: session.user.id,
// folder to move messages from
source: {
mailbox: session.user.mailbox
},
// folder to move messages to
destination: trashMailbox,
// list of UIDs to move
messages: messages.map(message => message.uid),
// add \Seen flags to deleted messages
markAsSeen: true
},
(err, success, meta) => {
if (err) {
return callback(err);
}
callback(null, (success && meta && meta.destinationUid && meta.destinationUid.length) || 0);
db.database.collection('mailboxes').findOne(
{
user: session.user.id,
specialUse: '\\Trash'
},
(err, trashMailbox) => {
if (err) {
return callback(err);
}
);
});
if (!trashMailbox) {
return callback(new Error('Trash mailbox not found for user'));
}
messageHandler.move(
{
user: session.user.id,
// folder to move messages from
source: {
mailbox: session.user.mailbox
},
// folder to move messages to
destination: trashMailbox,
// list of UIDs to move
messages: messages.map(message => message.uid),
// add \Seen flags to deleted messages
markAsSeen: true
},
(err, success, meta) => {
if (err) {
return callback(err);
}
callback(null, (success && meta && meta.destinationUid && meta.destinationUid.length) || 0);
}
);
}
);
}
function markAsSeen(session, messages, callback) {
let ids = messages.map(message => new ObjectID(message.id));
return db.database.collection('mailboxes').findOneAndUpdate({
_id: session.user.mailbox
}, {
$inc: {
modifyIndex: 1
}
}, {
returnOriginal: false
}, (err, item) => {
if (err) {
return callback(err);
}
let mailboxData = item && item.value;
if (!item) {
return callback(new Error('Mailbox does not exist'));
}
db.database.collection('messages').updateMany({
_id: {
$in: ids
},
user: session.user.id,
mailbox: mailboxData._id,
modseq: {
$lt: mailboxData.modifyIndex
return db.database.collection('mailboxes').findOneAndUpdate(
{
_id: session.user.mailbox
},
{
$inc: {
modifyIndex: 1
}
}, {
$set: {
modseq: mailboxData.modifyIndex,
unseen: false
},
$addToSet: {
flags: '\\Seen'
}
}, {
multi: true,
w: 1
}, err => {
},
{
returnOriginal: false
},
(err, item) => {
if (err) {
return callback(err);
}
messageHandler.notifier.addEntries(
mailboxData,
false,
messages.map(message => {
let result = {
command: 'FETCH',
uid: message.uid,
flags: message.flags.concat('\\Seen'),
message: new ObjectID(message.id),
let mailboxData = item && item.value;
if (!item) {
return callback(new Error('Mailbox does not exist'));
}
db.database.collection('messages').updateMany(
{
_id: {
$in: ids
},
user: session.user.id,
mailbox: mailboxData._id,
modseq: {
$lt: mailboxData.modifyIndex
}
},
{
$set: {
modseq: mailboxData.modifyIndex,
// Indicate that unseen values are changed. Not sure how much though
unseenChange: true
};
return result;
}),
() => {
messageHandler.notifier.fire(mailboxData.user, mailboxData.path);
callback(null, messages.length);
unseen: false
},
$addToSet: {
flags: '\\Seen'
}
},
{
multi: true,
w: 1
},
err => {
if (err) {
return callback(err);
}
messageHandler.notifier.addEntries(
mailboxData,
false,
messages.map(message => {
let result = {
command: 'FETCH',
uid: message.uid,
flags: message.flags.concat('\\Seen'),
message: new ObjectID(message.id),
modseq: mailboxData.modifyIndex,
// Indicate that unseen values are changed. Not sure how much though
unseenChange: true
};
return result;
}),
() => {
messageHandler.notifier.fire(mailboxData.user, mailboxData.path);
callback(null, messages.length);
}
);
}
);
});
});
}
);
}
module.exports = done => {