mirror of
https://github.com/nodemailer/wildduck.git
synced 2024-09-20 07:16:05 +08:00
Allow using SNI TLS certificates with IMAP/POP3
This commit is contained in:
parent
4edad37888
commit
eec5d66094
2
api.js
2
api.js
|
@ -39,6 +39,7 @@ const submitRoutes = require('./lib/api/submit');
|
|||
const auditRoutes = require('./lib/api/audit');
|
||||
const domainaliasRoutes = require('./lib/api/domainaliases');
|
||||
const dkimRoutes = require('./lib/api/dkim');
|
||||
const certsRoutes = require('./lib/api/certs');
|
||||
const webhooksRoutes = require('./lib/api/webhooks');
|
||||
|
||||
let userHandler;
|
||||
|
@ -500,6 +501,7 @@ module.exports = done => {
|
|||
auditRoutes(db, server, auditHandler);
|
||||
domainaliasRoutes(db, server);
|
||||
dkimRoutes(db, server);
|
||||
certsRoutes(db, server);
|
||||
webhooksRoutes(db, server);
|
||||
|
||||
server.on('error', err => {
|
||||
|
|
|
@ -80,6 +80,11 @@ maxForwards=2000
|
|||
[dkim]
|
||||
# @include "dkim.toml"
|
||||
|
||||
[certs]
|
||||
# Encrypt stored TLS private keys
|
||||
#cipher="aes192"
|
||||
#secret="a secret cat"
|
||||
|
||||
[plugins]
|
||||
# @include "plugins/*.toml"
|
||||
|
||||
|
|
|
@ -66,6 +66,7 @@ secure=true
|
|||
## If certificate path is not defined, use global or built-in self-signed certs
|
||||
#key="/path/to/server/key.pem"
|
||||
#cert="/path/to/server/cert.pem"
|
||||
#dhparam="/path/to/server/dhparam.pem"
|
||||
|
||||
## You can also define extra options for specific TLS settings:
|
||||
|
||||
|
|
|
@ -75,6 +75,13 @@
|
|||
"delete:any": ["*"]
|
||||
},
|
||||
|
||||
"certs": {
|
||||
"create:any": ["*"],
|
||||
"read:any": ["*"],
|
||||
"update:any": ["*"],
|
||||
"delete:any": ["*"]
|
||||
},
|
||||
|
||||
"dkim": {
|
||||
"create:any": ["*"],
|
||||
"read:any": ["*"],
|
||||
|
@ -152,6 +159,13 @@
|
|||
"delete:any": ["*"]
|
||||
},
|
||||
|
||||
"certs": {
|
||||
"create:any": ["*"],
|
||||
"read:any": ["*"],
|
||||
"update:any": ["*"],
|
||||
"delete:any": ["*"]
|
||||
},
|
||||
|
||||
"dkim": {
|
||||
"create:any": ["*"],
|
||||
"read:any": ["*"],
|
||||
|
|
|
@ -2,3 +2,4 @@
|
|||
#key="/path/to/server/key.pem"
|
||||
#ca=["/path/to/server/ca1.pem", "/path/to/server/ca2.pem"]
|
||||
#cert="/path/to/server/cert.pem"
|
||||
#dhparam="/path/to/server/dhparam.pem"
|
|
@ -13,20 +13,28 @@ tags:
|
|||
- name: Addresses
|
||||
- name: ApplicationPasswords
|
||||
- name: Archive
|
||||
description: Archive includes all deleted messages. Once messages are old enough then these are permanenetly deleted from the archive as well. Until then you can restore the deleted messages.
|
||||
- name: Audit
|
||||
description: 'Auditing allows to monitor an email account. All existing, deleted and new emails are copied to the auditing system. See also https://github.com/nodemailer/wildduck-audit-manager'
|
||||
- name: Authentication
|
||||
- name: Autoreplies
|
||||
- name: Certs
|
||||
description: WildDuck allows to register TLS certificates to be used with SNI connections. These certificates are used by IMAP and POP3 servers when a SNI capable client establishes a TLS connection.
|
||||
- name: DKIM
|
||||
description: Whenever an email is sent WildDuck checks if there is a DKIM key registered for the domain name of the sender address and uses it to sign the message.
|
||||
- name: DomainAccess
|
||||
description: Add sender domain names to allowlist (messages are all accepted) or blocklist (messages are sent to Spam folder)
|
||||
- name: DomainAliases
|
||||
- name: Filters
|
||||
- name: Mailboxes
|
||||
- name: Messages
|
||||
- name: Storage
|
||||
description: Storage allows easier attachment handling when composing Draft messages. Instead of uploading the attachmnent with every draft update, you store the attachment to the Storage and then link stored file for the Draft.
|
||||
- name: Submission
|
||||
- name: TwoFactorAuth
|
||||
- name: Users
|
||||
- name: Webhooks
|
||||
|
||||
paths:
|
||||
'/addresses/forwarded/{address}':
|
||||
delete:
|
||||
|
@ -751,6 +759,7 @@ paths:
|
|||
required: true
|
||||
schema:
|
||||
type: string
|
||||
|
||||
'/dkim/{dkim}':
|
||||
delete:
|
||||
tags:
|
||||
|
@ -854,7 +863,7 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ResolveDkimResponse'
|
||||
$ref: '#/components/schemas/ResolveIdResponse'
|
||||
parameters:
|
||||
- name: domain
|
||||
in: path
|
||||
|
@ -862,6 +871,120 @@ paths:
|
|||
required: true
|
||||
schema:
|
||||
type: string
|
||||
|
||||
'/certs/{cert}':
|
||||
delete:
|
||||
tags:
|
||||
- Certs
|
||||
summary: Delete a TLS certificate
|
||||
operationId: deleteTlsCert
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SuccessResponse'
|
||||
get:
|
||||
tags:
|
||||
- Certs
|
||||
summary: Request TLS certificate information
|
||||
operationId: getTLSCerticate
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GetTLSCertResult'
|
||||
parameters:
|
||||
- name: cert
|
||||
in: path
|
||||
description: ID of the TLS certificate
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
/certs:
|
||||
get:
|
||||
tags:
|
||||
- Certs
|
||||
summary: List registered TLS certificates
|
||||
operationId: getTLSCerticates
|
||||
parameters:
|
||||
- name: query
|
||||
in: query
|
||||
description: Partial match of a servername
|
||||
schema:
|
||||
type: string
|
||||
- name: limit
|
||||
in: query
|
||||
description: How many records to return
|
||||
schema:
|
||||
type: number
|
||||
- name: page
|
||||
in: query
|
||||
description: 'Current page number. Informational only, page numbers start from 1'
|
||||
schema:
|
||||
type: number
|
||||
- name: next
|
||||
in: query
|
||||
description: 'Cursor value for next page, retrieved from nextCursor response value'
|
||||
schema:
|
||||
type: number
|
||||
- name: previous
|
||||
in: query
|
||||
description: 'Cursor value for previous page, retrieved from previousCursor response value'
|
||||
schema:
|
||||
type: number
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GetTLSCertsResponse'
|
||||
post:
|
||||
tags:
|
||||
- Certs
|
||||
summary: Create or update TLS certificate for server name
|
||||
description: Add a new TLS certificate for a server name or update existing one. There can be single certificate key registered for each server name.
|
||||
operationId: updateTLSCertificate
|
||||
requestBody:
|
||||
description: Add a new TLS certificate for a server name or update existing one. There can be single certificate key registered for each server name.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateTLSCertRequest'
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateTLSCertResponse'
|
||||
|
||||
'/certs/resolve/{servername}':
|
||||
get:
|
||||
tags:
|
||||
- Certs
|
||||
summary: Resolve ID for a server name
|
||||
operationId: resolveTLSCertificate
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ResolveIdResponse'
|
||||
parameters:
|
||||
- name: servername
|
||||
in: path
|
||||
description: Server name
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
|
||||
'/domainaccess/{domain}':
|
||||
delete:
|
||||
tags:
|
||||
|
@ -1062,7 +1185,7 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ResolveDomainAliasResponse'
|
||||
$ref: '#/components/schemas/ResolveIdResponse'
|
||||
parameters:
|
||||
- name: alias
|
||||
in: path
|
||||
|
@ -2204,7 +2327,7 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ResolveUserResponse'
|
||||
$ref: '#/components/schemas/ResolveIdResponse'
|
||||
parameters:
|
||||
- name: username
|
||||
in: path
|
||||
|
@ -3736,6 +3859,7 @@ components:
|
|||
type: string
|
||||
description: Datestring of the Event time
|
||||
format: date-time
|
||||
|
||||
GetAutoreplyResponse:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -3774,6 +3898,7 @@ components:
|
|||
- text
|
||||
- start
|
||||
- end
|
||||
|
||||
GetDkimKeyResponse:
|
||||
required:
|
||||
- success
|
||||
|
@ -3814,6 +3939,7 @@ components:
|
|||
type: string
|
||||
description: Datestring
|
||||
format: date-time
|
||||
|
||||
GetDkimKeysResponse:
|
||||
required:
|
||||
- success
|
||||
|
@ -3843,7 +3969,39 @@ components:
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/GetDkimKeysResult'
|
||||
description: Aliases listing
|
||||
description: DKIM listing
|
||||
|
||||
GetTLSCertsResponse:
|
||||
required:
|
||||
- success
|
||||
- total
|
||||
- page
|
||||
- previousCursor
|
||||
- nextCursor
|
||||
- results
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
description: Indicates successful response
|
||||
total:
|
||||
type: number
|
||||
description: How many results were found
|
||||
page:
|
||||
type: number
|
||||
description: Current page number. Derived from page query argument
|
||||
previousCursor:
|
||||
type: string
|
||||
description: Either a cursor string or false if there are not any previous results
|
||||
nextCursor:
|
||||
type: string
|
||||
description: Either a cursor string or false if there are not any next results
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/GetTLSCertResult'
|
||||
description: Certificate listing
|
||||
|
||||
UpdateDkimKeyResponse:
|
||||
required:
|
||||
- success
|
||||
|
@ -3879,7 +4037,32 @@ components:
|
|||
description: 'Public key in DNS format (no prefix/suffix, single line)'
|
||||
dnsTxt:
|
||||
$ref: '#/components/schemas/DnsTxt'
|
||||
ResolveDkimResponse:
|
||||
|
||||
UpdateTLSCertResponse:
|
||||
required:
|
||||
- success
|
||||
- id
|
||||
- servername
|
||||
- fingerprint
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
description: Indicates successful response
|
||||
id:
|
||||
type: string
|
||||
description: ID of the certificate
|
||||
domain:
|
||||
type: string
|
||||
description: The server name this certificate applies to
|
||||
description:
|
||||
type: string
|
||||
description: Key description
|
||||
fingerprint:
|
||||
type: string
|
||||
description: Key fingerprint (SHA1)
|
||||
|
||||
ResolveIdResponse:
|
||||
required:
|
||||
- success
|
||||
- id
|
||||
|
@ -3890,7 +4073,8 @@ components:
|
|||
description: Indicates successful response
|
||||
id:
|
||||
type: string
|
||||
description: DKIM unique ID (24 byte hex)
|
||||
description: Unique ID (24 byte hex)
|
||||
|
||||
GetAllowedDomainResponse:
|
||||
required:
|
||||
- success
|
||||
|
@ -3998,6 +4182,7 @@ components:
|
|||
items:
|
||||
$ref: '#/components/schemas/GetDomainAliasesResult'
|
||||
description: Aliases listing
|
||||
|
||||
CreateDomainAliasResponse:
|
||||
required:
|
||||
- success
|
||||
|
@ -4010,18 +4195,7 @@ components:
|
|||
id:
|
||||
type: string
|
||||
description: ID of the Domain Alias
|
||||
ResolveDomainAliasResponse:
|
||||
required:
|
||||
- success
|
||||
- id
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
description: Indicates successful response
|
||||
id:
|
||||
type: string
|
||||
description: Alias unique ID (24 byte hex)
|
||||
|
||||
GetFilterResponse:
|
||||
required:
|
||||
- success
|
||||
|
@ -4513,18 +4687,7 @@ components:
|
|||
suspended:
|
||||
type: boolean
|
||||
description: If true then the user can not authenticate
|
||||
ResolveUserResponse:
|
||||
required:
|
||||
- success
|
||||
- id
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
description: Indicates successful response
|
||||
id:
|
||||
type: string
|
||||
description: Users unique ID (24 byte hex)
|
||||
|
||||
GetUsersResponse:
|
||||
required:
|
||||
- success
|
||||
|
@ -4907,6 +5070,7 @@ components:
|
|||
type: string
|
||||
description: Datestring of the end of the autoreply or boolean false to disable end checks
|
||||
format: date-time
|
||||
|
||||
UpdateDkimKeyRequest:
|
||||
required:
|
||||
- domain
|
||||
|
@ -4925,6 +5089,27 @@ components:
|
|||
privateKey:
|
||||
type: string
|
||||
description: 'Pem formatted DKIM private key. If not set then a new 2048 bit RSA key is generated, beware though that it can take several seconds to complete.'
|
||||
|
||||
UpdateTLSCertRequest:
|
||||
required:
|
||||
- servername
|
||||
- privateKey
|
||||
- cert
|
||||
type: object
|
||||
properties:
|
||||
servername:
|
||||
type: string
|
||||
description: Server name this TLS certificate applies to.
|
||||
cert:
|
||||
type: string
|
||||
description: PEM formatted TLS certificate or a certificate bundle
|
||||
privateKey:
|
||||
type: string
|
||||
description: PEM formatted TLS private key
|
||||
description:
|
||||
type: string
|
||||
description: Certificate description
|
||||
|
||||
CreateAllowedDomainRequest:
|
||||
required:
|
||||
- domain
|
||||
|
@ -5666,6 +5851,7 @@ components:
|
|||
type: string
|
||||
description: Datestring of the Event time
|
||||
format: date-time
|
||||
|
||||
GetDkimKeysResult:
|
||||
required:
|
||||
- id
|
||||
|
@ -5695,6 +5881,33 @@ components:
|
|||
type: string
|
||||
description: Datestring
|
||||
format: date-time
|
||||
|
||||
GetTLSCertResult:
|
||||
required:
|
||||
- id
|
||||
- servername
|
||||
- description
|
||||
- fingerprint
|
||||
- created
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: ID of the certificate
|
||||
domain:
|
||||
type: string
|
||||
description: The server name this certificate applies to
|
||||
description:
|
||||
type: string
|
||||
description: Key description
|
||||
fingerprint:
|
||||
type: string
|
||||
description: Key fingerprint (SHA1)
|
||||
created:
|
||||
type: string
|
||||
description: Datestring
|
||||
format: date-time
|
||||
|
||||
GetAllowedDomainResult:
|
||||
required:
|
||||
- id
|
||||
|
|
|
@ -37,9 +37,22 @@ function upgrade(connection) {
|
|||
secureContext,
|
||||
isServer: true,
|
||||
server: connection._server.server,
|
||||
|
||||
SNICallback: (servername, cb) => {
|
||||
cb(null, connection._server.secureContext.get(connection._server._normalizeHostname(servername)) || connection._server.secureContext.get('*'));
|
||||
// eslint-disable-next-line new-cap
|
||||
connection._server.options.SNICallback(servername, (err, context) => {
|
||||
if (err) {
|
||||
connection._server.logger.error(
|
||||
{
|
||||
tnx: 'sni',
|
||||
servername,
|
||||
err
|
||||
},
|
||||
'Failed to fetch SNI context for servername %s',
|
||||
servername
|
||||
);
|
||||
}
|
||||
return cb(null, context || connection._server.secureContext.get('*'));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -127,12 +127,14 @@ class IMAPConnection extends EventEmitter {
|
|||
this.logger.info(
|
||||
{
|
||||
tnx: 'connect',
|
||||
cid: this.id
|
||||
cid: this.id,
|
||||
servername: this._socket && this._socket.servername
|
||||
},
|
||||
'[%s] %s from %s to %s:%s',
|
||||
'[%s] %s from %s to %s %s:%s',
|
||||
this.id,
|
||||
this.secure ? 'Secure connection' : 'Connection',
|
||||
this.session.clientHostname,
|
||||
(this._socket && this._socket.servername) || os.hostname(),
|
||||
this._socket && this._socket.localAddress,
|
||||
this._socket && this._socket.localPort
|
||||
);
|
||||
|
|
|
@ -233,9 +233,7 @@ class IMAPServer extends EventEmitter {
|
|||
socket.unshift(remainder);
|
||||
}
|
||||
|
||||
let header = Buffer.concat(chunks, chunklen)
|
||||
.toString()
|
||||
.trim();
|
||||
let header = Buffer.concat(chunks, chunklen).toString().trim();
|
||||
|
||||
let params = (header || '').toString().split(' ');
|
||||
let commandName = params.shift().toUpperCase();
|
||||
|
@ -288,7 +286,23 @@ class IMAPServer extends EventEmitter {
|
|||
secureContext: this.secureContext.get('*'),
|
||||
isServer: true,
|
||||
server: this.server,
|
||||
SNICallback: this.options.SNICallback
|
||||
SNICallback: (servername, cb) => {
|
||||
// eslint-disable-next-line new-cap
|
||||
this.options.SNICallback(this._normalizeHostname(servername), (err, context) => {
|
||||
if (err) {
|
||||
this.logger.error(
|
||||
{
|
||||
tnx: 'sni',
|
||||
servername,
|
||||
err
|
||||
},
|
||||
'Failed to fetch SNI context for servername %s',
|
||||
servername
|
||||
);
|
||||
}
|
||||
return cb(null, context || this.secureContext.get('*'));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let remoteAddress = socket.remoteAddress;
|
||||
|
@ -336,6 +350,7 @@ class IMAPServer extends EventEmitter {
|
|||
return onError();
|
||||
}
|
||||
};
|
||||
|
||||
tlsSocket.once('close', onCloseError);
|
||||
tlsSocket.once('error', onError);
|
||||
tlsSocket.once('_tlsError', onError);
|
||||
|
@ -397,7 +412,7 @@ class IMAPServer extends EventEmitter {
|
|||
if (typeof this.options.SNICallback !== 'function') {
|
||||
// create default SNI handler
|
||||
this.options.SNICallback = (servername, cb) => {
|
||||
cb(null, this.secureContext.get(this._normalizeHostname(servername)) || this.secureContext.get('*'));
|
||||
cb(null, this.secureContext.get(servername));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,11 +54,7 @@ const tlsDefaults = {
|
|||
'-----END CERTIFICATE-----',
|
||||
honorCipherOrder: true,
|
||||
requestOCSP: false,
|
||||
sessionIdContext: crypto
|
||||
.createHash('sha1')
|
||||
.update(process.argv.join(' '))
|
||||
.digest('hex')
|
||||
.slice(0, 32)
|
||||
sessionIdContext: crypto.createHash('sha1').update(process.argv.join(' ')).digest('hex').slice(0, 32)
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -68,19 +64,5 @@ const tlsDefaults = {
|
|||
* @returns {Object} Object with mixed TLS values
|
||||
*/
|
||||
function getTLSOptions(opts) {
|
||||
let result = {};
|
||||
|
||||
opts = opts || {};
|
||||
|
||||
Object.keys(opts).forEach(key => {
|
||||
result[key] = opts[key];
|
||||
});
|
||||
|
||||
Object.keys(tlsDefaults).forEach(key => {
|
||||
if (!(key in result)) {
|
||||
result[key] = tlsDefaults[key];
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
return Object.assign({}, tlsDefaults, opts || {});
|
||||
}
|
||||
|
|
9
imap.js
9
imap.js
|
@ -82,7 +82,14 @@ let createInterface = (ifaceOptions, callback) => {
|
|||
|
||||
enableCompression: !!config.imap.enableCompression,
|
||||
|
||||
skipFetchLog: config.log.skipFetchLog
|
||||
skipFetchLog: config.log.skipFetchLog,
|
||||
|
||||
SNICallback(servername, cb) {
|
||||
certs
|
||||
.getContextForServername(servername, serverOptions)
|
||||
.then(context => cb(null, context))
|
||||
.catch(err => cb(err));
|
||||
}
|
||||
};
|
||||
|
||||
certs.loadTLSOptions(serverOptions, 'imap');
|
||||
|
|
14
indexes.yaml
14
indexes.yaml
|
@ -595,6 +595,20 @@ indexes:
|
|||
task: 1
|
||||
user: 1
|
||||
|
||||
- collection: certs
|
||||
index:
|
||||
name: servername
|
||||
unique: true
|
||||
key:
|
||||
servername: 1
|
||||
|
||||
- collection: certs
|
||||
index:
|
||||
name: servername_version
|
||||
key:
|
||||
servername: 1
|
||||
v: 1
|
||||
|
||||
- collection: audits
|
||||
index:
|
||||
name: user_expire_time
|
||||
|
|
385
lib/api/certs.js
Normal file
385
lib/api/certs.js
Normal file
|
@ -0,0 +1,385 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('wild-config');
|
||||
const Joi = require('joi');
|
||||
const MongoPaging = require('mongo-cursor-pagination');
|
||||
const ObjectID = require('mongodb').ObjectID;
|
||||
const CertHandler = require('../cert-handler');
|
||||
const tools = require('../tools');
|
||||
const roles = require('../roles');
|
||||
const { nextPageCursorSchema, previousPageCursorSchema, pageNrSchema, sessSchema, sessIPSchema } = require('../schemas');
|
||||
|
||||
module.exports = (db, server) => {
|
||||
const certHandler = new CertHandler({
|
||||
cipher: config.certs && config.certs.cipher,
|
||||
secret: config.certs && config.certs.secret,
|
||||
database: db.database,
|
||||
redis: db.redis
|
||||
});
|
||||
|
||||
server.get(
|
||||
{ name: 'cert', path: '/certs' },
|
||||
tools.asyncifyJson(async (req, res, next) => {
|
||||
res.charSet('utf-8');
|
||||
|
||||
const schema = Joi.object().keys({
|
||||
query: Joi.string().empty('').trim().max(255),
|
||||
limit: Joi.number().default(20).min(1).max(250),
|
||||
next: nextPageCursorSchema,
|
||||
previous: previousPageCursorSchema,
|
||||
page: pageNrSchema,
|
||||
sess: sessSchema,
|
||||
ip: sessIPSchema
|
||||
});
|
||||
|
||||
const result = schema.validate(req.params, {
|
||||
abortEarly: false,
|
||||
convert: true,
|
||||
allowUnknown: true
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
res.status(400);
|
||||
res.json({
|
||||
error: result.error.message,
|
||||
code: 'InputValidationError',
|
||||
details: tools.validationErrors(result)
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
// permissions check
|
||||
req.validate(roles.can(req.role).readAny('certs'));
|
||||
|
||||
let query = result.value.query;
|
||||
let limit = result.value.limit;
|
||||
let page = result.value.page;
|
||||
let pageNext = result.value.next;
|
||||
let pagePrevious = result.value.previous;
|
||||
|
||||
let filter = query
|
||||
? {
|
||||
servername: {
|
||||
$regex: query.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'),
|
||||
$options: ''
|
||||
}
|
||||
}
|
||||
: {};
|
||||
|
||||
let total = await db.database.collection('certs').countDocuments(filter);
|
||||
|
||||
let opts = {
|
||||
limit,
|
||||
query: filter,
|
||||
paginatedField: 'servername',
|
||||
sortAscending: true
|
||||
};
|
||||
|
||||
if (pageNext) {
|
||||
opts.next = pageNext;
|
||||
} else if ((!page || page > 1) && pagePrevious) {
|
||||
opts.previous = pagePrevious;
|
||||
}
|
||||
|
||||
let listing;
|
||||
try {
|
||||
listing = await MongoPaging.find(db.database.collection('certs'), opts);
|
||||
} catch (err) {
|
||||
res.status(500);
|
||||
res.json({
|
||||
error: 'MongoDB Error: ' + err.message,
|
||||
code: 'InternalDatabaseError'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!listing.hasPrevious) {
|
||||
page = 1;
|
||||
}
|
||||
|
||||
let response = {
|
||||
success: true,
|
||||
query,
|
||||
total,
|
||||
page,
|
||||
previousCursor: listing.hasPrevious ? listing.previous : false,
|
||||
nextCursor: listing.hasNext ? listing.next : false,
|
||||
results: (listing.results || []).map(certData => ({
|
||||
id: certData._id.toString(),
|
||||
servername: certData.servername,
|
||||
description: certData.description,
|
||||
fingerprint: certData.fingerprint,
|
||||
created: certData.created
|
||||
}))
|
||||
};
|
||||
|
||||
res.json(response);
|
||||
return next();
|
||||
})
|
||||
);
|
||||
|
||||
server.get(
|
||||
'/certs/resolve/:servername',
|
||||
tools.asyncifyJson(async (req, res, next) => {
|
||||
res.charSet('utf-8');
|
||||
|
||||
const schema = Joi.object().keys({
|
||||
servername: Joi.string()
|
||||
.max(255)
|
||||
//.hostname()
|
||||
.required(),
|
||||
sess: sessSchema,
|
||||
ip: sessIPSchema
|
||||
});
|
||||
|
||||
const result = schema.validate(req.params, {
|
||||
abortEarly: false,
|
||||
convert: true
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
res.status(400);
|
||||
res.json({
|
||||
error: result.error.message,
|
||||
code: 'InputValidationError',
|
||||
details: tools.validationErrors(result)
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
// permissions check
|
||||
req.validate(roles.can(req.role).readAny('certs'));
|
||||
|
||||
let servername = tools.normalizeDomain(result.value.servername);
|
||||
|
||||
let certData;
|
||||
|
||||
try {
|
||||
certData = await db.database.collection('certs').findOne(
|
||||
{
|
||||
servername
|
||||
},
|
||||
{
|
||||
projection: { _id: 1 }
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
res.status(500);
|
||||
res.json({
|
||||
error: 'MongoDB Error: ' + err.message,
|
||||
code: 'InternalDatabaseError'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!certData) {
|
||||
res.status(404);
|
||||
res.json({
|
||||
error: 'This servername does not exist',
|
||||
code: 'CertNotFound'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
id: certData._id.toString()
|
||||
});
|
||||
|
||||
return next();
|
||||
})
|
||||
);
|
||||
|
||||
server.post(
|
||||
'/certs',
|
||||
tools.asyncifyJson(async (req, res, next) => {
|
||||
res.charSet('utf-8');
|
||||
|
||||
const schema = Joi.object().keys({
|
||||
servername: Joi.string().max(255).hostname().required(),
|
||||
privateKey: Joi.string()
|
||||
.empty('')
|
||||
.trim()
|
||||
.regex(/^-----BEGIN (RSA )?PRIVATE KEY-----/, 'Certificate key format')
|
||||
.required(),
|
||||
cert: Joi.string().empty('').trim().required(),
|
||||
description: Joi.string()
|
||||
.max(255)
|
||||
//.hostname()
|
||||
.trim(),
|
||||
sess: sessSchema,
|
||||
ip: sessIPSchema
|
||||
});
|
||||
|
||||
const result = schema.validate(req.params, {
|
||||
abortEarly: false,
|
||||
convert: true
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
res.status(400);
|
||||
res.json({
|
||||
error: result.error.message,
|
||||
code: 'InputValidationError',
|
||||
details: tools.validationErrors(result)
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
// permissions check
|
||||
req.validate(roles.can(req.role).createAny('certs'));
|
||||
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await certHandler.set(result.value);
|
||||
} catch (err) {
|
||||
switch (err.code) {
|
||||
case 'InputValidationError':
|
||||
res.status(400);
|
||||
break;
|
||||
case 'CertNotFound':
|
||||
res.status(404);
|
||||
break;
|
||||
default:
|
||||
res.status(500);
|
||||
}
|
||||
|
||||
res.json({
|
||||
error: err.message,
|
||||
code: err.code
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
if (response) {
|
||||
response.success = true;
|
||||
}
|
||||
|
||||
res.json(response);
|
||||
return next();
|
||||
})
|
||||
);
|
||||
|
||||
server.get(
|
||||
'/certs/:certs',
|
||||
tools.asyncifyJson(async (req, res, next) => {
|
||||
res.charSet('utf-8');
|
||||
|
||||
const schema = Joi.object().keys({
|
||||
certs: Joi.string().hex().lowercase().length(24).required(),
|
||||
sess: sessSchema,
|
||||
ip: sessIPSchema
|
||||
});
|
||||
|
||||
const result = schema.validate(req.params, {
|
||||
abortEarly: false,
|
||||
convert: true
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
res.status(400);
|
||||
res.json({
|
||||
error: result.error.message,
|
||||
code: 'InputValidationError',
|
||||
details: tools.validationErrors(result)
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
// permissions check
|
||||
req.validate(roles.can(req.role).readAny('certs'));
|
||||
|
||||
let certs = new ObjectID(result.value.certs);
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await certHandler.get({ _id: certs }, false);
|
||||
} catch (err) {
|
||||
switch (err.code) {
|
||||
case 'InputValidationError':
|
||||
res.status(400);
|
||||
break;
|
||||
case 'CertNotFound':
|
||||
res.status(404);
|
||||
break;
|
||||
default:
|
||||
res.status(500);
|
||||
}
|
||||
res.json({
|
||||
error: err.message,
|
||||
code: err.code
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
if (response) {
|
||||
response.success = true;
|
||||
}
|
||||
|
||||
res.json(response);
|
||||
return next();
|
||||
})
|
||||
);
|
||||
|
||||
server.del(
|
||||
'/certs/:certs',
|
||||
tools.asyncifyJson(async (req, res, next) => {
|
||||
res.charSet('utf-8');
|
||||
|
||||
const schema = Joi.object().keys({
|
||||
certs: Joi.string().hex().lowercase().length(24).required(),
|
||||
sess: sessSchema,
|
||||
ip: sessIPSchema
|
||||
});
|
||||
|
||||
const result = schema.validate(req.params, {
|
||||
abortEarly: false,
|
||||
convert: true
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
res.status(400);
|
||||
res.json({
|
||||
error: result.error.message,
|
||||
code: 'InputValidationError',
|
||||
details: tools.validationErrors(result)
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
// permissions check
|
||||
req.validate(roles.can(req.role).deleteAny('certs'));
|
||||
|
||||
let certs = new ObjectID(result.value.certs);
|
||||
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await certHandler.del({ _id: certs });
|
||||
} catch (err) {
|
||||
switch (err.code) {
|
||||
case 'InputValidationError':
|
||||
res.status(400);
|
||||
break;
|
||||
case 'CertNotFound':
|
||||
res.status(404);
|
||||
break;
|
||||
default:
|
||||
res.status(500);
|
||||
}
|
||||
res.json({
|
||||
error: err.message,
|
||||
code: err.code
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: response
|
||||
});
|
||||
|
||||
return next();
|
||||
})
|
||||
);
|
||||
};
|
234
lib/cert-handler.js
Normal file
234
lib/cert-handler.js
Normal file
|
@ -0,0 +1,234 @@
|
|||
'use strict';
|
||||
|
||||
const ObjectID = require('mongodb').ObjectID;
|
||||
const fingerprint = require('key-fingerprint').fingerprint;
|
||||
const crypto = require('crypto');
|
||||
const tls = require('tls');
|
||||
const tools = require('./tools');
|
||||
const { publish, CERT_CREATED, CERT_UPDATED, CERT_DELETED } = require('./events');
|
||||
|
||||
class CertHandler {
|
||||
constructor(options) {
|
||||
options = options || {};
|
||||
this.cipher = options.cipher;
|
||||
this.secret = options.secret;
|
||||
|
||||
this.database = options.database;
|
||||
this.redis = options.redis;
|
||||
|
||||
this.loggelf = options.loggelf || (() => false);
|
||||
}
|
||||
|
||||
async set(options) {
|
||||
const servername = tools.normalizeDomain(options.servername);
|
||||
const description = options.description;
|
||||
|
||||
let privateKey = options.privateKey;
|
||||
const cert = options.cert;
|
||||
|
||||
let fp;
|
||||
try {
|
||||
fp = fingerprint(privateKey, 'sha256', true);
|
||||
} catch (E) {
|
||||
let err = new Error('Invalid or incompatible private key. ' + E.message);
|
||||
err.code = 'InputValidationError';
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (this.secret) {
|
||||
try {
|
||||
let cipher = crypto.createCipher(this.cipher || 'aes192', this.secret);
|
||||
privateKey = '$' + cipher.update(privateKey, 'utf8', 'hex');
|
||||
privateKey += cipher.final('hex');
|
||||
} catch (E) {
|
||||
let err = new Error('Failed to encrypt private key. ' + E.message);
|
||||
err.code = 'InternalConfigError';
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
let certData = {
|
||||
privateKey,
|
||||
cert,
|
||||
fingerprint: fp,
|
||||
updated: new Date()
|
||||
};
|
||||
|
||||
if (description) {
|
||||
certData.description = description;
|
||||
}
|
||||
|
||||
try {
|
||||
// should fail on invalid input
|
||||
tls.createSecureContext({
|
||||
key: privateKey,
|
||||
cert
|
||||
});
|
||||
} catch (E) {
|
||||
let err = new Error('Invalid or incompatible key and certificate. ' + E.message);
|
||||
err.code = 'InputValidationError';
|
||||
throw err;
|
||||
}
|
||||
|
||||
let r;
|
||||
try {
|
||||
r = await this.database.collection('certs').findOneAndUpdate(
|
||||
{
|
||||
servername
|
||||
},
|
||||
{ $set: certData, $inc: { v: 1 }, $setOnInsert: { servername, created: new Date() } },
|
||||
{
|
||||
upsert: true,
|
||||
returnOriginal: false
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
if (err) {
|
||||
err.code = 'InternalDatabaseError';
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
if (!r.value) {
|
||||
let err = new Error('Failed to insert Cert key');
|
||||
err.code = 'InternalDatabaseError';
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (this.redis) {
|
||||
try {
|
||||
if (r.lastErrorObject.upserted) {
|
||||
await publish(this.redis, {
|
||||
ev: CERT_CREATED,
|
||||
cert: r.value._id.toString(),
|
||||
servername,
|
||||
fingerprint: fp
|
||||
});
|
||||
} else if (r.lastErrorObject.updatedExisting) {
|
||||
await publish(this.redis, {
|
||||
ev: CERT_UPDATED,
|
||||
cert: r.value._id.toString(),
|
||||
servername,
|
||||
fingerprint: fp
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore?
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: r.value._id.toString(),
|
||||
servername: certData.servername,
|
||||
description: certData.description,
|
||||
fingerprint: certData.fingerprint
|
||||
};
|
||||
}
|
||||
|
||||
async get(options, includePrivateKey) {
|
||||
let query = {};
|
||||
options = options || {};
|
||||
|
||||
if (options.servername) {
|
||||
query.servername = tools.normalizeDomain(options.servername);
|
||||
} else if (options._id && tools.isId(options._id)) {
|
||||
query._id = new ObjectID(options._id);
|
||||
} else {
|
||||
let err = new Error('Invalid or unknown cert');
|
||||
err.code = 'CertNotFound';
|
||||
throw err;
|
||||
}
|
||||
|
||||
let certData;
|
||||
try {
|
||||
certData = await this.database.collection('certs').findOne(query);
|
||||
} catch (err) {
|
||||
err.code = 'InternalDatabaseError';
|
||||
throw err;
|
||||
}
|
||||
if (!certData) {
|
||||
let err = new Error('Invalid or unknown cert');
|
||||
err.code = 'CertNotFound';
|
||||
throw err;
|
||||
}
|
||||
|
||||
let privateKey;
|
||||
if (includePrivateKey) {
|
||||
privateKey = this.decodeKey(certData.privateKey);
|
||||
}
|
||||
|
||||
return {
|
||||
id: certData._id.toString(),
|
||||
servername: certData.servername,
|
||||
description: certData.description,
|
||||
fingerprint: certData.fingerprint,
|
||||
privateKey,
|
||||
created: certData.created
|
||||
};
|
||||
}
|
||||
|
||||
decodeKey(privateKey) {
|
||||
if (privateKey.charAt(0) === '$') {
|
||||
if (this.secret) {
|
||||
try {
|
||||
let decipher = crypto.createDecipher(this.cipher || 'aes192', this.secret);
|
||||
privateKey = decipher.update(privateKey.substr(1), 'hex', 'utf-8');
|
||||
privateKey += decipher.final('utf8');
|
||||
} catch (E) {
|
||||
let err = new Error('Failed to decrypt private key. ' + E.message);
|
||||
err.code = 'InternalConfigError';
|
||||
throw err;
|
||||
}
|
||||
} else {
|
||||
let err = new Error('Can not use decrypted key');
|
||||
err.code = 'InternalConfigError';
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
async del(options) {
|
||||
let query = {};
|
||||
|
||||
if (options.servername) {
|
||||
query.servername = tools.normalizeDomain(options.servername);
|
||||
} else if (options._id && tools.isId(options._id)) {
|
||||
query._id = new ObjectID(options._id);
|
||||
} else {
|
||||
let err = new Error('Invalid or unknown cert');
|
||||
err.code = 'CertNotFound';
|
||||
throw err;
|
||||
}
|
||||
|
||||
// delete cert key from database
|
||||
let r;
|
||||
try {
|
||||
r = await this.database.collection('certs').findOneAndDelete(query);
|
||||
} catch (err) {
|
||||
err.code = 'InternalDatabaseError';
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!r.value) {
|
||||
let err = new Error('Invalid or unknown cert');
|
||||
err.code = 'CertNotFound';
|
||||
throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
await publish(this.redis, {
|
||||
ev: CERT_DELETED,
|
||||
cert: r.value._id,
|
||||
servername: r.value.servername,
|
||||
fingerprint: r.value.fingerprint
|
||||
});
|
||||
} catch (err) {
|
||||
// ignore?
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CertHandler;
|
73
lib/certs.js
73
lib/certs.js
|
@ -2,9 +2,16 @@
|
|||
|
||||
const config = require('wild-config');
|
||||
const fs = require('fs');
|
||||
const tls = require('tls');
|
||||
const db = require('./db');
|
||||
const tlsOptions = require('../imap-core/lib/tls-options');
|
||||
const CertHandler = require('./cert-handler');
|
||||
|
||||
const certs = new Map();
|
||||
const servers = [];
|
||||
const ctxCache = new Map();
|
||||
|
||||
let certHandler;
|
||||
|
||||
module.exports.reload = () => {
|
||||
// load certificate files
|
||||
|
@ -31,7 +38,7 @@ module.exports.reload = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
let key, cert, ca;
|
||||
let key, cert, ca, dhparam;
|
||||
|
||||
if (tlsconf.key) {
|
||||
key = fs.readFileSync(tlsconf.key, 'ascii');
|
||||
|
@ -45,6 +52,10 @@ module.exports.reload = () => {
|
|||
cert = fs.readFileSync(tlsconf.cert, 'ascii');
|
||||
}
|
||||
|
||||
if (tlsconf.dhparam) {
|
||||
dhparam = fs.readFileSync(tlsconf.dhparam, 'ascii');
|
||||
}
|
||||
|
||||
if (tlsconf.ca) {
|
||||
ca = [].concat(tlsconf.ca || []).map(ca => fs.readFileSync(ca, 'ascii'));
|
||||
if (!ca.length) {
|
||||
|
@ -55,7 +66,8 @@ module.exports.reload = () => {
|
|||
certs.set(type || 'default', {
|
||||
key,
|
||||
cert,
|
||||
ca
|
||||
ca,
|
||||
dhparam
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -74,19 +86,25 @@ module.exports.get = type => (certs.has(type) ? certs.get(type) : certs.get('def
|
|||
|
||||
module.exports.loadTLSOptions = (serverOptions, name) => {
|
||||
Object.keys(config[name].tls || {}).forEach(key => {
|
||||
if (!['key', 'cert', 'ca'].includes(key)) {
|
||||
if (!['key', 'cert', 'ca', 'dhparam'].includes(key)) {
|
||||
serverOptions[key] = config[name].tls[key];
|
||||
}
|
||||
});
|
||||
|
||||
let serverCerts = certs.get(name);
|
||||
let serverCerts = module.exports.get(name);
|
||||
|
||||
if (serverCerts) {
|
||||
serverOptions.key = serverCerts.key;
|
||||
|
||||
if (serverCerts.ca) {
|
||||
serverOptions.ca = serverCerts.ca;
|
||||
}
|
||||
|
||||
serverOptions.cert = serverCerts.cert;
|
||||
|
||||
if (serverCerts.dhparam) {
|
||||
serverOptions.dhparam = serverCerts.dhparam;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -94,6 +112,50 @@ module.exports.registerReload = (server, name) => {
|
|||
servers.push({ server, name });
|
||||
};
|
||||
|
||||
module.exports.getContextForServername = async (servername, serverOptions) => {
|
||||
const query = { servername };
|
||||
let cachedContext = false;
|
||||
if (ctxCache.has(servername)) {
|
||||
cachedContext = ctxCache.get(servername);
|
||||
if (cachedContext.entry && cachedContext.entry.v) {
|
||||
// check for updates
|
||||
query.v = { $ne: cachedContext.entry.v };
|
||||
}
|
||||
}
|
||||
|
||||
const certData = await db.database.collection('certs').findOne(query);
|
||||
if (!certData) {
|
||||
return (cachedContext && cachedContext.context) || false;
|
||||
}
|
||||
|
||||
if (!certHandler) {
|
||||
certHandler = new CertHandler({
|
||||
cipher: config.certs && config.certs.cipher,
|
||||
secret: config.certs && config.certs.secret,
|
||||
database: db.database,
|
||||
redis: db.redis
|
||||
});
|
||||
}
|
||||
|
||||
// key might be encrypted
|
||||
let privateKey = certHandler.decodeKey(certData.privateKey);
|
||||
|
||||
let serviceCtxOpts = { key: privateKey, cert: certData.cert };
|
||||
for (let key of ['dhparam']) {
|
||||
if (serverOptions[key]) {
|
||||
serviceCtxOpts[key] = serverOptions[key];
|
||||
}
|
||||
}
|
||||
|
||||
let ctxOptions = tlsOptions(serviceCtxOpts);
|
||||
|
||||
let context = tls.createSecureContext(ctxOptions);
|
||||
|
||||
ctxCache.set(servername, { entry: certData, context });
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
config.on('reload', () => {
|
||||
module.exports.reload();
|
||||
servers.forEach(entry => {
|
||||
|
@ -105,6 +167,9 @@ config.on('reload', () => {
|
|||
certOptions.ca = serverCerts.ca;
|
||||
}
|
||||
certOptions.cert = serverCerts.cert;
|
||||
if (serverCerts.dhparam) {
|
||||
certOptions.dhparam = serverCerts.dhparam;
|
||||
}
|
||||
entry.server.updateSecureContext(certOptions);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -11,6 +11,9 @@ module.exports = {
|
|||
DKIM_CREATED: 'dkim.created',
|
||||
DKIM_UPDATED: 'dkim.updated',
|
||||
DKIM_DELETED: 'dkim.deleted',
|
||||
CERT_CREATED: 'cert.created',
|
||||
CERT_UPDATED: 'cert.updated',
|
||||
CERT_DELETED: 'cert.deleted',
|
||||
DOMAINALIAS_CREATED: 'domainalias.created',
|
||||
DOMAINALIAS_DELETED: 'domainalias.deleted',
|
||||
ADDRESS_USER_CREATED: 'address.user.created',
|
||||
|
|
|
@ -5,6 +5,7 @@ const EventEmitter = require('events');
|
|||
const base32 = require('base32.js');
|
||||
const packageData = require('../../package.json');
|
||||
const DataStream = require('nodemailer/lib/smtp-connection/data-stream');
|
||||
const os = require('os');
|
||||
|
||||
const SOCKET_TIMEOUT = 60 * 1000;
|
||||
|
||||
|
@ -52,8 +53,11 @@ class POP3Connection extends EventEmitter {
|
|||
cid: this.id,
|
||||
host: this.remoteAddress
|
||||
},
|
||||
'Connection from %s',
|
||||
this.remoteAddress
|
||||
'Connection from %s to %s %s:%s',
|
||||
this.remoteAddress,
|
||||
(this._socket && this._socket.servername) || os.hostname(),
|
||||
this._socket && this._socket.localAddress,
|
||||
this._socket && this._socket.localPort
|
||||
);
|
||||
this.send(
|
||||
'+OK ' +
|
||||
|
|
|
@ -78,7 +78,23 @@ class POP3Server extends EventEmitter {
|
|||
secureContext: this.secureContext.get('*'),
|
||||
isServer: true,
|
||||
server: this.server,
|
||||
SNICallback: this.options.SNICallback
|
||||
SNICallback: (servername, cb) => {
|
||||
// eslint-disable-next-line new-cap
|
||||
this.options.SNICallback(this._normalizeHostname(servername), (err, context) => {
|
||||
if (err) {
|
||||
this.logger.error(
|
||||
{
|
||||
tnx: 'sni',
|
||||
servername,
|
||||
err
|
||||
},
|
||||
'Failed to fetch SNI context for servername %s',
|
||||
servername
|
||||
);
|
||||
}
|
||||
return cb(null, context || this.secureContext.get('*'));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let remoteAddress = socket.remoteAddress;
|
||||
|
@ -187,7 +203,7 @@ class POP3Server extends EventEmitter {
|
|||
if (typeof this.options.SNICallback !== 'function') {
|
||||
// create default SNI handler
|
||||
this.options.SNICallback = (servername, cb) => {
|
||||
cb(null, this.secureContext.get(this._normalizeHostname(servername)) || this.secureContext.get('*'));
|
||||
cb(null, this.secureContext.get(servername));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,10 +16,10 @@
|
|||
"author": "Andris Reinman",
|
||||
"license": "EUPL-1.2",
|
||||
"devDependencies": {
|
||||
"ajv": "8.2.0",
|
||||
"ajv": "8.3.0",
|
||||
"chai": "4.3.4",
|
||||
"docsify-cli": "4.4.3",
|
||||
"eslint": "7.25.0",
|
||||
"eslint": "7.26.0",
|
||||
"eslint-config-nodemailer": "1.2.0",
|
||||
"eslint-config-prettier": "8.3.0",
|
||||
"grunt": "1.4.0",
|
||||
|
@ -30,7 +30,7 @@
|
|||
"grunt-wait": "0.3.0",
|
||||
"imapflow": "1.0.57",
|
||||
"mailparser": "3.2.0",
|
||||
"mocha": "8.3.2",
|
||||
"mocha": "8.4.0",
|
||||
"request": "2.88.2",
|
||||
"supertest": "6.1.3"
|
||||
},
|
||||
|
@ -41,7 +41,7 @@
|
|||
"axios": "0.21.1",
|
||||
"base32.js": "0.1.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"bull": "3.22.4",
|
||||
"bull": "3.22.5",
|
||||
"gelf": "2.0.1",
|
||||
"generate-password": "1.6.0",
|
||||
"he": "1.2.0",
|
||||
|
|
7
pop3.js
7
pop3.js
|
@ -39,6 +39,13 @@ const serverOptions = {
|
|||
version: config.pop3.version || packageData.version
|
||||
},
|
||||
|
||||
SNICallback(servername, cb) {
|
||||
certs
|
||||
.getContextForServername(servername, serverOptions)
|
||||
.then(context => cb(null, context))
|
||||
.catch(err => cb(err));
|
||||
},
|
||||
|
||||
// log to console
|
||||
logger: {
|
||||
info(...args) {
|
||||
|
|
Loading…
Reference in a new issue