@abtnode/core 1.6.6 → 1.6.7

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/cert.js ADDED
@@ -0,0 +1,116 @@
1
+ const { EventEmitter } = require('events');
2
+ const CertificateManager = require('@abtnode/certificate-manager/sdk/manager');
3
+ const logger = require('@abtnode/logger')('@abtnode/core:cert');
4
+ const states = require('./states');
5
+
6
+ const onCertExpired = async (cert) => {
7
+ logger.info('send certificate expire notification', { domain: cert.domain });
8
+ states.notification.create({
9
+ title: 'SSL Certificate Expired',
10
+ description: `Your SSL certificate for domain ${cert.domain} has expired, please update it in Blocklet Server`,
11
+ severity: 'error',
12
+ entityType: 'certificate',
13
+ entityId: cert.id,
14
+ });
15
+ };
16
+
17
+ const onCertAboutExpire = (cert) => {
18
+ logger.info('send certificate about-expire notification', { domain: cert.domain });
19
+ states.notification.create({
20
+ title: 'SSL Certificate Expire Warning',
21
+ description: `Your SSL certificate for domain ${cert.domain} will expire in ${
22
+ cert.expireInDays
23
+ } days (on ${new Date(cert.validTo).toLocaleString()}), please remember to update it in Blocklet Server`,
24
+ severity: 'warning',
25
+ entityType: 'certificate',
26
+ entityId: cert.id, // eslint-disable-line no-underscore-dangle
27
+ });
28
+ };
29
+
30
+ const onCertIssued = (cert) => {
31
+ states.notification.create({
32
+ title: 'Certificate Issued',
33
+ description: `The ${cert.domain} certificate is issued successfully`,
34
+ severity: 'success',
35
+ entityType: 'certificate',
36
+ entityId: cert.id,
37
+ });
38
+ };
39
+
40
+ class Cert extends EventEmitter {
41
+ constructor({ maintainerEmail, dataDir }) {
42
+ super();
43
+
44
+ this.manager = new CertificateManager({ maintainerEmail, dataDir });
45
+
46
+ this.manager.on('cert.issued', (cert) => {
47
+ this.emit('cert.issued', cert);
48
+ onCertIssued(cert);
49
+ });
50
+
51
+ this.manager.on('cert.expired', onCertExpired);
52
+ this.manager.on('cert.about_to_expire', onCertAboutExpire);
53
+ }
54
+
55
+ start() {
56
+ return this.manager.start();
57
+ }
58
+
59
+ static fixCertificate(entity) {
60
+ // Hack: this logic exists because gql does not allow line breaks in arg values
61
+ entity.privateKey = entity.privateKey.split('|').join('\n');
62
+ entity.certificate = entity.certificate.split('|').join('\n');
63
+ }
64
+
65
+ getAll() {
66
+ return this.manager.getAll();
67
+ }
68
+
69
+ getAllNormal() {
70
+ return this.manager.getAllNormal();
71
+ }
72
+
73
+ getByDomain(domain) {
74
+ return this.manager.getByDomain(domain);
75
+ }
76
+
77
+ async add(data) {
78
+ if (!data.certificate || !data.privateKey) {
79
+ throw new Error('certificate and privateKey is required');
80
+ }
81
+
82
+ Cert.fixCertificate(data);
83
+
84
+ await this.manager.add(data);
85
+ logger.info('add certificate result', { name: data.name });
86
+ this.emit('cert.added', data);
87
+ }
88
+
89
+ async issue({ domain }) {
90
+ logger.info(`generate certificate for ${domain}`);
91
+
92
+ return this.manager.issue(domain);
93
+ }
94
+
95
+ async upsertByDomain(data) {
96
+ return this.manager.upsertByDomain(data);
97
+ }
98
+
99
+ async update(data) {
100
+ return this.manager.update(data.id, { name: data.name, public: data.public });
101
+ }
102
+
103
+ async remove({ id }) {
104
+ await this.manager.remove(id);
105
+ logger.info('delete certificate', { id });
106
+ this.emit('cert.removed');
107
+
108
+ return {};
109
+ }
110
+
111
+ async addWithoutValidations(data) {
112
+ return this.manager.addWithoutValidations(data);
113
+ }
114
+ }
115
+
116
+ module.exports = Cert;
package/lib/index.js CHANGED
@@ -11,6 +11,7 @@ const {
11
11
  toBlockletSource,
12
12
  } = require('@blocklet/meta/lib/constants');
13
13
  const { listProviders } = require('@abtnode/router-provider');
14
+ const { DEFAULT_CERTIFICATE_EMAIL } = require('@abtnode/constant');
14
15
 
15
16
  const RoutingSnapshot = require('./states/routing-snapshot');
16
17
  const BlockletRegistry = require('./blocklet/registry');
@@ -22,6 +23,7 @@ const NodeAPI = require('./api/node');
22
23
  const TeamAPI = require('./api/team');
23
24
  const WebHook = require('./webhook');
24
25
  const states = require('./states');
26
+ const Cert = require('./cert');
25
27
 
26
28
  const IP = require('./util/ip');
27
29
  const DomainStatus = require('./util/domain-status');
@@ -108,7 +110,11 @@ function ABTNode(options) {
108
110
  // Initialize storage
109
111
  states.init(dataDirs, options);
110
112
 
111
- const routerManager = new RouterManager();
113
+ const certManager = new Cert({
114
+ maintainerEmail: DEFAULT_CERTIFICATE_EMAIL,
115
+ dataDir: dataDirs.certManagerModule,
116
+ });
117
+ const routerManager = new RouterManager({ certManager });
112
118
  const routingSnapshot = new RoutingSnapshot({
113
119
  baseDir: dataDirs.core,
114
120
  getRoutingData: async () => {
@@ -117,7 +123,6 @@ function ABTNode(options) {
117
123
  return { sites };
118
124
  },
119
125
  });
120
-
121
126
  const blockletRegistry = new BlockletRegistry();
122
127
  const blockletManager = new BlockletManager({
123
128
  dataDirs,
@@ -127,6 +132,8 @@ function ABTNode(options) {
127
132
  daemon: options.daemon,
128
133
  });
129
134
 
135
+ certManager.start(); // 启动证书服务
136
+
130
137
  const {
131
138
  handleRouting,
132
139
  getRoutingRulesByDid,
@@ -141,10 +148,10 @@ function ABTNode(options) {
141
148
  getCertificates,
142
149
  getSitesFromSnapshot,
143
150
  getRoutingCrons,
144
- } = getRouterHelpers({ dataDirs, routingSnapshot, routerManager, blockletManager });
151
+ ensureDashboardCertificate,
152
+ } = getRouterHelpers({ dataDirs, routingSnapshot, routerManager, blockletManager, certManager });
145
153
 
146
154
  const teamManager = new TeamManager({ nodeDid: options.nodeDid, dataDirs, states });
147
-
148
155
  const nodeAPI = new NodeAPI(states.node, blockletRegistry);
149
156
  const teamAPI = new TeamAPI({ states, teamManager });
150
157
 
@@ -311,6 +318,7 @@ function ABTNode(options) {
311
318
  takeRoutingSnapshot,
312
319
  getSitesFromSnapshot,
313
320
  ensureDashboardRouting,
321
+ ensureDashboardCertificate,
314
322
 
315
323
  addDomainAlias: routerManager.addDomainAlias.bind(routerManager),
316
324
  deleteDomainAlias: routerManager.deleteDomainAlias.bind(routerManager),
@@ -319,11 +327,13 @@ function ABTNode(options) {
319
327
  checkDomains: domainStatus.checkDomainsStatus.bind(domainStatus),
320
328
  handleRouting,
321
329
 
322
- updateNginxHttpsCert: routerManager.updateNginxHttpsCert.bind(routerManager),
330
+ certManager,
331
+ updateNginxHttpsCert: certManager.update.bind(certManager), // TODO: 重命名:updateCert
323
332
  getCertificates,
324
- addCertificate: routerManager.addCertificate.bind(routerManager),
325
- deleteCertificate: routerManager.deleteCertificate.bind(routerManager),
326
- findCertificateByDomain: routerManager.findCertificateByDomain.bind(routerManager),
333
+ addCertificate: certManager.add.bind(certManager),
334
+ deleteCertificate: certManager.remove.bind(certManager),
335
+ findCertificateByDomain: certManager.getByDomain.bind(certManager),
336
+ issueLetsEncryptCert: certManager.issue.bind(certManager),
327
337
 
328
338
  // Access Key
329
339
  getAccessKeys: states.accessKey.list.bind(states.accessKey),
@@ -0,0 +1,30 @@
1
+ module.exports = async ({ states, node, printInfo }) => {
2
+ printInfo('Migrate certificates...');
3
+ const certs = await states.certificate.find(
4
+ {},
5
+ {
6
+ domain: 1,
7
+ privateKey: 1,
8
+ certificate: 1,
9
+ createdAt: 1,
10
+ updatedAt: 1,
11
+ }
12
+ );
13
+
14
+ const tasks = certs.map((cert) => {
15
+ const data = {
16
+ _id: cert.id,
17
+ domain: cert.domain,
18
+ privateKey: cert.privateKey,
19
+ certificate: cert.certificate,
20
+ createdAt: cert.createdAt,
21
+ updatedAt: cert.updatedAt,
22
+ source: 'lets_encrypt',
23
+ status: 'generated',
24
+ };
25
+
26
+ return node.certManager.addWithoutValidations(data);
27
+ });
28
+
29
+ await Promise.all(tasks);
30
+ };
@@ -21,15 +21,13 @@ const {
21
21
  NAME_FOR_WELLKNOWN_SITE,
22
22
  DEFAULT_HTTP_PORT,
23
23
  DEFAULT_HTTPS_PORT,
24
- DAY_IN_MS,
25
24
  NODE_MODES,
26
25
  ROUTING_RULE_TYPES,
27
26
  CERTIFICATE_EXPIRES_OFFSET,
28
- CERTIFICATE_EXPIRES_WARNING_OFFSET,
29
- DEFAULT_DAEMON_PORT,
30
27
  DEFAULT_SERVICE_PATH,
31
28
  SLOT_FOR_IP_DNS_SITE,
32
29
  BLOCKLET_SITE_GROUP_SUFFIX,
30
+ WELLKNOWN_ACME_CHALLENGE_PREFIX,
33
31
  } = require('@abtnode/constant');
34
32
  const {
35
33
  BLOCKLET_DYNAMIC_PATH_PREFIX,
@@ -403,11 +401,10 @@ const decompressCertificates = async (source, dest) => {
403
401
  return dest;
404
402
  };
405
403
 
406
- module.exports = function getRouterHelpers({ dataDirs, routingSnapshot, routerManager, blockletManager }) {
404
+ module.exports = function getRouterHelpers({ dataDirs, routingSnapshot, routerManager, blockletManager, certManager }) {
407
405
  const nodeState = states.node;
408
406
  const blockletState = states.blocklet;
409
407
  const siteState = states.site;
410
- const httpsCertState = states.certificate;
411
408
  const notification = states.notification;
412
409
 
413
410
  // site level duplication detection
@@ -468,58 +465,105 @@ module.exports = function getRouterHelpers({ dataDirs, routingSnapshot, routerMa
468
465
  const certificate = fs.readFileSync(certificateFilePath).toString();
469
466
  const privateKey = fs.readFileSync(privateKeyFilePath).toString();
470
467
 
471
- await routerManager.updateNginxHttpsCert({ domain: dashboardDomain, privateKey, certificate, isProtected: true });
468
+ await certManager.upsertByDomain({
469
+ domain: dashboardDomain,
470
+ privateKey,
471
+ certificate,
472
+ isProtected: true,
473
+ });
472
474
  logger.info('dashboard certificate updated');
473
475
  } catch (error) {
474
- logger.error('Update dashboard certificate failed', { error });
476
+ logger.error('update dashboard certificate failed', { error });
477
+ throw error;
475
478
  } finally {
476
479
  fs.removeSync(destFolder);
477
480
  }
478
481
  };
479
482
 
483
+ const ensureDashboardCertificate = async () => {
484
+ const info = await nodeState.read();
485
+ const downloadUrl = get(info, 'routing.dashboardCertDownloadAddress', '');
486
+ const dashboardDomain = get(info, 'routing.dashboardDomain', '');
487
+ if (!dashboardDomain || !downloadUrl) {
488
+ throw new Error('dashboardCertDownloadAddress and dashboardDomain are not found in the routing configs');
489
+ }
490
+
491
+ const cert = await certManager.getByDomain(dashboardDomain);
492
+ if (cert) {
493
+ return { status: 'existed' };
494
+ }
495
+
496
+ logger.debug('downloading certificate', { url: downloadUrl, dashboardDomain });
497
+ await updateDashboardCertificate({ checkExpire: false });
498
+ logger.debug('downloading certificate', { url: downloadUrl, dashboardDomain });
499
+ return { status: 'downloaded' };
500
+ };
501
+
480
502
  const addWellknownSite = async (sites, context) => {
481
503
  const site = (sites || []).find((x) => x.name === NAME_FOR_WELLKNOWN_SITE);
482
504
 
483
505
  try {
484
- const pathPrefix = joinUrl(WELLKNOWN_PATH_PREFIX, '/did.json');
506
+ const info = await nodeState.read();
507
+ const proxyTarget = {
508
+ port: info.port,
509
+ type: ROUTING_RULE_TYPES.GENERAL_PROXY,
510
+ interfaceName: BLOCKLET_INTERFACE_WELLKNOWN,
511
+ };
512
+
485
513
  const didResolverWellknownRule = {
486
- from: { pathPrefix },
487
- to: {
488
- port: DEFAULT_DAEMON_PORT,
489
- type: ROUTING_RULE_TYPES.GENERAL_PROXY,
490
- interfaceName: BLOCKLET_INTERFACE_WELLKNOWN,
491
- },
514
+ from: { pathPrefix: joinUrl(WELLKNOWN_PATH_PREFIX, '/did.json') },
515
+ to: proxyTarget,
516
+ };
517
+
518
+ const acmeChallengeWellknownRule = {
519
+ from: { pathPrefix: WELLKNOWN_ACME_CHALLENGE_PREFIX },
520
+ to: proxyTarget,
492
521
  };
493
522
 
494
523
  if (site) {
495
- if (site.rules.find((r) => r.from.pathPrefix === normalizePathPrefix(pathPrefix))) {
496
- return false;
524
+ let changed = false;
525
+ const exists = (prefix) => !!site.rules.find((r) => r.from.pathPrefix === normalizePathPrefix(prefix));
526
+
527
+ if (!exists(didResolverWellknownRule.from.pathPrefix)) {
528
+ await routerManager.addRoutingRule(
529
+ {
530
+ id: site.id,
531
+ rule: didResolverWellknownRule,
532
+ },
533
+ context
534
+ );
535
+ changed = true;
497
536
  }
498
537
 
499
- await routerManager.addRoutingRule(
500
- {
501
- id: site.id,
502
- rule: didResolverWellknownRule,
503
- },
504
- context
505
- );
506
- } else {
507
- await routerManager.addRoutingSite(
508
- {
509
- site: {
510
- domain: DOMAIN_FOR_INTERNAL_SITE,
511
- port: await getWellknownSitePort(),
512
- name: NAME_FOR_WELLKNOWN_SITE,
513
- rules: [didResolverWellknownRule],
514
- isProtected: true,
538
+ if (!exists(acmeChallengeWellknownRule.from.pathPrefix)) {
539
+ await routerManager.addRoutingRule(
540
+ {
541
+ id: site.id,
542
+ rule: acmeChallengeWellknownRule,
515
543
  },
516
- skipCheckDynamicBlacklist: true,
517
- skipValidation: true,
518
- },
519
- context
520
- );
544
+ context
545
+ );
546
+ changed = true;
547
+ }
548
+
549
+ return changed;
521
550
  }
522
551
 
552
+ await routerManager.addRoutingSite(
553
+ {
554
+ site: {
555
+ domain: DOMAIN_FOR_INTERNAL_SITE,
556
+ port: await getWellknownSitePort(),
557
+ name: NAME_FOR_WELLKNOWN_SITE,
558
+ rules: [didResolverWellknownRule, acmeChallengeWellknownRule],
559
+ isProtected: true,
560
+ },
561
+ skipCheckDynamicBlacklist: true,
562
+ skipValidation: true,
563
+ },
564
+ context
565
+ );
566
+
523
567
  return true;
524
568
  } catch (err) {
525
569
  console.error('add well-known site failed:', err);
@@ -533,7 +577,7 @@ module.exports = function getRouterHelpers({ dataDirs, routingSnapshot, routerMa
533
577
  *
534
578
  * @returns {boolean} if routing changed
535
579
  */
536
- const ensureDashboardRouting = async (context = {}, output) => {
580
+ const ensureDashboardRouting = async (context = {}) => {
537
581
  const info = await nodeState.read();
538
582
 
539
583
  const provider = getProviderFromNodeInfo(info);
@@ -618,18 +662,6 @@ module.exports = function getRouterHelpers({ dataDirs, routingSnapshot, routerMa
618
662
  updatedResult.push(wellknownRes);
619
663
  }
620
664
 
621
- // Download dashboard certificates if not exists
622
- const certDownloadAddress = get(info, 'routing.dashboardCertDownloadAddress', '');
623
- if (dashboardDomain && certDownloadAddress) {
624
- const cert = await routerManager.findCertificateByDomain(dashboardDomain);
625
- if (!cert) {
626
- await updateDashboardCertificate({ checkExpire: false });
627
- if (typeof output === 'function') {
628
- output('Dashboard HTTPS certificate was downloaded successfully!');
629
- }
630
- }
631
- }
632
-
633
665
  if (updatedResult.length) {
634
666
  const hash = await takeRoutingSnapshot({ message: 'ensure dashboard routing rules', dryRun: false }, context);
635
667
  logger.info('take routing snapshot on ensure dashboard routing rules', { updatedResult, hash });
@@ -1068,9 +1100,7 @@ module.exports = function getRouterHelpers({ dataDirs, routingSnapshot, routerMa
1068
1100
  sites = await ensureAuthService(sites, blockletManager);
1069
1101
  sites = await ensureServiceRule(sites);
1070
1102
 
1071
- const certificates = httpsEnabled
1072
- ? await httpsCertState.find({ type: providerName }, { domain: 1, certificate: 1, privateKey: 1 })
1073
- : [];
1103
+ const certificates = httpsEnabled ? await certManager.getAllNormal() : [];
1074
1104
 
1075
1105
  return {
1076
1106
  sites,
@@ -1093,9 +1123,9 @@ module.exports = function getRouterHelpers({ dataDirs, routingSnapshot, routerMa
1093
1123
  },
1094
1124
  });
1095
1125
 
1096
- routerManager.on('router.certificate.add', () => routers[providerName].restart());
1097
- routerManager.on('router.certificate.updated', () => routers[providerName].restart());
1098
- routerManager.on('router.certificate.remove', () => routers[providerName].restart());
1126
+ certManager.on('cert.added', () => routers[providerName].restart());
1127
+ certManager.on('cert.removed', () => routers[providerName].restart());
1128
+ certManager.on('cert.issued', () => routers[providerName].restart());
1099
1129
 
1100
1130
  await routers[providerName].start();
1101
1131
  }
@@ -1257,18 +1287,21 @@ module.exports = function getRouterHelpers({ dataDirs, routingSnapshot, routerMa
1257
1287
  };
1258
1288
 
1259
1289
  const getCertificates = async () => {
1260
- const certificates = await httpsCertState.find();
1290
+ const certificates = await certManager.getAll();
1261
1291
  const sites = await getSitesFromSnapshot();
1292
+
1293
+ const isMatch = (cert, domain) =>
1294
+ domain !== DOMAIN_FOR_DEFAULT_SITE && domain && routerManager.isCertMatchedDomain(cert, domain);
1295
+
1262
1296
  return certificates.map((cert) => {
1263
1297
  cert.matchedSites = [];
1264
1298
  sites.forEach((site) => {
1265
- if (
1266
- site.domain !== DOMAIN_FOR_DEFAULT_SITE &&
1267
- site.domain &&
1268
- routerManager.isCertMatchedDomain(cert, site.domain)
1269
- ) {
1270
- cert.matchedSites.push({ id: site.id, domain: site.domain });
1271
- }
1299
+ const domains = [site.domain, ...(site.domainAliases || []).map((x) => x.value)];
1300
+ domains.forEach((domain) => {
1301
+ if (isMatch(cert, domain)) {
1302
+ cert.matchedSites.push({ id: site.id, domain });
1303
+ }
1304
+ });
1272
1305
  });
1273
1306
 
1274
1307
  return cert;
@@ -1281,42 +1314,6 @@ module.exports = function getRouterHelpers({ dataDirs, routingSnapshot, routerMa
1281
1314
  return { domain, isHttps: !!matchedCert, matchedCert };
1282
1315
  };
1283
1316
 
1284
- const checkCertificatesExpiration = async () => {
1285
- const now = Date.now();
1286
-
1287
- const certificates = await getCertificates();
1288
- for (let i = 0; i < certificates.length; i++) {
1289
- const cert = certificates[i];
1290
- const alreadyExpired = now >= cert.validTo;
1291
- const aboutToExpire = cert.validTo - now > 0 && cert.validTo - now < CERTIFICATE_EXPIRES_WARNING_OFFSET;
1292
-
1293
- if (alreadyExpired) {
1294
- logger.info('send certificate expire notification', { domain: cert.domain });
1295
- notification.create({
1296
- title: 'SSL Certificate Expired',
1297
- description: `Your SSL certificate for domain ${cert.domain} has expired, please update it in Blocklet Server`,
1298
- severity: 'error',
1299
- entityType: 'certificate',
1300
- entityId: cert._id, // eslint-disable-line no-underscore-dangle
1301
- });
1302
- } else if (aboutToExpire) {
1303
- logger.info('send certificate about-expire notification', { domain: cert.domain });
1304
- const expireInDays = Math.ceil((cert.validTo - now) / DAY_IN_MS);
1305
- notification.create({
1306
- title: 'SSL Certificate Expire Warning',
1307
- description: `Your SSL certificate for domain ${
1308
- cert.domain
1309
- } will expire in ${expireInDays} days (on ${new Date(
1310
- cert.validTo
1311
- ).toLocaleString()}), please remember to update it in Blocklet Server`,
1312
- severity: 'warning',
1313
- entityType: 'certificate',
1314
- entityId: cert._id, // eslint-disable-line no-underscore-dangle
1315
- });
1316
- }
1317
- }
1318
- };
1319
-
1320
1317
  return {
1321
1318
  ensureDashboardRouting,
1322
1319
  ensureBlockletRouting,
@@ -1328,9 +1325,10 @@ module.exports = function getRouterHelpers({ dataDirs, routingSnapshot, routerMa
1328
1325
  takeRoutingSnapshot,
1329
1326
  getRoutingSites,
1330
1327
  getSnapshotSites,
1331
- getCertificates,
1332
1328
  getSitesFromSnapshot,
1329
+ getCertificates,
1333
1330
  checkDomain,
1331
+ ensureDashboardCertificate,
1334
1332
 
1335
1333
  getRoutingCrons: () => [
1336
1334
  {
@@ -1339,12 +1337,6 @@ module.exports = function getRouterHelpers({ dataDirs, routingSnapshot, routerMa
1339
1337
  fn: () => updateDashboardCertificate({ checkExpire: true }),
1340
1338
  options: { runOnInit: true },
1341
1339
  },
1342
- {
1343
- name: 'check-certificate-expiration',
1344
- time: '0 0 9 * * *', // check on 09:00 every day
1345
- fn: checkCertificatesExpiration,
1346
- options: { runOnInit: false },
1347
- },
1348
1340
  {
1349
1341
  name: 'rotate-log-files',
1350
1342
  time: '5 0 0 * * *', // rotate at 05:00 every day
@@ -70,8 +70,9 @@ const normalizeRedirectUrl = (url) => {
70
70
  };
71
71
 
72
72
  class RouterManager extends EventEmitter {
73
- constructor() {
73
+ constructor({ certManager }) {
74
74
  super();
75
+ this.certManager = certManager;
75
76
 
76
77
  // HACK: do not emit any events from CLI
77
78
  if (isCLI()) {
@@ -366,7 +367,7 @@ class RouterManager extends EventEmitter {
366
367
  domain,
367
368
  });
368
369
  logger.info('add certificate result', { domain: newCert.domain });
369
- this.emit('router.certificate.add', { type: 'nginx', data: newCert });
370
+ this.emit('cert.added', { type: 'nginx', data: newCert });
370
371
  }
371
372
 
372
373
  // eslint-disable-next-line no-unused-vars
@@ -379,7 +380,7 @@ class RouterManager extends EventEmitter {
379
380
  const removeResult = await states.certificate.remove({ _id: id });
380
381
 
381
382
  logger.info('delete certificate', { removeResult, domain: tmpCert.domain });
382
- this.emit('router.certificate.remove', { type: 'nginx', data: { domain: tmpCert.domain } });
383
+ this.emit('cert.removed', { type: 'nginx', data: { domain: tmpCert.domain } });
383
384
  return {};
384
385
  }
385
386
 
@@ -389,7 +390,7 @@ class RouterManager extends EventEmitter {
389
390
  this.fixCertificate(entity);
390
391
  this.validateCertificate(entity, entity.domain);
391
392
  const dbEntity = await states.certificate.upsert(entity);
392
- this.emit('router.certificate.updated', { type: 'nginx', data: dbEntity });
393
+ this.emit('cert.issued', { type: 'nginx', data: dbEntity });
393
394
  }
394
395
 
395
396
  findCertificateByDomain(domain) {
@@ -451,7 +452,7 @@ class RouterManager extends EventEmitter {
451
452
  }
452
453
 
453
454
  async getMatchedCert(domain) {
454
- const certs = await states.certificate.find();
455
+ const certs = await this.certManager.getAll();
455
456
  const matchedCert = certs.find((cert) => this.isCertMatchedDomain(cert, domain));
456
457
 
457
458
  if (matchedCert) {
@@ -1,234 +1,17 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const util = require('util');
4
- const { EventEmitter } = require('events');
5
- const cloneDeep = require('lodash/cloneDeep');
6
- const DataStore =
7
- process.env.NODE_ENV === 'test' ? require('@nedb/core') : require('@nedb/multi')(Number(process.env.NEDB_MULTI_PORT));
1
+ const DB = require('@abtnode/db');
8
2
  const logger = require('@abtnode/logger')('@abtnode/core:states');
9
3
 
10
4
  const { isCLI } = require('../util');
11
5
 
12
- class BaseState extends EventEmitter {
6
+ class BaseState extends DB {
13
7
  constructor(baseDir, options) {
14
- super();
8
+ super(baseDir, options);
15
9
 
16
10
  // HACK: do not emit any events from CLI
17
11
  if (isCLI() && process.env.NODE_ENV !== 'test') {
18
12
  this.emit = (name) => logger.debug('stopped state db event in CLI', name);
19
13
  }
20
-
21
- const dbOptions = options.db || {};
22
- this.filename = path.join(baseDir, options.filename);
23
- this.options = Object.freeze(cloneDeep(options));
24
- this.db = new DataStore({
25
- filename: this.filename,
26
- timestampData: true,
27
- ...dbOptions,
28
- });
29
-
30
- logger.info('initialized', { filename: this.filename });
31
-
32
- this.ready = false;
33
- this.readyCallbacks = [];
34
- this.db.loadDatabase((err) => {
35
- if (err) {
36
- logger.error(`failed to load disk database ${this.filename}`, { error: err });
37
- console.error(err);
38
- } else {
39
- this.ready = true;
40
- if (this.readyCallbacks.length) {
41
- this.readyCallbacks.forEach((x) => x());
42
- }
43
- }
44
- });
45
-
46
- this.asyncDB = new Proxy(this.db, {
47
- get(target, property) {
48
- if (typeof target[property] === 'function') {
49
- return util
50
- .promisify((...args) => {
51
- const cb = args[args.length - 1];
52
- const rest = args.slice(0, args.length - 1);
53
-
54
- target[property](...rest, (err, ...result) => {
55
- if (err) {
56
- return cb(err);
57
- }
58
-
59
- if (result.length === 1) {
60
- return cb(null, result[0]);
61
- }
62
-
63
- return cb(null, result);
64
- });
65
- })
66
- .bind(target);
67
- }
68
-
69
- return target[property];
70
- },
71
- });
72
- }
73
-
74
- onReady(cb) {
75
- if (this.ready) {
76
- cb();
77
- } else {
78
- this.readyCallbacks.push(cb);
79
- }
80
- }
81
-
82
- createNotification(payload) {
83
- if (this.notification && typeof this.notification.create === 'function') {
84
- this.notification.create(payload);
85
- }
86
- }
87
-
88
- paginate(conditions, sort, paging) {
89
- const { pageSize: size = 20, page = 1 } = paging || {};
90
- const pageSize = Math.min(100, size);
91
-
92
- return new Promise((resolve, reject) => {
93
- this.db
94
- .find(conditions)
95
- .sort(sort)
96
- .exec((err, docs) => {
97
- if (err) {
98
- return reject(err);
99
- }
100
-
101
- const pageCount = Math.ceil(docs.length / pageSize);
102
- const total = docs.length;
103
- const skip = (page - 1) * pageSize;
104
- const list = docs.slice(skip, skip + pageSize);
105
-
106
- list.forEach((doc) => {
107
- // eslint-disable-next-line no-underscore-dangle
108
- doc.id = doc._id;
109
- });
110
-
111
- return resolve({
112
- list,
113
- paging: {
114
- total,
115
- pageSize,
116
- pageCount,
117
- page,
118
- },
119
- });
120
- });
121
- });
122
- }
123
-
124
- async updateById(id, updates, options = {}) {
125
- const [, doc] = await this.asyncDB.update({ _id: id }, updates, {
126
- multi: false,
127
- upsert: false,
128
- returnUpdatedDocs: true,
129
- ...options,
130
- });
131
-
132
- return doc;
133
- }
134
-
135
- update(...args) {
136
- if (args.length === 0) {
137
- throw new Error('param is required by update method');
138
- }
139
-
140
- if (typeof args[0] === 'string') {
141
- return this.updateById(...args);
142
- }
143
-
144
- return this.asyncDB.update(args[0], args[1], { returnUpdatedDocs: true, ...(args[2] || {}) });
145
- }
146
-
147
- count(conditions = {}) {
148
- return new Promise((resolve, reject) => {
149
- this.db.count(conditions, (err, num) => {
150
- if (err) {
151
- return reject(err);
152
- }
153
-
154
- return resolve(num);
155
- });
156
- });
157
- }
158
-
159
- remove(conditions = {}, options = { multi: false }) {
160
- return new Promise((resolve, reject) => {
161
- this.db.remove(conditions, options, (err, num) => {
162
- if (err) {
163
- return reject(err);
164
- }
165
-
166
- return resolve(num);
167
- });
168
- });
169
- }
170
-
171
- reset() {
172
- fs.unlinkSync(this.filename);
173
- }
174
-
175
- find(...args) {
176
- if (args.length === 0) {
177
- return this.asyncDB.find({});
178
- }
179
-
180
- return this.asyncDB.find(...args);
181
- }
182
-
183
- findOne(...args) {
184
- if (args.length === 0) {
185
- return this.asyncDB.findOne({});
186
- }
187
-
188
- return this.asyncDB.findOne(...args);
189
- }
190
-
191
- insert(...args) {
192
- return this.asyncDB.insert(...args);
193
- }
194
-
195
- compactDatafile() {
196
- this.db.persistence.compactDatafile((err) => {
197
- if (err) {
198
- console.error(`failed to compact datafile: ${this.filename}`, err);
199
- }
200
- });
201
14
  }
202
15
  }
203
16
 
204
- /**
205
- * Rename _id field name to id, this method has side effects
206
- * @param {object} entities
207
- */
208
- const renameIdFiledName = (entities, from = '_id', to = 'id') => {
209
- /* eslint-disable no-underscore-dangle, no-param-reassign */
210
-
211
- if (!entities) {
212
- return entities;
213
- }
214
-
215
- const mapEntity = (entity) => {
216
- if (entity[from]) {
217
- entity[to] = entity[from];
218
- delete entity[from];
219
- }
220
- };
221
-
222
- if (!Array.isArray(entities)) {
223
- mapEntity(entities);
224
- return entities;
225
- }
226
-
227
- entities.forEach(mapEntity);
228
-
229
- return entities;
230
- };
231
-
232
- BaseState.renameIdFiledName = renameIdFiledName;
233
-
234
17
  module.exports = BaseState;
@@ -1,4 +1,4 @@
1
- const BaseState = require('./base');
1
+ const stateFactory = require('@abtnode/db/lib/factory');
2
2
  const NodeState = require('./node');
3
3
  const ChallengeState = require('./challenge');
4
4
  const BlockletState = require('./blocklet');
@@ -12,8 +12,6 @@ const SessionState = require('./session');
12
12
  const ExtrasState = require('./blocklet-extras');
13
13
  const CacheState = require('./cache');
14
14
 
15
- const states = {};
16
-
17
15
  const init = (dataDirs, options) => {
18
16
  const notificationState = new NotificationState(dataDirs.core, options);
19
17
  const nodeState = new NodeState(dataDirs.core, options, dataDirs, notificationState);
@@ -28,7 +26,7 @@ const init = (dataDirs, options) => {
28
26
  const extrasState = new ExtrasState(dataDirs.core, options);
29
27
  const cacheState = new CacheState(dataDirs.core, options);
30
28
 
31
- Object.assign(states, {
29
+ return {
32
30
  node: nodeState,
33
31
  blocklet: blockletState,
34
32
  notification: notificationState,
@@ -41,22 +39,7 @@ const init = (dataDirs, options) => {
41
39
  session: sessionState,
42
40
  blockletExtras: extrasState,
43
41
  cache: cacheState,
44
- });
42
+ };
45
43
  };
46
44
 
47
- module.exports = new Proxy(
48
- {},
49
- {
50
- get(target, prop) {
51
- if (prop === 'init') {
52
- return init;
53
- }
54
-
55
- if (states[prop] instanceof BaseState) {
56
- return states[prop];
57
- }
58
-
59
- throw new Error(`State ${String(prop)} may not be initialized`);
60
- },
61
- }
62
- );
45
+ module.exports = stateFactory(init);
@@ -1,4 +1,5 @@
1
1
  /* eslint-disable no-underscore-dangle */
2
+ const semver = require('semver');
2
3
  const omit = require('lodash/omit');
3
4
  const isEqual = require('lodash/isEqual');
4
5
  const isEmpty = require('lodash/isEmpty');
@@ -163,7 +164,7 @@ class NodeState extends BaseState {
163
164
 
164
165
  cleanupDirtyUpgradeState() {
165
166
  return this.read().then((doc) => {
166
- if (doc.nextVersion === doc.version) {
167
+ if (doc.nextVersion && semver.lte(doc.nextVersion, doc.version)) {
167
168
  const updates = { nextVersion: '', upgradeSessionId: '' };
168
169
 
169
170
  // FIXME: this may cause the node exit some mode unexpectedly if it is not being upgraded
package/lib/util/index.js CHANGED
@@ -332,6 +332,8 @@ const getDataDirs = (dataDir) => ({
332
332
  tmp: path.join(dataDir, 'tmp'),
333
333
  blocklets: path.join(dataDir, 'blocklets'),
334
334
  services: path.join(dataDir, 'services'),
335
+ modules: path.join(dataDir, 'modules'),
336
+ certManagerModule: path.join(dataDir, 'modules', 'certificate-manager'),
335
337
  });
336
338
 
337
339
  // Ensure data dir for Blocklet Server exists
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.6.6",
6
+ "version": "1.6.7",
7
7
  "description": "",
8
8
  "main": "lib/index.js",
9
9
  "files": [
@@ -19,26 +19,28 @@
19
19
  "author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
20
20
  "license": "MIT",
21
21
  "dependencies": {
22
- "@abtnode/constant": "1.6.6",
23
- "@abtnode/cron": "1.6.6",
24
- "@abtnode/logger": "1.6.6",
25
- "@abtnode/queue": "1.6.6",
26
- "@abtnode/rbac": "1.6.6",
27
- "@abtnode/router-provider": "1.6.6",
28
- "@abtnode/static-server": "1.6.6",
29
- "@abtnode/timemachine": "1.6.6",
30
- "@abtnode/util": "1.6.6",
31
- "@arcblock/did": "^1.13.77",
32
- "@arcblock/event-hub": "1.13.77",
22
+ "@abtnode/certificate-manager": "1.6.7",
23
+ "@abtnode/constant": "1.6.7",
24
+ "@abtnode/cron": "1.6.7",
25
+ "@abtnode/db": "1.6.7",
26
+ "@abtnode/logger": "1.6.7",
27
+ "@abtnode/queue": "1.6.7",
28
+ "@abtnode/rbac": "1.6.7",
29
+ "@abtnode/router-provider": "1.6.7",
30
+ "@abtnode/static-server": "1.6.7",
31
+ "@abtnode/timemachine": "1.6.7",
32
+ "@abtnode/util": "1.6.7",
33
+ "@arcblock/did": "^1.13.79",
34
+ "@arcblock/event-hub": "1.13.79",
33
35
  "@arcblock/pm2-events": "^0.0.5",
34
- "@arcblock/vc": "^1.13.77",
35
- "@blocklet/meta": "1.6.6",
36
+ "@arcblock/vc": "^1.13.79",
37
+ "@blocklet/meta": "1.6.7",
36
38
  "@fidm/x509": "^1.2.1",
37
39
  "@nedb/core": "^1.2.2",
38
40
  "@nedb/multi": "^1.2.2",
39
- "@ocap/mcrypto": "^1.13.77",
40
- "@ocap/util": "^1.13.77",
41
- "@ocap/wallet": "^1.13.77",
41
+ "@ocap/mcrypto": "^1.13.79",
42
+ "@ocap/util": "^1.13.79",
43
+ "@ocap/wallet": "^1.13.79",
42
44
  "@slack/webhook": "^5.0.3",
43
45
  "axios": "^0.21.4",
44
46
  "axon": "^2.0.3",
@@ -51,7 +53,7 @@
51
53
  "is-base64": "^1.1.0",
52
54
  "is-ip": "^3.1.0",
53
55
  "is-url": "^1.2.4",
54
- "joi": "^17.4.0",
56
+ "joi": "^17.5.0",
55
57
  "js-yaml": "^3.14.0",
56
58
  "lodash": "^4.17.21",
57
59
  "lru-cache": "^6.0.0",
@@ -71,7 +73,7 @@
71
73
  "compression": "^1.7.4",
72
74
  "expand-tilde": "^2.0.2",
73
75
  "express": "^4.17.1",
74
- "jest": "^27.3.1"
76
+ "jest": "^27.4.5"
75
77
  },
76
- "gitHead": "59b5e0b6f2f0e8e27aa4a50722866beda6f91b84"
78
+ "gitHead": "d681c03a398e3c7d4dbc878db93b2ab778dded81"
77
79
  }