elasticio-sailor-nodejs 2.7.7-dev1 → 3.0.0-dev1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +12 -146
- package/CHANGELOG.md +0 -5
- package/config/local.json +19 -0
- package/lib/encryptor.js +1 -0
- package/lib/executor.js +0 -9
- package/lib/proxy-client.js +666 -0
- package/lib/sailor.js +67 -186
- package/lib/settings.js +13 -20
- package/lib/utils.js +8 -0
- package/package.json +8 -6
- package/run.js +1 -1
- package/run.local.js +14 -0
- package/tsconfig.json +23 -0
- package/lib/amqp.js +0 -624
- package/lib/messagesDB.js +0 -37
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
const log = require('./logging.js');
|
|
2
|
+
const Encryptor = require('./encryptor.js');
|
|
3
|
+
const _ = require('lodash');
|
|
4
|
+
const eventToPromise = require('event-to-promise');
|
|
5
|
+
const uuid = require('uuid');
|
|
6
|
+
const http2 = require('http2');
|
|
7
|
+
const { getJitteredDelay } = require('./utils.js');
|
|
8
|
+
const { Promise } = require('q');
|
|
9
|
+
const pThrottle = require('p-throttle');
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
HTTP2_HEADER_PATH,
|
|
13
|
+
HTTP2_HEADER_METHOD,
|
|
14
|
+
HTTP2_HEADER_AUTHORIZATION,
|
|
15
|
+
HTTP2_HEADER_STATUS
|
|
16
|
+
} = http2.constants;
|
|
17
|
+
|
|
18
|
+
const HEADER_ROUTING_KEY = 'x-eio-routing-key';
|
|
19
|
+
const AMQP_HEADER_META_PREFIX = 'x-eio-meta-';
|
|
20
|
+
const OBJECT_ID_HEADER = 'x-ipaas-object-storage-id';
|
|
21
|
+
const PROXY_FORWARD_HEADER_PREFIX = 'x-sailor-proxy-forward-';
|
|
22
|
+
const MESSAGE_PROCESSING_STATUS = {
|
|
23
|
+
SUCCESS: 'success',
|
|
24
|
+
ERROR: 'error'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
class ProxyClient {
|
|
28
|
+
constructor(settings) {
|
|
29
|
+
this.settings = settings;
|
|
30
|
+
this._encryptor = new Encryptor(this.settings.MESSAGE_CRYPTO_PASSWORD, this.settings.MESSAGE_CRYPTO_IV);
|
|
31
|
+
this.closed = true;
|
|
32
|
+
this.clientSession = null;
|
|
33
|
+
this.reconnecting = false;
|
|
34
|
+
this.reconnectAttempts = 0;
|
|
35
|
+
this.reconnectTimer = null;
|
|
36
|
+
|
|
37
|
+
const username = settings.API_USERNAME;
|
|
38
|
+
const password = settings.API_KEY;
|
|
39
|
+
if (!username || !password) {
|
|
40
|
+
throw new Error('API_USERNAME and API_KEY must be set to connect to Sailor Proxy');
|
|
41
|
+
}
|
|
42
|
+
this.authHeader = 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64');
|
|
43
|
+
|
|
44
|
+
this.throttles = {
|
|
45
|
+
// 100 Messages per Second
|
|
46
|
+
data: pThrottle(() => Promise.resolve(true),
|
|
47
|
+
settings.DATA_RATE_LIMIT,
|
|
48
|
+
settings.RATE_INTERVAL),
|
|
49
|
+
error: pThrottle(() => Promise.resolve(true),
|
|
50
|
+
settings.ERROR_RATE_LIMIT,
|
|
51
|
+
settings.RATE_INTERVAL),
|
|
52
|
+
snapshot: pThrottle(() => Promise.resolve(true),
|
|
53
|
+
settings.SNAPSHOT_RATE_LIMIT,
|
|
54
|
+
settings.RATE_INTERVAL)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Health check method for K8s
|
|
59
|
+
// Returns true during reconnection attempts to prevent pod restarts during transient issues
|
|
60
|
+
// Only returns false if:
|
|
61
|
+
// - Connection was intentionally closed (this.closed = true)
|
|
62
|
+
// - Max reconnection attempts exhausted (reconnecting = false, closed = true)
|
|
63
|
+
isConnected() {
|
|
64
|
+
// For K8s health checks: consider the client "connected" if we're actively trying to reconnect
|
|
65
|
+
// This prevents pod restarts during transient network issues
|
|
66
|
+
if (this.reconnecting && !this.closed) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
return !this.closed && this.clientSession && !this.clientSession.destroyed;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async connect() {
|
|
73
|
+
try {
|
|
74
|
+
this.clientSession = http2.connect(this.settings.SAILOR_PROXY_URI);
|
|
75
|
+
this.closed = false;
|
|
76
|
+
this.reconnectAttempts = 0;
|
|
77
|
+
|
|
78
|
+
// Set up event listeners for connection management
|
|
79
|
+
this._setupConnectionListeners();
|
|
80
|
+
|
|
81
|
+
await eventToPromise(this.clientSession, 'connect');
|
|
82
|
+
log.info('Successfully connected to Sailor Proxy');
|
|
83
|
+
} catch (err) {
|
|
84
|
+
log.error({ err }, 'Failed to connect to Sailor Proxy');
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
_setupConnectionListeners() {
|
|
90
|
+
if (!this.clientSession) return;
|
|
91
|
+
|
|
92
|
+
// Handle connection errors
|
|
93
|
+
this.clientSession.on('error', (err) => {
|
|
94
|
+
log.error({ err, closed: this.closed }, 'HTTP2 session error');
|
|
95
|
+
if (!this.closed) {
|
|
96
|
+
this._handleDisconnection('error', err);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Handle connection close
|
|
101
|
+
this.clientSession.on('close', () => {
|
|
102
|
+
log.warn({ closed: this.closed, reconnecting: this.reconnecting }, 'HTTP2 session closed');
|
|
103
|
+
if (!this.closed && !this.reconnecting) {
|
|
104
|
+
this._handleDisconnection('close');
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Handle GOAWAY frames (server-initiated shutdown)
|
|
109
|
+
this.clientSession.on('goaway', (errorCode, lastStreamID, opaqueData) => {
|
|
110
|
+
log.warn({ errorCode, lastStreamID, closed: this.closed }, 'Received GOAWAY from server');
|
|
111
|
+
if (!this.closed) {
|
|
112
|
+
this._handleDisconnection('goaway', { errorCode, lastStreamID });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Handle timeout
|
|
117
|
+
this.clientSession.on('timeout', () => {
|
|
118
|
+
log.warn({ closed: this.closed }, 'HTTP2 session timeout');
|
|
119
|
+
if (!this.closed) {
|
|
120
|
+
this._handleDisconnection('timeout');
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_handleDisconnection(reason, details) {
|
|
126
|
+
if (this.reconnecting || this.closed) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
log.warn({ reason, details }, 'Connection lost, initiating reconnection');
|
|
131
|
+
this.reconnecting = true;
|
|
132
|
+
|
|
133
|
+
// Clean up the current session
|
|
134
|
+
if (this.clientSession && !this.clientSession.destroyed) {
|
|
135
|
+
this.clientSession.destroy();
|
|
136
|
+
}
|
|
137
|
+
this.clientSession = null;
|
|
138
|
+
|
|
139
|
+
this._scheduleReconnect();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
_scheduleReconnect() {
|
|
143
|
+
if (this.closed) {
|
|
144
|
+
log.info('Connection closed intentionally, skipping reconnection');
|
|
145
|
+
this.reconnecting = false;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (this.reconnectAttempts >= this.settings.PROXY_RECONNECT_MAX_RETRIES) {
|
|
150
|
+
log.error({ attempts: this.reconnectAttempts }, 'Max reconnection attempts reached, giving up');
|
|
151
|
+
this.reconnecting = false;
|
|
152
|
+
this.closed = true; // Mark as closed so K8s health check will fail and restart the pod
|
|
153
|
+
// Optionally emit an event or call a callback here
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.reconnectAttempts++;
|
|
158
|
+
const baseDelay = Math.min(
|
|
159
|
+
this.settings.PROXY_RECONNECT_INITIAL_DELAY *
|
|
160
|
+
Math.pow(this.settings.PROXY_RECONNECT_BACKOFF_MULTIPLIER, this.reconnectAttempts - 1),
|
|
161
|
+
this.settings.PROXY_RECONNECT_MAX_DELAY
|
|
162
|
+
);
|
|
163
|
+
// Apply jitter to avoid thundering herd problem
|
|
164
|
+
const delay = getJitteredDelay(baseDelay, this.settings.PROXY_RECONNECT_JITTER_FACTOR);
|
|
165
|
+
|
|
166
|
+
log.info({ attempt: this.reconnectAttempts, baseDelayMs: baseDelay, jitteredDelayMs: delay }, 'Scheduling reconnection attempt');
|
|
167
|
+
|
|
168
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
169
|
+
try {
|
|
170
|
+
log.info({ attempt: this.reconnectAttempts }, 'Attempting to reconnect');
|
|
171
|
+
await this._reconnect();
|
|
172
|
+
log.info('Successfully reconnected to Sailor Proxy');
|
|
173
|
+
this.reconnecting = false;
|
|
174
|
+
} catch (err) {
|
|
175
|
+
log.error({ attempt: this.reconnectAttempts, error: err }, 'Reconnection attempt failed');
|
|
176
|
+
this._scheduleReconnect();
|
|
177
|
+
}
|
|
178
|
+
}, delay);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async _reconnect() {
|
|
182
|
+
this.clientSession = http2.connect(this.settings.SAILOR_PROXY_URI);
|
|
183
|
+
this._setupConnectionListeners();
|
|
184
|
+
await eventToPromise(this.clientSession, 'connect');
|
|
185
|
+
this.reconnectAttempts = 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async disconnect() {
|
|
189
|
+
this.closed = true;
|
|
190
|
+
this.reconnecting = false;
|
|
191
|
+
|
|
192
|
+
// Clear any pending reconnection timers
|
|
193
|
+
if (this.reconnectTimer) {
|
|
194
|
+
clearTimeout(this.reconnectTimer);
|
|
195
|
+
this.reconnectTimer = null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return new Promise((resolve) => {
|
|
199
|
+
if (!this.clientSession || this.clientSession.destroyed) {
|
|
200
|
+
log.debug('Session already destroyed');
|
|
201
|
+
return resolve();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this.clientSession.close(() => {
|
|
205
|
+
log.debug('Successfully closed HTTP2 connection');
|
|
206
|
+
resolve();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async _ensureConnection() {
|
|
212
|
+
if (this.isConnected()) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (this.reconnecting) {
|
|
217
|
+
// Wait for reconnection to complete
|
|
218
|
+
log.info('Waiting for reconnection to complete');
|
|
219
|
+
const maxWait = 30000; // 30 seconds
|
|
220
|
+
const startTime = Date.now();
|
|
221
|
+
while (this.reconnecting && (Date.now() - startTime) < maxWait) {
|
|
222
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!this.isConnected()) {
|
|
226
|
+
throw new Error('Failed to reconnect within timeout period');
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (this.closed) {
|
|
232
|
+
throw new Error('Connection is closed. Call connect() first.');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// If we get here, connection was lost but reconnection hasn't started
|
|
236
|
+
throw new Error('Connection lost and no reconnection in progress');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async fetchMessageBody(message, logger) {
|
|
240
|
+
await this._ensureConnection();
|
|
241
|
+
|
|
242
|
+
const { body, headers } = message;
|
|
243
|
+
|
|
244
|
+
logger.info('Checking if incoming messages is lightweight...');
|
|
245
|
+
|
|
246
|
+
if (!headers) {
|
|
247
|
+
logger.info('Empty headers so not lightweight.');
|
|
248
|
+
return body;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const { [OBJECT_ID_HEADER]: objectId } = headers;
|
|
252
|
+
|
|
253
|
+
if (!objectId) {
|
|
254
|
+
logger.trace('No object id header so not lightweight.');
|
|
255
|
+
return body;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
logger.info('Object id header found, message is lightweight.', { objectId });
|
|
259
|
+
|
|
260
|
+
let object;
|
|
261
|
+
|
|
262
|
+
logger.info('Going to fetch message body.', { objectId });
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const getObjectStream = this.clientSession.request({
|
|
266
|
+
[HTTP2_HEADER_PATH]: `/object/${objectId}`,
|
|
267
|
+
[HTTP2_HEADER_METHOD]: 'GET',
|
|
268
|
+
[HTTP2_HEADER_AUTHORIZATION]: this.authHeader
|
|
269
|
+
}).pipe(this._encryptor.createDecipher());
|
|
270
|
+
// TODO: Add retries
|
|
271
|
+
object = await new Promise((resolve, reject) => {
|
|
272
|
+
const chunks = [];
|
|
273
|
+
getObjectStream.on('data', chunk => {
|
|
274
|
+
chunks.push(chunk);
|
|
275
|
+
});
|
|
276
|
+
getObjectStream.on('error', (err) => {
|
|
277
|
+
logger.error(err, 'Error during fetching message body');
|
|
278
|
+
reject(err);
|
|
279
|
+
});
|
|
280
|
+
getObjectStream.on('end', () => {
|
|
281
|
+
logger.info('Message stream ended by server');
|
|
282
|
+
const buffer = Buffer.concat(chunks);
|
|
283
|
+
logger.info({ messageSize: buffer.length }, 'Received complete message from server');
|
|
284
|
+
resolve({ data: JSON.parse(buffer.toString()) });
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
} catch (e) {
|
|
288
|
+
log.error(e);
|
|
289
|
+
throw new Error(`Failed to get message body with id=${objectId}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
logger.info('Successfully obtained message body.', { objectId });
|
|
293
|
+
logger.trace('Message body object received');
|
|
294
|
+
|
|
295
|
+
return object.data;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async uploadMessageBody(bodyBuf) {
|
|
299
|
+
await this._ensureConnection();
|
|
300
|
+
|
|
301
|
+
return new Promise((resolve, reject) => {
|
|
302
|
+
// TODO: Add retries
|
|
303
|
+
const postMessageStream = this.clientSession.request({
|
|
304
|
+
[HTTP2_HEADER_PATH]: '/object',
|
|
305
|
+
[HTTP2_HEADER_METHOD]: 'POST',
|
|
306
|
+
[HTTP2_HEADER_AUTHORIZATION]: this.authHeader
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
postMessageStream.on('response', (headers, flags) => {
|
|
310
|
+
const status = headers[http2.constants.HTTP2_HEADER_STATUS];
|
|
311
|
+
if (status !== 200) {
|
|
312
|
+
return reject(new Error(`Failed to upload message body, status code: ${status}`));
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
let responseData = '';
|
|
316
|
+
postMessageStream.on('data', chunk => {
|
|
317
|
+
responseData += chunk;
|
|
318
|
+
});
|
|
319
|
+
postMessageStream.on('error', (err) => {
|
|
320
|
+
log.error(err, 'Error during upload message body');
|
|
321
|
+
reject(err);
|
|
322
|
+
});
|
|
323
|
+
postMessageStream.on('end', () => {
|
|
324
|
+
try {
|
|
325
|
+
const responseJson = JSON.parse(responseData);
|
|
326
|
+
resolve(responseJson.objectId);
|
|
327
|
+
} catch (e) {
|
|
328
|
+
log.error(e, 'Failed to parse upload message body response');
|
|
329
|
+
reject(e);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const cipher = this._encryptor.createCipher();
|
|
334
|
+
cipher.pipe(postMessageStream);
|
|
335
|
+
cipher.write(bodyBuf);
|
|
336
|
+
cipher.end();
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async listenForMessages(messageHandler) {
|
|
341
|
+
while (!this.closed) {
|
|
342
|
+
try {
|
|
343
|
+
await this._ensureConnection();
|
|
344
|
+
|
|
345
|
+
const stepId = this.settings.STEP_ID;
|
|
346
|
+
const prefetch = this.settings.PROXY_PREFETCH_SAILOR;
|
|
347
|
+
await Promise.all(new Array(prefetch).fill().map(async () => {
|
|
348
|
+
const queryParams = new URLSearchParams({
|
|
349
|
+
stepId,
|
|
350
|
+
prefetch
|
|
351
|
+
}).toString();
|
|
352
|
+
log.info({ stepId, prefetch }, 'Requesting message from proxy');
|
|
353
|
+
const getMessageStream = this.clientSession.request({
|
|
354
|
+
[HTTP2_HEADER_PATH]: `/message?${queryParams}`,
|
|
355
|
+
[HTTP2_HEADER_METHOD]: 'GET',
|
|
356
|
+
[HTTP2_HEADER_AUTHORIZATION]: this.authHeader
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const { headers, body } = await new Promise((resolve, reject) => {
|
|
360
|
+
getMessageStream.on('response', (headers, flags) => {
|
|
361
|
+
log.info({ headers, flags }, 'Connected to message stream');
|
|
362
|
+
if (headers[HTTP2_HEADER_STATUS] !== 200) {
|
|
363
|
+
return reject(new Error(`Failed to get message, status code: ${headers[HTTP2_HEADER_STATUS]}`));
|
|
364
|
+
}
|
|
365
|
+
const messageId = headers['x-message-id'];
|
|
366
|
+
const chunks = [];
|
|
367
|
+
getMessageStream.on('data', chunk => {
|
|
368
|
+
chunks.push(chunk);
|
|
369
|
+
});
|
|
370
|
+
getMessageStream.on('end', () => {
|
|
371
|
+
log.info('Message stream ended by server');
|
|
372
|
+
const body = Buffer.concat(chunks);
|
|
373
|
+
log.info({
|
|
374
|
+
messageId,
|
|
375
|
+
messageSize: body.length
|
|
376
|
+
}, 'Received complete message from server');
|
|
377
|
+
log.trace({ body: body.toString() }, 'Message body as string');
|
|
378
|
+
resolve({ headers, body });
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
getMessageStream.on('close', () => {
|
|
383
|
+
log.warn('Message stream closed by server');
|
|
384
|
+
reject(new Error('Message stream closed by server'));
|
|
385
|
+
});
|
|
386
|
+
getMessageStream.on('error', (err) => {
|
|
387
|
+
log.error(err, 'Error on message stream');
|
|
388
|
+
reject(err);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const proxyHeaders = this._extractProxyHeaders(headers);
|
|
393
|
+
const message = this._decodeMessage(body, headers);
|
|
394
|
+
log.debug({ proxyHeaders, message }, 'Processing received message');
|
|
395
|
+
await messageHandler(proxyHeaders, message);
|
|
396
|
+
}));
|
|
397
|
+
} catch (err) {
|
|
398
|
+
if (this.closed) {
|
|
399
|
+
log.info('Connection closed, stopping message listener');
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (!this.reconnecting) {
|
|
404
|
+
log.error(err, 'Error in listenForMessages, waiting for reconnection');
|
|
405
|
+
} else {
|
|
406
|
+
log.debug('Currently reconnecting, will retry listening for messages after reconnection');
|
|
407
|
+
}
|
|
408
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async sendMessage({
|
|
414
|
+
incomingMessageId,
|
|
415
|
+
type,
|
|
416
|
+
data,
|
|
417
|
+
headers
|
|
418
|
+
}) {
|
|
419
|
+
await this._ensureConnection();
|
|
420
|
+
|
|
421
|
+
const throttledSend = this.throttles[type];
|
|
422
|
+
if (throttledSend) {
|
|
423
|
+
log.debug({ incomingMessageId, type, headers }, 'Applying rate limiting for message send');
|
|
424
|
+
await throttledSend();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
log.debug({ incomingMessageId, type, headers }, 'Sending message to proxy');
|
|
428
|
+
log.trace({ data }, 'Message data to send to proxy');
|
|
429
|
+
const proxyHeaders = this._createProxyHeaders(headers);
|
|
430
|
+
const encryptedData = this.encryptMessageContent(data, headers.protocolVersion);
|
|
431
|
+
if (encryptedData.length > this.settings.OUTGOING_MESSAGE_SIZE_LIMIT) {
|
|
432
|
+
const error = new Error(`Outgoing message size ${encryptedData.length}`
|
|
433
|
+
+ ` exceeds limit of ${this.settings.OUTGOING_MESSAGE_SIZE_LIMIT}.`);
|
|
434
|
+
log.error(error);
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const messageHeaders = _.mapKeys(data.headers || {}, (value, key) => key.toLowerCase());
|
|
439
|
+
const customRoutingKey = messageHeaders[HEADER_ROUTING_KEY];
|
|
440
|
+
const queryParams = new URLSearchParams({
|
|
441
|
+
incomingMessageId,
|
|
442
|
+
stepId: this.settings.STEP_ID,
|
|
443
|
+
type,
|
|
444
|
+
...(customRoutingKey ? { customRoutingKey } : {})
|
|
445
|
+
}).toString();
|
|
446
|
+
// TODO: Add retries
|
|
447
|
+
const postMessageStream = this.clientSession.request({
|
|
448
|
+
...proxyHeaders,
|
|
449
|
+
[HTTP2_HEADER_PATH]: `/message?${queryParams}`,
|
|
450
|
+
[HTTP2_HEADER_METHOD]: 'POST',
|
|
451
|
+
[HTTP2_HEADER_AUTHORIZATION]: this.authHeader
|
|
452
|
+
});
|
|
453
|
+
postMessageStream.write(encryptedData);
|
|
454
|
+
postMessageStream.end();
|
|
455
|
+
|
|
456
|
+
return new Promise((resolve, reject) => {
|
|
457
|
+
postMessageStream.on('response', (headers) => {
|
|
458
|
+
log.debug({ status: headers[HTTP2_HEADER_STATUS] }, 'Send message response');
|
|
459
|
+
if (headers[HTTP2_HEADER_STATUS] !== 200) {
|
|
460
|
+
log.error({ headers }, 'Failed to send message');
|
|
461
|
+
return reject(new Error(`Failed to send message, status code: ${headers[HTTP2_HEADER_STATUS]}`));
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
postMessageStream.on('error', (err) => {
|
|
465
|
+
log.error(err, 'Error during sending message');
|
|
466
|
+
reject(err);
|
|
467
|
+
});
|
|
468
|
+
postMessageStream.on('end', () => {
|
|
469
|
+
log.debug('Send message end event');
|
|
470
|
+
resolve();
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
_decodeMessage(originalMessage, headers) {
|
|
476
|
+
log.trace('Message received');
|
|
477
|
+
let message;
|
|
478
|
+
if (this.settings.INPUT_FORMAT === 'error') {
|
|
479
|
+
message = this._decodeErrorMessage(originalMessage, headers);
|
|
480
|
+
} else {
|
|
481
|
+
message = this._decodeDefaultMessage(originalMessage, headers);
|
|
482
|
+
}
|
|
483
|
+
message.headers = message.headers || {};
|
|
484
|
+
if (headers.replyTo) {
|
|
485
|
+
message.headers.reply_to = headers.replyTo;
|
|
486
|
+
}
|
|
487
|
+
return message;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
_decodeDefaultMessage(originalMessage, headers) {
|
|
491
|
+
const protocolVersion = Number(headers.protocolVersion || 1);
|
|
492
|
+
return this._encryptor.decryptMessageContent(
|
|
493
|
+
originalMessage,
|
|
494
|
+
protocolVersion < 2 ? 'base64' : undefined
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
_decodeErrorMessage(originalMessage, headers) {
|
|
499
|
+
const errorBody = JSON.parse(originalMessage.toString());
|
|
500
|
+
if (errorBody.error) {
|
|
501
|
+
errorBody.error = this._encryptor.decryptMessageContent(Buffer.from(errorBody.error), 'base64');
|
|
502
|
+
}
|
|
503
|
+
if (errorBody.errorInput) {
|
|
504
|
+
errorBody.errorInput = this._encryptor.decryptMessageContent(errorBody.errorInput, 'base64');
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
body: errorBody,
|
|
508
|
+
headers
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async finishProcessing(incomingHeaders, status) {
|
|
513
|
+
await this._ensureConnection();
|
|
514
|
+
|
|
515
|
+
if (Object.values(MESSAGE_PROCESSING_STATUS).indexOf(status) === -1) {
|
|
516
|
+
throw new Error(`Invalid message processing status: ${status}`);
|
|
517
|
+
}
|
|
518
|
+
const incomingMessageId = incomingHeaders.messageId;
|
|
519
|
+
log.debug({ incomingMessageId, status }, 'Finishing processing of message');
|
|
520
|
+
const queryParams = new URLSearchParams({
|
|
521
|
+
incomingMessageId,
|
|
522
|
+
status
|
|
523
|
+
}).toString();
|
|
524
|
+
// TODO: Add retries
|
|
525
|
+
const postMessageStream = this.clientSession.request({
|
|
526
|
+
[HTTP2_HEADER_PATH]: `/finish-processing?${queryParams}`,
|
|
527
|
+
[HTTP2_HEADER_METHOD]: 'POST',
|
|
528
|
+
[HTTP2_HEADER_AUTHORIZATION]: this.authHeader
|
|
529
|
+
});
|
|
530
|
+
postMessageStream.end();
|
|
531
|
+
|
|
532
|
+
return new Promise((resolve, reject) => {
|
|
533
|
+
postMessageStream.on('response', (headers) => {
|
|
534
|
+
log.debug({ status: headers[HTTP2_HEADER_STATUS] }, 'Finish processing response event');
|
|
535
|
+
if (headers[HTTP2_HEADER_STATUS] !== 200) {
|
|
536
|
+
log.error({ headers }, 'Failed to finish processing message');
|
|
537
|
+
return reject(new Error(`Failed to finish processing message, status code: ${headers[HTTP2_HEADER_STATUS]}`));
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
postMessageStream.on('end', () => {
|
|
542
|
+
log.debug('Finish processing end event');
|
|
543
|
+
resolve();
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
postMessageStream.on('error', reject);
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
encryptMessageContent(body, protocolVersion = 1) {
|
|
551
|
+
return this._encryptor.encryptMessageContent(
|
|
552
|
+
body,
|
|
553
|
+
protocolVersion < 2
|
|
554
|
+
? 'base64'
|
|
555
|
+
: undefined
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async sendError(err, headers, originalMessage, incomingHeaders) {
|
|
560
|
+
await this._ensureConnection();
|
|
561
|
+
|
|
562
|
+
const settings = this.settings;
|
|
563
|
+
|
|
564
|
+
const encryptedError = this._encryptor.encryptMessageContent({
|
|
565
|
+
name: err.name,
|
|
566
|
+
message: err.message,
|
|
567
|
+
stack: err.stack
|
|
568
|
+
}, 'base64').toString();
|
|
569
|
+
|
|
570
|
+
const payload = {
|
|
571
|
+
error: encryptedError
|
|
572
|
+
};
|
|
573
|
+
if (originalMessage) {
|
|
574
|
+
const protocolVersion = Number(incomingHeaders.protocolVersion || 1);
|
|
575
|
+
if (protocolVersion >= 2) {
|
|
576
|
+
payload.errorInput = this._encryptor.encryptMessageContent(
|
|
577
|
+
originalMessage,
|
|
578
|
+
'base64'
|
|
579
|
+
).toString();
|
|
580
|
+
} else {
|
|
581
|
+
payload.errorInput = originalMessage;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
const errorPayload = JSON.stringify(payload);
|
|
585
|
+
|
|
586
|
+
let result = await this.sendMessage({
|
|
587
|
+
incomingMessageId: incomingHeaders ? incomingHeaders.messageId : undefined,
|
|
588
|
+
type: 'error',
|
|
589
|
+
data: errorPayload,
|
|
590
|
+
headers
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
return result;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async sendRebound(reboundError, incomingHeaders, outgoingHeaders) {
|
|
597
|
+
await this._ensureConnection();
|
|
598
|
+
|
|
599
|
+
outgoingHeaders.end = new Date().getTime();
|
|
600
|
+
outgoingHeaders.reboundReason = reboundError.message;
|
|
601
|
+
return this.sendMessage({
|
|
602
|
+
type: 'rebound',
|
|
603
|
+
headers: outgoingHeaders,
|
|
604
|
+
incomingMessageId: incomingHeaders ? incomingHeaders.messageId : undefined,
|
|
605
|
+
data: reboundError
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async sendSnapshot(data, headers) {
|
|
610
|
+
await this._ensureConnection();
|
|
611
|
+
|
|
612
|
+
const payload = JSON.stringify(data);
|
|
613
|
+
const properties = this._createProxyHeaders(headers);
|
|
614
|
+
return this.sendMessage({
|
|
615
|
+
type: 'snapshot',
|
|
616
|
+
data: payload,
|
|
617
|
+
headers: properties
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
_createProxyHeaders(headers) {
|
|
622
|
+
headers.messageId = headers.messageId || uuid.v4();
|
|
623
|
+
return Object.entries(headers || {}).reduce((acc, [key, value]) => {
|
|
624
|
+
acc[`${PROXY_FORWARD_HEADER_PREFIX}${_.kebabCase(key)}`] = value;
|
|
625
|
+
return acc;
|
|
626
|
+
}, {});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
_extractProxyHeaders(proxyHeaders) {
|
|
630
|
+
log.trace({ proxyHeaders }, 'Extracting proxy headers');
|
|
631
|
+
const headers = Object.entries(proxyHeaders || {}).reduce((acc, [key, value]) => {
|
|
632
|
+
if (key.startsWith(PROXY_FORWARD_HEADER_PREFIX)) {
|
|
633
|
+
const originalKey = key.substring(PROXY_FORWARD_HEADER_PREFIX.length);
|
|
634
|
+
acc[_.camelCase(originalKey)] = value;
|
|
635
|
+
}
|
|
636
|
+
return acc;
|
|
637
|
+
}, {});
|
|
638
|
+
|
|
639
|
+
const metaHeaderNames = Object.keys(headers)
|
|
640
|
+
.filter(key => key.toLowerCase().startsWith(AMQP_HEADER_META_PREFIX));
|
|
641
|
+
|
|
642
|
+
const metaHeaders = _.pick(headers, metaHeaderNames);
|
|
643
|
+
const metaHeadersLowerCased = _.mapKeys(metaHeaders, (value, key) => key.toLowerCase());
|
|
644
|
+
|
|
645
|
+
const result = {
|
|
646
|
+
stepId: headers.stepId,
|
|
647
|
+
...metaHeadersLowerCased,
|
|
648
|
+
threadId: headers.threadId || metaHeadersLowerCased['x-eio-meta-trace-id'],
|
|
649
|
+
messageId: headers.messageId,
|
|
650
|
+
parentMessageId: headers.parentMessageId
|
|
651
|
+
};
|
|
652
|
+
if (!result.threadId) {
|
|
653
|
+
const threadId = uuid.v4();
|
|
654
|
+
log.debug({ threadId }, 'Initiate new thread as it is not started ATM');
|
|
655
|
+
result.threadId = threadId;
|
|
656
|
+
}
|
|
657
|
+
if (headers.replyTo) {
|
|
658
|
+
result.replyTo = headers.replyTo;
|
|
659
|
+
}
|
|
660
|
+
log.debug({ result }, 'Extracted proxy headers');
|
|
661
|
+
return result;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
exports.ProxyClient = ProxyClient;
|
|
666
|
+
exports.MESSAGE_PROCESSING_STATUS = MESSAGE_PROCESSING_STATUS;
|