@serve.zone/dcrouter 6.2.4 → 6.4.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/dist_serve/bundle.js +559 -452
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +26 -0
- package/dist_ts/classes.dcrouter.js +49 -5
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
- package/dist_ts/opsserver/classes.opsserver.js +3 -1
- package/dist_ts/opsserver/handlers/index.d.ts +1 -0
- package/dist_ts/opsserver/handlers/index.js +2 -1
- package/dist_ts/opsserver/handlers/remoteingress.handler.d.ts +8 -0
- package/dist_ts/opsserver/handlers/remoteingress.handler.js +107 -0
- package/dist_ts/paths.d.ts +19 -9
- package/dist_ts/paths.js +30 -28
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +56 -0
- package/dist_ts/remoteingress/classes.remoteingress-manager.js +133 -0
- package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +45 -0
- package/dist_ts/remoteingress/classes.tunnel-manager.js +107 -0
- package/dist_ts/remoteingress/index.d.ts +2 -0
- package/dist_ts/remoteingress/index.js +3 -0
- package/dist_ts/security/classes.contentscanner.js +1 -2
- package/dist_ts_interfaces/data/index.d.ts +1 -0
- package/dist_ts_interfaces/data/index.js +2 -1
- package/dist_ts_interfaces/data/remoteingress.d.ts +24 -0
- package/dist_ts_interfaces/data/remoteingress.js +2 -0
- package/dist_ts_interfaces/requests/index.d.ts +1 -0
- package/dist_ts_interfaces/requests/index.js +2 -1
- package/dist_ts_interfaces/requests/remoteingress.d.ts +89 -0
- package/dist_ts_interfaces/requests/remoteingress.js +3 -0
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +19 -0
- package/dist_ts_web/appstate.js +124 -2
- package/dist_ts_web/elements/index.d.ts +1 -0
- package/dist_ts_web/elements/index.js +2 -1
- package/dist_ts_web/elements/ops-dashboard.js +6 -1
- package/dist_ts_web/elements/ops-view-remoteingress.d.ts +20 -0
- package/dist_ts_web/elements/ops-view-remoteingress.js +317 -0
- package/dist_ts_web/router.d.ts +1 -1
- package/dist_ts_web/router.js +2 -2
- package/package.json +2 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +79 -6
- package/ts/index.ts +3 -0
- package/ts/opsserver/classes.opsserver.ts +2 -0
- package/ts/opsserver/handlers/index.ts +2 -1
- package/ts/opsserver/handlers/remoteingress.handler.ts +163 -0
- package/ts/paths.ts +32 -31
- package/ts/plugins.ts +3 -1
- package/ts/remoteingress/classes.remoteingress-manager.ts +160 -0
- package/ts/remoteingress/classes.tunnel-manager.ts +126 -0
- package/ts/remoteingress/index.ts +2 -0
- package/ts/security/classes.contentscanner.ts +0 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +180 -1
- package/ts_web/elements/index.ts +1 -0
- package/ts_web/elements/ops-dashboard.ts +5 -0
- package/ts_web/elements/ops-view-remoteingress.ts +290 -0
- package/ts_web/router.ts +1 -1
package/ts/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ export class OpsServer {
|
|
|
19
19
|
private radiusHandler: handlers.RadiusHandler;
|
|
20
20
|
private emailOpsHandler: handlers.EmailOpsHandler;
|
|
21
21
|
private certificateHandler: handlers.CertificateHandler;
|
|
22
|
+
private remoteIngressHandler: handlers.RemoteIngressHandler;
|
|
22
23
|
|
|
23
24
|
constructor(dcRouterRefArg: DcRouter) {
|
|
24
25
|
this.dcRouterRef = dcRouterRefArg;
|
|
@@ -59,6 +60,7 @@ export class OpsServer {
|
|
|
59
60
|
this.radiusHandler = new handlers.RadiusHandler(this);
|
|
60
61
|
this.emailOpsHandler = new handlers.EmailOpsHandler(this);
|
|
61
62
|
this.certificateHandler = new handlers.CertificateHandler(this);
|
|
63
|
+
this.remoteIngressHandler = new handlers.RemoteIngressHandler(this);
|
|
62
64
|
|
|
63
65
|
console.log('✅ OpsServer TypedRequest handlers initialized');
|
|
64
66
|
}
|
|
@@ -5,4 +5,5 @@ export * from './security.handler.js';
|
|
|
5
5
|
export * from './stats.handler.js';
|
|
6
6
|
export * from './radius.handler.js';
|
|
7
7
|
export * from './email-ops.handler.js';
|
|
8
|
-
export * from './certificate.handler.js';
|
|
8
|
+
export * from './certificate.handler.js';
|
|
9
|
+
export * from './remoteingress.handler.js';
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import type { OpsServer } from '../classes.opsserver.js';
|
|
3
|
+
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
|
+
|
|
5
|
+
export class RemoteIngressHandler {
|
|
6
|
+
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
7
|
+
|
|
8
|
+
constructor(private opsServerRef: OpsServer) {
|
|
9
|
+
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
10
|
+
this.registerHandlers();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private registerHandlers(): void {
|
|
14
|
+
// Get all remote ingress edges
|
|
15
|
+
this.typedrouter.addTypedHandler(
|
|
16
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngresses>(
|
|
17
|
+
'getRemoteIngresses',
|
|
18
|
+
async (dataArg, toolsArg) => {
|
|
19
|
+
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
|
|
20
|
+
if (!manager) {
|
|
21
|
+
return { edges: [] };
|
|
22
|
+
}
|
|
23
|
+
// Return edges without secrets
|
|
24
|
+
const edges = manager.getAllEdges().map((e) => ({
|
|
25
|
+
...e,
|
|
26
|
+
secret: '********', // Never expose secrets via API
|
|
27
|
+
}));
|
|
28
|
+
return { edges };
|
|
29
|
+
},
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Create a new remote ingress edge
|
|
34
|
+
this.typedrouter.addTypedHandler(
|
|
35
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateRemoteIngress>(
|
|
36
|
+
'createRemoteIngress',
|
|
37
|
+
async (dataArg, toolsArg) => {
|
|
38
|
+
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
|
|
39
|
+
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
|
|
40
|
+
|
|
41
|
+
if (!manager) {
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
edge: null as any,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const edge = await manager.createEdge(
|
|
49
|
+
dataArg.name,
|
|
50
|
+
dataArg.listenPorts,
|
|
51
|
+
dataArg.tags,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Sync allowed edges with the hub
|
|
55
|
+
if (tunnelManager) {
|
|
56
|
+
await tunnelManager.syncAllowedEdges();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { success: true, edge };
|
|
60
|
+
},
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Delete a remote ingress edge
|
|
65
|
+
this.typedrouter.addTypedHandler(
|
|
66
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteRemoteIngress>(
|
|
67
|
+
'deleteRemoteIngress',
|
|
68
|
+
async (dataArg, toolsArg) => {
|
|
69
|
+
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
|
|
70
|
+
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
|
|
71
|
+
|
|
72
|
+
if (!manager) {
|
|
73
|
+
return { success: false, message: 'RemoteIngress not configured' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const deleted = await manager.deleteEdge(dataArg.id);
|
|
77
|
+
if (deleted && tunnelManager) {
|
|
78
|
+
await tunnelManager.syncAllowedEdges();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
success: deleted,
|
|
83
|
+
message: deleted ? undefined : 'Edge not found',
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Update a remote ingress edge
|
|
90
|
+
this.typedrouter.addTypedHandler(
|
|
91
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateRemoteIngress>(
|
|
92
|
+
'updateRemoteIngress',
|
|
93
|
+
async (dataArg, toolsArg) => {
|
|
94
|
+
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
|
|
95
|
+
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
|
|
96
|
+
|
|
97
|
+
if (!manager) {
|
|
98
|
+
return { success: false, edge: null as any };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const edge = await manager.updateEdge(dataArg.id, {
|
|
102
|
+
name: dataArg.name,
|
|
103
|
+
listenPorts: dataArg.listenPorts,
|
|
104
|
+
enabled: dataArg.enabled,
|
|
105
|
+
tags: dataArg.tags,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!edge) {
|
|
109
|
+
return { success: false, edge: null as any };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Sync allowed edges if enabled status changed
|
|
113
|
+
if (tunnelManager && dataArg.enabled !== undefined) {
|
|
114
|
+
await tunnelManager.syncAllowedEdges();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { success: true, edge: { ...edge, secret: '********' } };
|
|
118
|
+
},
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Regenerate secret for an edge
|
|
123
|
+
this.typedrouter.addTypedHandler(
|
|
124
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RegenerateRemoteIngressSecret>(
|
|
125
|
+
'regenerateRemoteIngressSecret',
|
|
126
|
+
async (dataArg, toolsArg) => {
|
|
127
|
+
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
|
|
128
|
+
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
|
|
129
|
+
|
|
130
|
+
if (!manager) {
|
|
131
|
+
return { success: false, secret: '' };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const secret = await manager.regenerateSecret(dataArg.id);
|
|
135
|
+
if (!secret) {
|
|
136
|
+
return { success: false, secret: '' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Sync allowed edges since secret changed
|
|
140
|
+
if (tunnelManager) {
|
|
141
|
+
await tunnelManager.syncAllowedEdges();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { success: true, secret };
|
|
145
|
+
},
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Get runtime status of all edges
|
|
150
|
+
this.typedrouter.addTypedHandler(
|
|
151
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressStatus>(
|
|
152
|
+
'getRemoteIngressStatus',
|
|
153
|
+
async (dataArg, toolsArg) => {
|
|
154
|
+
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
|
|
155
|
+
if (!tunnelManager) {
|
|
156
|
+
return { statuses: [] };
|
|
157
|
+
}
|
|
158
|
+
return { statuses: tunnelManager.getEdgeStatuses() };
|
|
159
|
+
},
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
package/ts/paths.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
export const baseDir = process.cwd();
|
|
3
|
+
// Code/asset paths (not affected by baseDir)
|
|
5
4
|
export const packageDir = plugins.path.join(
|
|
6
5
|
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
|
7
6
|
'../'
|
|
@@ -20,35 +19,37 @@ export const dataDir = process.env.DATA_DIR
|
|
|
20
19
|
// Default TsmDB path for CacheDb
|
|
21
20
|
export const defaultTsmDbPath = plugins.path.join(dcrouterHomeDir, 'tsmdb');
|
|
22
21
|
|
|
23
|
-
// MTA
|
|
24
|
-
export const keysDir = plugins.path.join(dataDir, 'keys');
|
|
22
|
+
// DNS records directory (only surviving MTA directory reference)
|
|
25
23
|
export const dnsRecordsDir = plugins.path.join(dataDir, 'dns');
|
|
26
|
-
export const sentEmailsDir = plugins.path.join(dataDir, 'emails', 'sent');
|
|
27
|
-
export const receivedEmailsDir = plugins.path.join(dataDir, 'emails', 'received');
|
|
28
|
-
export const failedEmailsDir = plugins.path.join(dataDir, 'emails', 'failed'); // For failed emails
|
|
29
|
-
export const logsDir = plugins.path.join(dataDir, 'logs'); // For logs
|
|
30
24
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
export
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Resolve all data paths from a given baseDir.
|
|
27
|
+
* When no baseDir is provided, falls back to ~/.serve.zone/dcrouter.
|
|
28
|
+
* Specific overrides (e.g. DATA_DIR env) take precedence.
|
|
29
|
+
*/
|
|
30
|
+
export function resolvePaths(baseDir?: string) {
|
|
31
|
+
const root = baseDir ?? plugins.path.join(plugins.os.homedir(), '.serve.zone', 'dcrouter');
|
|
32
|
+
const resolvedDataDir = process.env.DATA_DIR ?? plugins.path.join(root, 'data');
|
|
33
|
+
return {
|
|
34
|
+
dcrouterHomeDir: root,
|
|
35
|
+
dataDir: resolvedDataDir,
|
|
36
|
+
defaultTsmDbPath: plugins.path.join(root, 'tsmdb'),
|
|
37
|
+
defaultStoragePath: plugins.path.join(root, 'storage'),
|
|
38
|
+
dnsRecordsDir: plugins.path.join(resolvedDataDir, 'dns'),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Ensure only the data directories that are actually used exist.
|
|
44
|
+
*/
|
|
45
|
+
export function ensureDataDirectories(resolvedPaths: ReturnType<typeof resolvePaths>) {
|
|
46
|
+
plugins.fsUtils.ensureDirSync(resolvedPaths.dataDir);
|
|
47
|
+
plugins.fsUtils.ensureDirSync(resolvedPaths.dnsRecordsDir);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Legacy wrapper — delegates to ensureDataDirectories with module-level defaults.
|
|
52
|
+
*/
|
|
41
53
|
export function ensureDirectories() {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
plugins.fsUtils.ensureDirSync(keysDir);
|
|
45
|
-
plugins.fsUtils.ensureDirSync(dnsRecordsDir);
|
|
46
|
-
plugins.fsUtils.ensureDirSync(sentEmailsDir);
|
|
47
|
-
plugins.fsUtils.ensureDirSync(receivedEmailsDir);
|
|
48
|
-
plugins.fsUtils.ensureDirSync(failedEmailsDir);
|
|
49
|
-
plugins.fsUtils.ensureDirSync(logsDir);
|
|
50
|
-
|
|
51
|
-
// Ensure email template directories
|
|
52
|
-
plugins.fsUtils.ensureDirSync(emailTemplatesDir);
|
|
53
|
-
plugins.fsUtils.ensureDirSync(MtaAttachmentsDir);
|
|
54
|
-
}
|
|
54
|
+
ensureDataDirectories(resolvePaths());
|
|
55
|
+
}
|
package/ts/plugins.ts
CHANGED
|
@@ -23,9 +23,11 @@ export {
|
|
|
23
23
|
|
|
24
24
|
// @serve.zone scope
|
|
25
25
|
import * as servezoneInterfaces from '@serve.zone/interfaces';
|
|
26
|
+
import * as remoteingress from '@serve.zone/remoteingress';
|
|
26
27
|
|
|
27
28
|
export {
|
|
28
|
-
servezoneInterfaces
|
|
29
|
+
servezoneInterfaces,
|
|
30
|
+
remoteingress,
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
// @api.global scope
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import type { StorageManager } from '../storage/classes.storagemanager.js';
|
|
3
|
+
import type { IRemoteIngress } from '../../ts_interfaces/data/remoteingress.js';
|
|
4
|
+
|
|
5
|
+
const STORAGE_PREFIX = '/remote-ingress/';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Manages CRUD for remote ingress edge registrations.
|
|
9
|
+
* Persists edge configs via StorageManager and provides
|
|
10
|
+
* the allowed edges list for the Rust hub.
|
|
11
|
+
*/
|
|
12
|
+
export class RemoteIngressManager {
|
|
13
|
+
private storageManager: StorageManager;
|
|
14
|
+
private edges: Map<string, IRemoteIngress> = new Map();
|
|
15
|
+
|
|
16
|
+
constructor(storageManager: StorageManager) {
|
|
17
|
+
this.storageManager = storageManager;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Load all edge registrations from storage into memory.
|
|
22
|
+
*/
|
|
23
|
+
public async initialize(): Promise<void> {
|
|
24
|
+
const keys = await this.storageManager.list(STORAGE_PREFIX);
|
|
25
|
+
for (const key of keys) {
|
|
26
|
+
const edge = await this.storageManager.getJSON<IRemoteIngress>(key);
|
|
27
|
+
if (edge) {
|
|
28
|
+
this.edges.set(edge.id, edge);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create a new edge registration.
|
|
35
|
+
*/
|
|
36
|
+
public async createEdge(
|
|
37
|
+
name: string,
|
|
38
|
+
listenPorts: number[],
|
|
39
|
+
tags?: string[],
|
|
40
|
+
): Promise<IRemoteIngress> {
|
|
41
|
+
const id = plugins.uuid.v4();
|
|
42
|
+
const secret = plugins.crypto.randomBytes(32).toString('hex');
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
|
|
45
|
+
const edge: IRemoteIngress = {
|
|
46
|
+
id,
|
|
47
|
+
name,
|
|
48
|
+
secret,
|
|
49
|
+
listenPorts,
|
|
50
|
+
enabled: true,
|
|
51
|
+
tags: tags || [],
|
|
52
|
+
createdAt: now,
|
|
53
|
+
updatedAt: now,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
await this.storageManager.setJSON(`${STORAGE_PREFIX}${id}`, edge);
|
|
57
|
+
this.edges.set(id, edge);
|
|
58
|
+
return edge;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get an edge by ID.
|
|
63
|
+
*/
|
|
64
|
+
public getEdge(id: string): IRemoteIngress | undefined {
|
|
65
|
+
return this.edges.get(id);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get all edge registrations.
|
|
70
|
+
*/
|
|
71
|
+
public getAllEdges(): IRemoteIngress[] {
|
|
72
|
+
return Array.from(this.edges.values());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Update an edge registration.
|
|
77
|
+
*/
|
|
78
|
+
public async updateEdge(
|
|
79
|
+
id: string,
|
|
80
|
+
updates: {
|
|
81
|
+
name?: string;
|
|
82
|
+
listenPorts?: number[];
|
|
83
|
+
enabled?: boolean;
|
|
84
|
+
tags?: string[];
|
|
85
|
+
},
|
|
86
|
+
): Promise<IRemoteIngress | null> {
|
|
87
|
+
const edge = this.edges.get(id);
|
|
88
|
+
if (!edge) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (updates.name !== undefined) edge.name = updates.name;
|
|
93
|
+
if (updates.listenPorts !== undefined) edge.listenPorts = updates.listenPorts;
|
|
94
|
+
if (updates.enabled !== undefined) edge.enabled = updates.enabled;
|
|
95
|
+
if (updates.tags !== undefined) edge.tags = updates.tags;
|
|
96
|
+
edge.updatedAt = Date.now();
|
|
97
|
+
|
|
98
|
+
await this.storageManager.setJSON(`${STORAGE_PREFIX}${id}`, edge);
|
|
99
|
+
this.edges.set(id, edge);
|
|
100
|
+
return edge;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Delete an edge registration.
|
|
105
|
+
*/
|
|
106
|
+
public async deleteEdge(id: string): Promise<boolean> {
|
|
107
|
+
if (!this.edges.has(id)) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
await this.storageManager.delete(`${STORAGE_PREFIX}${id}`);
|
|
111
|
+
this.edges.delete(id);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Regenerate the secret for an edge.
|
|
117
|
+
*/
|
|
118
|
+
public async regenerateSecret(id: string): Promise<string | null> {
|
|
119
|
+
const edge = this.edges.get(id);
|
|
120
|
+
if (!edge) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
edge.secret = plugins.crypto.randomBytes(32).toString('hex');
|
|
125
|
+
edge.updatedAt = Date.now();
|
|
126
|
+
|
|
127
|
+
await this.storageManager.setJSON(`${STORAGE_PREFIX}${id}`, edge);
|
|
128
|
+
this.edges.set(id, edge);
|
|
129
|
+
return edge.secret;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Verify an edge's secret using constant-time comparison.
|
|
134
|
+
*/
|
|
135
|
+
public verifySecret(id: string, secret: string): boolean {
|
|
136
|
+
const edge = this.edges.get(id);
|
|
137
|
+
if (!edge) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
const expected = Buffer.from(edge.secret);
|
|
141
|
+
const provided = Buffer.from(secret);
|
|
142
|
+
if (expected.length !== provided.length) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return plugins.crypto.timingSafeEqual(expected, provided);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get the list of allowed edges (enabled only) for the Rust hub.
|
|
150
|
+
*/
|
|
151
|
+
public getAllowedEdges(): Array<{ id: string; secret: string }> {
|
|
152
|
+
const result: Array<{ id: string; secret: string }> = [];
|
|
153
|
+
for (const edge of this.edges.values()) {
|
|
154
|
+
if (edge.enabled) {
|
|
155
|
+
result.push({ id: edge.id, secret: edge.secret });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import type { IRemoteIngressStatus } from '../../ts_interfaces/data/remoteingress.js';
|
|
3
|
+
import type { RemoteIngressManager } from './classes.remoteingress-manager.js';
|
|
4
|
+
|
|
5
|
+
export interface ITunnelManagerConfig {
|
|
6
|
+
tunnelPort?: number;
|
|
7
|
+
targetHost?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Manages the RemoteIngressHub instance and tracks connected edge statuses.
|
|
12
|
+
*/
|
|
13
|
+
export class TunnelManager {
|
|
14
|
+
private hub: InstanceType<typeof plugins.remoteingress.RemoteIngressHub>;
|
|
15
|
+
private manager: RemoteIngressManager;
|
|
16
|
+
private config: ITunnelManagerConfig;
|
|
17
|
+
private edgeStatuses: Map<string, IRemoteIngressStatus> = new Map();
|
|
18
|
+
|
|
19
|
+
constructor(manager: RemoteIngressManager, config: ITunnelManagerConfig = {}) {
|
|
20
|
+
this.manager = manager;
|
|
21
|
+
this.config = config;
|
|
22
|
+
this.hub = new plugins.remoteingress.RemoteIngressHub();
|
|
23
|
+
|
|
24
|
+
// Listen for edge connect/disconnect events
|
|
25
|
+
this.hub.on('edgeConnected', (data: { edgeId: string }) => {
|
|
26
|
+
const existing = this.edgeStatuses.get(data.edgeId);
|
|
27
|
+
this.edgeStatuses.set(data.edgeId, {
|
|
28
|
+
edgeId: data.edgeId,
|
|
29
|
+
connected: true,
|
|
30
|
+
publicIp: existing?.publicIp ?? null,
|
|
31
|
+
activeTunnels: 0,
|
|
32
|
+
lastHeartbeat: Date.now(),
|
|
33
|
+
connectedAt: Date.now(),
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
this.hub.on('edgeDisconnected', (data: { edgeId: string }) => {
|
|
38
|
+
const existing = this.edgeStatuses.get(data.edgeId);
|
|
39
|
+
if (existing) {
|
|
40
|
+
existing.connected = false;
|
|
41
|
+
existing.activeTunnels = 0;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
this.hub.on('streamOpened', (data: { edgeId: string; streamId: number }) => {
|
|
46
|
+
const existing = this.edgeStatuses.get(data.edgeId);
|
|
47
|
+
if (existing) {
|
|
48
|
+
existing.activeTunnels++;
|
|
49
|
+
existing.lastHeartbeat = Date.now();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.hub.on('streamClosed', (data: { edgeId: string; streamId: number }) => {
|
|
54
|
+
const existing = this.edgeStatuses.get(data.edgeId);
|
|
55
|
+
if (existing && existing.activeTunnels > 0) {
|
|
56
|
+
existing.activeTunnels--;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Start the tunnel hub and load allowed edges.
|
|
63
|
+
*/
|
|
64
|
+
public async start(): Promise<void> {
|
|
65
|
+
await this.hub.start({
|
|
66
|
+
tunnelPort: this.config.tunnelPort ?? 8443,
|
|
67
|
+
targetHost: this.config.targetHost ?? '127.0.0.1',
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Send allowed edges to the hub
|
|
71
|
+
await this.syncAllowedEdges();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Stop the tunnel hub.
|
|
76
|
+
*/
|
|
77
|
+
public async stop(): Promise<void> {
|
|
78
|
+
await this.hub.stop();
|
|
79
|
+
this.edgeStatuses.clear();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Sync allowed edges from the manager to the hub.
|
|
84
|
+
* Call this after creating/deleting/updating edges.
|
|
85
|
+
*/
|
|
86
|
+
public async syncAllowedEdges(): Promise<void> {
|
|
87
|
+
const edges = this.manager.getAllowedEdges();
|
|
88
|
+
await this.hub.updateAllowedEdges(edges);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get runtime statuses for all known edges.
|
|
93
|
+
*/
|
|
94
|
+
public getEdgeStatuses(): IRemoteIngressStatus[] {
|
|
95
|
+
return Array.from(this.edgeStatuses.values());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get status for a specific edge.
|
|
100
|
+
*/
|
|
101
|
+
public getEdgeStatus(edgeId: string): IRemoteIngressStatus | undefined {
|
|
102
|
+
return this.edgeStatuses.get(edgeId);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get the count of connected edges.
|
|
107
|
+
*/
|
|
108
|
+
public getConnectedCount(): number {
|
|
109
|
+
let count = 0;
|
|
110
|
+
for (const status of this.edgeStatuses.values()) {
|
|
111
|
+
if (status.connected) count++;
|
|
112
|
+
}
|
|
113
|
+
return count;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get the total number of active tunnels across all edges.
|
|
118
|
+
*/
|
|
119
|
+
public getTotalActiveTunnels(): number {
|
|
120
|
+
let total = 0;
|
|
121
|
+
for (const status of this.edgeStatuses.values()) {
|
|
122
|
+
total += status.activeTunnels;
|
|
123
|
+
}
|
|
124
|
+
return total;
|
|
125
|
+
}
|
|
126
|
+
}
|