@abtnode/core 1.6.3 → 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/blocklet/extras.js +26 -1
- package/lib/blocklet/manager/disk.js +126 -131
- package/lib/cert.js +116 -0
- package/lib/index.js +26 -9
- package/lib/migrations/1.6.4-security.js +59 -0
- package/lib/migrations/1.6.5-security.js +60 -0
- package/lib/migrations/1.6.7-certificate.js +30 -0
- package/lib/router/helper.js +99 -107
- package/lib/router/index.js +1 -1
- package/lib/router/manager.js +10 -6
- package/lib/states/base.js +3 -212
- package/lib/states/blocklet-extras.js +20 -15
- package/lib/states/blocklet.js +58 -17
- package/lib/states/index.js +4 -21
- package/lib/states/node.js +9 -3
- package/lib/util/blocklet.js +1 -1
- package/lib/util/index.js +2 -0
- package/package.json +22 -20
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
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
const
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
2
3
|
const md5 = require('@abtnode/util/lib/md5');
|
|
3
4
|
const Cron = require('@abtnode/cron');
|
|
4
5
|
|
|
@@ -9,6 +10,8 @@ const {
|
|
|
9
10
|
fromBlockletSource,
|
|
10
11
|
toBlockletSource,
|
|
11
12
|
} = require('@blocklet/meta/lib/constants');
|
|
13
|
+
const { listProviders } = require('@abtnode/router-provider');
|
|
14
|
+
const { DEFAULT_CERTIFICATE_EMAIL } = require('@abtnode/constant');
|
|
12
15
|
|
|
13
16
|
const RoutingSnapshot = require('./states/routing-snapshot');
|
|
14
17
|
const BlockletRegistry = require('./blocklet/registry');
|
|
@@ -20,6 +23,7 @@ const NodeAPI = require('./api/node');
|
|
|
20
23
|
const TeamAPI = require('./api/team');
|
|
21
24
|
const WebHook = require('./webhook');
|
|
22
25
|
const states = require('./states');
|
|
26
|
+
const Cert = require('./cert');
|
|
23
27
|
|
|
24
28
|
const IP = require('./util/ip');
|
|
25
29
|
const DomainStatus = require('./util/domain-status');
|
|
@@ -53,6 +57,11 @@ function ABTNode(options) {
|
|
|
53
57
|
throw new Error('Can not initialize ABTNode without dataDir');
|
|
54
58
|
}
|
|
55
59
|
|
|
60
|
+
const ekFile = path.join(options.dataDir, '.sock');
|
|
61
|
+
if (fs.existsSync(ekFile)) {
|
|
62
|
+
options.dek = fs.readFileSync(ekFile);
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
if (typeof options.daemon === 'undefined') {
|
|
57
66
|
options.daemon = false;
|
|
58
67
|
}
|
|
@@ -101,7 +110,11 @@ function ABTNode(options) {
|
|
|
101
110
|
// Initialize storage
|
|
102
111
|
states.init(dataDirs, options);
|
|
103
112
|
|
|
104
|
-
const
|
|
113
|
+
const certManager = new Cert({
|
|
114
|
+
maintainerEmail: DEFAULT_CERTIFICATE_EMAIL,
|
|
115
|
+
dataDir: dataDirs.certManagerModule,
|
|
116
|
+
});
|
|
117
|
+
const routerManager = new RouterManager({ certManager });
|
|
105
118
|
const routingSnapshot = new RoutingSnapshot({
|
|
106
119
|
baseDir: dataDirs.core,
|
|
107
120
|
getRoutingData: async () => {
|
|
@@ -110,7 +123,6 @@ function ABTNode(options) {
|
|
|
110
123
|
return { sites };
|
|
111
124
|
},
|
|
112
125
|
});
|
|
113
|
-
|
|
114
126
|
const blockletRegistry = new BlockletRegistry();
|
|
115
127
|
const blockletManager = new BlockletManager({
|
|
116
128
|
dataDirs,
|
|
@@ -120,6 +132,8 @@ function ABTNode(options) {
|
|
|
120
132
|
daemon: options.daemon,
|
|
121
133
|
});
|
|
122
134
|
|
|
135
|
+
certManager.start(); // 启动证书服务
|
|
136
|
+
|
|
123
137
|
const {
|
|
124
138
|
handleRouting,
|
|
125
139
|
getRoutingRulesByDid,
|
|
@@ -134,10 +148,10 @@ function ABTNode(options) {
|
|
|
134
148
|
getCertificates,
|
|
135
149
|
getSitesFromSnapshot,
|
|
136
150
|
getRoutingCrons,
|
|
137
|
-
|
|
151
|
+
ensureDashboardCertificate,
|
|
152
|
+
} = getRouterHelpers({ dataDirs, routingSnapshot, routerManager, blockletManager, certManager });
|
|
138
153
|
|
|
139
154
|
const teamManager = new TeamManager({ nodeDid: options.nodeDid, dataDirs, states });
|
|
140
|
-
|
|
141
155
|
const nodeAPI = new NodeAPI(states.node, blockletRegistry);
|
|
142
156
|
const teamAPI = new TeamAPI({ states, teamManager });
|
|
143
157
|
|
|
@@ -304,6 +318,7 @@ function ABTNode(options) {
|
|
|
304
318
|
takeRoutingSnapshot,
|
|
305
319
|
getSitesFromSnapshot,
|
|
306
320
|
ensureDashboardRouting,
|
|
321
|
+
ensureDashboardCertificate,
|
|
307
322
|
|
|
308
323
|
addDomainAlias: routerManager.addDomainAlias.bind(routerManager),
|
|
309
324
|
deleteDomainAlias: routerManager.deleteDomainAlias.bind(routerManager),
|
|
@@ -312,11 +327,13 @@ function ABTNode(options) {
|
|
|
312
327
|
checkDomains: domainStatus.checkDomainsStatus.bind(domainStatus),
|
|
313
328
|
handleRouting,
|
|
314
329
|
|
|
315
|
-
|
|
330
|
+
certManager,
|
|
331
|
+
updateNginxHttpsCert: certManager.update.bind(certManager), // TODO: 重命名:updateCert
|
|
316
332
|
getCertificates,
|
|
317
|
-
addCertificate:
|
|
318
|
-
deleteCertificate:
|
|
319
|
-
findCertificateByDomain:
|
|
333
|
+
addCertificate: certManager.add.bind(certManager),
|
|
334
|
+
deleteCertificate: certManager.remove.bind(certManager),
|
|
335
|
+
findCertificateByDomain: certManager.getByDomain.bind(certManager),
|
|
336
|
+
issueLetsEncryptCert: certManager.issue.bind(certManager),
|
|
320
337
|
|
|
321
338
|
// Access Key
|
|
322
339
|
getAccessKeys: states.accessKey.list.bind(states.accessKey),
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/* eslint-disable no-await-in-loop */
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const yaml = require('js-yaml');
|
|
6
|
+
const cloneDeep = require('lodash/cloneDeep');
|
|
7
|
+
const security = require('@abtnode/util/lib/security');
|
|
8
|
+
|
|
9
|
+
module.exports = async ({ states, configFile, dataDir }) => {
|
|
10
|
+
if (process.env.CI || process.env.NODE_ENV === 'test') {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const file = path.join(dataDir, '.sock');
|
|
15
|
+
if (fs.existsSync(file)) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
fs.writeFileSync(file, crypto.randomBytes(32), { encoding: 'binary', mode: '0600' });
|
|
21
|
+
|
|
22
|
+
const config = yaml.safeLoad(fs.readFileSync(configFile).toString(), { json: true });
|
|
23
|
+
config.node.sk = security.encrypt(config.node.sk, config.node.did, fs.readFileSync(file));
|
|
24
|
+
fs.writeFileSync(configFile, yaml.dump(config));
|
|
25
|
+
await states.node.updateNodeInfo({ sk: config.node.sk });
|
|
26
|
+
|
|
27
|
+
const items = await states.blockletExtras.find();
|
|
28
|
+
for (const item of items) {
|
|
29
|
+
const newConfigs = cloneDeep(item.configs || []).map((c) => {
|
|
30
|
+
if (c.secure) {
|
|
31
|
+
c.value = security.encrypt(c.value, item.did, fs.readFileSync(file));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return c;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const newChildren = cloneDeep(item.children || []).map((x) => {
|
|
38
|
+
return {
|
|
39
|
+
...x,
|
|
40
|
+
configs: cloneDeep(x.configs || []).map((c) => {
|
|
41
|
+
if (c.secure) {
|
|
42
|
+
c.value = security.encrypt(c.value, item.did, fs.readFileSync(file));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return c;
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await states.blockletExtras.update({ did: item.did }, { $set: { configs: newConfigs, children: newChildren } });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
states.node.compactDatafile();
|
|
54
|
+
states.blockletExtras.compactDatafile();
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error(err);
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/* eslint-disable no-await-in-loop */
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const uniq = require('lodash/uniq');
|
|
5
|
+
const cloneDeep = require('lodash/cloneDeep');
|
|
6
|
+
const security = require('@abtnode/util/lib/security');
|
|
7
|
+
const { forEachBlocklet } = require('@blocklet/meta/lib/util');
|
|
8
|
+
|
|
9
|
+
module.exports = async ({ states, dataDir }) => {
|
|
10
|
+
if (process.env.CI || process.env.NODE_ENV === 'test') {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const file = path.join(dataDir, '.sock');
|
|
15
|
+
if (fs.existsSync(file) === false) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const extras = await states.blockletExtras.find();
|
|
21
|
+
const blocklets = await states.blocklet.find();
|
|
22
|
+
for (const blocklet of blocklets) {
|
|
23
|
+
forEachBlocklet(
|
|
24
|
+
blocklet,
|
|
25
|
+
(b) => {
|
|
26
|
+
let configKeys;
|
|
27
|
+
const extra = extras.find((x) => x.did === blocklet.meta.did);
|
|
28
|
+
const rootKeys = (extra.configs || []).map((x) => x.key);
|
|
29
|
+
if (b.meta.did === blocklet.meta.did) {
|
|
30
|
+
configKeys = rootKeys;
|
|
31
|
+
} else {
|
|
32
|
+
const childExtra = (extra.children || []).find((x) => x.did === b.meta.did);
|
|
33
|
+
const childKeys = childExtra ? childExtra.configs.map((x) => x.key) : [];
|
|
34
|
+
configKeys = uniq([...rootKeys, ...childKeys]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
b.environments = cloneDeep(b.environments || [])
|
|
38
|
+
.filter((x) => configKeys.includes(x.key) === false)
|
|
39
|
+
.map((c) => {
|
|
40
|
+
if (c.key === 'BLOCKLET_APP_SK') {
|
|
41
|
+
c.value = security.encrypt(c.value, b.meta.did, fs.readFileSync(file));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return c;
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
{ sync: true }
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
await states.blocklet.updateById(blocklet._id, {
|
|
51
|
+
$set: { environments: blocklet.environments, children: blocklet.children, migratedFrom: '1.6.5' },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
states.blocklet.compactDatafile();
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.error(err);
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
@@ -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
|
+
};
|
package/lib/router/helper.js
CHANGED
|
@@ -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
|
|
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('
|
|
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
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
496
|
-
|
|
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
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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 = {}
|
|
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
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
|
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
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
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
|