@serve.zone/dcrouter 13.44.1 → 14.0.0
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/deno.json +1 -1
- package/dist_serve/bundle.js +1882 -1453
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/acme/manager.acme-config.d.ts +1 -14
- package/dist_ts/acme/manager.acme-config.js +4 -65
- package/dist_ts/classes.dcrouter.d.ts +7 -2
- package/dist_ts/classes.dcrouter.js +105 -27
- package/dist_ts/config/classes.api-token-manager.js +3 -3
- package/dist_ts/db/documents/classes.acme-config.doc.d.ts +1 -3
- package/dist_ts/db/documents/classes.acme-config.doc.js +2 -4
- package/dist_ts/dns/manager.dns.d.ts +0 -13
- package/dist_ts/dns/manager.dns.js +1 -81
- package/dist_ts/opsserver/handlers/certificate.handler.d.ts +0 -9
- package/dist_ts/opsserver/handlers/certificate.handler.js +1 -40
- package/dist_ts/opsserver/handlers/config.handler.js +11 -12
- package/dist_ts/opsserver/handlers/email-settings.handler.js +2 -2
- package/dist_ts_interfaces/data/acme-config.d.ts +1 -3
- package/dist_ts_interfaces/requests/certificate.d.ts +0 -12
- package/dist_ts_migrations/index.js +2 -2
- package/dist_ts_web/00_commitinfo_data.js +2 -2
- package/dist_ts_web/elements/network/ops-view-routes.js +118 -142
- package/package.json +4 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/acme/manager.acme-config.ts +3 -77
- package/ts/classes.dcrouter.ts +120 -28
- package/ts/config/classes.api-token-manager.ts +2 -2
- package/ts/db/documents/classes.acme-config.doc.ts +1 -3
- package/ts/dns/manager.dns.ts +0 -103
- package/ts/opsserver/handlers/certificate.handler.ts +0 -47
- package/ts/opsserver/handlers/config.handler.ts +10 -11
- package/ts/opsserver/handlers/email-settings.handler.ts +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/network/ops-view-routes.ts +124 -146
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { logger } from '../logger.js';
|
|
2
2
|
import { AcmeConfigDoc } from '../db/documents/index.js';
|
|
3
|
-
import type { IDcRouterOptions } from '../classes.dcrouter.js';
|
|
4
3
|
import type { IAcmeConfig } from '../../ts_interfaces/data/acme-config.js';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* AcmeConfigManager — owns the singleton ACME configuration in the DB.
|
|
8
7
|
*
|
|
9
8
|
* Lifecycle:
|
|
10
|
-
* - `start()` — loads
|
|
11
|
-
* fields (`tls.contactEmail`, `smartProxyConfig.acme.*`) on first boot.
|
|
9
|
+
* - `start()` — loads the DB-backed singleton configuration.
|
|
12
10
|
* - `getConfig()` — returns the in-memory cached `IAcmeConfig` (or null)
|
|
13
11
|
* - `updateConfig(args, updatedBy)` — upserts and refreshes the cache
|
|
14
12
|
*
|
|
@@ -20,32 +18,12 @@ import type { IAcmeConfig } from '../../ts_interfaces/data/acme-config.js';
|
|
|
20
18
|
export class AcmeConfigManager {
|
|
21
19
|
private cached: IAcmeConfig | null = null;
|
|
22
20
|
|
|
23
|
-
constructor(private options: IDcRouterOptions) {}
|
|
24
|
-
|
|
25
21
|
public async start(): Promise<void> {
|
|
26
22
|
logger.log('info', 'AcmeConfigManager: starting');
|
|
27
|
-
|
|
23
|
+
const doc = await AcmeConfigDoc.load();
|
|
28
24
|
|
|
29
25
|
if (!doc) {
|
|
30
|
-
|
|
31
|
-
const seed = this.deriveSeedFromOptions();
|
|
32
|
-
if (seed) {
|
|
33
|
-
doc = await this.createSeedDoc(seed);
|
|
34
|
-
logger.log(
|
|
35
|
-
'info',
|
|
36
|
-
`AcmeConfigManager: seeded from constructor legacy fields (accountEmail=${seed.accountEmail}, useProduction=${seed.useProduction})`,
|
|
37
|
-
);
|
|
38
|
-
} else {
|
|
39
|
-
logger.log(
|
|
40
|
-
'info',
|
|
41
|
-
'AcmeConfigManager: no AcmeConfig in DB and no legacy constructor fields — ACME disabled until configured via Domains > Certificates > Settings.',
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
} else if (this.deriveSeedFromOptions()) {
|
|
45
|
-
logger.log(
|
|
46
|
-
'warn',
|
|
47
|
-
'AcmeConfigManager: ignoring constructor tls.contactEmail / smartProxyConfig.acme — DB already has AcmeConfigDoc. Manage via Domains > Certificates > Settings.',
|
|
48
|
-
);
|
|
26
|
+
logger.log('info', 'AcmeConfigManager: no AcmeConfig in DB — ACME disabled until configured via Domains > Certificates > Settings.');
|
|
49
27
|
}
|
|
50
28
|
|
|
51
29
|
this.cached = doc ? this.toPlain(doc) : null;
|
|
@@ -116,58 +94,6 @@ export class AcmeConfigManager {
|
|
|
116
94
|
// Internal helpers
|
|
117
95
|
// ==========================================================================
|
|
118
96
|
|
|
119
|
-
/**
|
|
120
|
-
* Build a seed object from the legacy constructor fields. Returns null
|
|
121
|
-
* if the user has not provided any of them.
|
|
122
|
-
*
|
|
123
|
-
* Supports BOTH `tls.contactEmail` (short form) and `smartProxyConfig.acme`
|
|
124
|
-
* (full form). `smartProxyConfig.acme` wins when both are present.
|
|
125
|
-
*/
|
|
126
|
-
private deriveSeedFromOptions(): Omit<IAcmeConfig, 'updatedAt' | 'updatedBy'> | null {
|
|
127
|
-
const acme = this.options.smartProxyConfig?.acme;
|
|
128
|
-
const tls = this.options.tls;
|
|
129
|
-
|
|
130
|
-
// Prefer the explicit smartProxyConfig.acme block if present.
|
|
131
|
-
if (acme?.accountEmail) {
|
|
132
|
-
return {
|
|
133
|
-
accountEmail: acme.accountEmail,
|
|
134
|
-
enabled: acme.enabled !== false,
|
|
135
|
-
useProduction: acme.useProduction !== false,
|
|
136
|
-
autoRenew: acme.autoRenew !== false,
|
|
137
|
-
renewThresholdDays: acme.renewThresholdDays ?? 30,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Fall back to the short tls.contactEmail form.
|
|
142
|
-
if (tls?.contactEmail) {
|
|
143
|
-
return {
|
|
144
|
-
accountEmail: tls.contactEmail,
|
|
145
|
-
enabled: true,
|
|
146
|
-
useProduction: true,
|
|
147
|
-
autoRenew: true,
|
|
148
|
-
renewThresholdDays: 30,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private async createSeedDoc(
|
|
156
|
-
seed: Omit<IAcmeConfig, 'updatedAt' | 'updatedBy'>,
|
|
157
|
-
): Promise<AcmeConfigDoc> {
|
|
158
|
-
const doc = new AcmeConfigDoc();
|
|
159
|
-
doc.configId = 'acme-config';
|
|
160
|
-
doc.accountEmail = seed.accountEmail;
|
|
161
|
-
doc.enabled = seed.enabled;
|
|
162
|
-
doc.useProduction = seed.useProduction;
|
|
163
|
-
doc.autoRenew = seed.autoRenew;
|
|
164
|
-
doc.renewThresholdDays = seed.renewThresholdDays;
|
|
165
|
-
doc.updatedAt = Date.now();
|
|
166
|
-
doc.updatedBy = 'seed';
|
|
167
|
-
await doc.save();
|
|
168
|
-
return doc;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
97
|
private toPlain(doc: AcmeConfigDoc): IAcmeConfig {
|
|
172
98
|
return {
|
|
173
99
|
accountEmail: doc.accountEmail,
|
package/ts/classes.dcrouter.ts
CHANGED
|
@@ -454,14 +454,13 @@ export class DcRouter {
|
|
|
454
454
|
// AcmeConfigManager: optional, depends on DcRouterDb — owns the singleton
|
|
455
455
|
// ACME configuration (accountEmail, useProduction, etc.). Must run before
|
|
456
456
|
// SmartProxy so setupSmartProxy() can read the ACME config from the DB.
|
|
457
|
-
// On first boot, seeds from legacy `tls.contactEmail` / `smartProxyConfig.acme`.
|
|
458
457
|
if (this.options.dbConfig?.enabled !== false) {
|
|
459
458
|
this.serviceManager.addService(
|
|
460
459
|
new plugins.taskbuffer.Service('AcmeConfigManager')
|
|
461
460
|
.optional()
|
|
462
461
|
.dependsOn('DcRouterDb')
|
|
463
462
|
.withStart(async () => {
|
|
464
|
-
this.acmeConfigManager = new AcmeConfigManager(
|
|
463
|
+
this.acmeConfigManager = new AcmeConfigManager();
|
|
465
464
|
await this.acmeConfigManager.start();
|
|
466
465
|
})
|
|
467
466
|
.withStop(async () => {
|
|
@@ -813,7 +812,7 @@ export class DcRouter {
|
|
|
813
812
|
?? false;
|
|
814
813
|
}
|
|
815
814
|
|
|
816
|
-
private
|
|
815
|
+
private getRemoteIngressHubSettingsMigrationSeed(): TRemoteIngressHubSettingsUpdate {
|
|
817
816
|
const remoteIngressConfig = this.options.remoteIngressConfig;
|
|
818
817
|
const seed: TRemoteIngressHubSettingsUpdate = {};
|
|
819
818
|
if (remoteIngressConfig?.enabled !== undefined) {
|
|
@@ -831,7 +830,7 @@ export class DcRouter {
|
|
|
831
830
|
return seed;
|
|
832
831
|
}
|
|
833
832
|
|
|
834
|
-
private
|
|
833
|
+
private getEmailSettingsMigrationSeed(): IEmailServerSettingsSeed {
|
|
835
834
|
const seed: IEmailServerSettingsSeed = {};
|
|
836
835
|
if (this.options.emailConfig) {
|
|
837
836
|
seed.enabled = true;
|
|
@@ -1106,8 +1105,8 @@ export class DcRouter {
|
|
|
1106
1105
|
// Run any pending data migrations before anything else reads from the DB.
|
|
1107
1106
|
// This must complete before ConfigManagers loads profiles.
|
|
1108
1107
|
const migration = await createMigrationRunner(this.dcRouterDb.getDb(), commitinfo.version, {
|
|
1109
|
-
remoteIngressHubSettings: this.
|
|
1110
|
-
emailServerSettings: this.
|
|
1108
|
+
remoteIngressHubSettings: this.getRemoteIngressHubSettingsMigrationSeed(),
|
|
1109
|
+
emailServerSettings: this.getEmailSettingsMigrationSeed(),
|
|
1111
1110
|
});
|
|
1112
1111
|
const migrationResult = await migration.run();
|
|
1113
1112
|
if (migrationResult.stepsApplied.length > 0) {
|
|
@@ -1172,7 +1171,7 @@ export class DcRouter {
|
|
|
1172
1171
|
// Combined routes for SmartProxy bootstrap (before DB routes are loaded)
|
|
1173
1172
|
let routes: plugins.smartproxy.IRouteConfig[] = [
|
|
1174
1173
|
...this.seedConfigRoutes,
|
|
1175
|
-
...this.seedEmailRoutes,
|
|
1174
|
+
...this.getRuntimeEmailRoutes(this.seedEmailRoutes as IDcRouterRouteConfig[]),
|
|
1176
1175
|
...this.runtimeDnsRoutes,
|
|
1177
1176
|
];
|
|
1178
1177
|
|
|
@@ -1715,6 +1714,115 @@ export class DcRouter {
|
|
|
1715
1714
|
return dnsRoutes;
|
|
1716
1715
|
}
|
|
1717
1716
|
|
|
1717
|
+
private getRuntimeEmailRoutes(emailRoutes: IDcRouterRouteConfig[]): plugins.smartproxy.IRouteConfig[] {
|
|
1718
|
+
return emailRoutes.map((route) => this.createServerFirstEmailRuntimeRoute(route) || route);
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
private getCurrentGeneratedEmailRouteNames(): Set<string> {
|
|
1722
|
+
const sourceRoutes = this.seedEmailRoutes.length > 0
|
|
1723
|
+
? this.seedEmailRoutes
|
|
1724
|
+
: this.options.emailConfig
|
|
1725
|
+
? this.generateEmailRoutes(this.options.emailConfig)
|
|
1726
|
+
: [];
|
|
1727
|
+
return new Set(sourceRoutes.map((route) => route.name).filter(Boolean) as string[]);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
private shouldHydrateGeneratedEmailRoute(storedRoute: IRoute): boolean {
|
|
1731
|
+
if (storedRoute.origin !== 'email') {
|
|
1732
|
+
return false;
|
|
1733
|
+
}
|
|
1734
|
+
const routeName = storedRoute.route.name;
|
|
1735
|
+
if (!routeName || !this.getCurrentGeneratedEmailRouteNames().has(routeName)) {
|
|
1736
|
+
return false;
|
|
1737
|
+
}
|
|
1738
|
+
const expectedSystemKey = `email:${routeName}`;
|
|
1739
|
+
return !storedRoute.systemKey || storedRoute.systemKey === expectedSystemKey;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
private createServerFirstEmailRuntimeRoute(
|
|
1743
|
+
route: plugins.smartproxy.IRouteConfig,
|
|
1744
|
+
): plugins.smartproxy.IRouteConfig | undefined {
|
|
1745
|
+
const action = route.action as any;
|
|
1746
|
+
if (action?.type !== 'forward') {
|
|
1747
|
+
return undefined;
|
|
1748
|
+
}
|
|
1749
|
+
const tlsMode = action.tls?.mode;
|
|
1750
|
+
if (tlsMode === 'terminate' || tlsMode === 'terminate-and-reencrypt') {
|
|
1751
|
+
return undefined;
|
|
1752
|
+
}
|
|
1753
|
+
const routePorts = plugins.smartproxy.expandPortRange(route.match?.ports as any) as number[];
|
|
1754
|
+
if (routePorts.length !== 1) {
|
|
1755
|
+
return undefined;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
const target = action.targets?.[0];
|
|
1759
|
+
if (!target || action.targets.length !== 1 || typeof target.port !== 'number') {
|
|
1760
|
+
return undefined;
|
|
1761
|
+
}
|
|
1762
|
+
if (typeof target.host !== 'string') {
|
|
1763
|
+
return undefined;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
const targetHost = target.host === 'localhost' ? '127.0.0.1' : target.host;
|
|
1767
|
+
return {
|
|
1768
|
+
...route,
|
|
1769
|
+
action: {
|
|
1770
|
+
type: 'socket-handler' as any,
|
|
1771
|
+
socketHandler: this.createEmailSocketProxyHandler(targetHost, target.port),
|
|
1772
|
+
} as any,
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
private createEmailSocketProxyHandler(
|
|
1777
|
+
targetHost: string,
|
|
1778
|
+
targetPort: number,
|
|
1779
|
+
): NonNullable<plugins.smartproxy.IRouteConfig['action']['socketHandler']> {
|
|
1780
|
+
return (clientSocket) => {
|
|
1781
|
+
let backendSocket: plugins.net.Socket | undefined;
|
|
1782
|
+
let connectTimeout: ReturnType<typeof setTimeout> & { unref?: () => void };
|
|
1783
|
+
let cleanupDone = false;
|
|
1784
|
+
|
|
1785
|
+
const cleanup = () => {
|
|
1786
|
+
if (cleanupDone) return;
|
|
1787
|
+
cleanupDone = true;
|
|
1788
|
+
clearTimeout(connectTimeout);
|
|
1789
|
+
clientSocket.removeListener('timeout', cleanup);
|
|
1790
|
+
clientSocket.removeListener('error', cleanup);
|
|
1791
|
+
clientSocket.removeListener('end', cleanup);
|
|
1792
|
+
clientSocket.removeListener('close', cleanup);
|
|
1793
|
+
backendSocket?.removeListener('timeout', cleanup);
|
|
1794
|
+
backendSocket?.removeListener('error', cleanup);
|
|
1795
|
+
backendSocket?.removeListener('end', cleanup);
|
|
1796
|
+
backendSocket?.removeListener('close', cleanup);
|
|
1797
|
+
clientSocket.destroy();
|
|
1798
|
+
backendSocket?.destroy();
|
|
1799
|
+
};
|
|
1800
|
+
|
|
1801
|
+
connectTimeout = setTimeout(() => {
|
|
1802
|
+
cleanup();
|
|
1803
|
+
}, 30_000);
|
|
1804
|
+
connectTimeout.unref?.();
|
|
1805
|
+
|
|
1806
|
+
clientSocket.setTimeout(300_000);
|
|
1807
|
+
clientSocket.on('timeout', cleanup);
|
|
1808
|
+
clientSocket.on('error', cleanup);
|
|
1809
|
+
clientSocket.on('end', cleanup);
|
|
1810
|
+
clientSocket.on('close', cleanup);
|
|
1811
|
+
|
|
1812
|
+
backendSocket = plugins.net.connect(targetPort, targetHost, () => {
|
|
1813
|
+
clearTimeout(connectTimeout);
|
|
1814
|
+
backendSocket?.setTimeout(300_000);
|
|
1815
|
+
clientSocket.pipe(backendSocket!);
|
|
1816
|
+
backendSocket!.pipe(clientSocket);
|
|
1817
|
+
});
|
|
1818
|
+
backendSocket.setTimeout(30_000);
|
|
1819
|
+
backendSocket.on('timeout', cleanup);
|
|
1820
|
+
backendSocket.on('error', cleanup);
|
|
1821
|
+
backendSocket.on('end', cleanup);
|
|
1822
|
+
backendSocket.on('close', cleanup);
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1718
1826
|
private hydrateStoredRouteForRuntime(storedRoute: IRoute): plugins.smartproxy.IRouteConfig | undefined {
|
|
1719
1827
|
const routeName = storedRoute.route.name || '';
|
|
1720
1828
|
const isDohRoute = storedRoute.origin === 'dns'
|
|
@@ -1722,6 +1830,9 @@ export class DcRouter {
|
|
|
1722
1830
|
&& routeName.startsWith('dns-over-https-');
|
|
1723
1831
|
|
|
1724
1832
|
if (!isDohRoute) {
|
|
1833
|
+
if (this.shouldHydrateGeneratedEmailRoute(storedRoute)) {
|
|
1834
|
+
return this.createServerFirstEmailRuntimeRoute(storedRoute.route);
|
|
1835
|
+
}
|
|
1725
1836
|
return undefined;
|
|
1726
1837
|
}
|
|
1727
1838
|
|
|
@@ -1860,28 +1971,9 @@ export class DcRouter {
|
|
|
1860
1971
|
465: 10465 // SMTPS
|
|
1861
1972
|
};
|
|
1862
1973
|
|
|
1863
|
-
// Transform domains if they are provided as strings
|
|
1864
|
-
let transformedDomains = this.options.emailConfig.domains;
|
|
1865
|
-
if (transformedDomains && transformedDomains.length > 0) {
|
|
1866
|
-
// Check if domains are strings (for backward compatibility)
|
|
1867
|
-
if (typeof transformedDomains[0] === 'string') {
|
|
1868
|
-
transformedDomains = (transformedDomains as any).map((domain: string) => ({
|
|
1869
|
-
domain,
|
|
1870
|
-
dnsMode: 'external-dns' as const,
|
|
1871
|
-
dkim: {
|
|
1872
|
-
selector: 'default',
|
|
1873
|
-
keySize: 2048,
|
|
1874
|
-
rotateKeys: false,
|
|
1875
|
-
rotationInterval: 90
|
|
1876
|
-
}
|
|
1877
|
-
}));
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
1974
|
// Create config with mapped ports
|
|
1882
1975
|
const emailConfig: IUnifiedEmailServerOptions = await this.workAppMailManager.applyStoredIdentitiesToEmailConfig({
|
|
1883
1976
|
...this.options.emailConfig,
|
|
1884
|
-
domains: transformedDomains,
|
|
1885
1977
|
ports: this.options.emailConfig.ports.map(port => portMapping[port] || port + 10000),
|
|
1886
1978
|
persistRoutes: this.options.emailConfig.persistRoutes ?? false,
|
|
1887
1979
|
queue: {
|
|
@@ -2251,8 +2343,8 @@ export class DcRouter {
|
|
|
2251
2343
|
// Ensure DKIM keys exist for internal-dns domains before generating records.
|
|
2252
2344
|
await this.initializeDkimForEmailDomains();
|
|
2253
2345
|
|
|
2254
|
-
|
|
2255
|
-
|
|
2346
|
+
// Generate DKIM records directly from smartmta.
|
|
2347
|
+
const dkimRecords = await this.loadDkimRecords();
|
|
2256
2348
|
|
|
2257
2349
|
// Combine all records: authoritative, email, DKIM, and user-defined
|
|
2258
2350
|
const allRecords = [...authoritativeRecords, ...emailDnsRecords, ...dkimRecords];
|
|
@@ -111,13 +111,13 @@ export class ApiTokenManager {
|
|
|
111
111
|
const scopes = new Set<TApiTokenScope>([...token.scopes, ...(token.policy?.scopes || [])]);
|
|
112
112
|
if (scopes.has(scope)) return true;
|
|
113
113
|
|
|
114
|
-
const
|
|
114
|
+
const equivalentScopes: Partial<Record<TApiTokenScope, TApiTokenScope[]>> = {
|
|
115
115
|
'gateway-clients:read': ['workhosters:read'],
|
|
116
116
|
'gateway-clients:write': ['workhosters:write'],
|
|
117
117
|
'workhosters:read': ['gateway-clients:read'],
|
|
118
118
|
'workhosters:write': ['gateway-clients:write'],
|
|
119
119
|
};
|
|
120
|
-
return Boolean(
|
|
120
|
+
return Boolean(equivalentScopes[scope]?.some((alias) => scopes.has(alias)));
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/**
|
|
@@ -8,9 +8,7 @@ const getDb = () => DcRouterDb.getInstance().getDb();
|
|
|
8
8
|
* keyed on the fixed `configId = 'acme-config'` following the
|
|
9
9
|
* `VpnServerKeysDoc` pattern.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* constructor fields. Managed via the OpsServer UI at
|
|
13
|
-
* **Domains > Certificates > Settings**.
|
|
11
|
+
* Managed via the OpsServer UI at **Domains > Certificates > Settings**.
|
|
14
12
|
*/
|
|
15
13
|
@plugins.smartdata.Collection(() => getDb())
|
|
16
14
|
export class AcmeConfigDoc extends plugins.smartdata.SmartDataDbDoc<AcmeConfigDoc, AcmeConfigDoc> {
|
package/ts/dns/manager.dns.ts
CHANGED
|
@@ -24,7 +24,6 @@ import type {
|
|
|
24
24
|
*
|
|
25
25
|
* Responsibilities:
|
|
26
26
|
* - Load Domain/DnsRecord docs from the DB on start
|
|
27
|
-
* - First-boot seeding from legacy constructor config (dnsScopes/dnsRecords/dnsNsDomains)
|
|
28
27
|
* - Register dcrouter-hosted domain records with smartdns.DnsServer at startup
|
|
29
28
|
* - Provide CRUD methods used by OpsServer handlers (dcrouter-hosted domains hit
|
|
30
29
|
* smartdns, provider domains hit the provider API)
|
|
@@ -53,13 +52,8 @@ export class DnsManager {
|
|
|
53
52
|
// Lifecycle
|
|
54
53
|
// ==========================================================================
|
|
55
54
|
|
|
56
|
-
/**
|
|
57
|
-
* Called from DcRouter after DcRouterDb is up. Performs first-boot seeding
|
|
58
|
-
* from legacy constructor config if (and only if) the DB is empty.
|
|
59
|
-
*/
|
|
60
55
|
public async start(): Promise<void> {
|
|
61
56
|
logger.log('info', 'DnsManager: starting');
|
|
62
|
-
await this.seedFromConstructorConfigIfEmpty();
|
|
63
57
|
}
|
|
64
58
|
|
|
65
59
|
public async stop(): Promise<void> {
|
|
@@ -77,103 +71,6 @@ export class DnsManager {
|
|
|
77
71
|
await this.applyDcrouterDomainsToDnsServer();
|
|
78
72
|
}
|
|
79
73
|
|
|
80
|
-
// ==========================================================================
|
|
81
|
-
// First-boot seeding
|
|
82
|
-
// ==========================================================================
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* If no DomainDocs exist yet but the constructor has legacy DNS fields,
|
|
86
|
-
* seed them as dcrouter-hosted (`domain.source: 'dcrouter'`) zones with
|
|
87
|
-
* local (`record.source: 'local'`) records. On subsequent boots (DB has
|
|
88
|
-
* entries), constructor config is ignored with a warning.
|
|
89
|
-
*/
|
|
90
|
-
private async seedFromConstructorConfigIfEmpty(): Promise<void> {
|
|
91
|
-
const existingDomains = await DomainDoc.findAll();
|
|
92
|
-
const hasLegacyConfig =
|
|
93
|
-
(this.options.dnsScopes && this.options.dnsScopes.length > 0) ||
|
|
94
|
-
(this.options.dnsRecords && this.options.dnsRecords.length > 0);
|
|
95
|
-
|
|
96
|
-
if (existingDomains.length > 0) {
|
|
97
|
-
if (hasLegacyConfig) {
|
|
98
|
-
logger.log(
|
|
99
|
-
'warn',
|
|
100
|
-
'DnsManager: DB has DomainDoc entries — ignoring legacy dnsScopes/dnsRecords constructor config. ' +
|
|
101
|
-
'dnsNsDomains is still required for nameserver and DoH bootstrap unless that moves into DB-backed config.',
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (!hasLegacyConfig) {
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
logger.log('info', 'DnsManager: seeding DB from legacy constructor DNS config');
|
|
112
|
-
|
|
113
|
-
const now = Date.now();
|
|
114
|
-
const seededDomains = new Map<string, DomainDoc>();
|
|
115
|
-
|
|
116
|
-
// Create one DomainDoc per dnsScope (these are the authoritative zones)
|
|
117
|
-
for (const scope of this.options.dnsScopes ?? []) {
|
|
118
|
-
const domain = new DomainDoc();
|
|
119
|
-
domain.id = plugins.uuid.v4();
|
|
120
|
-
domain.name = scope.toLowerCase();
|
|
121
|
-
domain.source = 'dcrouter';
|
|
122
|
-
domain.authoritative = true;
|
|
123
|
-
domain.createdAt = now;
|
|
124
|
-
domain.updatedAt = now;
|
|
125
|
-
domain.createdBy = 'seed';
|
|
126
|
-
await domain.save();
|
|
127
|
-
seededDomains.set(domain.name, domain);
|
|
128
|
-
logger.log('info', `DnsManager: seeded DomainDoc for ${domain.name}`);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Map each legacy dnsRecord to its parent DomainDoc
|
|
132
|
-
for (const rec of this.options.dnsRecords ?? []) {
|
|
133
|
-
const parent = this.findParentDomain(rec.name, seededDomains);
|
|
134
|
-
if (!parent) {
|
|
135
|
-
logger.log(
|
|
136
|
-
'warn',
|
|
137
|
-
`DnsManager: legacy dnsRecord '${rec.name}' has no matching dnsScope — skipping seed`,
|
|
138
|
-
);
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
const record = new DnsRecordDoc();
|
|
142
|
-
record.id = plugins.uuid.v4();
|
|
143
|
-
record.domainId = parent.id;
|
|
144
|
-
record.name = rec.name.toLowerCase();
|
|
145
|
-
record.type = rec.type as TDnsRecordType;
|
|
146
|
-
record.value = rec.value;
|
|
147
|
-
record.ttl = rec.ttl ?? 300;
|
|
148
|
-
record.source = 'local';
|
|
149
|
-
record.createdAt = now;
|
|
150
|
-
record.updatedAt = now;
|
|
151
|
-
record.createdBy = 'seed';
|
|
152
|
-
await record.save();
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
logger.log(
|
|
156
|
-
'info',
|
|
157
|
-
`DnsManager: seeded ${seededDomains.size} domain(s) and ${this.options.dnsRecords?.length ?? 0} record(s) from legacy config`,
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
private findParentDomain(
|
|
162
|
-
recordName: string,
|
|
163
|
-
domains: Map<string, DomainDoc>,
|
|
164
|
-
): DomainDoc | null {
|
|
165
|
-
const lower = recordName.toLowerCase().replace(/^\*\./, '');
|
|
166
|
-
let candidate: DomainDoc | null = null;
|
|
167
|
-
for (const [name, doc] of domains) {
|
|
168
|
-
if (lower === name || lower.endsWith(`.${name}`)) {
|
|
169
|
-
if (!candidate || name.length > candidate.name.length) {
|
|
170
|
-
candidate = doc;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return candidate;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
74
|
// ==========================================================================
|
|
178
75
|
// DcRouter-hosted domain DnsServer wiring
|
|
179
76
|
// ==========================================================================
|
|
@@ -61,17 +61,6 @@ export class CertificateHandler {
|
|
|
61
61
|
)
|
|
62
62
|
);
|
|
63
63
|
|
|
64
|
-
// Legacy route-based reprovision (backward compat)
|
|
65
|
-
router.addTypedHandler(
|
|
66
|
-
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificate>(
|
|
67
|
-
'reprovisionCertificate',
|
|
68
|
-
async (dataArg) => {
|
|
69
|
-
await this.requireAuth(dataArg, 'certificates:write');
|
|
70
|
-
return this.reprovisionCertificateByRoute(dataArg.routeName);
|
|
71
|
-
}
|
|
72
|
-
)
|
|
73
|
-
);
|
|
74
|
-
|
|
75
64
|
// Domain-based reprovision (preferred)
|
|
76
65
|
router.addTypedHandler(
|
|
77
66
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificateDomain>(
|
|
@@ -336,42 +325,6 @@ export class CertificateHandler {
|
|
|
336
325
|
return summary;
|
|
337
326
|
}
|
|
338
327
|
|
|
339
|
-
/**
|
|
340
|
-
* Legacy route-based reprovisioning. Kept for backward compatibility with
|
|
341
|
-
* older clients that send `reprovisionCertificate` typed-requests.
|
|
342
|
-
*
|
|
343
|
-
* Like reprovisionCertificateDomain, this triggers the full route apply
|
|
344
|
-
* pipeline rather than smartProxy.provisionCertificate(routeName) — which
|
|
345
|
-
* is a no-op when certProvisionFunction is set (Rust ACME disabled).
|
|
346
|
-
*/
|
|
347
|
-
private async reprovisionCertificateByRoute(routeName: string): Promise<{ success: boolean; message?: string }> {
|
|
348
|
-
const dcRouter = this.opsServerRef.dcRouterRef;
|
|
349
|
-
const smartProxy = dcRouter.smartProxy;
|
|
350
|
-
|
|
351
|
-
if (!smartProxy) {
|
|
352
|
-
return { success: false, message: 'SmartProxy is not running' };
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Clear event-based status for domains in this route so the
|
|
356
|
-
// certificate-issued event can refresh them
|
|
357
|
-
for (const [domain, entry] of dcRouter.certificateStatusMap) {
|
|
358
|
-
if (entry.routeNames.includes(routeName)) {
|
|
359
|
-
dcRouter.certificateStatusMap.delete(domain);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
try {
|
|
364
|
-
if (dcRouter.routeConfigManager) {
|
|
365
|
-
await dcRouter.routeConfigManager.applyRoutes();
|
|
366
|
-
} else {
|
|
367
|
-
await smartProxy.updateRoutes(smartProxy.routeManager.getRoutes());
|
|
368
|
-
}
|
|
369
|
-
return { success: true, message: `Certificate reprovisioning triggered for route '${routeName}'` };
|
|
370
|
-
} catch (err: unknown) {
|
|
371
|
-
return { success: false, message: (err as Error).message || 'Failed to reprovision certificate' };
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
328
|
/**
|
|
376
329
|
* Domain-based reprovisioning — clears backoff first, refreshes the smartacme
|
|
377
330
|
* cert (when forceRenew is set), then re-applies routes so the running Rust
|
|
@@ -59,15 +59,15 @@ export class ConfigHandler {
|
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
// --- SmartProxy ---
|
|
62
|
+
const acmeConfig = dcRouter.acmeConfigManager?.getConfig();
|
|
62
63
|
let acmeInfo: interfaces.requests.IConfigData['smartProxy']['acme'] = null;
|
|
63
|
-
if (
|
|
64
|
-
const acme = opts.smartProxyConfig.acme;
|
|
64
|
+
if (acmeConfig) {
|
|
65
65
|
acmeInfo = {
|
|
66
|
-
enabled:
|
|
67
|
-
accountEmail:
|
|
68
|
-
useProduction:
|
|
69
|
-
autoRenew:
|
|
70
|
-
renewThresholdDays:
|
|
66
|
+
enabled: acmeConfig.enabled,
|
|
67
|
+
accountEmail: acmeConfig.accountEmail,
|
|
68
|
+
useProduction: acmeConfig.useProduction,
|
|
69
|
+
autoRenew: acmeConfig.autoRenew,
|
|
70
|
+
renewThresholdDays: acmeConfig.renewThresholdDays,
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -127,8 +127,7 @@ export class ConfigHandler {
|
|
|
127
127
|
ttl: r.ttl,
|
|
128
128
|
}));
|
|
129
129
|
|
|
130
|
-
// dnsChallenge: true when at least one DnsProviderDoc exists in the DB
|
|
131
|
-
// (replaces the legacy `dnsChallenge.cloudflareApiKey` constructor field).
|
|
130
|
+
// dnsChallenge: true when at least one DnsProviderDoc exists in the DB.
|
|
132
131
|
let dnsChallengeEnabled = false;
|
|
133
132
|
try {
|
|
134
133
|
dnsChallengeEnabled = (await dcRouter.dnsManager?.hasAnyManagedDomain()) ?? false;
|
|
@@ -150,12 +149,12 @@ export class ConfigHandler {
|
|
|
150
149
|
let tlsSource: 'acme' | 'static' | 'none' = 'none';
|
|
151
150
|
if (opts.tls?.certPath && opts.tls?.keyPath) {
|
|
152
151
|
tlsSource = 'static';
|
|
153
|
-
} else if (
|
|
152
|
+
} else if (acmeConfig?.enabled) {
|
|
154
153
|
tlsSource = 'acme';
|
|
155
154
|
}
|
|
156
155
|
|
|
157
156
|
const tls: interfaces.requests.IConfigData['tls'] = {
|
|
158
|
-
contactEmail:
|
|
157
|
+
contactEmail: acmeConfig?.accountEmail || null,
|
|
159
158
|
domain: opts.tls?.domain || null,
|
|
160
159
|
source: tlsSource,
|
|
161
160
|
certPath: opts.tls?.certPath || null,
|