oak-backend-base 4.1.8 → 4.1.10
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/lib/Synchronizer.js +71 -10
- package/package.json +4 -4
package/lib/Synchronizer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const tslib_1 = require("tslib");
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
4
5
|
const types_1 = require("oak-domain/lib/types");
|
|
5
6
|
const relationPath_1 = require("oak-domain/lib/utils/relationPath");
|
|
6
7
|
const assert_1 = tslib_1.__importDefault(require("assert"));
|
|
@@ -11,6 +12,52 @@ const uuid_1 = require("oak-domain/lib/utils/uuid");
|
|
|
11
12
|
const lodash_2 = require("lodash");
|
|
12
13
|
const OAK_SYNC_HEADER_ENTITY = 'oak-sync-entity';
|
|
13
14
|
const OAK_SYNC_HEADER_ENTITY_ID = 'oak-sync-entity-id';
|
|
15
|
+
const OAK_SYNC_HEADER_TIMESTAMP = 'oak-sync-timestamp';
|
|
16
|
+
const OAK_SYNC_HEADER_NONCE = 'oak-sync-nonce';
|
|
17
|
+
const OAK_SYNC_HEADER_SIGN = 'oak-sync-sign';
|
|
18
|
+
function generateSignStr(body, ts, nonce) {
|
|
19
|
+
return `${body}\n${ts}\n${nonce}`;
|
|
20
|
+
}
|
|
21
|
+
async function sign(privateKey, body) {
|
|
22
|
+
const ts = Date.now();
|
|
23
|
+
const nonce = await (0, uuid_1.generateNewIdAsync)();
|
|
24
|
+
const sign2 = (0, crypto_1.createSign)('SHA256');
|
|
25
|
+
sign2.update(generateSignStr(body, `${ts}`, nonce));
|
|
26
|
+
sign2.end();
|
|
27
|
+
const signature = sign2.sign(privateKey).toString('hex');
|
|
28
|
+
return {
|
|
29
|
+
ts,
|
|
30
|
+
nonce,
|
|
31
|
+
signature,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function verify(publicKey, body, ts, nonce, signature) {
|
|
35
|
+
const verify2 = (0, crypto_1.createVerify)('SHA256');
|
|
36
|
+
verify2.update(generateSignStr(body, ts, nonce));
|
|
37
|
+
verify2.end();
|
|
38
|
+
return verify2.verify(publicKey, signature, 'hex');
|
|
39
|
+
}
|
|
40
|
+
async function fetchWithTimeout(url, options, timeout = 5000) {
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
const signal = controller.signal;
|
|
43
|
+
// 设置超时
|
|
44
|
+
const timeoutId = setTimeout(() => {
|
|
45
|
+
controller.abort();
|
|
46
|
+
}, timeout);
|
|
47
|
+
// 发起 fetch 请求并传递 signal
|
|
48
|
+
return fetch(url, Object.assign({}, options, { signal }))
|
|
49
|
+
.then(response => {
|
|
50
|
+
clearTimeout(timeoutId); // 如果请求成功,清除超时
|
|
51
|
+
return response;
|
|
52
|
+
})
|
|
53
|
+
.catch(error => {
|
|
54
|
+
clearTimeout(timeoutId); // 如果请求失败,清除超时
|
|
55
|
+
if (error.name === 'AbortError') {
|
|
56
|
+
throw new types_1.OakRequestTimeoutException();
|
|
57
|
+
}
|
|
58
|
+
throw error; // 其他错误
|
|
59
|
+
});
|
|
60
|
+
}
|
|
14
61
|
class Synchronizer {
|
|
15
62
|
config;
|
|
16
63
|
schema;
|
|
@@ -19,7 +66,7 @@ class Synchronizer {
|
|
|
19
66
|
contextBuilder;
|
|
20
67
|
pushAccessMap = {};
|
|
21
68
|
async startChannel2(context, channel) {
|
|
22
|
-
const { queue, api, selfEncryptInfo, entity, entityId, onFailed } = channel;
|
|
69
|
+
const { queue, api, selfEncryptInfo, entity, entityId, onFailed, timeout } = channel;
|
|
23
70
|
// todo 加密
|
|
24
71
|
const opers = queue.map(ele => ele.oper);
|
|
25
72
|
if (process.env.NODE_ENV === 'development') {
|
|
@@ -31,15 +78,20 @@ class Synchronizer {
|
|
|
31
78
|
const finalApi = (0, path_1.join)(api, selfEncryptInfo.id);
|
|
32
79
|
channel.queue = [];
|
|
33
80
|
try {
|
|
34
|
-
const
|
|
81
|
+
const body = JSON.stringify(opers);
|
|
82
|
+
const { ts, nonce, signature } = await sign(selfEncryptInfo.privateKey, body);
|
|
83
|
+
const res = await fetchWithTimeout(finalApi, {
|
|
35
84
|
method: 'post',
|
|
36
85
|
headers: {
|
|
37
86
|
'Content-Type': 'application/json',
|
|
38
87
|
[OAK_SYNC_HEADER_ENTITY]: entity,
|
|
39
88
|
[OAK_SYNC_HEADER_ENTITY_ID]: entityId,
|
|
89
|
+
[OAK_SYNC_HEADER_TIMESTAMP]: `${ts}`,
|
|
90
|
+
[OAK_SYNC_HEADER_NONCE]: nonce,
|
|
91
|
+
[OAK_SYNC_HEADER_SIGN]: signature,
|
|
40
92
|
},
|
|
41
|
-
body
|
|
42
|
-
});
|
|
93
|
+
body,
|
|
94
|
+
}, timeout || 5000);
|
|
43
95
|
if (res.status !== 200) {
|
|
44
96
|
throw new Error(`sync数据时,访问api「${finalApi}」的结果不是200。「${res.status}」`);
|
|
45
97
|
}
|
|
@@ -133,7 +185,7 @@ class Synchronizer {
|
|
|
133
185
|
}
|
|
134
186
|
}));
|
|
135
187
|
}
|
|
136
|
-
pushOperToChannel(oper, userId, url, endpoint, remoteEntity, remoteEntityId, selfEncryptInfo, onSynchronized, onFailed) {
|
|
188
|
+
pushOperToChannel(oper, userId, url, endpoint, remoteEntity, remoteEntityId, selfEncryptInfo, onSynchronized, onFailed, timeout) {
|
|
137
189
|
if (!this.channelDict[userId]) {
|
|
138
190
|
// channel上缓存这些信息,暂不支持动态更新
|
|
139
191
|
this.channelDict[userId] = {
|
|
@@ -143,6 +195,7 @@ class Synchronizer {
|
|
|
143
195
|
entityId: remoteEntityId,
|
|
144
196
|
selfEncryptInfo,
|
|
145
197
|
onFailed,
|
|
198
|
+
timeout,
|
|
146
199
|
};
|
|
147
200
|
}
|
|
148
201
|
else {
|
|
@@ -189,7 +242,7 @@ class Synchronizer {
|
|
|
189
242
|
if (pushEntityNodes && pushEntityNodes.length > 0) {
|
|
190
243
|
// 每个pushEntityNode代表配置的一个remoteEntity
|
|
191
244
|
await Promise.all(pushEntityNodes.map(async (node) => {
|
|
192
|
-
const { projection, groupByUsers, getRemotePushInfo: getRemoteAccessInfo, groupBySelfEntity, endpoint, actions, onSynchronized, onFailed } = node;
|
|
245
|
+
const { projection, groupByUsers, getRemotePushInfo: getRemoteAccessInfo, groupBySelfEntity, endpoint, actions, onSynchronized, onFailed, timeout } = node;
|
|
193
246
|
// 定义中应该不可能没有actions
|
|
194
247
|
if (!actions || actions.includes(action)) {
|
|
195
248
|
const rows = await context.select(targetEntity, {
|
|
@@ -223,7 +276,7 @@ class Synchronizer {
|
|
|
223
276
|
userId,
|
|
224
277
|
remoteEntityId: entityId,
|
|
225
278
|
});
|
|
226
|
-
this.pushOperToChannel(oper2, userId, url, endpoint, entity, entityId, selfEncryptInfo, onSynchronized, onFailed);
|
|
279
|
+
this.pushOperToChannel(oper2, userId, url, endpoint, entity, entityId, selfEncryptInfo, onSynchronized, onFailed, timeout);
|
|
227
280
|
};
|
|
228
281
|
for (const userId in userSendDict) {
|
|
229
282
|
if (userId !== operatorId || !oper.bornAt) {
|
|
@@ -334,7 +387,7 @@ class Synchronizer {
|
|
|
334
387
|
const { remotes, self } = config;
|
|
335
388
|
// 根据remotes定义,建立从entity到需要同步的远端结点信息的Map
|
|
336
389
|
remotes.forEach((remote) => {
|
|
337
|
-
const { getPushInfo, pushEntities: pushEntityDefs, endpoint, pathToUser, relationName: rnRemote, onFailed } = remote;
|
|
390
|
+
const { getPushInfo, pushEntities: pushEntityDefs, endpoint, pathToUser, relationName: rnRemote, onFailed, timeout } = remote;
|
|
338
391
|
if (pushEntityDefs) {
|
|
339
392
|
const pushEntities = [];
|
|
340
393
|
const endpoint2 = (0, path_1.join)(endpoint || 'sync', self.entity);
|
|
@@ -402,6 +455,7 @@ class Synchronizer {
|
|
|
402
455
|
actions,
|
|
403
456
|
onSynchronized,
|
|
404
457
|
onFailed,
|
|
458
|
+
timeout,
|
|
405
459
|
}];
|
|
406
460
|
}
|
|
407
461
|
else {
|
|
@@ -415,6 +469,7 @@ class Synchronizer {
|
|
|
415
469
|
actions,
|
|
416
470
|
onSynchronized,
|
|
417
471
|
onFailed,
|
|
472
|
+
timeout,
|
|
418
473
|
});
|
|
419
474
|
}
|
|
420
475
|
}
|
|
@@ -462,7 +517,7 @@ class Synchronizer {
|
|
|
462
517
|
fn: async (context, params, headers, req, body) => {
|
|
463
518
|
// body中是传过来的oper数组信息
|
|
464
519
|
const { entity, entityId } = params;
|
|
465
|
-
const { [OAK_SYNC_HEADER_ENTITY]: meEntity, [OAK_SYNC_HEADER_ENTITY_ID]: meEntityId } = headers;
|
|
520
|
+
const { [OAK_SYNC_HEADER_ENTITY]: meEntity, [OAK_SYNC_HEADER_ENTITY_ID]: meEntityId, [OAK_SYNC_HEADER_NONCE]: syncNonce, [OAK_SYNC_HEADER_TIMESTAMP]: syncTs, [OAK_SYNC_HEADER_SIGN]: syncSign } = headers;
|
|
466
521
|
if (process.env.NODE_ENV === 'development') {
|
|
467
522
|
console.log('接收到来自远端的sync数据', entity, JSON.stringify(body));
|
|
468
523
|
}
|
|
@@ -493,7 +548,13 @@ class Synchronizer {
|
|
|
493
548
|
if (cxtInfo) {
|
|
494
549
|
await context.initialize(cxtInfo);
|
|
495
550
|
}
|
|
496
|
-
|
|
551
|
+
const syncTimestamp = parseInt(syncTs, 10);
|
|
552
|
+
if (!(Date.now() - syncTimestamp < 10000)) {
|
|
553
|
+
throw new Error('同步时钟漂移过长');
|
|
554
|
+
}
|
|
555
|
+
if (!verify(publicKey, JSON.stringify(body), syncTs, syncNonce, syncSign)) {
|
|
556
|
+
throw new Error('sync验签失败');
|
|
557
|
+
}
|
|
497
558
|
const opers = body;
|
|
498
559
|
const ids = opers.map(ele => ele.id);
|
|
499
560
|
const existsIds = (await context.select('oper', {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oak-backend-base",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.10",
|
|
4
4
|
"description": "oak-backend-base",
|
|
5
5
|
"main": "lib/index",
|
|
6
6
|
"author": {
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
"mysql": "^2.18.1",
|
|
21
21
|
"mysql2": "^2.3.3",
|
|
22
22
|
"node-schedule": "^2.1.0",
|
|
23
|
-
"oak-common-aspect": "^3.0.
|
|
23
|
+
"oak-common-aspect": "^3.0.4",
|
|
24
24
|
"oak-db": "^3.3.2",
|
|
25
|
-
"oak-domain": "^5.1.
|
|
26
|
-
"oak-frontend-base": "^5.3.
|
|
25
|
+
"oak-domain": "^5.1.10",
|
|
26
|
+
"oak-frontend-base": "^5.3.19",
|
|
27
27
|
"socket.io": "^4.7.2",
|
|
28
28
|
"socket.io-client": "^4.7.2",
|
|
29
29
|
"uuid": "^8.3.2"
|