chore(download): Add support for segmented download

This commit is contained in:
Vlad Gramuzov 2021-05-14 01:16:50 +03:00
parent fe0f585b27
commit 59515f21e3
No known key found for this signature in database
GPG Key ID: 6D48E708EDC4E583
4 changed files with 70 additions and 29 deletions

View File

@ -147,14 +147,35 @@ module.exports = class Client {
async download(order) { async download(order) {
if (this.tracesStorage) if (this.tracesStorage)
this.tracesStorage.new().ofType('ORDER.DOWNLOAD'); this.tracesStorage.new().ofType('ORDER.DOWNLOAD');
const res = await this.ebicsRequest(order);
const orderData = [];
let res = await this.ebicsRequest(order);
let transactionKey = res.transactionKey();
order.transactionId = res.transactionId(); order.transactionId = res.transactionId();
order.segmentNumber = res.segmentNumber();
if (res.orderData()) orderData.push(res.orderData());
// In case of multi-segment download transaction is
// usually supplied during INITIALISATION phase,
// whereas the actual data is delivered in following segments
while (res.isSegmented() && !res.isLastSegment()) {
order.segmentNumber = res.segmentNumber() + 1;
res = await this.ebicsRequest(order);
transactionKey = transactionKey || res.transactionKey();
res.obtainedTransactionKey = transactionKey;
if (res.orderData()) orderData.push(res.orderData());
}
if (res.isSegmented() && res.isLastSegment()) { if (res.isSegmented() && res.isLastSegment()) {
if (this.tracesStorage) if (this.tracesStorage)
this.tracesStorage.connect().ofType('RECEIPT.ORDER.DOWNLOAD'); this.tracesStorage.connect().ofType('RECEIPT.ORDER.DOWNLOAD');
order.segmentNumber = null; // Clear segment number for receipt request
await this.ebicsRequest(order); await this.ebicsRequest(order);
} }
@ -162,7 +183,7 @@ module.exports = class Client {
const returnedBusinessCode = res.businessCode(); const returnedBusinessCode = res.businessCode();
return { return {
orderData: res.orderData(), orderData: orderData.length === 1 ? orderData[0] : orderData, // backward compatibility
orderId: res.orderId(), orderId: res.orderId(),
technicalCode: returnedTechnicalCode, technicalCode: returnedTechnicalCode,

View File

@ -22,6 +22,7 @@ const lastChild = (node) => {
module.exports = (xml, keys) => ({ module.exports = (xml, keys) => ({
keys, keys,
doc: new DOMParser().parseFromString(xml, 'text/xml'), doc: new DOMParser().parseFromString(xml, 'text/xml'),
obtainedTransactionKey: null,
isSegmented() { isSegmented() {
const select = xpath.useNamespaces({ xmlns: 'urn:org:ebics:H004' }); const select = xpath.useNamespaces({ xmlns: 'urn:org:ebics:H004' });
@ -30,6 +31,13 @@ module.exports = (xml, keys) => ({
return !!node.length; return !!node.length;
}, },
segmentNumber() {
const select = xpath.useNamespaces({ xmlns: 'urn:org:ebics:H004' });
const node = select('//xmlns:header/xmlns:mutable/xmlns:SegmentNumber', this.doc);
return node.length ? Number.parseInt(node[0].textContent, 10) : null;
},
isLastSegment() { isLastSegment() {
const select = xpath.useNamespaces({ xmlns: 'urn:org:ebics:H004' }); const select = xpath.useNamespaces({ xmlns: 'urn:org:ebics:H004' });
const node = select("//xmlns:header/xmlns:mutable/*[@lastSegment='true']", this.doc); const node = select("//xmlns:header/xmlns:mutable/*[@lastSegment='true']", this.doc);
@ -40,7 +48,7 @@ module.exports = (xml, keys) => ({
orderData() { orderData() {
const orderDataNode = this.doc.getElementsByTagNameNS('urn:org:ebics:H004', 'OrderData'); const orderDataNode = this.doc.getElementsByTagNameNS('urn:org:ebics:H004', 'OrderData');
if (!orderDataNode.length) return {}; if (!orderDataNode.length) return null;
const orderData = orderDataNode[0].textContent; const orderData = orderDataNode[0].textContent;
const decipher = crypto.createDecipheriv('aes-128-cbc', this.transactionKey(), DEFAULT_IV).setAutoPadding(false); const decipher = crypto.createDecipheriv('aes-128-cbc', this.transactionKey(), DEFAULT_IV).setAutoPadding(false);
@ -50,6 +58,7 @@ module.exports = (xml, keys) => ({
}, },
transactionKey() { transactionKey() {
if (this.obtainedTransactionKey) return this.obtainedTransactionKey;
const keyNodeText = this.doc.getElementsByTagNameNS('urn:org:ebics:H004', 'TransactionKey')[0].textContent; const keyNodeText = this.doc.getElementsByTagNameNS('urn:org:ebics:H004', 'TransactionKey')[0].textContent;
return Crypto.privateDecrypt(this.keys.e(), Buffer.from(keyNodeText, 'base64')); return Crypto.privateDecrypt(this.keys.e(), Buffer.from(keyNodeText, 'base64'));
}, },

View File

@ -13,7 +13,7 @@ module.exports = {
userId: client.userId, userId: client.userId,
hostId: client.hostId, hostId: client.hostId,
}; };
const { orderDetails, transactionId } = order; const { orderDetails, transactionId, segmentNumber } = order;
const { const {
rootName, xmlOptions, xmlSchema, receipt, transfer, productString, rootName, xmlOptions, xmlSchema, receipt, transfer, productString,
} = genericSerializer(client.hostId, transactionId); } = genericSerializer(client.hostId, transactionId);
@ -25,35 +25,46 @@ module.exports = {
this.receipt = receipt; this.receipt = receipt;
this.transfer = transfer; this.transfer = transfer;
if (transactionId) return this.receipt(); if (!segmentNumber && transactionId) return this.receipt();
this.xmlSchema.header = { this.xmlSchema.header = {
'@': { authenticate: true }, '@': { authenticate: true },
static: { static: {
HostID: ebicsAccount.hostId, HostID: ebicsAccount.hostId,
Nonce: Crypto.nonce(), ...!transactionId && {
Timestamp: Crypto.timestamp(), Nonce: Crypto.nonce(),
PartnerID: ebicsAccount.partnerId, Timestamp: Crypto.timestamp(),
UserID: ebicsAccount.userId, PartnerID: ebicsAccount.partnerId,
Product: { UserID: ebicsAccount.userId,
'@': { Language: 'en' }, Product: {
'#': productString, '@': { Language: 'en' },
}, '#': productString,
OrderDetails: orderDetails,
BankPubKeyDigests: {
Authentication: {
'@': { Version: 'X002', Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256' },
'#': Crypto.digestPublicKey(keys.bankX()),
}, },
Encryption: { OrderDetails: orderDetails,
'@': { Version: 'E002', Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256' }, BankPubKeyDigests: {
'#': Crypto.digestPublicKey(keys.bankE()), Authentication: {
'@': { Version: 'X002', Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256' },
'#': Crypto.digestPublicKey(keys.bankX()),
},
Encryption: {
'@': { Version: 'E002', Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256' },
'#': Crypto.digestPublicKey(keys.bankE()),
},
}, },
SecurityMedium: '0000',
},
...transactionId && {
TransactionID: transactionId,
}, },
SecurityMedium: '0000',
}, },
mutable: { mutable: {
TransactionPhase: 'Initialisation', TransactionPhase: segmentNumber ? 'Transfer' : 'Initialisation',
...segmentNumber && {
SegmentNumber: {
'@': { lastSegment: false },
'#': segmentNumber,
},
},
}, },
}; };

12
package-lock.json generated
View File

@ -1967,9 +1967,9 @@
"dev": true "dev": true
}, },
"hosted-git-info": { "hosted-git-info": {
"version": "2.8.5", "version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
"integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==", "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"dev": true "dev": true
}, },
"html-escaper": { "html-escaper": {
@ -2536,9 +2536,9 @@
} }
}, },
"lodash": { "lodash": {
"version": "4.17.19", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true "dev": true
}, },
"lodash.flattendeep": { "lodash.flattendeep": {