mirror of
https://github.com/nodemailer/wildduck.git
synced 2024-11-11 01:54:04 +08:00
276 lines
8.2 KiB
JavaScript
276 lines
8.2 KiB
JavaScript
'use strict';
|
|
|
|
const config = require('wild-config');
|
|
const restify = require('restify');
|
|
const log = require('npmlog');
|
|
const logger = require('restify-logger');
|
|
const UserHandler = require('./lib/user-handler');
|
|
const MailboxHandler = require('./lib/mailbox-handler');
|
|
const MessageHandler = require('./lib/message-handler');
|
|
const ImapNotifier = require('./lib/imap-notifier');
|
|
const db = require('./lib/db');
|
|
const certs = require('./lib/certs');
|
|
const tools = require('./lib/tools');
|
|
const crypto = require('crypto');
|
|
|
|
const usersRoutes = require('./lib/api/users');
|
|
const addressesRoutes = require('./lib/api/addresses');
|
|
const mailboxesRoutes = require('./lib/api/mailboxes');
|
|
const messagesRoutes = require('./lib/api/messages');
|
|
const filtersRoutes = require('./lib/api/filters');
|
|
const aspsRoutes = require('./lib/api/asps');
|
|
const totpRoutes = require('./lib/api/2fa/totp');
|
|
const custom2faRoutes = require('./lib/api/2fa/custom');
|
|
const u2fRoutes = require('./lib/api/2fa/u2f');
|
|
const updatesRoutes = require('./lib/api/updates');
|
|
const authRoutes = require('./lib/api/auth');
|
|
const autoreplyRoutes = require('./lib/api/autoreply');
|
|
const submitRoutes = require('./lib/api/submit');
|
|
const domainaliasRoutes = require('./lib/api/domainaliases');
|
|
const dkimRoutes = require('./lib/api/dkim');
|
|
|
|
const serverOptions = {
|
|
name: 'WildDuck API',
|
|
strictRouting: true,
|
|
formatters: {
|
|
'application/json; q=0.4': (req, res, body) => {
|
|
let data = body ? JSON.stringify(body, false, 2) + '\n' : 'null';
|
|
res.setHeader('Content-Length', Buffer.byteLength(data));
|
|
return data;
|
|
}
|
|
}
|
|
};
|
|
|
|
let certOptions = {};
|
|
certs.loadTLSOptions(certOptions, 'api');
|
|
|
|
if (config.api.secure && certOptions.key) {
|
|
serverOptions.key = certOptions.key;
|
|
if (certOptions.ca) {
|
|
serverOptions.ca = certOptions.ca;
|
|
}
|
|
serverOptions.certificate = certOptions.cert;
|
|
}
|
|
|
|
const server = restify.createServer(serverOptions);
|
|
|
|
let userHandler;
|
|
let mailboxHandler;
|
|
let messageHandler;
|
|
let notifier;
|
|
|
|
// disable compression for EventSource response
|
|
// this needs to be called before gzipResponse
|
|
server.use((req, res, next) => {
|
|
if (req.route.path === '/users/:user/updates') {
|
|
req.headers['accept-encoding'] = '';
|
|
}
|
|
next();
|
|
});
|
|
|
|
server.use(restify.plugins.gzipResponse());
|
|
|
|
server.use(restify.plugins.queryParser());
|
|
server.use(
|
|
restify.plugins.bodyParser({
|
|
maxBodySize: 0,
|
|
mapParams: true,
|
|
mapFiles: false,
|
|
overrideParams: false
|
|
})
|
|
);
|
|
|
|
server.use(
|
|
tools.asyncifyJson(async (req, res, next) => {
|
|
let accessToken = req.query.accessToken || req.headers['x-access-token'] || false;
|
|
|
|
if (req.query.accessToken) {
|
|
req.query.accessToken = '';
|
|
}
|
|
|
|
if (req.headers['x-access-token']) {
|
|
req.headers['x-access-token'] = '';
|
|
}
|
|
|
|
let tokenRequired = false;
|
|
|
|
let fail = () => {
|
|
res.status(403);
|
|
res.charSet('utf-8');
|
|
return res.json({
|
|
error: 'Invalid accessToken value',
|
|
code: 'InvalidToken'
|
|
});
|
|
};
|
|
|
|
req.validate = permission => {
|
|
if (!permission.granted) {
|
|
let err = new Error('Not enough privileges');
|
|
err.responseCode = 403;
|
|
err.code = 'MissingPrivileges';
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
// hard coded master token
|
|
if (config.api.accessToken) {
|
|
tokenRequired = true;
|
|
if (config.api.accessToken === accessToken) {
|
|
req.role = 'root';
|
|
req.user = 'root';
|
|
return next();
|
|
}
|
|
}
|
|
|
|
if (config.api.accessControl.enabled) {
|
|
tokenRequired = true;
|
|
if (accessToken && accessToken.length === 40 && /^[a-fA-F0-9]{40}$/.test(accessToken)) {
|
|
let tokenData;
|
|
let tokenHash = crypto
|
|
.createHash('sha256')
|
|
.update(accessToken)
|
|
.digest('hex');
|
|
|
|
try {
|
|
let key = 'tn:token:' + tokenHash;
|
|
tokenData = await db.redis.hgetall(key);
|
|
} catch (err) {
|
|
err.responseCode = 500;
|
|
err.code = 'InternalDatabaseError';
|
|
throw err;
|
|
}
|
|
|
|
if (tokenData && tokenData.user && tokenData.role && config.api.roles[tokenData.role]) {
|
|
let signature = crypto
|
|
.createHmac('sha256', config.api.accessControl.secret)
|
|
.update(
|
|
JSON.stringify({
|
|
token: accessToken,
|
|
user: tokenData.user,
|
|
role: tokenData.role
|
|
})
|
|
)
|
|
.digest('hex');
|
|
|
|
if (signature !== tokenData.s) {
|
|
// rogue token
|
|
try {
|
|
await db.redis
|
|
.multi()
|
|
.del('tn:token:' + tokenHash)
|
|
.srem('tn:user:' + tokenData.user, tokenHash)
|
|
.exec();
|
|
} catch (err) {
|
|
// ignore
|
|
}
|
|
} else {
|
|
req.role = tokenData.role;
|
|
req.user = tokenData.user;
|
|
}
|
|
return next();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (tokenRequired) {
|
|
// no valid token found
|
|
return fail();
|
|
}
|
|
|
|
// allow all
|
|
req.role = 'root';
|
|
req.user = 'root';
|
|
next();
|
|
})
|
|
);
|
|
|
|
logger.token('user', req => (req.user && req.user.toString()) || '-');
|
|
logger.token('url', req => {
|
|
if (/\baccessToken=/.test(req.url)) {
|
|
return req.url.replace(/\baccessToken=[^&]+/g, 'accessToken=' + 'x'.repeat(6));
|
|
}
|
|
return req.url;
|
|
});
|
|
|
|
server.use(
|
|
logger(':remote-addr :user :method :url :status :time-spent :append', {
|
|
stream: {
|
|
write: message => {
|
|
message = (message || '').toString();
|
|
if (message) {
|
|
log.http('API', message.replace('\n', '').trim());
|
|
}
|
|
}
|
|
}
|
|
})
|
|
);
|
|
|
|
module.exports = done => {
|
|
if (!config.api.enabled) {
|
|
return setImmediate(() => done(null, false));
|
|
}
|
|
|
|
let started = false;
|
|
|
|
notifier = new ImapNotifier({
|
|
database: db.database,
|
|
redis: db.redis
|
|
});
|
|
|
|
messageHandler = new MessageHandler({
|
|
database: db.database,
|
|
users: db.users,
|
|
redis: db.redis,
|
|
gridfs: db.gridfs,
|
|
attachments: config.attachments
|
|
});
|
|
|
|
userHandler = new UserHandler({
|
|
database: db.database,
|
|
users: db.users,
|
|
redis: db.redis,
|
|
messageHandler,
|
|
authlogExpireDays: config.log.authlogExpireDays
|
|
});
|
|
|
|
mailboxHandler = new MailboxHandler({
|
|
database: db.database,
|
|
users: db.users,
|
|
redis: db.redis,
|
|
notifier
|
|
});
|
|
|
|
usersRoutes(db, server, userHandler);
|
|
addressesRoutes(db, server);
|
|
mailboxesRoutes(db, server, mailboxHandler);
|
|
messagesRoutes(db, server, messageHandler);
|
|
filtersRoutes(db, server);
|
|
aspsRoutes(db, server, userHandler);
|
|
totpRoutes(db, server, userHandler);
|
|
custom2faRoutes(db, server, userHandler);
|
|
u2fRoutes(db, server, userHandler);
|
|
updatesRoutes(db, server, notifier);
|
|
authRoutes(db, server, userHandler);
|
|
autoreplyRoutes(db, server);
|
|
submitRoutes(db, server, messageHandler, userHandler);
|
|
domainaliasRoutes(db, server);
|
|
dkimRoutes(db, server);
|
|
|
|
server.on('error', err => {
|
|
if (!started) {
|
|
started = true;
|
|
return done(err);
|
|
}
|
|
|
|
log.error('API', err);
|
|
});
|
|
|
|
server.listen(config.api.port, config.api.host, () => {
|
|
if (started) {
|
|
return server.close();
|
|
}
|
|
started = true;
|
|
log.info('API', 'Server listening on %s:%s', config.api.host || '0.0.0.0', config.api.port);
|
|
done(null, server);
|
|
});
|
|
};
|