libas2
Version:
Implementation of the AS2 protocol as presented in RFC 4130 and related RFCs
261 lines (260 loc) • 11.3 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.AS2Disposition = void 0;
const AS2MimeNode_1 = require("../AS2MimeNode");
const Helpers_1 = require("../Helpers");
const AS2DispositionNotification_1 = require("./AS2DispositionNotification");
const Constants_1 = require("../Constants");
const AS2Crypto_1 = require("../AS2Crypto");
const { AS2_VERSION, EXPLANATION, ERROR, STANDARD_HEADER } = Constants_1.AS2Constants;
/** Options for composing a message disposition notification (MDN).
* @typedef {object} AS2DispositionOptions
* @property {string} explanation
* @property {AS2DispositionNotification} notification
* @property {AS2MimeNode|boolean} [returned]
*/
/** Options for generating an outgoing MDN.
* @typedef {object} OutgoingDispositionOptions
* @property {AS2MimeNode} node - The mime node to verify and/or decrypt; used construct the outgoing disposition.
* @property {AgreementOptions} agreement - The partner agreement to use when sending the outgoing disposition.
* @property {boolean} [returnNode] - Whether to attach the mime node to the disposition as the returned payload.
*/
const toNotification = function toNotification(key, value) {
let result = {};
const parts = value.split(/;/gu).map(part => part.trim());
const newKey = (str) => str
.toLowerCase()
.split('-')
.map((chars, index) => index === 0 ? chars.toLowerCase() : chars.charAt(0).toUpperCase() + chars.toLowerCase().substring(1))
.join('');
switch (key.toLowerCase()) {
case 'reporting-ua':
case 'mdn-gateway':
case 'original-message-id':
result = value;
key = newKey(key);
break;
case 'original-recipient':
case 'final-recipient':
result.value = parts.slice(1).join('; ');
result.type = parts[0];
key = newKey(key);
break;
case 'disposition':
const [type, action] = parts[0].split('/');
result.value = action;
result.type = type;
for (const part of parts.slice(1)) {
let index = part.indexOf('=');
if (index === -1)
index = part.length;
let partKey = part
.slice(0, index)
.trim()
.toLowerCase();
let partValue = part.slice(index + 1).trim();
if (partKey.startsWith('processed') || partKey.startsWith('failed')) {
let [attrKey, attrProp] = partKey.split('/');
result.processed = attrKey === 'processed';
if (attrProp !== undefined) {
result.description = {
type: attrProp.toLowerCase(),
text: partValue
};
}
continue;
}
if (result.attributes === undefined)
result.attributes = {};
if (result.attributes[partKey] === undefined) {
result.attributes[partKey] = partValue || true;
}
}
key = newKey(key);
break;
case 'received-content-mic':
const [micValue, micalg] = value.split(',').map(val => val.trim());
result.mic = micValue;
result.algorithm = micalg.toLowerCase();
key = newKey(key);
break;
default:
result[key] = value;
key = 'headers';
break;
}
return [key, result];
};
/** Class for describing and constructing a Message Disposition Notification. */
class AS2Disposition {
constructor(mdn) {
if (mdn instanceof AS2MimeNode_1.AS2MimeNode) {
// Always get the Message ID of the root node; enveloped MDNs may not have this value on child nodes.
const messageId = mdn.messageId();
// Travel mime node tree for content type multipart/report.
mdn = Helpers_1.getReportNode(mdn);
// https://tools.ietf.org/html/rfc3462
if (mdn) {
this.messageId = messageId;
// Get the human-readable message, the first part of the report.
this.explanation = mdn.childNodes[0].content.toString('utf8').trim();
// Get the message/disposition-notification and parse, which is the second part.
this.notification = new AS2DispositionNotification_1.AS2DispositionNotification(Helpers_1.parseHeaderString(mdn.childNodes[1].content.toString('utf8'), toNotification), 'incoming');
// Get the optional thid part, if present; it is the returned message content.
this.returned = mdn.childNodes[2];
}
}
else if (mdn.explanation && mdn.notification) {
this.explanation = mdn.explanation;
this.notification =
mdn.notification instanceof AS2DispositionNotification_1.AS2DispositionNotification
? mdn.notification
: new AS2DispositionNotification_1.AS2DispositionNotification(mdn.notification);
this.returned = typeof mdn.returned === 'boolean' ? undefined : mdn.returned;
this.messageId = AS2MimeNode_1.AS2MimeNode.generateMessageId();
}
else {
throw new Error('Argument must be either options to construct a disposition report, or a disposition report as an AS2MimeNode');
}
}
/**
* This instance to an AS2MimeNode.
* @returns {AS2MimeNode} - An MDN as an AS2MimeNode.
*/
toMimeNode() {
const rootNode = new AS2MimeNode_1.AS2MimeNode({
contentType: 'multipart/report; report-type=disposition-notification',
messageId: this.messageId
});
rootNode.appendChild(new AS2MimeNode_1.AS2MimeNode({
contentType: 'text/plain',
content: this.explanation
}));
rootNode.appendChild(new AS2MimeNode_1.AS2MimeNode({
contentType: 'message/disposition-notification',
content: this.notification.toString()
}));
if (this.returned) {
rootNode.appendChild(this.returned);
}
return rootNode;
}
// TODO: Needs to output both the content node and the disposition node.
/** Convenience method to decrypt and/or verify a mime node and construct an outgoing message disposition.
* @param {OutgoingDispositionOptions} - The options for generating an outgoing MDN.
* @returns {Promise<object>} - The content node, disposition object, and the generated outgoing MDN as an AS2MimeNode.
*/
static async outgoing(options) {
if (Helpers_1.isNullOrUndefined(options.node)) {
throw new Error(ERROR.DISPOSITION_NODE);
}
const notification = {
originalMessageId: options.node.messageId(),
finalRecipient: options.node.getHeader('As2-To'),
disposition: {
processed: true,
type: 'automatic-action'
}
};
let explanation = EXPLANATION.SUCCESS;
let rootNode = options.node;
let errored = false;
if (Helpers_1.isNullOrUndefined(notification.finalRecipient)) {
throw new Error(ERROR.FINAL_RECIPIENT_MISSING);
}
if (options.agreement.host.decrypt) {
try {
rootNode = await rootNode.decrypt({
cert: options.agreement.host.certificate,
key: options.agreement.host.privateKey
});
}
catch (error) {
errored = true;
notification.disposition.processed = false;
notification.disposition.description = {
type: 'failure',
text: error.message
};
explanation = EXPLANATION.FAILED_DECRYPTION;
}
}
if (options.agreement.partner.verify && !errored) {
try {
const cert = options.agreement.partner.certificate;
const verified = await AS2Crypto_1.AS2Crypto.verify(rootNode, { cert }, true);
if (verified) {
rootNode = rootNode.childNodes[0];
notification.receivedContentMic = {
mic: verified.digest.toString('base64'),
algorithm: verified.algorithm
};
}
else {
rootNode = undefined;
}
}
catch (error) {
errored = true;
notification.disposition.processed = false;
notification.disposition.description = {
type: 'failure',
text: error.message
};
explanation = EXPLANATION.FAILED_GENERALLY;
}
if (Helpers_1.isNullOrUndefined(rootNode) && !errored) {
notification.disposition.processed = false;
notification.disposition.description = {
type: 'failure',
text: 'Could not verify signature'
};
explanation = EXPLANATION.FAILED_VERIFICATION;
}
}
const mdn = new AS2Disposition({
explanation,
notification,
returned: options.returnNode ? options.node : undefined
});
let mdnMime = mdn.toMimeNode();
if (options.agreement.partner.mdn && options.agreement.partner.mdn.signing) {
mdnMime = await mdnMime.sign({
cert: options.agreement.host.certificate,
key: options.agreement.host.privateKey,
algorithm: options.agreement.partner.mdn.signing
});
}
// Set AS2 headers.
mdnMime.setHeader([
{ key: STANDARD_HEADER.FROM, value: options.agreement.host.id },
{ key: STANDARD_HEADER.TO, value: options.agreement.partner.id },
{ key: STANDARD_HEADER.VERSION, value: AS2_VERSION }
]);
mdnMime.messageId(true);
return {
contentNode: rootNode,
dispositionNode: mdnMime,
disposition: mdn
};
}
/** Deconstruct a mime node into an incoming message disposition.
* @param {AS2MimeNode} node - An AS2MimeNode containing an incoming MDN.
* @param {VerificationOptions} [signed] - Options for verifying the MDN if necessary.
* @returns {Promise<AS2Disposition>} The incoming message disposition notification.
*/
static async incoming(node, signed) {
let rootNode = node;
if (Helpers_1.isNullOrUndefined(node)) {
throw new Error(ERROR.DISPOSITION_NODE);
}
if (typeof signed !== 'undefined') {
rootNode = await node.verify(signed);
if (Helpers_1.isNullOrUndefined(rootNode)) {
throw new Error(ERROR.CONTENT_VERIFY);
}
}
return new AS2Disposition(rootNode);
}
}
exports.AS2Disposition = AS2Disposition;
;