wildduck/lib/api/dkim.js
NickOvt 905b463a63
fix(api-listings-pagination): ZMS-225 Encode page in next and previous cursor (#818)
* implement a mongopaging wrapper to encode page in the cursor

* update mongopaging validation schema to properly use base64url encoding

* list messages endpoint use embedded page in cursor instead of explicit user-provided

* addresses, auth, certs for listing endpoints use mongopaging wrapper

* dkim, domainaliases, filters - listing endpoints use mongopaging wrapper

* messages, storage, users, webhooks - listing endpoints use mongopaging wrapper

* mongopaging wrapper - if page negative due to externally crafter cursor, default to page 1
2025-05-27 15:39:04 +03:00

520 lines
19 KiB
JavaScript

'use strict';
const config = require('wild-config');
const Joi = require('joi');
const ObjectId = require('mongodb').ObjectId;
const DkimHandler = require('../dkim-handler');
const tools = require('../tools');
const roles = require('../roles');
const { nextPageCursorSchema, previousPageCursorSchema, sessSchema, sessIPSchema } = require('../schemas');
const { successRes, totalRes, pageRes, previousCursorRes, nextCursorRes } = require('../schemas/response/general-schemas');
const { mongopagingFindWrapper } = require('../mongopaging-find-wrapper');
module.exports = (db, server) => {
const dkimHandler = new DkimHandler({
cipher: config.dkim.cipher,
secret: config.dkim.secret,
database: db.database,
redis: db.redis
});
server.get(
{
name: 'getDkimKeys',
path: '/dkim',
tags: ['DKIM'],
summary: 'List registered DKIM keys',
validationObjs: {
requestBody: {},
queryParams: {
query: Joi.string().empty('').trim().max(255).description('Partial match of a Domain name'),
limit: Joi.number().default(20).min(1).max(250).description('How many records to return'),
next: nextPageCursorSchema,
previous: previousPageCursorSchema,
sess: sessSchema,
ip: sessIPSchema
},
pathParams: {},
response: {
200: {
description: 'Success',
model: Joi.object({
success: successRes,
total: totalRes,
page: pageRes,
previousCursor: previousCursorRes,
nextCursor: nextCursorRes,
query: Joi.string().required().description('Query string. Partial match of a Domain name'),
results: Joi.array()
.required()
.items(
Joi.object({
id: Joi.string().required().description('ID of the DKIM'),
domain: Joi.string().required().description('The domain this DKIM key applies to'),
selector: Joi.string().required().description('DKIM selector'),
description: Joi.string().required().description('Key description'),
fingerprint: Joi.string().required().description('Key fingerprint (SHA1)'),
created: Joi.date().required().description('DKIM created datestring')
})
.$_setFlag('objectName', 'GetDkimKeysResult')
.required()
)
.description('DKIM listing')
}).$_setFlag('objectName', 'GetDkimKeysResponse')
}
}
}
},
tools.responseWrapper(async (req, res) => {
res.charSet('utf-8');
const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs;
const schema = Joi.object({
...pathParams,
...queryParams,
...requestBody
});
const result = schema.validate(req.params, {
abortEarly: false,
convert: true,
allowUnknown: true
});
if (result.error) {
res.status(400);
return res.json({
error: result.error.message,
code: 'InputValidationError',
details: tools.validationErrors(result)
});
}
// permissions check
req.validate(roles.can(req.role).readAny('dkim'));
let query = result.value.query;
let limit = result.value.limit;
let pageNext = result.value.next;
let pagePrevious = result.value.previous;
let filter = query
? {
domain: {
$regex: tools.escapeRegexStr(query),
$options: ''
}
}
: {};
let total = await db.database.collection('dkim').countDocuments(filter);
let opts = {
limit,
query: filter,
paginatedField: 'domain',
sortAscending: true
};
if (pageNext) {
opts.next = pageNext;
}
if (pagePrevious) {
opts.previous = pagePrevious;
}
let listingWrapper;
try {
listingWrapper = await mongopagingFindWrapper(db.database.collection('dkim'), opts);
} catch (err) {
res.status(500);
return res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
}
let response = {
success: true,
query,
total,
page: listingWrapper.page,
previousCursor: listingWrapper.previousCursor,
nextCursor: listingWrapper.nextCursor,
results: (listingWrapper.listing.results || []).map(dkimData => ({
id: dkimData._id.toString(),
domain: dkimData.domain,
selector: dkimData.selector,
description: dkimData.description,
fingerprint: dkimData.fingerprint,
created: dkimData.created
}))
};
return res.json(response);
})
);
server.get(
{
path: '/dkim/resolve/:domain',
tags: ['DKIM'],
name: 'resolveDkim',
summary: 'Resolve ID for a DKIM domain',
validationObjs: {
requestBody: {},
queryParams: {
sess: sessSchema,
ip: sessIPSchema
},
pathParams: {
domain: Joi.string()
.max(255)
//.hostname()
.required()
.description('DKIM domain')
},
response: {
200: {
description: 'Success',
model: Joi.object({
success: successRes,
id: Joi.string().required().description('DKIM unique ID (24 byte hex)').example('609d201236d1d936948f23b1')
}).$_setFlag('objectName', 'ResolveIdResponse')
}
}
}
},
tools.responseWrapper(async (req, res) => {
res.charSet('utf-8');
const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs;
const schema = Joi.object({
...pathParams,
...queryParams,
...requestBody
});
const result = schema.validate(req.params, {
abortEarly: false,
convert: true
});
if (result.error) {
res.status(400);
return res.json({
error: result.error.message,
code: 'InputValidationError',
details: tools.validationErrors(result)
});
}
// permissions check
req.validate(roles.can(req.role).readAny('dkim'));
let domain = tools.normalizeDomain(result.value.domain);
let dkimData;
try {
dkimData = await db.database.collection('dkim').findOne(
{
domain
},
{
projection: { _id: 1 }
}
);
} catch (err) {
res.status(500);
return res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
}
if (!dkimData) {
res.status(404);
return res.json({
error: 'This domain does not exist',
code: 'DkimNotFound'
});
}
return res.json({
success: true,
id: dkimData._id.toString()
});
})
);
server.post(
{
path: '/dkim',
tags: ['DKIM'],
summary: 'Create or update DKIM key for domain',
name: 'updateDkimKey',
description: 'Add a new DKIM key for a Domain or update existing one. There can be single DKIM key registered for each domain name.',
validationObjs: {
requestBody: {
domain: Joi.string()
.max(255)
//.hostname()
.required()
.description(
'Domain name this DKIM key applies to. Use "*" as a special value that will be used for domains that do not have their own DKIM key set'
),
selector: Joi.string()
.max(255)
//.hostname()
.trim()
.required()
.description('Selector for the key'),
privateKey: Joi.alternatives()
.try(
Joi.string()
.empty('')
.trim()
.regex(/^-----BEGIN (RSA )?PRIVATE KEY-----/, 'DKIM key format')
.description('PEM format RSA or ED25519 string'),
Joi.string().empty('').trim().base64().length(44).description('Raw ED25519 key 44 bytes long if using base64')
)
.description(
'Pem formatted DKIM private key, raw ED25519 is also allowed. If not set then a new 2048 bit RSA key is generated, beware though that it can take several seconds to complete.'
),
description: Joi.string()
.max(255)
//.hostname()
.trim()
.description('Key description'),
sess: sessSchema,
ip: sessIPSchema
},
queryParams: {},
pathParams: {},
response: {
200: {
description: 'Success',
model: Joi.object({
success: successRes,
id: Joi.string().required().description('ID of the DKIM'),
domain: Joi.string().required().description('The domain this DKIM key applies to'),
selector: Joi.string().required().description('DKIM selector'),
description: Joi.string().required().description('Key description'),
fingerprint: Joi.string().required().description('Key fingerprint (SHA1)'),
publicKey: Joi.string().required().description('Public key in DNS format (no prefix/suffix, single line)'),
dnsTxt: Joi.object({
name: Joi.string().required().description('Is the domain name of TXT'),
value: Joi.string().required().description('Is the value of TXT')
})
.required()
.description('Value for DNS TXT entry')
.$_setFlag('objectName', 'DnsTxt')
}).$_setFlag('objectName', 'UpdateDkimKeyResponse')
}
}
}
},
tools.responseWrapper(async (req, res) => {
res.charSet('utf-8');
const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs;
const schema = Joi.object({
...pathParams,
...queryParams,
...requestBody
});
const result = schema.validate(req.params, {
abortEarly: false,
convert: true
});
if (result.error) {
res.status(400);
return res.json({
error: result.error.message,
code: 'InputValidationError',
details: tools.validationErrors(result)
});
}
// permissions check
req.validate(roles.can(req.role).createAny('dkim'));
let response;
try {
response = await dkimHandler.set(result.value);
} catch (err) {
res.status(err.responseCode || 500);
return res.json({
error: err.message,
code: err.code
});
}
if (response) {
response.success = true;
}
return res.json(response);
})
);
server.get(
{
path: '/dkim/:dkim',
tags: ['DKIM'],
summary: 'Request DKIM information',
name: 'getDkimKey',
validationObjs: {
requestBody: {},
queryParams: {
sess: sessSchema,
ip: sessIPSchema
},
pathParams: {
dkim: Joi.string().hex().lowercase().length(24).required().description('ID of the DKIM')
},
response: {
200: {
description: 'Success',
model: Joi.object({
success: successRes,
id: Joi.string().required().description('ID of the DKIM'),
domain: Joi.string().required().description('The domain this DKIM key applies to'),
selector: Joi.string().required().description('DKIM selector'),
description: Joi.string().required().description('Key description'),
fingerprint: Joi.string().required().description('Key fingerprint (SHA1)'),
publicKey: Joi.string().required().description('Public key in DNS format (no prefix/suffix, single line)'),
dnsTxt: Joi.object({
name: Joi.string().required().description('Is the domain name of TXT'),
value: Joi.string().required().description('Is the value of TXT')
})
.required()
.description('Value for DNS TXT entry')
.$_setFlag('objectName', 'DnsTxt'),
created: Joi.date().required().description('DKIM created datestring')
}).$_setFlag('objectName', 'GetDkimKeyResponse')
}
}
}
},
tools.responseWrapper(async (req, res) => {
res.charSet('utf-8');
const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs;
const schema = Joi.object({
...pathParams,
...queryParams,
...requestBody
});
const result = schema.validate(req.params, {
abortEarly: false,
convert: true
});
if (result.error) {
res.status(400);
return res.json({
error: result.error.message,
code: 'InputValidationError',
details: tools.validationErrors(result)
});
}
// permissions check
req.validate(roles.can(req.role).readAny('dkim'));
let dkim = new ObjectId(result.value.dkim);
let response;
try {
response = await dkimHandler.get({ _id: dkim }, false);
} catch (err) {
res.status(err.responseCode || 500);
return res.json({
error: err.message,
code: err.code
});
}
if (response) {
response.success = true;
}
return res.json(response);
})
);
server.del(
{
path: '/dkim/:dkim',
tags: ['DKIM'],
summary: 'Delete a DKIM key',
name: 'deleteDkimKey',
validationObjs: {
requestBody: {},
queryParams: {
sess: sessSchema,
ip: sessIPSchema
},
pathParams: {
dkim: Joi.string().hex().lowercase().length(24).required().description('ID of the DKIM')
},
response: { 200: { description: 'Success', model: Joi.object({ success: successRes }).$_setFlag('objectName', 'SuccessResponse') } }
}
},
tools.responseWrapper(async (req, res) => {
res.charSet('utf-8');
const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs;
const schema = Joi.object({
...pathParams,
...queryParams,
...requestBody
});
const result = schema.validate(req.params, {
abortEarly: false,
convert: true
});
if (result.error) {
res.status(400);
return res.json({
error: result.error.message,
code: 'InputValidationError',
details: tools.validationErrors(result)
});
}
// permissions check
req.validate(roles.can(req.role).deleteAny('dkim'));
let dkim = new ObjectId(result.value.dkim);
let response;
try {
response = await dkimHandler.del({ _id: dkim });
} catch (err) {
res.status(err.responseCode || 500);
return res.json({
error: err.message,
code: err.code
});
}
return res.json({
success: response
});
})
);
};