@abtnode/core 1.16.38 → 1.16.39-beta-20250209-032436-5ceca2cb
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/blocklet/migration-dist/migration.cjs +6 -1
- package/lib/index.js +4 -0
- package/lib/router/helper.js +119 -29
- package/lib/router/index.js +17 -1
- package/lib/router/monitor.js +55 -0
- package/lib/router/security/blocker.js +27 -0
- package/lib/router/security/limiter.js +54 -0
- package/lib/router/security/scanner.js +46 -0
- package/lib/router/watcher.js +135 -0
- package/lib/states/audit-log.js +40 -6
- package/lib/states/blacklist.js +46 -0
- package/lib/states/index.js +4 -0
- package/lib/validators/node.js +13 -0
- package/package.json +23 -22
- package/lib/util/monitor.js +0 -167
|
@@ -537,6 +537,7 @@ module.exports = Object.freeze({
|
|
|
537
537
|
WELLKNOWN_PATH_PREFIX: '/.well-known',
|
|
538
538
|
WELLKNOWN_ACME_CHALLENGE_PREFIX: '/.well-known/acme-challenge',
|
|
539
539
|
WELLKNOWN_DID_RESOLVER_PREFIX: '/.well-known/did.json', // server wellknown endpoint
|
|
540
|
+
WELLKNOWN_BLACKLIST_PREFIX: '/.well-known/blacklist',
|
|
540
541
|
WELLKNOWN_PING_PREFIX: '/.well-known/ping',
|
|
541
542
|
WELLKNOWN_ANALYTICS_PREFIX: '/.well-known/analytics',
|
|
542
543
|
WELLKNOWN_SERVICE_PATH_PREFIX: '/.well-known/service',
|
|
@@ -747,6 +748,10 @@ module.exports = Object.freeze({
|
|
|
747
748
|
PUSH: 'push',
|
|
748
749
|
WEBHOOK: 'webhook',
|
|
749
750
|
},
|
|
751
|
+
|
|
752
|
+
BLACKLIST_SCOPE: {
|
|
753
|
+
ROUTER: 'router',
|
|
754
|
+
},
|
|
750
755
|
});
|
|
751
756
|
|
|
752
757
|
|
|
@@ -38649,7 +38654,7 @@ module.exports = require("zlib");
|
|
|
38649
38654
|
/***/ ((module) => {
|
|
38650
38655
|
|
|
38651
38656
|
"use strict";
|
|
38652
|
-
module.exports = /*#__PURE__*/JSON.parse('{"name":"@abtnode/core","publishConfig":{"access":"public"},"version":"1.16.
|
|
38657
|
+
module.exports = /*#__PURE__*/JSON.parse('{"name":"@abtnode/core","publishConfig":{"access":"public"},"version":"1.16.38","description":"","main":"lib/index.js","files":["lib"],"scripts":{"lint":"eslint tests lib --ignore-pattern \'tests/assets/*\'","lint:fix":"eslint --fix tests lib","test":"node tools/jest.js","coverage":"npm run test -- --coverage"},"keywords":[],"author":"wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)","license":"Apache-2.0","dependencies":{"@abtnode/analytics":"1.16.38","@abtnode/auth":"1.16.38","@abtnode/certificate-manager":"1.16.38","@abtnode/constant":"1.16.38","@abtnode/cron":"1.16.38","@abtnode/docker-utils":"1.16.38","@abtnode/logger":"1.16.38","@abtnode/models":"1.16.38","@abtnode/queue":"1.16.38","@abtnode/rbac":"1.16.38","@abtnode/router-provider":"1.16.38","@abtnode/static-server":"1.16.38","@abtnode/timemachine":"1.16.38","@abtnode/util":"1.16.38","@arcblock/did":"1.19.9","@arcblock/did-auth":"1.19.9","@arcblock/did-ext":"^1.19.9","@arcblock/did-motif":"^1.1.13","@arcblock/did-util":"1.19.9","@arcblock/event-hub":"1.19.9","@arcblock/jwt":"^1.19.9","@arcblock/pm2-events":"^0.0.5","@arcblock/validator":"^1.19.9","@arcblock/vc":"1.19.9","@blocklet/constant":"1.16.38","@blocklet/did-space-js":"^1.0.9","@blocklet/env":"1.16.38","@blocklet/meta":"1.16.38","@blocklet/resolver":"1.16.38","@blocklet/sdk":"1.16.38","@blocklet/store":"1.16.38","@fidm/x509":"^1.2.1","@ocap/mcrypto":"1.19.9","@ocap/util":"1.19.9","@ocap/wallet":"1.19.9","@slack/webhook":"^5.0.4","archiver":"^7.0.1","axios":"^1.7.9","axon":"^2.0.3","chalk":"^4.1.2","cross-spawn":"^7.0.3","deep-diff":"^1.0.2","detect-port":"^1.5.1","envfile":"^7.1.0","escape-string-regexp":"^4.0.0","fast-glob":"^3.3.2","filesize":"^10.1.1","flat":"^5.0.2","rate-limiter-flexible":"^5.0.5","fs-extra":"^11.2.0","get-port":"^5.1.1","hasha":"^5.2.2","is-base64":"^1.1.0","is-cidr":"4","is-ip":"3","is-url":"^1.2.4","joi":"17.12.2","joi-extension-semver":"^5.0.0","js-yaml":"^4.1.0","kill-port":"^2.0.1","lodash":"^4.17.21","lru-cache":"^11.0.2","node-stream-zip":"^1.15.0","p-all":"^3.0.0","p-limit":"^3.1.0","p-map":"^4.0.0","p-retry":"^4.6.2","read-last-lines":"^1.8.0","semver":"^7.6.3","sequelize":"^6.35.0","shelljs":"^0.8.5","ssri":"^8.0.1","stream-throttle":"^0.1.3","stream-to-promise":"^3.0.0","systeminformation":"^5.23.3","tail":"^2.2.4","tar":"^6.1.11","transliteration":"^2.3.5","ua-parser-js":"^1.0.2","ufo":"^1.5.3","uuid":"^9.0.1","valid-url":"^1.0.9","which":"^2.0.2","xbytes":"^1.8.0"},"devDependencies":{"expand-tilde":"^2.0.2","express":"^4.18.2","jest":"^29.7.0","unzipper":"^0.10.11"},"gitHead":"e5764f753181ed6a7c615cd4fc6682aacf0cb7cd"}');
|
|
38653
38658
|
|
|
38654
38659
|
/***/ }),
|
|
38655
38660
|
|
package/lib/index.js
CHANGED
|
@@ -42,6 +42,7 @@ const getMetaFromUrl = require('./util/get-meta-from-url');
|
|
|
42
42
|
const getDynamicComponents = require('./util/get-dynamic-components');
|
|
43
43
|
const SecurityAPI = require('./blocklet/security');
|
|
44
44
|
const dockerRestartAllContainers = require('./util/docker/docker-restart-all-containers');
|
|
45
|
+
const RouterBlocker = require('./router/security/blocker');
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* @typedef {{} & import('./api/team')}} TNode
|
|
@@ -275,6 +276,7 @@ function ABTNode(options) {
|
|
|
275
276
|
deleteRoutingRule,
|
|
276
277
|
addDomainAlias,
|
|
277
278
|
deleteDomainAlias,
|
|
279
|
+
getGatewayBlacklist,
|
|
278
280
|
} = getRouterHelpers({
|
|
279
281
|
dataDirs,
|
|
280
282
|
routingSnapshot,
|
|
@@ -590,6 +592,7 @@ function ABTNode(options) {
|
|
|
590
592
|
addDomainAlias,
|
|
591
593
|
deleteDomainAlias,
|
|
592
594
|
isDidDomain: routerManager.isDidDomain.bind(routerManager),
|
|
595
|
+
getGatewayBlacklist,
|
|
593
596
|
|
|
594
597
|
getRoutingProviders: () => listProviders(dataDirs.router),
|
|
595
598
|
checkDomains: domainStatus.checkDomainsStatus.bind(domainStatus),
|
|
@@ -704,6 +707,7 @@ function ABTNode(options) {
|
|
|
704
707
|
LogRotator.getCron(),
|
|
705
708
|
...getStateCrons(states),
|
|
706
709
|
...nodeAPI.getCrons(),
|
|
710
|
+
...RouterBlocker.getSecurityCrons(),
|
|
707
711
|
].filter((x) => options.daemon || (options.service && get(x, 'options.runInService'))),
|
|
708
712
|
onError: (error, name) => {
|
|
709
713
|
states.notification.create({
|
package/lib/router/helper.js
CHANGED
|
@@ -9,7 +9,9 @@ const isUrl = require('is-url');
|
|
|
9
9
|
const get = require('lodash/get');
|
|
10
10
|
const cloneDeep = require('lodash/cloneDeep');
|
|
11
11
|
const groupBy = require('lodash/groupBy');
|
|
12
|
+
const uniq = require('lodash/uniq');
|
|
12
13
|
const isEqual = require('lodash/isEqual');
|
|
14
|
+
const countBy = require('lodash/countBy');
|
|
13
15
|
const { joinURL } = require('ufo');
|
|
14
16
|
const {
|
|
15
17
|
replaceSlotToIp,
|
|
@@ -47,6 +49,7 @@ const {
|
|
|
47
49
|
LOG_RETAIN_IN_DAYS,
|
|
48
50
|
EVENTS,
|
|
49
51
|
DEFAULT_IP_DOMAIN,
|
|
52
|
+
WELLKNOWN_BLACKLIST_PREFIX,
|
|
50
53
|
} = require('@abtnode/constant');
|
|
51
54
|
const {
|
|
52
55
|
BLOCKLET_DYNAMIC_PATH_PREFIX,
|
|
@@ -75,7 +78,10 @@ const { getFromCache: getAccessibleExternalNodeIp } = require('../util/get-acces
|
|
|
75
78
|
|
|
76
79
|
const Router = require('./index');
|
|
77
80
|
const states = require('../states');
|
|
78
|
-
const monitor = require('
|
|
81
|
+
const monitor = require('./monitor');
|
|
82
|
+
const scanner = require('./security/scanner');
|
|
83
|
+
const blocker = require('./security/blocker');
|
|
84
|
+
const { createLimiter } = require('./security/limiter');
|
|
79
85
|
const {
|
|
80
86
|
getBlockletDomainGroupName,
|
|
81
87
|
getDidFromDomainGroupName,
|
|
@@ -840,6 +846,12 @@ module.exports = function getRouterHelpers({
|
|
|
840
846
|
to: proxyTarget,
|
|
841
847
|
};
|
|
842
848
|
|
|
849
|
+
const blacklistWellknownRule = {
|
|
850
|
+
isProtected: true,
|
|
851
|
+
from: { pathPrefix: WELLKNOWN_BLACKLIST_PREFIX },
|
|
852
|
+
to: proxyTarget,
|
|
853
|
+
};
|
|
854
|
+
|
|
843
855
|
const acmeChallengeWellknownRule = {
|
|
844
856
|
isProtected: true,
|
|
845
857
|
from: { pathPrefix: WELLKNOWN_ACME_CHALLENGE_PREFIX },
|
|
@@ -849,9 +861,10 @@ module.exports = function getRouterHelpers({
|
|
|
849
861
|
if (site) {
|
|
850
862
|
const didResolverRuleUpdateRes = await upsertSiteRule({ site, rule: didResolverWellknownRule }, context);
|
|
851
863
|
const acmeRuleUpdateRes = await upsertSiteRule({ site, rule: acmeChallengeWellknownRule }, context);
|
|
864
|
+
const blacklistUpdateRes = await upsertSiteRule({ site, rule: blacklistWellknownRule }, context);
|
|
852
865
|
const pingRuleRes = await upsertSiteRule({ site, rule: pingWellknownRule }, context);
|
|
853
866
|
const analyticsRuleRes = await upsertSiteRule({ site, rule: analyticsWellknownRule }, context);
|
|
854
|
-
return didResolverRuleUpdateRes || acmeRuleUpdateRes || pingRuleRes || analyticsRuleRes;
|
|
867
|
+
return didResolverRuleUpdateRes || acmeRuleUpdateRes || blacklistUpdateRes || pingRuleRes || analyticsRuleRes;
|
|
855
868
|
}
|
|
856
869
|
|
|
857
870
|
await routerManager.addRoutingSite(
|
|
@@ -860,7 +873,13 @@ module.exports = function getRouterHelpers({
|
|
|
860
873
|
domain: DOMAIN_FOR_INTERNAL_SITE,
|
|
861
874
|
port: await getWellknownSitePort(),
|
|
862
875
|
name: NAME_FOR_WELLKNOWN_SITE,
|
|
863
|
-
rules: [
|
|
876
|
+
rules: [
|
|
877
|
+
didResolverWellknownRule,
|
|
878
|
+
acmeChallengeWellknownRule,
|
|
879
|
+
blacklistWellknownRule,
|
|
880
|
+
pingWellknownRule,
|
|
881
|
+
analyticsWellknownRule,
|
|
882
|
+
],
|
|
864
883
|
isProtected: true,
|
|
865
884
|
},
|
|
866
885
|
skipCheckDynamicBlacklist: true,
|
|
@@ -1302,6 +1321,81 @@ module.exports = function getRouterHelpers({
|
|
|
1302
1321
|
}
|
|
1303
1322
|
|
|
1304
1323
|
const providers = {}; // we need to keep reference for different router instances
|
|
1324
|
+
|
|
1325
|
+
const startAccessLogWatcher = (info) => {
|
|
1326
|
+
const providerName = get(info, 'routing.provider', null);
|
|
1327
|
+
if (!providerName || !providers[providerName]) {
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
if (daemon && process.env.ABT_NODE_MONITOR_GATEWAY_5XX === '1') {
|
|
1332
|
+
monitor.stopLogWatcher();
|
|
1333
|
+
const logs = providers[providerName].getLogFilesForToday();
|
|
1334
|
+
monitor.startLogWatcher(logs.access, (logEntries) => {
|
|
1335
|
+
notification.create({
|
|
1336
|
+
title: 'Server 5xx Alert',
|
|
1337
|
+
description: `5xx request detected: ${JSON.stringify(logEntries.slice(0, 1), null, 2)}`,
|
|
1338
|
+
entityType: 'server',
|
|
1339
|
+
severity: 'error',
|
|
1340
|
+
sticky: true,
|
|
1341
|
+
});
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
|
|
1346
|
+
const startErrorLogWatcher = async (info, shouldScheduleReload = false) => {
|
|
1347
|
+
const providerName = get(info, 'routing.provider', null);
|
|
1348
|
+
if (!providerName || !providers[providerName]) {
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
if (daemon && info.routing.blockPolicy?.autoBlocking?.enabled && providers[providerName].supportsModSecurity()) {
|
|
1353
|
+
scanner.stopLogWatcher();
|
|
1354
|
+
const logs = providers[providerName].getLogFilesForToday();
|
|
1355
|
+
const limiter = createLimiter(info.routing.blockPolicy.autoBlocking);
|
|
1356
|
+
scanner.startLogWatcher(logs.error, async (logEntries) => {
|
|
1357
|
+
const blocked = [];
|
|
1358
|
+
const grouped = countBy(logEntries, 'ip');
|
|
1359
|
+
for (const [ip, count] of Object.entries(grouped)) {
|
|
1360
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1361
|
+
const timeout = await limiter.check(ip, count);
|
|
1362
|
+
if (timeout > 0) {
|
|
1363
|
+
blocked.push(timeout);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
logger.debug('router error detected', { logEntries, grouped, blocked, timestamp: dayjs().unix() });
|
|
1368
|
+
|
|
1369
|
+
if (blocked.length) {
|
|
1370
|
+
providers[providerName].throttledReload();
|
|
1371
|
+
// Reload 1 second after block expiration
|
|
1372
|
+
uniq(blocked).forEach((timeout) => {
|
|
1373
|
+
setTimeout(() => {
|
|
1374
|
+
logger.info('router reload on block expire', { timeout, timestamp: dayjs().unix() });
|
|
1375
|
+
providers[providerName].throttledReload();
|
|
1376
|
+
}, timeout + 1000);
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// Schedule router reload for active blacklist
|
|
1383
|
+
if (daemon && shouldScheduleReload) {
|
|
1384
|
+
const blacklist = await blocker.getActiveBlacklist(false);
|
|
1385
|
+
if (blacklist.length) {
|
|
1386
|
+
logger.info('schedule router reload for active blacklist', blacklist);
|
|
1387
|
+
const now = dayjs().unix();
|
|
1388
|
+
blacklist.forEach((x) => {
|
|
1389
|
+
const timeout = x.expiresAt - now + 1;
|
|
1390
|
+
setTimeout(() => {
|
|
1391
|
+
logger.info('router reload on block expire', { ip: x.key });
|
|
1392
|
+
providers[providerName].throttledReload();
|
|
1393
|
+
}, timeout * 1000);
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1305
1399
|
const handleRouting = async (nodeInfo) => {
|
|
1306
1400
|
const now = Date.now();
|
|
1307
1401
|
logger.info('start handle routing');
|
|
@@ -1315,6 +1409,8 @@ module.exports = function getRouterHelpers({
|
|
|
1315
1409
|
throw new Error(`${providerName} pre-check failed, ${checkResult.error}`);
|
|
1316
1410
|
}
|
|
1317
1411
|
|
|
1412
|
+
const shouldScheduleReload = !providers[providerName];
|
|
1413
|
+
|
|
1318
1414
|
if (providers[providerName]) {
|
|
1319
1415
|
await providers[providerName].reload();
|
|
1320
1416
|
} else {
|
|
@@ -1364,19 +1460,8 @@ module.exports = function getRouterHelpers({
|
|
|
1364
1460
|
await providers[providerName].start();
|
|
1365
1461
|
}
|
|
1366
1462
|
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
const logs = providers[providerName].getLogFilesForToday();
|
|
1370
|
-
monitor.startLogWatcher(logs.access, (logEntries) => {
|
|
1371
|
-
notification.create({
|
|
1372
|
-
title: 'Server 5xx Alert',
|
|
1373
|
-
description: `5xx request detected: ${JSON.stringify(logEntries.slice(0, 1), null, 2)}`,
|
|
1374
|
-
entityType: 'server',
|
|
1375
|
-
severity: 'error',
|
|
1376
|
-
sticky: true,
|
|
1377
|
-
});
|
|
1378
|
-
});
|
|
1379
|
-
}
|
|
1463
|
+
await startAccessLogWatcher(nodeInfo);
|
|
1464
|
+
await startErrorLogWatcher(nodeInfo, shouldScheduleReload);
|
|
1380
1465
|
|
|
1381
1466
|
logger.info('done handle routing', { cost: Date.now() - now });
|
|
1382
1467
|
};
|
|
@@ -1386,20 +1471,9 @@ module.exports = function getRouterHelpers({
|
|
|
1386
1471
|
const providerName = get(info, 'routing.provider', null);
|
|
1387
1472
|
|
|
1388
1473
|
if (providerName && providers[providerName] && typeof providers[providerName].rotateLogs === 'function') {
|
|
1389
|
-
monitor.stopLogWatcher();
|
|
1390
1474
|
await providers[providerName].rotateLogs({ retain: LOG_RETAIN_IN_DAYS });
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
monitor.startLogWatcher(logs.access, (logEntries) => {
|
|
1394
|
-
notification.create({
|
|
1395
|
-
title: 'Server 5xx Alert',
|
|
1396
|
-
description: `5xx request detected: ${JSON.stringify(logEntries.slice(0, 1), null, 2)}`,
|
|
1397
|
-
entityType: 'server',
|
|
1398
|
-
severity: 'error',
|
|
1399
|
-
sticky: true,
|
|
1400
|
-
});
|
|
1401
|
-
});
|
|
1402
|
-
}
|
|
1475
|
+
await startAccessLogWatcher(info);
|
|
1476
|
+
await startErrorLogWatcher(info);
|
|
1403
1477
|
}
|
|
1404
1478
|
};
|
|
1405
1479
|
|
|
@@ -1692,6 +1766,21 @@ module.exports = function getRouterHelpers({
|
|
|
1692
1766
|
// @ts-ignore
|
|
1693
1767
|
const gatewayBlacklistRefreshInterval = +process.env.ABT_NODE_BLACKLIST_REFRESH_INTERVAL || 2;
|
|
1694
1768
|
|
|
1769
|
+
const getGatewayBlacklist = async (type = 'both') => {
|
|
1770
|
+
const info = await nodeState.read();
|
|
1771
|
+
const blockPolicy = info.routing.blockPolicy || { enabled: false, blacklist: [] };
|
|
1772
|
+
const blacklist = await expandBlacklist(blockPolicy.blacklist);
|
|
1773
|
+
const blockedIps = await blocker.getActiveBlacklist();
|
|
1774
|
+
if (type === 'both') {
|
|
1775
|
+
return uniq([...blacklist, ...blockedIps]);
|
|
1776
|
+
}
|
|
1777
|
+
if (type === 'dynamic') {
|
|
1778
|
+
return uniq([...blockedIps]);
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
return uniq([...blacklist]);
|
|
1782
|
+
};
|
|
1783
|
+
|
|
1695
1784
|
return {
|
|
1696
1785
|
ensureDashboardRouting,
|
|
1697
1786
|
ensureBlockletRouting,
|
|
@@ -1748,6 +1837,7 @@ module.exports = function getRouterHelpers({
|
|
|
1748
1837
|
addDomainAlias,
|
|
1749
1838
|
deleteDomainAlias,
|
|
1750
1839
|
isGatewayCacheEnabled,
|
|
1840
|
+
getGatewayBlacklist,
|
|
1751
1841
|
};
|
|
1752
1842
|
};
|
|
1753
1843
|
|
package/lib/router/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const get = require('lodash/get');
|
|
2
|
+
const uniq = require('lodash/uniq');
|
|
3
|
+
const throttle = require('lodash/throttle');
|
|
2
4
|
const pick = require('lodash/pick');
|
|
3
5
|
const isEqual = require('lodash/isEqual');
|
|
4
6
|
const cloneDeep = require('lodash/cloneDeep');
|
|
@@ -17,6 +19,7 @@ const logger = require('@abtnode/logger')('@abtnode/core:router');
|
|
|
17
19
|
|
|
18
20
|
const { isGatewayCacheEnabled, isBlockletSite } = require('../util');
|
|
19
21
|
const { expandBlacklist } = require('../util/router');
|
|
22
|
+
const { getActiveBlacklist } = require('./security/blocker');
|
|
20
23
|
const IP = require('../util/ip');
|
|
21
24
|
|
|
22
25
|
const isServiceFeDevelopment = process.env.ABT_NODE_SERVICE_FE_PORT;
|
|
@@ -101,6 +104,9 @@ class Router {
|
|
|
101
104
|
this.provider = provider;
|
|
102
105
|
this.getRoutingParams = getRoutingParams;
|
|
103
106
|
this.routingTable = [];
|
|
107
|
+
|
|
108
|
+
this.throttledReload = throttle(() => this.reload(), 5000, { leading: true, trailing: true });
|
|
109
|
+
this.throttledRestart = throttle(() => this.restart(), 5000, { leading: true, trailing: true });
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
async updateRoutingTable() {
|
|
@@ -128,6 +134,12 @@ class Router {
|
|
|
128
134
|
if (result?.internal) {
|
|
129
135
|
blockPolicy.blacklist = blockPolicy.blacklist.filter((x) => x !== result.internal);
|
|
130
136
|
}
|
|
137
|
+
|
|
138
|
+
// Append blocked ips from database
|
|
139
|
+
const blockedIps = await getActiveBlacklist();
|
|
140
|
+
logger.info('router auto blocked ips', blockedIps);
|
|
141
|
+
|
|
142
|
+
blockPolicy.blacklist = uniq([...blockPolicy.blacklist, ...blockedIps]);
|
|
131
143
|
}
|
|
132
144
|
|
|
133
145
|
const proxyPolicy = nodeInfo.routing.proxyPolicy || {
|
|
@@ -141,7 +153,7 @@ class Router {
|
|
|
141
153
|
enabled: false,
|
|
142
154
|
mode: 'DetectionOnly',
|
|
143
155
|
inboundAnomalyScoreThreshold: 10,
|
|
144
|
-
outboundAnomalyScoreThreshold:
|
|
156
|
+
outboundAnomalyScoreThreshold: 10,
|
|
145
157
|
};
|
|
146
158
|
|
|
147
159
|
await this.provider.update({
|
|
@@ -212,6 +224,10 @@ class Router {
|
|
|
212
224
|
clearCache(group) {
|
|
213
225
|
return this.provider.clearCache(group);
|
|
214
226
|
}
|
|
227
|
+
|
|
228
|
+
supportsModSecurity() {
|
|
229
|
+
return !!this.provider.capabilities?.modsecurity;
|
|
230
|
+
}
|
|
215
231
|
}
|
|
216
232
|
|
|
217
233
|
Router.formatSites = (sites = []) => {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const { createLogWatcher } = require('./watcher');
|
|
2
|
+
|
|
3
|
+
// Function to parse the log entry and extract relevant information
|
|
4
|
+
function parseLogEntry(line, check = true) {
|
|
5
|
+
const regex =
|
|
6
|
+
/^(\S+) - (\S+) \[([^\]]+)\] (\S+) "([^"]+)" "([^"]+)" (\d{3}) (\d+) "(.*?)" "(.*?)" "(.*?)" rt="(\S+)" uid="(.+?)" uos="(.+?)" uct="(\S+)" uht="(\S+)" urt="(\S+)"/;
|
|
7
|
+
const match = line.match(regex);
|
|
8
|
+
if (match) {
|
|
9
|
+
const logEntry = {
|
|
10
|
+
ip: match[1],
|
|
11
|
+
remoteUser: match[2],
|
|
12
|
+
timeIso8601: match[3],
|
|
13
|
+
requestId: match[4],
|
|
14
|
+
host: match[5],
|
|
15
|
+
request: match[6],
|
|
16
|
+
status: parseInt(match[7], 10),
|
|
17
|
+
bodyBytesSent: parseInt(match[8], 10),
|
|
18
|
+
referer: match[9],
|
|
19
|
+
userAgent: match[10],
|
|
20
|
+
forwardedFor: match[11],
|
|
21
|
+
requestTime: parseFloat(match[12]),
|
|
22
|
+
connectedDid: match[13],
|
|
23
|
+
connectedWalletOs: match[14],
|
|
24
|
+
upstreamConnectTime: parseFloat(match[15]),
|
|
25
|
+
upstreamHeaderTime: parseFloat(match[16]),
|
|
26
|
+
upstreamResponseTime: parseFloat(match[17]),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if (!check) {
|
|
30
|
+
return logEntry;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (
|
|
34
|
+
logEntry.status >= 500 &&
|
|
35
|
+
logEntry.status !== 503 &&
|
|
36
|
+
logEntry.status <= 599 &&
|
|
37
|
+
logEntry.request.includes('/.well-known/did.json') === false &&
|
|
38
|
+
logEntry.request.includes('/websocket?token=') === false &&
|
|
39
|
+
logEntry.request.includes('/.well-known/service/health') === false
|
|
40
|
+
) {
|
|
41
|
+
console.warn(`5xx request detected: ${logEntry.host}`, line);
|
|
42
|
+
return logEntry;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const logWatcher = createLogWatcher(parseLogEntry);
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
parseLogEntry,
|
|
53
|
+
startLogWatcher: logWatcher.start,
|
|
54
|
+
stopLogWatcher: logWatcher.stop,
|
|
55
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const { BLACKLIST_SCOPE } = require('@abtnode/constant');
|
|
2
|
+
const logger = require('@abtnode/logger')('@abtnode/core:router:security:blocker');
|
|
3
|
+
|
|
4
|
+
const states = require('../../states');
|
|
5
|
+
|
|
6
|
+
async function cleanupExpiredBlacklist() {
|
|
7
|
+
const result = await states.blacklist.removeExpiredByScope(BLACKLIST_SCOPE.ROUTER);
|
|
8
|
+
logger.info('Cleanup router blacklists', { result });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function getActiveBlacklist(justIp = true) {
|
|
12
|
+
const result = await states.blacklist.findActiveByScope(BLACKLIST_SCOPE.ROUTER);
|
|
13
|
+
return justIp ? result.map((item) => item.key) : result;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
getActiveBlacklist,
|
|
18
|
+
cleanupExpiredBlacklist,
|
|
19
|
+
getSecurityCrons: () => [
|
|
20
|
+
{
|
|
21
|
+
name: 'cleanup-router-blacklist',
|
|
22
|
+
time: '0 0 * * * *', // refresh blocking list every hour
|
|
23
|
+
fn: cleanupExpiredBlacklist,
|
|
24
|
+
options: { runOnInit: process.env.ABT_NODE_JOB_NAME === 'cleanup-router-blacklist' },
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// FIXME: use a crash safe store for the limiter
|
|
2
|
+
const dayjs = require('@abtnode/util/lib/dayjs');
|
|
3
|
+
const { BLACKLIST_SCOPE } = require('@abtnode/constant');
|
|
4
|
+
const { RateLimiterMemory } = require('rate-limiter-flexible');
|
|
5
|
+
|
|
6
|
+
const logger = require('@abtnode/logger')('@abtnode/core:router:security:limiter');
|
|
7
|
+
|
|
8
|
+
const states = require('../../states');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create a rate limiter for suspicious requests.
|
|
12
|
+
* https://github.com/animir/node-rate-limiter-flexible/wiki/Options
|
|
13
|
+
*
|
|
14
|
+
* @param {{
|
|
15
|
+
* windowSize: number;
|
|
16
|
+
* windowQuota: number;
|
|
17
|
+
* blockDuration: number;
|
|
18
|
+
* }} options
|
|
19
|
+
* @returns {{ check: (ip: string, points?: number) => Promise<number> }}
|
|
20
|
+
*/
|
|
21
|
+
function createLimiter(options) {
|
|
22
|
+
const limiter = new RateLimiterMemory({
|
|
23
|
+
points: options.windowQuota,
|
|
24
|
+
duration: options.windowSize,
|
|
25
|
+
blockDuration: options.blockDuration,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
logger.info('Rate limiter created', { options });
|
|
29
|
+
|
|
30
|
+
const check = async (ip, points = 1) => {
|
|
31
|
+
try {
|
|
32
|
+
const result = await limiter.consume(ip, points);
|
|
33
|
+
logger.debug('Rate limit not exceeded', { ip, result });
|
|
34
|
+
} catch (err) {
|
|
35
|
+
logger.info('Rate limit has exceeded', { ip, err });
|
|
36
|
+
const expiresAt = dayjs().add(err.msBeforeNext, 'ms').unix();
|
|
37
|
+
const added = await states.blacklist.addItem(BLACKLIST_SCOPE.ROUTER, ip, expiresAt);
|
|
38
|
+
if (added) {
|
|
39
|
+
logger.info('User IP blocked', { ip, expiresAt });
|
|
40
|
+
return err.msBeforeNext;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return 0;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
check,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = {
|
|
53
|
+
createLimiter,
|
|
54
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const { createLogWatcher } = require('../watcher');
|
|
2
|
+
|
|
3
|
+
// Function to parse the log entry and extract relevant information
|
|
4
|
+
function parseLogEntry(line) {
|
|
5
|
+
// Quick check before regex
|
|
6
|
+
if (!line.includes('ModSecurity:')) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const pattern =
|
|
11
|
+
/^(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}).*\[error\].*\[client (\d+\.\d+\.\d+\.\d+)\].*ModSecurity:.*\[id "(\d+)"\].*\[unique_id "([^"]+)"\]/;
|
|
12
|
+
|
|
13
|
+
const match = line.match(pattern);
|
|
14
|
+
if (match) {
|
|
15
|
+
const logEntry = {
|
|
16
|
+
type: 'modsecurity',
|
|
17
|
+
timestamp: match[1],
|
|
18
|
+
ip: match[2],
|
|
19
|
+
ruleId: match[3],
|
|
20
|
+
requestId: match[4],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Ignore old log entries
|
|
24
|
+
if (process.env.NODE_ENV === 'test') {
|
|
25
|
+
return logEntry;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
const timestamp = new Date(match[1]).getTime();
|
|
30
|
+
if (now - timestamp > 1000) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return logEntry;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const logWatcher = createLogWatcher(parseLogEntry);
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
parseLogEntry,
|
|
44
|
+
startLogWatcher: logWatcher.start,
|
|
45
|
+
stopLogWatcher: logWatcher.stop,
|
|
46
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/* eslint-disable no-await-in-loop */
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
const THROTTLE_DELAY = 1000; // 1 second throttle
|
|
5
|
+
|
|
6
|
+
async function readLogFile(filePath, startPosition = 0) {
|
|
7
|
+
let fd = null;
|
|
8
|
+
try {
|
|
9
|
+
if (!fs.existsSync(filePath)) {
|
|
10
|
+
return { lines: [], fileSize: 0 };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const realPath = await fs.promises.realpath(filePath);
|
|
14
|
+
fd = await fs.promises.open(realPath, 'r');
|
|
15
|
+
const stats = await fd.stat();
|
|
16
|
+
const fileSize = stats.size;
|
|
17
|
+
|
|
18
|
+
if (startPosition < 0) {
|
|
19
|
+
// eslint-disable-next-line no-param-reassign
|
|
20
|
+
startPosition = Math.max(0, fileSize + startPosition);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (startPosition >= fileSize) {
|
|
24
|
+
return { lines: [], fileSize };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const readSize = fileSize - startPosition;
|
|
28
|
+
if (readSize <= 0) {
|
|
29
|
+
return { lines: [], fileSize };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const buffer = Buffer.alloc(readSize);
|
|
33
|
+
const { bytesRead } = await fd.read(buffer, 0, readSize, startPosition);
|
|
34
|
+
const content = buffer.slice(0, bytesRead).toString();
|
|
35
|
+
|
|
36
|
+
const lines = content
|
|
37
|
+
.split('\n')
|
|
38
|
+
.map((line) => line.trim())
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
|
|
41
|
+
return { lines, fileSize };
|
|
42
|
+
} finally {
|
|
43
|
+
if (fd) {
|
|
44
|
+
await fd.close();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A generic log watcher that can be used for different log formats
|
|
50
|
+
function createLogWatcher(parseLogEntry, throttleDelay = THROTTLE_DELAY) {
|
|
51
|
+
let watcher;
|
|
52
|
+
process.on('SIGINT', () => {
|
|
53
|
+
watcher?.close();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
process.on('SIGTERM', () => {
|
|
57
|
+
watcher?.close();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function start(logFilePath, onLogEntry, initialBufferSize = 1024) {
|
|
61
|
+
if (!fs.existsSync(logFilePath)) {
|
|
62
|
+
console.error(`Log file ${logFilePath} does not exist`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.warn(`Start watching ${logFilePath}...`);
|
|
67
|
+
|
|
68
|
+
let isProcessing = false;
|
|
69
|
+
let isFirstRun = true;
|
|
70
|
+
let timeoutId = null;
|
|
71
|
+
let lastProcessedPosition = 0;
|
|
72
|
+
|
|
73
|
+
async function processLogChanges() {
|
|
74
|
+
isProcessing = true;
|
|
75
|
+
try {
|
|
76
|
+
const { lines, fileSize } = await readLogFile(
|
|
77
|
+
logFilePath,
|
|
78
|
+
isFirstRun ? -initialBufferSize : lastProcessedPosition
|
|
79
|
+
);
|
|
80
|
+
const entries = [];
|
|
81
|
+
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
const logEntry = parseLogEntry(line);
|
|
84
|
+
if (logEntry) {
|
|
85
|
+
entries.push(logEntry);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (entries.length > 0) {
|
|
90
|
+
onLogEntry(entries);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
lastProcessedPosition = fileSize;
|
|
94
|
+
isFirstRun = false;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error(`Error reading log file ${logFilePath}`, error);
|
|
97
|
+
} finally {
|
|
98
|
+
isProcessing = false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
watcher = fs.watch(logFilePath, { persistent: true }, async (eventType, filename) => {
|
|
103
|
+
if (!filename || eventType !== 'change') return;
|
|
104
|
+
|
|
105
|
+
// Clear any pending timeout
|
|
106
|
+
if (timeoutId) {
|
|
107
|
+
clearTimeout(timeoutId);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// If already processing, schedule for later
|
|
111
|
+
if (isProcessing) {
|
|
112
|
+
timeoutId = setTimeout(() => {
|
|
113
|
+
processLogChanges();
|
|
114
|
+
}, throttleDelay);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await processLogChanges();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function stop() {
|
|
123
|
+
watcher?.close();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
start,
|
|
128
|
+
stop,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = {
|
|
133
|
+
createLogWatcher,
|
|
134
|
+
readLogFile,
|
|
135
|
+
};
|
package/lib/states/audit-log.js
CHANGED
|
@@ -5,6 +5,7 @@ const get = require('lodash/get');
|
|
|
5
5
|
const uniq = require('lodash/uniq');
|
|
6
6
|
const isEqual = require('lodash/isEqual');
|
|
7
7
|
const { joinURL } = require('ufo');
|
|
8
|
+
const { Op } = require('sequelize');
|
|
8
9
|
const { getDisplayName } = require('@blocklet/meta/lib/util');
|
|
9
10
|
const { BLOCKLET_SITE_GROUP_SUFFIX, NODE_SERVICES, WHO_CAN_ACCESS } = require('@abtnode/constant');
|
|
10
11
|
const logger = require('@abtnode/logger')('@abtnode/core:states:audit-log');
|
|
@@ -706,12 +707,37 @@ class AuditLogState extends BaseState {
|
|
|
706
707
|
|
|
707
708
|
fixActor(user);
|
|
708
709
|
|
|
710
|
+
const content = (await getLogContent(action, args, context, result, info, node)).trim();
|
|
711
|
+
const actor = pick(user.actual || user, ['did', 'fullName', 'role']);
|
|
712
|
+
|
|
713
|
+
const teamDid = user.blockletDid || args.teamDid || get(args, 'did.0');
|
|
714
|
+
const userDid = user.did || args.userDid || get(args, 'user.did') || args.ownerDid;
|
|
715
|
+
if (teamDid && userDid && teamDid !== userDid) {
|
|
716
|
+
try {
|
|
717
|
+
const fullUser = await node.getUser({
|
|
718
|
+
teamDid,
|
|
719
|
+
user: { did: userDid },
|
|
720
|
+
options: {
|
|
721
|
+
enableConnectedAccount: true,
|
|
722
|
+
},
|
|
723
|
+
});
|
|
724
|
+
actor.avatar = fullUser?.avatar || '';
|
|
725
|
+
if (!actor.fullName) {
|
|
726
|
+
actor.fullName = fullUser?.fullName || '';
|
|
727
|
+
}
|
|
728
|
+
} catch (err) {
|
|
729
|
+
logger.error('get user info error', err);
|
|
730
|
+
}
|
|
731
|
+
} else {
|
|
732
|
+
actor.avatar = '';
|
|
733
|
+
}
|
|
734
|
+
|
|
709
735
|
const data = await this.insert({
|
|
710
736
|
scope: getScope(args) || info.did, // server or blocklet did
|
|
711
737
|
action,
|
|
712
738
|
category: await getLogCategory(action),
|
|
713
|
-
content: (
|
|
714
|
-
actor
|
|
739
|
+
content: content.slice(0, 10 * 1000),
|
|
740
|
+
actor,
|
|
715
741
|
extra: args,
|
|
716
742
|
env: pick(uaInfo, ['browser', 'os', 'device']),
|
|
717
743
|
ip,
|
|
@@ -727,13 +753,21 @@ class AuditLogState extends BaseState {
|
|
|
727
753
|
});
|
|
728
754
|
}
|
|
729
755
|
|
|
730
|
-
findPaginated({ scope, category, paging } = {}) {
|
|
731
|
-
const conditions = {
|
|
756
|
+
findPaginated({ scope, category, actionOrContent, paging } = {}) {
|
|
757
|
+
const conditions = {
|
|
758
|
+
where: {},
|
|
759
|
+
};
|
|
732
760
|
if (scope) {
|
|
733
|
-
conditions.scope = scope;
|
|
761
|
+
conditions.where.scope = scope;
|
|
734
762
|
}
|
|
735
763
|
if (category) {
|
|
736
|
-
conditions.category = category;
|
|
764
|
+
conditions.where.category = category;
|
|
765
|
+
}
|
|
766
|
+
if (actionOrContent) {
|
|
767
|
+
conditions.where[Op.or] = [
|
|
768
|
+
{ action: { [Op.like]: `%${actionOrContent}%` } },
|
|
769
|
+
{ content: { [Op.like]: `%${actionOrContent}%` } },
|
|
770
|
+
];
|
|
737
771
|
}
|
|
738
772
|
|
|
739
773
|
return super.paginate(conditions, { createdAt: -1 }, { pageSize: 20, ...paging });
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const { Op } = require('sequelize');
|
|
2
|
+
const dayjs = require('@abtnode/util/lib/dayjs');
|
|
3
|
+
const logger = require('@abtnode/logger')('@abtnode/core:state:blacklist');
|
|
4
|
+
|
|
5
|
+
const BaseState = require('./base');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* This db is used to save blacklist data.
|
|
9
|
+
* @extends BaseState<import('@abtnode/models').BlacklistState>
|
|
10
|
+
*/
|
|
11
|
+
class BlacklistState extends BaseState {
|
|
12
|
+
findActiveByScope(scope) {
|
|
13
|
+
const now = dayjs().unix();
|
|
14
|
+
return this.find({ where: { scope, expiresAt: { [Op.gt]: now } } });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
removeExpiredByScope(scope) {
|
|
18
|
+
const now = dayjs().unix();
|
|
19
|
+
return this.remove({ where: { scope, expiresAt: { [Op.lte]: now } } });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async addItem(scope, key, expiresAt) {
|
|
23
|
+
const now = dayjs().unix();
|
|
24
|
+
if (expiresAt < now) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const exist = await this.findOne({ where: { scope, key } });
|
|
29
|
+
if (exist) {
|
|
30
|
+
if (exist.expiresAt >= now) {
|
|
31
|
+
logger.debug('reuse existing item', { exist, scope, key, expiresAt });
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// if the item is expired, remove it
|
|
36
|
+
logger.debug('remove expired item on add', { exist });
|
|
37
|
+
await this.remove({ where: { scope, key } });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
logger.debug('add new item', { scope, key, expiresAt });
|
|
41
|
+
await this.insert({ scope, key, expiresAt });
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = BlacklistState;
|
package/lib/states/index.js
CHANGED
|
@@ -17,6 +17,7 @@ const JobState = require('./job');
|
|
|
17
17
|
const BackupState = require('./backup');
|
|
18
18
|
const TrafficInsightState = require('./traffic-insight');
|
|
19
19
|
const RuntimeInsightState = require('./runtime-insight');
|
|
20
|
+
const BlacklistState = require('./blacklist');
|
|
20
21
|
|
|
21
22
|
const { getDbFilePath } = require('../util');
|
|
22
23
|
|
|
@@ -44,6 +45,7 @@ const init = (dataDirs, config = {}) => {
|
|
|
44
45
|
const backupState = new BackupState(models.Backup, config);
|
|
45
46
|
const trafficInsight = new TrafficInsightState(models.TrafficInsight, config);
|
|
46
47
|
const runtimeInsight = new RuntimeInsightState(models.RuntimeInsight, { ...config, maxPageSize: 8640 });
|
|
48
|
+
const blacklistState = new BlacklistState(models.Blacklist, config);
|
|
47
49
|
|
|
48
50
|
return {
|
|
49
51
|
node: nodeState,
|
|
@@ -61,6 +63,7 @@ const init = (dataDirs, config = {}) => {
|
|
|
61
63
|
backup: backupState,
|
|
62
64
|
trafficInsight,
|
|
63
65
|
runtimeInsight,
|
|
66
|
+
blacklist: blacklistState,
|
|
64
67
|
};
|
|
65
68
|
};
|
|
66
69
|
/**
|
|
@@ -72,6 +75,7 @@ const init = (dataDirs, config = {}) => {
|
|
|
72
75
|
* notificationReceiver: import('./notification-receiver'),
|
|
73
76
|
* job: import('./job'),
|
|
74
77
|
* node: import('./node'),
|
|
78
|
+
* blacklist: import('./blacklist'),
|
|
75
79
|
* [key: string]: any
|
|
76
80
|
* }}
|
|
77
81
|
*/
|
package/lib/validators/node.js
CHANGED
|
@@ -106,6 +106,19 @@ const updateGatewaySchema = Joi.object({
|
|
|
106
106
|
.optional()
|
|
107
107
|
.max(256)
|
|
108
108
|
.default([]),
|
|
109
|
+
autoBlocking: Joi.object({
|
|
110
|
+
enabled: Joi.boolean().optional().default(false),
|
|
111
|
+
windowSize: Joi.number().integer().min(1).default(1), // in seconds
|
|
112
|
+
windowQuota: Joi.number().integer().min(2).default(5), // number of requests
|
|
113
|
+
statusCodes: Joi.array().items(Joi.number().integer().min(100).max(599)).default([403]), // status codes to check
|
|
114
|
+
blockDuration: Joi.number().integer().min(60).default(3600), // in seconds
|
|
115
|
+
}).default({
|
|
116
|
+
enabled: false,
|
|
117
|
+
windowSize: 1,
|
|
118
|
+
windowQuota: 5,
|
|
119
|
+
statusCodes: [403],
|
|
120
|
+
blockDuration: 3600,
|
|
121
|
+
}),
|
|
109
122
|
}),
|
|
110
123
|
proxyPolicy: Joi.object({
|
|
111
124
|
enabled: Joi.boolean().optional().default(false),
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.16.
|
|
6
|
+
"version": "1.16.39-beta-20250209-032436-5ceca2cb",
|
|
7
7
|
"description": "",
|
|
8
8
|
"main": "lib/index.js",
|
|
9
9
|
"files": [
|
|
@@ -19,20 +19,20 @@
|
|
|
19
19
|
"author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
|
|
20
20
|
"license": "Apache-2.0",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@abtnode/analytics": "1.16.
|
|
23
|
-
"@abtnode/auth": "1.16.
|
|
24
|
-
"@abtnode/certificate-manager": "1.16.
|
|
25
|
-
"@abtnode/constant": "1.16.
|
|
26
|
-
"@abtnode/cron": "1.16.
|
|
27
|
-
"@abtnode/docker-utils": "1.16.
|
|
28
|
-
"@abtnode/logger": "1.16.
|
|
29
|
-
"@abtnode/models": "1.16.
|
|
30
|
-
"@abtnode/queue": "1.16.
|
|
31
|
-
"@abtnode/rbac": "1.16.
|
|
32
|
-
"@abtnode/router-provider": "1.16.
|
|
33
|
-
"@abtnode/static-server": "1.16.
|
|
34
|
-
"@abtnode/timemachine": "1.16.
|
|
35
|
-
"@abtnode/util": "1.16.
|
|
22
|
+
"@abtnode/analytics": "1.16.39-beta-20250209-032436-5ceca2cb",
|
|
23
|
+
"@abtnode/auth": "1.16.39-beta-20250209-032436-5ceca2cb",
|
|
24
|
+
"@abtnode/certificate-manager": "1.16.39-beta-20250209-032436-5ceca2cb",
|
|
25
|
+
"@abtnode/constant": "1.16.39-beta-20250209-032436-5ceca2cb",
|
|
26
|
+
"@abtnode/cron": "1.16.39-beta-20250209-032436-5ceca2cb",
|
|
27
|
+
"@abtnode/docker-utils": "1.16.39-beta-20250209-032436-5ceca2cb",
|
|
28
|
+
"@abtnode/logger": "1.16.39-beta-20250209-032436-5ceca2cb",
|
|
29
|
+
"@abtnode/models": "1.16.39-beta-20250209-032436-5ceca2cb",
|
|
30
|
+
"@abtnode/queue": "1.16.39-beta-20250209-032436-5ceca2cb",
|
|
31
|
+
"@abtnode/rbac": "1.16.39-beta-20250209-032436-5ceca2cb",
|
|
32
|
+
"@abtnode/router-provider": "1.16.39-beta-20250209-032436-5ceca2cb",
|
|
33
|
+
"@abtnode/static-server": "1.16.39-beta-20250209-032436-5ceca2cb",
|
|
34
|
+
"@abtnode/timemachine": "1.16.39-beta-20250209-032436-5ceca2cb",
|
|
35
|
+
"@abtnode/util": "1.16.39-beta-20250209-032436-5ceca2cb",
|
|
36
36
|
"@arcblock/did": "1.19.9",
|
|
37
37
|
"@arcblock/did-auth": "1.19.9",
|
|
38
38
|
"@arcblock/did-ext": "^1.19.9",
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
"@arcblock/pm2-events": "^0.0.5",
|
|
44
44
|
"@arcblock/validator": "^1.19.9",
|
|
45
45
|
"@arcblock/vc": "1.19.9",
|
|
46
|
-
"@blocklet/constant": "1.16.
|
|
46
|
+
"@blocklet/constant": "1.16.39-beta-20250209-032436-5ceca2cb",
|
|
47
47
|
"@blocklet/did-space-js": "^1.0.9",
|
|
48
|
-
"@blocklet/env": "1.16.
|
|
49
|
-
"@blocklet/meta": "1.16.
|
|
50
|
-
"@blocklet/resolver": "1.16.
|
|
51
|
-
"@blocklet/sdk": "1.16.
|
|
52
|
-
"@blocklet/store": "1.16.
|
|
48
|
+
"@blocklet/env": "1.16.39-beta-20250209-032436-5ceca2cb",
|
|
49
|
+
"@blocklet/meta": "1.16.39-beta-20250209-032436-5ceca2cb",
|
|
50
|
+
"@blocklet/resolver": "1.16.39-beta-20250209-032436-5ceca2cb",
|
|
51
|
+
"@blocklet/sdk": "1.16.39-beta-20250209-032436-5ceca2cb",
|
|
52
|
+
"@blocklet/store": "1.16.39-beta-20250209-032436-5ceca2cb",
|
|
53
53
|
"@fidm/x509": "^1.2.1",
|
|
54
54
|
"@ocap/mcrypto": "1.19.9",
|
|
55
55
|
"@ocap/util": "1.19.9",
|
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
"p-limit": "^3.1.0",
|
|
86
86
|
"p-map": "^4.0.0",
|
|
87
87
|
"p-retry": "^4.6.2",
|
|
88
|
+
"rate-limiter-flexible": "^5.0.5",
|
|
88
89
|
"read-last-lines": "^1.8.0",
|
|
89
90
|
"semver": "^7.6.3",
|
|
90
91
|
"sequelize": "^6.35.0",
|
|
@@ -109,5 +110,5 @@
|
|
|
109
110
|
"jest": "^29.7.0",
|
|
110
111
|
"unzipper": "^0.10.11"
|
|
111
112
|
},
|
|
112
|
-
"gitHead": "
|
|
113
|
+
"gitHead": "949581a46cec2ebfb656a42632ca550ed3b0ba70"
|
|
113
114
|
}
|
package/lib/util/monitor.js
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-await-in-loop */
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
|
|
4
|
-
const THROTTLE_DELAY = 1000; // 1 second throttle
|
|
5
|
-
|
|
6
|
-
// Function to parse the log entry and extract relevant information
|
|
7
|
-
function parseLogEntry(line) {
|
|
8
|
-
const regex =
|
|
9
|
-
/^(\S+) - (\S+) \[([^\]]+)\] (\S+) "([^"]+)" "([^"]+)" (\d{3}) (\d+) "(.*?)" "(.*?)" "(.*?)" rt="(\S+)" uid="(.+?)" uos="(.+?)" uct="(\S+)" uht="(\S+)" urt="(\S+)"/;
|
|
10
|
-
const match = line.match(regex);
|
|
11
|
-
if (match) {
|
|
12
|
-
return {
|
|
13
|
-
ip: match[1],
|
|
14
|
-
remoteUser: match[2],
|
|
15
|
-
timeIso8601: match[3],
|
|
16
|
-
requestId: match[4],
|
|
17
|
-
host: match[5],
|
|
18
|
-
request: match[6],
|
|
19
|
-
status: parseInt(match[7], 10),
|
|
20
|
-
bodyBytesSent: parseInt(match[8], 10),
|
|
21
|
-
referer: match[9],
|
|
22
|
-
userAgent: match[10],
|
|
23
|
-
forwardedFor: match[11],
|
|
24
|
-
requestTime: parseFloat(match[12]),
|
|
25
|
-
connectedDid: match[13],
|
|
26
|
-
connectedWalletOs: match[14],
|
|
27
|
-
upstreamConnectTime: parseFloat(match[15]),
|
|
28
|
-
upstreamHeaderTime: parseFloat(match[16]),
|
|
29
|
-
upstreamResponseTime: parseFloat(match[17]),
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function readLogFile(filePath, startPosition = 0) {
|
|
36
|
-
let fd = null;
|
|
37
|
-
try {
|
|
38
|
-
if (!fs.existsSync(filePath)) {
|
|
39
|
-
return { lines: [], fileSize: 0 };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const realPath = await fs.promises.realpath(filePath);
|
|
43
|
-
fd = await fs.promises.open(realPath, 'r');
|
|
44
|
-
const stats = await fd.stat();
|
|
45
|
-
const fileSize = stats.size;
|
|
46
|
-
|
|
47
|
-
if (startPosition < 0) {
|
|
48
|
-
// eslint-disable-next-line no-param-reassign
|
|
49
|
-
startPosition = Math.max(0, fileSize + startPosition);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (startPosition >= fileSize) {
|
|
53
|
-
return { lines: [], fileSize };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const readSize = fileSize - startPosition;
|
|
57
|
-
if (readSize <= 0) {
|
|
58
|
-
return { lines: [], fileSize };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const buffer = Buffer.alloc(readSize);
|
|
62
|
-
const { bytesRead } = await fd.read(buffer, 0, readSize, startPosition);
|
|
63
|
-
const content = buffer.slice(0, bytesRead).toString();
|
|
64
|
-
|
|
65
|
-
const lines = content
|
|
66
|
-
.split('\n')
|
|
67
|
-
.map((line) => line.trim())
|
|
68
|
-
.filter(Boolean);
|
|
69
|
-
|
|
70
|
-
return { lines, fileSize };
|
|
71
|
-
} finally {
|
|
72
|
-
if (fd) {
|
|
73
|
-
await fd.close();
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
let watcher;
|
|
79
|
-
process.on('SIGINT', () => {
|
|
80
|
-
watcher?.close();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
process.on('SIGTERM', () => {
|
|
84
|
-
watcher?.close();
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
function startLogWatcher(logFilePath, onLogEntry, initialBufferSize = 10240) {
|
|
88
|
-
if (!fs.existsSync(logFilePath)) {
|
|
89
|
-
console.error(`Log file ${logFilePath} does not exist`);
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
console.warn(`Start watching ${logFilePath} for 5xx requests...`);
|
|
94
|
-
|
|
95
|
-
let isProcessing = false;
|
|
96
|
-
let isFirstRun = true;
|
|
97
|
-
let timeoutId = null;
|
|
98
|
-
let lastProcessedPosition = 0;
|
|
99
|
-
|
|
100
|
-
async function processLogChanges() {
|
|
101
|
-
isProcessing = true;
|
|
102
|
-
try {
|
|
103
|
-
const { lines, fileSize } = await readLogFile(
|
|
104
|
-
logFilePath,
|
|
105
|
-
isFirstRun ? -initialBufferSize : lastProcessedPosition
|
|
106
|
-
);
|
|
107
|
-
const entries = [];
|
|
108
|
-
|
|
109
|
-
for (const line of lines) {
|
|
110
|
-
const logEntry = parseLogEntry(line);
|
|
111
|
-
if (
|
|
112
|
-
logEntry &&
|
|
113
|
-
logEntry.status >= 500 &&
|
|
114
|
-
logEntry.status !== 503 &&
|
|
115
|
-
logEntry.status <= 599 &&
|
|
116
|
-
logEntry.request.includes('/.well-known/did.json') === false &&
|
|
117
|
-
logEntry.request.includes('/websocket?token=') === false &&
|
|
118
|
-
logEntry.request.includes('/.well-known/service/health') === false
|
|
119
|
-
) {
|
|
120
|
-
console.warn(`5xx request detected: ${logEntry.host}`, line);
|
|
121
|
-
entries.push(logEntry);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (entries.length > 0) {
|
|
126
|
-
onLogEntry(entries);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
lastProcessedPosition = fileSize;
|
|
130
|
-
isFirstRun = false;
|
|
131
|
-
} catch (error) {
|
|
132
|
-
console.error(`Error reading log file ${logFilePath}`, error);
|
|
133
|
-
} finally {
|
|
134
|
-
isProcessing = false;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
watcher = fs.watch(logFilePath, { persistent: true }, async (eventType, filename) => {
|
|
139
|
-
if (!filename || eventType !== 'change') return;
|
|
140
|
-
|
|
141
|
-
// Clear any pending timeout
|
|
142
|
-
if (timeoutId) {
|
|
143
|
-
clearTimeout(timeoutId);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// If already processing, schedule for later
|
|
147
|
-
if (isProcessing) {
|
|
148
|
-
timeoutId = setTimeout(() => {
|
|
149
|
-
processLogChanges();
|
|
150
|
-
}, THROTTLE_DELAY);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
await processLogChanges();
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function stopLogWatcher() {
|
|
159
|
-
watcher?.close();
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
module.exports = {
|
|
163
|
-
parseLogEntry,
|
|
164
|
-
readLogFile,
|
|
165
|
-
startLogWatcher,
|
|
166
|
-
stopLogWatcher,
|
|
167
|
-
};
|