node-opcua-server 2.164.2 → 2.165.1
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/base_server.d.ts +95 -18
- package/dist/base_server.js +211 -63
- package/dist/base_server.js.map +1 -1
- package/dist/opcua_server.d.ts +171 -123
- package/dist/opcua_server.js +419 -181
- package/dist/opcua_server.js.map +1 -1
- package/dist/server_end_point.d.ts +108 -12
- package/dist/server_end_point.js +146 -46
- package/dist/server_end_point.js.map +1 -1
- package/dist/server_engine.d.ts +32 -14
- package/dist/server_engine.js +155 -54
- package/dist/server_engine.js.map +1 -1
- package/package.json +44 -44
- package/source/base_server.ts +246 -84
- package/source/opcua_server.ts +621 -441
- package/source/server_end_point.ts +267 -83
- package/source/server_engine.ts +240 -130
|
@@ -3,42 +3,39 @@
|
|
|
3
3
|
* @module node-opcua-server
|
|
4
4
|
*/
|
|
5
5
|
// tslint:disable:no-console
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
import { Server, Socket } from "net";
|
|
6
|
+
|
|
7
|
+
import { EventEmitter } from "node:events";
|
|
8
|
+
import net, { type Server, type Socket } from "node:net";
|
|
9
9
|
import chalk from "chalk";
|
|
10
|
-
import async from "async";
|
|
11
10
|
|
|
12
11
|
import { assert } from "node-opcua-assert";
|
|
13
|
-
import { OPCUACertificateManager } from "node-opcua-certificate-manager";
|
|
14
|
-
import { Certificate,
|
|
12
|
+
import type { OPCUACertificateManager } from "node-opcua-certificate-manager";
|
|
13
|
+
import { type Certificate, makeSHA1Thumbprint, type PrivateKey, split_der } from "node-opcua-crypto/web";
|
|
15
14
|
import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
|
|
16
15
|
import { getFullyQualifiedDomainName, resolveFullyQualifiedDomainName } from "node-opcua-hostname";
|
|
17
16
|
import {
|
|
18
17
|
fromURI,
|
|
18
|
+
type IServerSessionBase,
|
|
19
|
+
type Message,
|
|
19
20
|
MessageSecurityMode,
|
|
20
21
|
SecurityPolicy,
|
|
21
22
|
ServerSecureChannelLayer,
|
|
22
|
-
ServerSecureChannelParent,
|
|
23
|
-
toURI
|
|
24
|
-
IServerSessionBase,
|
|
25
|
-
Message
|
|
23
|
+
type ServerSecureChannelParent,
|
|
24
|
+
toURI
|
|
26
25
|
} from "node-opcua-secure-channel";
|
|
27
|
-
import { UserTokenType } from "node-opcua-service-endpoints";
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import { UserTokenPolicyOptions } from "node-opcua-types";
|
|
31
|
-
import { IHelloAckLimits } from "node-opcua-transport";
|
|
26
|
+
import { ApplicationDescription, EndpointDescription, UserTokenType } from "node-opcua-service-endpoints";
|
|
27
|
+
import type { IHelloAckLimits } from "node-opcua-transport";
|
|
28
|
+
import type { UserTokenPolicyOptions } from "node-opcua-types";
|
|
32
29
|
|
|
33
|
-
import { IChannelData } from "./i_channel_data";
|
|
34
|
-
import { ISocketData } from "./i_socket_data";
|
|
30
|
+
import type { IChannelData } from "./i_channel_data";
|
|
31
|
+
import type { ISocketData } from "./i_socket_data";
|
|
35
32
|
|
|
36
33
|
const debugLog = make_debugLog(__filename);
|
|
37
34
|
const errorLog = make_errorLog(__filename);
|
|
38
35
|
const warningLog = make_warningLog(__filename);
|
|
39
36
|
const doDebug = checkDebugFlag(__filename);
|
|
40
37
|
|
|
41
|
-
const
|
|
38
|
+
const UATCP_UASC_UABINARY = "http://opcfoundation.org/UA-Profile/Transport/uatcp-uasc-uabinary";
|
|
42
39
|
|
|
43
40
|
function extractSocketData(socket: net.Socket, reason: string): ISocketData {
|
|
44
41
|
const { bytesRead, bytesWritten, remoteAddress, remoteFamily, remotePort, localAddress, localPort } = socket;
|
|
@@ -57,14 +54,7 @@ function extractSocketData(socket: net.Socket, reason: string): ISocketData {
|
|
|
57
54
|
}
|
|
58
55
|
|
|
59
56
|
function extractChannelData(channel: ServerSecureChannelLayer): IChannelData {
|
|
60
|
-
const {
|
|
61
|
-
channelId,
|
|
62
|
-
clientCertificate,
|
|
63
|
-
securityMode,
|
|
64
|
-
securityPolicy,
|
|
65
|
-
timeout,
|
|
66
|
-
transactionsCount
|
|
67
|
-
} = channel;
|
|
57
|
+
const { channelId, clientCertificate, securityMode, securityPolicy, timeout, transactionsCount } = channel;
|
|
68
58
|
|
|
69
59
|
const channelData: IChannelData = {
|
|
70
60
|
channelId,
|
|
@@ -95,6 +85,7 @@ function dumpChannelInfo(channels: ServerSecureChannelLayer[]): void {
|
|
|
95
85
|
console.log(" sessions = ", Object.keys(channel.sessionTokens).length);
|
|
96
86
|
console.log(Object.values(channel.sessionTokens).map(d).join("\n"));
|
|
97
87
|
|
|
88
|
+
// biome-ignore lint/suspicious/noExplicitAny: accessing internal transport for debug dump
|
|
98
89
|
const socket = (channel as any).transport?._socket;
|
|
99
90
|
if (!socket) {
|
|
100
91
|
console.log(" SOCKET IS CLOSED");
|
|
@@ -108,6 +99,7 @@ function dumpChannelInfo(channels: ServerSecureChannelLayer[]): void {
|
|
|
108
99
|
}
|
|
109
100
|
|
|
110
101
|
const emptyCertificate = Buffer.alloc(0);
|
|
102
|
+
// biome-ignore lint/suspicious/noExplicitAny: deliberate null→PrivateKey sentinel
|
|
111
103
|
const emptyPrivateKey = null as any as PrivateKey;
|
|
112
104
|
|
|
113
105
|
let OPCUAServerEndPointCounter = 0;
|
|
@@ -152,7 +144,7 @@ export interface OPCUAServerEndPointOptions {
|
|
|
152
144
|
|
|
153
145
|
serverInfo: ApplicationDescription;
|
|
154
146
|
|
|
155
|
-
objectFactory?:
|
|
147
|
+
objectFactory?: unknown;
|
|
156
148
|
|
|
157
149
|
transportSettings?: IServerTransportSettings;
|
|
158
150
|
}
|
|
@@ -167,10 +159,74 @@ export interface EndpointDescriptionParams {
|
|
|
167
159
|
resourcePath?: string;
|
|
168
160
|
alternateHostname?: string[];
|
|
169
161
|
hostname: string;
|
|
162
|
+
/**
|
|
163
|
+
* Override the port used in the endpoint URL.
|
|
164
|
+
* When set, the endpoint URL uses this port instead of the
|
|
165
|
+
* server's listen port. The server does NOT listen on this port.
|
|
166
|
+
* Useful for Docker port-mapping, reverse proxies, and NAT.
|
|
167
|
+
*/
|
|
168
|
+
advertisedPort?: number;
|
|
170
169
|
securityPolicies: SecurityPolicy[];
|
|
171
170
|
userTokenTypes: UserTokenType[];
|
|
172
171
|
}
|
|
173
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Per-URL security overrides for advertised endpoints.
|
|
175
|
+
*
|
|
176
|
+
* When `advertisedEndpoints` contains a config object, the endpoint
|
|
177
|
+
* descriptions generated for that URL use the overridden security
|
|
178
|
+
* settings instead of inheriting from the main endpoint.
|
|
179
|
+
*
|
|
180
|
+
* Any field that is omitted falls back to the main endpoint's value.
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```ts
|
|
184
|
+
* advertisedEndpoints: [
|
|
185
|
+
* // Public: SignAndEncrypt only, no anonymous
|
|
186
|
+
* {
|
|
187
|
+
* url: "opc.tcp://public.example.com:4840",
|
|
188
|
+
* securityModes: [MessageSecurityMode.SignAndEncrypt],
|
|
189
|
+
* allowAnonymous: false
|
|
190
|
+
* },
|
|
191
|
+
* // Internal: inherits everything from main endpoint
|
|
192
|
+
* "opc.tcp://internal:48480"
|
|
193
|
+
* ]
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
export interface AdvertisedEndpointConfig {
|
|
197
|
+
/** The full endpoint URL, e.g. `"opc.tcp://public.example.com:4840"` */
|
|
198
|
+
url: string;
|
|
199
|
+
/** Override security modes (default: inherit from main endpoint) */
|
|
200
|
+
securityModes?: MessageSecurityMode[];
|
|
201
|
+
/** Override security policies (default: inherit from main endpoint) */
|
|
202
|
+
securityPolicies?: SecurityPolicy[];
|
|
203
|
+
/** Override anonymous access (default: inherit from main endpoint) */
|
|
204
|
+
allowAnonymous?: boolean;
|
|
205
|
+
/** Override user token types (default: inherit from main endpoint) */
|
|
206
|
+
userTokenTypes?: UserTokenType[];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* An advertised endpoint entry — either a plain URL string (inherits
|
|
211
|
+
* all settings from the main endpoint) or a config object with
|
|
212
|
+
* per-URL security overrides.
|
|
213
|
+
*/
|
|
214
|
+
export type AdvertisedEndpoint = string | AdvertisedEndpointConfig;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Normalize any `advertisedEndpoints` input into a uniform
|
|
218
|
+
* `AdvertisedEndpointConfig[]`.
|
|
219
|
+
*
|
|
220
|
+
* This coercion is done early so that all downstream code
|
|
221
|
+
* (endpoint generation, IP/hostname extraction) only deals
|
|
222
|
+
* with one type.
|
|
223
|
+
*/
|
|
224
|
+
export function normalizeAdvertisedEndpoints(raw?: AdvertisedEndpoint | AdvertisedEndpoint[]): AdvertisedEndpointConfig[] {
|
|
225
|
+
if (!raw) return [];
|
|
226
|
+
const arr = Array.isArray(raw) ? raw : [raw];
|
|
227
|
+
return arr.map((entry) => (typeof entry === "string" ? { url: entry } : entry));
|
|
228
|
+
}
|
|
229
|
+
|
|
174
230
|
export interface AddStandardEndpointDescriptionsParam {
|
|
175
231
|
allowAnonymous?: boolean;
|
|
176
232
|
disableDiscovery?: boolean;
|
|
@@ -183,15 +239,61 @@ export interface AddStandardEndpointDescriptionsParam {
|
|
|
183
239
|
hostname?: string;
|
|
184
240
|
securityPolicies?: SecurityPolicy[];
|
|
185
241
|
userTokenTypes?: UserTokenType[];
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Additional endpoint URL(s) to advertise.
|
|
245
|
+
*
|
|
246
|
+
* Use when the server is behind Docker port-mapping,
|
|
247
|
+
* a reverse proxy, or a NAT gateway.
|
|
248
|
+
*
|
|
249
|
+
* Each entry can be a plain URL string (inherits all security
|
|
250
|
+
* settings from the main endpoint) or an
|
|
251
|
+
* `AdvertisedEndpointConfig` object with per-URL overrides.
|
|
252
|
+
*
|
|
253
|
+
* The server still listens on `port` — these are purely
|
|
254
|
+
* advertised aliases.
|
|
255
|
+
*
|
|
256
|
+
* @example Simple string (inherits main settings)
|
|
257
|
+
* ```ts
|
|
258
|
+
* advertisedEndpoints: "opc.tcp://localhost:48481"
|
|
259
|
+
* ```
|
|
260
|
+
*
|
|
261
|
+
* @example Mixed array with per-URL security overrides
|
|
262
|
+
* ```ts
|
|
263
|
+
* advertisedEndpoints: [
|
|
264
|
+
* "opc.tcp://internal:48480",
|
|
265
|
+
* {
|
|
266
|
+
* url: "opc.tcp://public.example.com:4840",
|
|
267
|
+
* securityModes: [MessageSecurityMode.SignAndEncrypt],
|
|
268
|
+
* allowAnonymous: false
|
|
269
|
+
* }
|
|
270
|
+
* ]
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
advertisedEndpoints?: AdvertisedEndpoint | AdvertisedEndpoint[];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Parse an `opc.tcp://hostname:port` URL and extract hostname and port.
|
|
278
|
+
* @internal
|
|
279
|
+
*/
|
|
280
|
+
export function parseOpcTcpUrl(url: string): { hostname: string; port: number } {
|
|
281
|
+
// URL class doesn't understand opc.tcp://, so swap to http://
|
|
282
|
+
const httpUrl = url.replace(/^opc\.tcp:\/\//i, "http://");
|
|
283
|
+
const parsed = new URL(httpUrl);
|
|
284
|
+
return {
|
|
285
|
+
hostname: parsed.hostname,
|
|
286
|
+
port: parsed.port ? Number.parseInt(parsed.port, 10) : 4840
|
|
287
|
+
};
|
|
186
288
|
}
|
|
187
289
|
|
|
188
290
|
function getUniqueName(name: string, collection: { [key: string]: number }) {
|
|
189
291
|
if (collection[name]) {
|
|
190
292
|
let counter = 0;
|
|
191
|
-
while (collection[name
|
|
293
|
+
while (collection[`${name}_${counter.toString()}`]) {
|
|
192
294
|
counter++;
|
|
193
295
|
}
|
|
194
|
-
name = name
|
|
296
|
+
name = `${name}_${counter.toString()}`;
|
|
195
297
|
collection[name] = 1;
|
|
196
298
|
return name;
|
|
197
299
|
} else {
|
|
@@ -224,12 +326,12 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
224
326
|
public transactionsCountOldChannels: number;
|
|
225
327
|
public securityTokenCountOldChannels: number;
|
|
226
328
|
public serverInfo: ApplicationDescription;
|
|
227
|
-
public objectFactory:
|
|
329
|
+
public objectFactory: unknown;
|
|
228
330
|
|
|
229
331
|
public _on_new_channel?: (channel: ServerSecureChannelLayer) => void;
|
|
230
332
|
public _on_close_channel?: (channel: ServerSecureChannelLayer) => void;
|
|
231
|
-
public _on_connectionRefused?: (socketData:
|
|
232
|
-
public _on_openSecureChannelFailure?: (socketData:
|
|
333
|
+
public _on_connectionRefused?: (socketData: ISocketData) => void;
|
|
334
|
+
public _on_openSecureChannelFailure?: (socketData: ISocketData, channelData: IChannelData) => void;
|
|
233
335
|
|
|
234
336
|
private _certificateChain: Certificate;
|
|
235
337
|
private _privateKey: PrivateKey;
|
|
@@ -307,7 +409,6 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
307
409
|
}
|
|
308
410
|
|
|
309
411
|
public toString(): string {
|
|
310
|
-
|
|
311
412
|
const txt =
|
|
312
413
|
" end point" +
|
|
313
414
|
this._counter +
|
|
@@ -316,7 +417,7 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
316
417
|
" l = " +
|
|
317
418
|
this._endpoints.length +
|
|
318
419
|
" " +
|
|
319
|
-
makeSHA1Thumbprint(this.getCertificateChain()).toString("hex")
|
|
420
|
+
makeSHA1Thumbprint(this.getCertificateChain()).toString("hex");
|
|
320
421
|
return txt;
|
|
321
422
|
}
|
|
322
423
|
|
|
@@ -392,7 +493,8 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
392
493
|
assert(resourcePath.length === 0 || resourcePath.charAt(0) === "/", "resourcePath should start with /");
|
|
393
494
|
|
|
394
495
|
const hostname = options.hostname || getFullyQualifiedDomainName();
|
|
395
|
-
const
|
|
496
|
+
const effectivePort = options.advertisedPort ?? this.port;
|
|
497
|
+
const endpointUrl = `opc.tcp://${hostname}:${effectivePort}${resourcePath}`;
|
|
396
498
|
|
|
397
499
|
const endpoint_desc = this.getEndpointDescription(securityMode, securityPolicy, endpointUrl);
|
|
398
500
|
|
|
@@ -421,6 +523,7 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
421
523
|
restricted: !!options.restricted,
|
|
422
524
|
securityPolicies: options.securityPolicies || [],
|
|
423
525
|
|
|
526
|
+
advertisedPort: options.advertisedPort,
|
|
424
527
|
userTokenTypes
|
|
425
528
|
},
|
|
426
529
|
this
|
|
@@ -431,7 +534,7 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
431
534
|
public addRestrictedEndpointDescription(options: EndpointDescriptionParams): void {
|
|
432
535
|
options = { ...options };
|
|
433
536
|
options.restricted = true;
|
|
434
|
-
|
|
537
|
+
this.addEndpointDescription(MessageSecurityMode.None, SecurityPolicy.None, options);
|
|
435
538
|
}
|
|
436
539
|
|
|
437
540
|
public addStandardEndpointDescriptions(options?: AddStandardEndpointDescriptionsParam): void {
|
|
@@ -487,6 +590,65 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
487
590
|
}
|
|
488
591
|
}
|
|
489
592
|
}
|
|
593
|
+
|
|
594
|
+
// ── Advertised endpoints (virtual — no TCP listener) ──────
|
|
595
|
+
// Normalize to AdvertisedEndpointConfig[] so downstream code
|
|
596
|
+
// only deals with one type.
|
|
597
|
+
const advertisedList = normalizeAdvertisedEndpoints(options.advertisedEndpoints);
|
|
598
|
+
|
|
599
|
+
// Main endpoint defaults (guaranteed non-null — assigned above)
|
|
600
|
+
const mainSecurityModes = options.securityModes || defaultSecurityModes;
|
|
601
|
+
const mainSecurityPolicies = options.securityPolicies || defaultSecurityPolicies;
|
|
602
|
+
const mainUserTokenTypes = options.userTokenTypes || defaultUserTokenTypes;
|
|
603
|
+
|
|
604
|
+
for (const config of advertisedList) {
|
|
605
|
+
const { hostname: advHostname, port: advPort } = parseOpcTcpUrl(config.url);
|
|
606
|
+
// Skip if this hostname+port combo was already covered
|
|
607
|
+
// by the regular hostname loop (same hostname, same port)
|
|
608
|
+
if (hostnames.some((h) => h.toLowerCase() === advHostname.toLowerCase()) && advPort === this.port) {
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Per-URL security overrides — fall back to main settings
|
|
613
|
+
const entrySecurityModes = config.securityModes ?? mainSecurityModes;
|
|
614
|
+
const entrySecurityPolicies = config.securityPolicies ?? mainSecurityPolicies;
|
|
615
|
+
let entryUserTokenTypes = config.userTokenTypes ?? mainUserTokenTypes;
|
|
616
|
+
|
|
617
|
+
// Handle allowAnonymous override: if explicitly false,
|
|
618
|
+
// filter out Anonymous even if the main config allows it
|
|
619
|
+
if (config.allowAnonymous === false) {
|
|
620
|
+
entryUserTokenTypes = entryUserTokenTypes.filter((t) => t !== UserTokenType.Anonymous);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const optionsE: EndpointDescriptionParams = {
|
|
624
|
+
hostname: advHostname,
|
|
625
|
+
advertisedPort: advPort,
|
|
626
|
+
securityPolicies: entrySecurityPolicies,
|
|
627
|
+
userTokenTypes: entryUserTokenTypes,
|
|
628
|
+
allowUnsecurePassword: options.allowUnsecurePassword,
|
|
629
|
+
alternateHostname: options.alternateHostname,
|
|
630
|
+
resourcePath: options.resourcePath
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
if (entrySecurityModes.indexOf(MessageSecurityMode.None) >= 0) {
|
|
634
|
+
this.addEndpointDescription(MessageSecurityMode.None, SecurityPolicy.None, optionsE);
|
|
635
|
+
} else {
|
|
636
|
+
if (!options.disableDiscovery) {
|
|
637
|
+
this.addRestrictedEndpointDescription(optionsE);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
for (const securityMode of entrySecurityModes) {
|
|
641
|
+
if (securityMode === MessageSecurityMode.None) {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
for (const securityPolicy of entrySecurityPolicies) {
|
|
645
|
+
if (securityPolicy === SecurityPolicy.None) {
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
this.addEndpointDescription(securityMode, securityPolicy, optionsE);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
490
652
|
}
|
|
491
653
|
|
|
492
654
|
/**
|
|
@@ -502,14 +664,19 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
502
664
|
assert(typeof callback === "function");
|
|
503
665
|
assert(!this._started, "OPCUAServerEndPoint is already listening");
|
|
504
666
|
|
|
667
|
+
if (!this._server) {
|
|
668
|
+
callback(new Error("Server is not initialized"));
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
|
|
505
672
|
this._listen_callback = callback;
|
|
506
673
|
|
|
507
|
-
this._server
|
|
508
|
-
debugLog(chalk.red.bold(" error")
|
|
674
|
+
this._server.on("error", (err: Error) => {
|
|
675
|
+
debugLog(`${chalk.red.bold(" error")} port = ${this.port}`, err);
|
|
509
676
|
this._started = false;
|
|
510
677
|
this._end_listen(err);
|
|
511
678
|
});
|
|
512
|
-
this._server
|
|
679
|
+
this._server.on("listening", () => {
|
|
513
680
|
debugLog("server is listening");
|
|
514
681
|
});
|
|
515
682
|
|
|
@@ -518,7 +685,7 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
518
685
|
host: this.host
|
|
519
686
|
};
|
|
520
687
|
|
|
521
|
-
this._server
|
|
688
|
+
this._server.listen(
|
|
522
689
|
listenOptions,
|
|
523
690
|
/*"::",*/ (err?: Error) => {
|
|
524
691
|
// 'listening' listener
|
|
@@ -526,8 +693,8 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
526
693
|
assert(!err, " cannot listen to port ");
|
|
527
694
|
this._started = true;
|
|
528
695
|
if (!this.port) {
|
|
529
|
-
const add = this._server
|
|
530
|
-
this.port = typeof add !== "string" ? add
|
|
696
|
+
const add = this._server?.address();
|
|
697
|
+
this.port = typeof add !== "string" ? add?.port || 0 : this.port;
|
|
531
698
|
}
|
|
532
699
|
this._end_listen();
|
|
533
700
|
}
|
|
@@ -536,8 +703,10 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
536
703
|
|
|
537
704
|
public killClientSockets(callback: (err?: Error) => void): void {
|
|
538
705
|
for (const channel of this.getChannels()) {
|
|
539
|
-
const hacked_channel = channel as
|
|
540
|
-
|
|
706
|
+
const hacked_channel = channel as unknown as {
|
|
707
|
+
transport: { _socket: { destroy: () => void; emit: (event: string, err: Error) => void } };
|
|
708
|
+
};
|
|
709
|
+
if (hacked_channel.transport?._socket) {
|
|
541
710
|
// hacked_channel.transport._socket.close();
|
|
542
711
|
hacked_channel.transport._socket.destroy();
|
|
543
712
|
hacked_channel.transport._socket.emit("error", new Error("EPIPE"));
|
|
@@ -547,8 +716,9 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
547
716
|
}
|
|
548
717
|
|
|
549
718
|
public suspendConnection(callback: (err?: Error) => void): void {
|
|
550
|
-
if (!this._started) {
|
|
551
|
-
|
|
719
|
+
if (!this._started || !this._server) {
|
|
720
|
+
callback(new Error("Connection already suspended !!"));
|
|
721
|
+
return;
|
|
552
722
|
}
|
|
553
723
|
|
|
554
724
|
// Stops the server from accepting new connections and keeps existing connections.
|
|
@@ -557,9 +727,9 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
557
727
|
// The optional callback will be called once the 'close' event occurs.
|
|
558
728
|
// Unlike that event, it will be called with an Error as its only argument
|
|
559
729
|
// if the server was not open when it was closed.
|
|
560
|
-
this._server
|
|
730
|
+
this._server.close(() => {
|
|
561
731
|
this._started = false;
|
|
562
|
-
debugLog(
|
|
732
|
+
debugLog(`Connection has been closed !${this.port}`);
|
|
563
733
|
});
|
|
564
734
|
this._started = false;
|
|
565
735
|
callback();
|
|
@@ -585,20 +755,30 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
585
755
|
this.suspendConnection(() => {
|
|
586
756
|
// shutdown all opened channels ...
|
|
587
757
|
const _channels = Object.values(this._channels);
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
758
|
+
const promises = _channels.map(
|
|
759
|
+
(channel) =>
|
|
760
|
+
new Promise<void>((resolve, reject) => {
|
|
761
|
+
this.shutdown_channel(channel, (err?: Error) => {
|
|
762
|
+
if (err) {
|
|
763
|
+
reject(err);
|
|
764
|
+
} else {
|
|
765
|
+
resolve();
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
})
|
|
769
|
+
);
|
|
770
|
+
Promise.all(promises)
|
|
771
|
+
.then(() => {
|
|
594
772
|
/* c8 ignore next */
|
|
595
773
|
if (!(Object.keys(this._channels).length === 0)) {
|
|
596
774
|
errorLog(" Bad !");
|
|
597
775
|
}
|
|
598
776
|
assert(Object.keys(this._channels).length === 0, "channel must have unregistered themselves");
|
|
599
|
-
callback(
|
|
600
|
-
}
|
|
601
|
-
|
|
777
|
+
callback();
|
|
778
|
+
})
|
|
779
|
+
.catch((err) => {
|
|
780
|
+
callback(err);
|
|
781
|
+
});
|
|
602
782
|
});
|
|
603
783
|
} else {
|
|
604
784
|
callback();
|
|
@@ -657,10 +837,10 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
657
837
|
}
|
|
658
838
|
|
|
659
839
|
private _dump_statistics() {
|
|
660
|
-
this._server
|
|
840
|
+
this._server?.getConnections((_err: Error | null, count: number) => {
|
|
661
841
|
debugLog(chalk.cyan("CONCURRENT CONNECTION = "), count);
|
|
662
842
|
});
|
|
663
|
-
debugLog(chalk.cyan("MAX CONNECTIONS = "), this._server
|
|
843
|
+
debugLog(chalk.cyan("MAX CONNECTIONS = "), this._server?.maxConnections);
|
|
664
844
|
}
|
|
665
845
|
|
|
666
846
|
private _setup_server() {
|
|
@@ -672,11 +852,11 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
672
852
|
|
|
673
853
|
this._listen_callback = undefined;
|
|
674
854
|
this._server
|
|
675
|
-
.on("connection", (socket:
|
|
855
|
+
.on("connection", (socket: Socket) => {
|
|
676
856
|
// c8 ignore next
|
|
677
857
|
if (doDebug) {
|
|
678
858
|
this._dump_statistics();
|
|
679
|
-
debugLog(
|
|
859
|
+
debugLog(`server connected with : ${socket.remoteAddress}:${socket.remotePort}`);
|
|
680
860
|
}
|
|
681
861
|
})
|
|
682
862
|
.on("close", () => {
|
|
@@ -710,7 +890,7 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
710
890
|
"The maximum number of connection has been reached - Connection is refused"
|
|
711
891
|
)
|
|
712
892
|
);
|
|
713
|
-
const reason =
|
|
893
|
+
const reason = `maxConnections reached (${this.maxConnections})`;
|
|
714
894
|
const socketData = extractSocketData(socket, reason);
|
|
715
895
|
this.emit("connectionRefused", socketData);
|
|
716
896
|
|
|
@@ -725,7 +905,7 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
725
905
|
" nbConnections ",
|
|
726
906
|
nbConnections,
|
|
727
907
|
" self._server.maxConnections",
|
|
728
|
-
this._server
|
|
908
|
+
this._server?.maxConnections,
|
|
729
909
|
this.maxConnections
|
|
730
910
|
);
|
|
731
911
|
deny_connection();
|
|
@@ -741,7 +921,7 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
741
921
|
timeout: this.timeout,
|
|
742
922
|
adjustTransportLimits: this.transportSettings?.adjustTransportLimits
|
|
743
923
|
});
|
|
744
|
-
|
|
924
|
+
|
|
745
925
|
debugLog("channel Timeout = >", channel.timeout);
|
|
746
926
|
|
|
747
927
|
socket.resume();
|
|
@@ -752,7 +932,7 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
752
932
|
this._un_pre_registerChannel(channel);
|
|
753
933
|
debugLog(chalk.yellow.bold("Channel#init done"), err);
|
|
754
934
|
if (err) {
|
|
755
|
-
const reason =
|
|
935
|
+
const reason = `openSecureChannel has Failed ${err.message}`;
|
|
756
936
|
const socketData = extractSocketData(socket, reason);
|
|
757
937
|
const channelData = extractChannelData(channel);
|
|
758
938
|
this.emit("openSecureChannelFailure", socketData, channelData);
|
|
@@ -804,7 +984,7 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
804
984
|
delete this._channels[channel.hashKey];
|
|
805
985
|
const channelPriv = <ServerSecureChannelLayerPriv>channel;
|
|
806
986
|
if (typeof channelPriv._unpreregisterChannelEvent === "function") {
|
|
807
|
-
channel.removeListener("abort", channelPriv._unpreregisterChannelEvent
|
|
987
|
+
channel.removeListener("abort", channelPriv._unpreregisterChannelEvent);
|
|
808
988
|
channelPriv._unpreregisterChannelEvent = undefined;
|
|
809
989
|
}
|
|
810
990
|
}
|
|
@@ -870,8 +1050,7 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
870
1050
|
|
|
871
1051
|
private _end_listen(err?: Error) {
|
|
872
1052
|
if (!this._listen_callback) return;
|
|
873
|
-
|
|
874
|
-
this._listen_callback!(err);
|
|
1053
|
+
this._listen_callback(err);
|
|
875
1054
|
this._listen_callback = undefined;
|
|
876
1055
|
}
|
|
877
1056
|
|
|
@@ -900,17 +1079,18 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
900
1079
|
|
|
901
1080
|
if (nbConnections >= this.maxConnections) {
|
|
902
1081
|
// c8 ignore next
|
|
903
|
-
errorLog(chalk.bgRed.white(
|
|
1082
|
+
errorLog(chalk.bgRed.white(`PREVENTING DDOS ATTACK => maxConnection =${this.maxConnections}`));
|
|
904
1083
|
|
|
905
1084
|
const unused_channels: ServerSecureChannelLayer[] = this.getChannels().filter((channel1: ServerSecureChannelLayer) => {
|
|
906
1085
|
return !channel1.hasSession;
|
|
907
1086
|
});
|
|
908
1087
|
if (unused_channels.length === 0) {
|
|
909
|
-
doDebug &&
|
|
910
|
-
|
|
911
|
-
.
|
|
912
|
-
|
|
913
|
-
|
|
1088
|
+
doDebug &&
|
|
1089
|
+
console.log(
|
|
1090
|
+
this.getChannels()
|
|
1091
|
+
.map(({ status, isOpened, hasSession }) => `${status} ${isOpened} ${hasSession}\n`)
|
|
1092
|
+
.join(" ")
|
|
1093
|
+
);
|
|
914
1094
|
// all channels are in used , we cannot get any
|
|
915
1095
|
errorLog(`All channels are in used ! we cannot cancel any ${this.getChannels().length}`);
|
|
916
1096
|
// c8 ignore next
|
|
@@ -990,6 +1170,12 @@ interface MakeEndpointDescriptionOptions {
|
|
|
990
1170
|
securityPolicies: SecurityPolicy[];
|
|
991
1171
|
|
|
992
1172
|
userTokenTypes: UserTokenType[];
|
|
1173
|
+
/**
|
|
1174
|
+
* Override the port used in the dynamic endpointUrl getter.
|
|
1175
|
+
* When set, the endpoint URL advertises this port instead of
|
|
1176
|
+
* the parent's listen port.
|
|
1177
|
+
*/
|
|
1178
|
+
advertisedPort?: number;
|
|
993
1179
|
/**
|
|
994
1180
|
*
|
|
995
1181
|
* default value: false;
|
|
@@ -1031,7 +1217,6 @@ function estimateSecurityLevel(securityMode: MessageSecurityMode, securityPolicy
|
|
|
1031
1217
|
return 7 + offset;
|
|
1032
1218
|
|
|
1033
1219
|
default:
|
|
1034
|
-
case SecurityPolicy.None:
|
|
1035
1220
|
return 1;
|
|
1036
1221
|
}
|
|
1037
1222
|
}
|
|
@@ -1052,7 +1237,7 @@ function _makeEndpointDescription(options: MakeEndpointDescriptionOptions, paren
|
|
|
1052
1237
|
options.securityLevel === undefined
|
|
1053
1238
|
? estimateSecurityLevel(options.securityMode, options.securityPolicy)
|
|
1054
1239
|
: options.securityLevel;
|
|
1055
|
-
assert(isFinite(options.securityLevel), "expecting a valid securityLevel");
|
|
1240
|
+
assert(Number.isFinite(options.securityLevel), "expecting a valid securityLevel");
|
|
1056
1241
|
|
|
1057
1242
|
const securityPolicyUri = toURI(options.securityPolicy);
|
|
1058
1243
|
|
|
@@ -1175,14 +1360,15 @@ function _makeEndpointDescription(options: MakeEndpointDescriptionOptions, paren
|
|
|
1175
1360
|
userIdentityTokens,
|
|
1176
1361
|
|
|
1177
1362
|
securityLevel: options.securityLevel,
|
|
1178
|
-
transportProfileUri:
|
|
1363
|
+
transportProfileUri: UATCP_UASC_UABINARY
|
|
1179
1364
|
}) as EndpointDescriptionEx;
|
|
1180
1365
|
endpoint._parent = parent;
|
|
1181
1366
|
|
|
1182
1367
|
// endpointUrl is dynamic as port number may be adjusted
|
|
1183
1368
|
// when the tcp socket start listening
|
|
1369
|
+
// biome-ignore lint/suspicious/noExplicitAny: __defineGetter__ not in standard typings
|
|
1184
1370
|
(endpoint as any).__defineGetter__("endpointUrl", () => {
|
|
1185
|
-
const port = endpoint._parent.port;
|
|
1371
|
+
const port = options.advertisedPort ?? endpoint._parent.port;
|
|
1186
1372
|
const resourcePath = options.resourcePath || "";
|
|
1187
1373
|
const hostname = options.hostname;
|
|
1188
1374
|
const endpointUrl = `opc.tcp://${hostname}:${port}${resourcePath}`;
|
|
@@ -1212,7 +1398,7 @@ function matching_endpoint(
|
|
|
1212
1398
|
): boolean {
|
|
1213
1399
|
assert(endpoint instanceof EndpointDescription);
|
|
1214
1400
|
const endpoint_securityPolicy = fromURI(endpoint.securityPolicyUri);
|
|
1215
|
-
if (endpointUrl && endpoint.endpointUrl
|
|
1401
|
+
if (endpointUrl && endpoint.endpointUrl !== endpointUrl) {
|
|
1216
1402
|
return false;
|
|
1217
1403
|
}
|
|
1218
1404
|
return endpoint.securityMode === securityMode && endpoint_securityPolicy === securityPolicy;
|
|
@@ -1220,9 +1406,7 @@ function matching_endpoint(
|
|
|
1220
1406
|
|
|
1221
1407
|
const defaultSecurityModes = [MessageSecurityMode.None, MessageSecurityMode.Sign, MessageSecurityMode.SignAndEncrypt];
|
|
1222
1408
|
|
|
1223
|
-
|
|
1224
1409
|
const defaultSecurityPolicies = [
|
|
1225
|
-
|
|
1226
1410
|
// now deprecated Basic128Rs15 shall be disabled by default
|
|
1227
1411
|
// see https://profiles.opcfoundation.org/profile/1532
|
|
1228
1412
|
// SecurityPolicy.Basic128Rsa15,
|
|
@@ -1230,10 +1414,10 @@ const defaultSecurityPolicies = [
|
|
|
1230
1414
|
// now deprecated Basic256 shall be disabled by default
|
|
1231
1415
|
// see https://profiles.opcfoundation.org/profile/2062
|
|
1232
1416
|
// SecurityPolicy.Basic256,
|
|
1233
|
-
|
|
1417
|
+
|
|
1234
1418
|
// xx UNUSED!! SecurityPolicy.Basic192Rsa15,
|
|
1235
1419
|
// xx UNUSED!! SecurityPolicy.Basic256Rsa15,
|
|
1236
|
-
|
|
1420
|
+
|
|
1237
1421
|
SecurityPolicy.Basic256Sha256,
|
|
1238
1422
|
SecurityPolicy.Aes128_Sha256_RsaOaep,
|
|
1239
1423
|
SecurityPolicy.Aes256_Sha256_RsaPss
|