node-opcua-server 2.164.2 → 2.165.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/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 +42 -42
- 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
package/source/base_server.ts
CHANGED
|
@@ -2,51 +2,45 @@
|
|
|
2
2
|
* @module node-opcua-server
|
|
3
3
|
*/
|
|
4
4
|
// tslint:disable:no-console
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
import os from "os";
|
|
8
|
-
import
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { isIP } from "node:net";
|
|
10
|
+
import { withLock } from "@ster5/global-mutex";
|
|
9
11
|
import async from "async";
|
|
10
12
|
import chalk from "chalk";
|
|
11
13
|
import { assert } from "node-opcua-assert";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
makeSubject,
|
|
18
|
-
OPCUACertificateManager
|
|
19
|
-
} from "node-opcua-certificate-manager";
|
|
20
|
-
import { IOPCUASecureObjectOptions, makeApplicationUrn, OPCUASecureObject } from "node-opcua-common";
|
|
21
|
-
import { coerceLocalizedText, LocalizedText } from "node-opcua-data-model";
|
|
14
|
+
import { getDefaultCertificateManager, makeSubject, type OPCUACertificateManager } from "node-opcua-certificate-manager";
|
|
15
|
+
import { performCertificateSanityCheck } from "node-opcua-client";
|
|
16
|
+
import { type IOPCUASecureObjectOptions, makeApplicationUrn, OPCUASecureObject } from "node-opcua-common";
|
|
17
|
+
import { exploreCertificate } from "node-opcua-crypto/web";
|
|
18
|
+
import { coerceLocalizedText } from "node-opcua-data-model";
|
|
22
19
|
import { installPeriodicClockAdjustment, uninstallPeriodicClockAdjustment } from "node-opcua-date-time";
|
|
23
|
-
import { checkDebugFlag, make_debugLog, make_errorLog } from "node-opcua-debug";
|
|
24
|
-
import { displayTraceFromThisProjectOnly } from "node-opcua-debug";
|
|
20
|
+
import { checkDebugFlag, displayTraceFromThisProjectOnly, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
|
|
25
21
|
import {
|
|
26
22
|
extractFullyQualifiedDomainName,
|
|
27
23
|
getFullyQualifiedDomainName,
|
|
28
24
|
getHostname,
|
|
25
|
+
getIpAddresses,
|
|
26
|
+
ipv4ToHex,
|
|
29
27
|
resolveFullyQualifiedDomainName
|
|
30
28
|
} from "node-opcua-hostname";
|
|
31
|
-
import { Message, Response, ServerSecureChannelLayer
|
|
29
|
+
import type { Message, Response, ServerSecureChannelLayer } from "node-opcua-secure-channel";
|
|
32
30
|
import { FindServersRequest, FindServersResponse } from "node-opcua-service-discovery";
|
|
33
|
-
import { ApplicationType, GetEndpointsResponse } from "node-opcua-service-endpoints";
|
|
34
|
-
import { ApplicationDescription } from "node-opcua-service-endpoints";
|
|
31
|
+
import { ApplicationDescription, ApplicationType, GetEndpointsResponse } from "node-opcua-service-endpoints";
|
|
35
32
|
import { ServiceFault } from "node-opcua-service-secure-channel";
|
|
36
|
-
import { StatusCode, StatusCodes } from "node-opcua-status-code";
|
|
37
|
-
import { ApplicationDescriptionOptions } from "node-opcua-types";
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
|
|
41
|
-
import {
|
|
42
|
-
import { OPCUAServerEndPoint } from "./server_end_point";
|
|
43
|
-
import { IChannelData } from "./i_channel_data";
|
|
44
|
-
import { ISocketData } from "./i_socket_data";
|
|
33
|
+
import { type StatusCode, StatusCodes } from "node-opcua-status-code";
|
|
34
|
+
import type { ApplicationDescriptionOptions, EndpointDescription, GetEndpointsRequest } from "node-opcua-types";
|
|
35
|
+
import { checkFileExistsAndIsNotEmpty, matchUri } from "node-opcua-utils";
|
|
36
|
+
import type { IChannelData } from "./i_channel_data";
|
|
37
|
+
import type { ISocketData } from "./i_socket_data";
|
|
38
|
+
import type { OPCUAServerEndPoint } from "./server_end_point";
|
|
45
39
|
|
|
46
40
|
const doDebug = checkDebugFlag(__filename);
|
|
47
41
|
const debugLog = make_debugLog(__filename);
|
|
48
42
|
const errorLog = make_errorLog(__filename);
|
|
49
|
-
const warningLog =
|
|
43
|
+
const warningLog = make_warningLog(__filename);
|
|
50
44
|
|
|
51
45
|
const default_server_info = {
|
|
52
46
|
// The globally unique identifier for the application instance. This URI is used as
|
|
@@ -122,7 +116,7 @@ export class OPCUABaseServer extends OPCUASecureObject {
|
|
|
122
116
|
public endpoints: OPCUAServerEndPoint[];
|
|
123
117
|
public readonly serverCertificateManager: OPCUACertificateManager;
|
|
124
118
|
public capabilitiesForMDNS: string[];
|
|
125
|
-
protected _preInitTask:
|
|
119
|
+
protected _preInitTask: (() => Promise<void>)[];
|
|
126
120
|
|
|
127
121
|
protected options: OPCUABaseServerOptions;
|
|
128
122
|
|
|
@@ -155,9 +149,9 @@ export class OPCUABaseServer extends OPCUASecureObject {
|
|
|
155
149
|
errorLog("[NODE-OPCUA-E06] application name cannot be a urn", this.serverInfo.applicationName.toString());
|
|
156
150
|
}
|
|
157
151
|
|
|
158
|
-
this.serverInfo.applicationName
|
|
152
|
+
this.serverInfo.applicationName.locale = this.serverInfo.applicationName.locale || "en";
|
|
159
153
|
|
|
160
|
-
if (!this.serverInfo.applicationName
|
|
154
|
+
if (!this.serverInfo.applicationName.locale) {
|
|
161
155
|
warningLog(
|
|
162
156
|
"[NODE-OPCUA-W24] the server applicationName must have a valid locale : ",
|
|
163
157
|
this.serverInfo.applicationName.toString()
|
|
@@ -166,10 +160,13 @@ export class OPCUABaseServer extends OPCUASecureObject {
|
|
|
166
160
|
|
|
167
161
|
const __applicationUri = serverInfo.applicationUri || "";
|
|
168
162
|
|
|
169
|
-
(this.serverInfo
|
|
163
|
+
Object.defineProperty(this.serverInfo, "applicationUri", {
|
|
164
|
+
get: () => resolveFullyQualifiedDomainName(__applicationUri),
|
|
165
|
+
configurable: true
|
|
166
|
+
});
|
|
170
167
|
|
|
171
168
|
this._preInitTask.push(async () => {
|
|
172
|
-
|
|
169
|
+
await extractFullyQualifiedDomainName();
|
|
173
170
|
});
|
|
174
171
|
|
|
175
172
|
this._preInitTask.push(async () => {
|
|
@@ -177,35 +174,72 @@ export class OPCUABaseServer extends OPCUASecureObject {
|
|
|
177
174
|
});
|
|
178
175
|
}
|
|
179
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Return additional DNS hostnames to include in the self-signed
|
|
179
|
+
* certificate's SubjectAlternativeName (SAN).
|
|
180
|
+
*
|
|
181
|
+
* The base implementation returns an empty array. Subclasses
|
|
182
|
+
* (e.g. `OPCUAServer`) override this to include hostnames from
|
|
183
|
+
* `alternateHostname` and `advertisedEndpoints`.
|
|
184
|
+
*
|
|
185
|
+
* @internal
|
|
186
|
+
*/
|
|
187
|
+
protected getConfiguredHostnames(): string[] {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Return additional IP addresses to include in the self-signed
|
|
193
|
+
* certificate's SubjectAlternativeName (SAN) iPAddress entries.
|
|
194
|
+
*
|
|
195
|
+
* The base implementation returns an empty array. Subclasses
|
|
196
|
+
* (e.g. `OPCUAServer`) override this to include IP literals
|
|
197
|
+
* found in `alternateHostname` and `advertisedEndpoints`.
|
|
198
|
+
*
|
|
199
|
+
* These IPs are considered **explicitly configured** by the
|
|
200
|
+
* user and are therefore checked by `checkCertificateSAN()`.
|
|
201
|
+
* In contrast, auto-detected IPs from `getIpAddresses()` are
|
|
202
|
+
* included in the certificate at creation time but are NOT
|
|
203
|
+
* checked later — see `checkCertificateSAN()` for rationale.
|
|
204
|
+
*
|
|
205
|
+
* @internal
|
|
206
|
+
*/
|
|
207
|
+
protected getConfiguredIPs(): string[] {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
|
|
180
211
|
protected async createDefaultCertificate(): Promise<void> {
|
|
181
212
|
if (fs.existsSync(this.certificateFile)) {
|
|
182
213
|
return;
|
|
183
214
|
}
|
|
184
215
|
|
|
185
|
-
// collect all hostnames
|
|
186
|
-
const hostnames = [];
|
|
187
|
-
for (const e of this.endpoints) {
|
|
188
|
-
for (const ee of e.endpointDescriptions()) {
|
|
189
|
-
/* to do */
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
216
|
if (!checkFileExistsAndIsNotEmpty(this.certificateFile)) {
|
|
193
|
-
await withLock({ fileToLock: this.certificateFile
|
|
217
|
+
await withLock({ fileToLock: `${this.certificateFile}.mutex` }, async () => {
|
|
194
218
|
if (checkFileExistsAndIsNotEmpty(this.certificateFile)) {
|
|
195
219
|
return;
|
|
196
220
|
}
|
|
197
|
-
const applicationUri = this.serverInfo.applicationUri
|
|
221
|
+
const applicationUri = this.serverInfo.applicationUri || "<missing application uri>";
|
|
198
222
|
const fqdn = getFullyQualifiedDomainName();
|
|
199
223
|
const hostname = getHostname();
|
|
200
|
-
const dns = [...new Set([fqdn, hostname])];
|
|
224
|
+
const dns = [...new Set([fqdn, hostname, ...this.getConfiguredHostnames()])].sort();
|
|
225
|
+
|
|
226
|
+
// Include both auto-detected IPs and explicitly configured IPs.
|
|
227
|
+
// Auto-detected IPs (getIpAddresses) are ephemeral — they depend on
|
|
228
|
+
// the current network state (WiFi, tethering, VPN, roaming) and may
|
|
229
|
+
// change between reboots. They are included here so that the initial
|
|
230
|
+
// certificate covers the current network configuration, but they are
|
|
231
|
+
// NOT checked by checkCertificateSAN() to avoid noisy warnings when
|
|
232
|
+
// the network changes. Only explicitly configured IPs (from
|
|
233
|
+
// alternateHostname / advertisedEndpoints) are checked at startup.
|
|
234
|
+
const ip = [...new Set([...getIpAddresses(), ...this.getConfiguredIPs()])].sort();
|
|
201
235
|
|
|
202
236
|
await this.serverCertificateManager.createSelfSignedCertificate({
|
|
203
237
|
applicationUri,
|
|
204
238
|
dns,
|
|
205
|
-
|
|
239
|
+
ip,
|
|
206
240
|
outputFile: this.certificateFile,
|
|
207
241
|
|
|
208
|
-
subject: makeSubject(this.serverInfo.applicationName.text
|
|
242
|
+
subject: makeSubject(this.serverInfo.applicationName.text || "<missing application name>", hostname),
|
|
209
243
|
|
|
210
244
|
startDate: new Date(),
|
|
211
245
|
validity: 365 * 10 // 10 years
|
|
@@ -219,17 +253,128 @@ export class OPCUABaseServer extends OPCUASecureObject {
|
|
|
219
253
|
await this.createDefaultCertificate();
|
|
220
254
|
debugLog("privateKey = ", this.privateKeyFile, this.serverCertificateManager.privateKey);
|
|
221
255
|
debugLog("certificateFile = ", this.certificateFile);
|
|
222
|
-
|
|
256
|
+
this._checkCertificateSanMismatch();
|
|
257
|
+
await performCertificateSanityCheck(this, "server", this.serverCertificateManager, this.serverInfo.applicationUri || "");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Compare the current certificate's SAN entries against all
|
|
262
|
+
* explicitly configured hostnames and IPs, and return any
|
|
263
|
+
* that are missing.
|
|
264
|
+
*
|
|
265
|
+
* Returns an empty array when the certificate covers every
|
|
266
|
+
* configured hostname and IP.
|
|
267
|
+
*
|
|
268
|
+
* **Important — ephemeral IP mitigation:**
|
|
269
|
+
* Auto-detected IPs (from `getIpAddresses()`) are deliberately
|
|
270
|
+
* NOT included in this check. Network interfaces are transient
|
|
271
|
+
* — WiFi IPs change on reconnect, tethering IPs appear/disappear,
|
|
272
|
+
* VPN adapters come and go. Including them would cause the
|
|
273
|
+
* `[NODE-OPCUA-W26]` warning to fire on every server restart
|
|
274
|
+
* whenever the network state differs from when the certificate
|
|
275
|
+
* was originally created.
|
|
276
|
+
*
|
|
277
|
+
* Only **explicitly configured** values are checked:
|
|
278
|
+
* - Hostnames: FQDN, os.hostname(), `alternateHostname` (non-IP),
|
|
279
|
+
* hostnames from `advertisedEndpoints` URLs
|
|
280
|
+
* - IPs: IP literals from `alternateHostname`, IP literals
|
|
281
|
+
* from `advertisedEndpoints` URLs
|
|
282
|
+
*
|
|
283
|
+
* The certificate itself still includes auto-detected IPs at
|
|
284
|
+
* creation time — this is fine because it captures the network
|
|
285
|
+
* state at that moment. But the *mismatch warning* only fires
|
|
286
|
+
* for things the user explicitly asked for.
|
|
287
|
+
*/
|
|
288
|
+
public checkCertificateSAN(): string[] {
|
|
289
|
+
const certDer = this.getCertificate();
|
|
290
|
+
const info = exploreCertificate(certDer);
|
|
291
|
+
const sanDns: string[] = info.tbsCertificate.extensions?.subjectAltName?.dNSName || [];
|
|
292
|
+
const sanIpsHex: string[] = info.tbsCertificate.extensions?.subjectAltName?.iPAddress || [];
|
|
293
|
+
|
|
294
|
+
const fqdn = getFullyQualifiedDomainName();
|
|
295
|
+
const hostname = getHostname();
|
|
296
|
+
const expectedDns = [...new Set([fqdn, hostname, ...this.getConfiguredHostnames()])].sort();
|
|
297
|
+
|
|
298
|
+
// Only check explicitly configured IPs — NOT auto-detected ones.
|
|
299
|
+
// See JSDoc above for the rationale (ephemeral network interfaces).
|
|
300
|
+
const expectedIps = [...new Set(this.getConfiguredIPs())].sort();
|
|
301
|
+
|
|
302
|
+
const missingDns = expectedDns.filter((name) => !sanDns.includes(name));
|
|
303
|
+
// exploreCertificate returns iPAddress entries as hex strings
|
|
304
|
+
// Only IPv4 addresses can be converted with ipv4ToHex here; IPv6 (and invalid) IPs are skipped.
|
|
305
|
+
const missingIps = expectedIps.filter((ip) => {
|
|
306
|
+
const family = isIP(ip);
|
|
307
|
+
if (family === 4) {
|
|
308
|
+
return !sanIpsHex.includes(ipv4ToHex(ip));
|
|
309
|
+
}
|
|
310
|
+
// IPv6 or invalid literals are currently not matched against SAN iPAddress entries here.
|
|
311
|
+
return false;
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
return [...missingDns, ...missingIps];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Delete the existing self-signed certificate and create a new
|
|
319
|
+
* one that includes all currently configured hostnames.
|
|
320
|
+
*
|
|
321
|
+
* @throws if the current certificate was NOT self-signed
|
|
322
|
+
* (i.e. issued by a CA or GDS)
|
|
323
|
+
*/
|
|
324
|
+
public async regenerateSelfSignedCertificate(): Promise<void> {
|
|
325
|
+
// guard: only allow regeneration of self-signed certs
|
|
326
|
+
const certDer = this.getCertificate();
|
|
327
|
+
const info = exploreCertificate(certDer);
|
|
328
|
+
const issuer = info.tbsCertificate.issuer;
|
|
329
|
+
const subject = info.tbsCertificate.subject;
|
|
330
|
+
const isSelfSigned = issuer.commonName === subject.commonName && issuer.organizationName === subject.organizationName;
|
|
331
|
+
if (!isSelfSigned) {
|
|
332
|
+
throw new Error("Cannot regenerate certificate: current certificate is not self-signed (issued by a CA or GDS)");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// delete old cert
|
|
336
|
+
if (fs.existsSync(this.certificateFile)) {
|
|
337
|
+
fs.unlinkSync(this.certificateFile);
|
|
338
|
+
}
|
|
339
|
+
// recreate with current hostnames
|
|
340
|
+
await this.createDefaultCertificate();
|
|
341
|
+
// invalidate cached cert so next getCertificate() reloads from disk
|
|
342
|
+
const priv = this as unknown as { $$certificate: null; $$certificateChain: null };
|
|
343
|
+
priv.$$certificate = null;
|
|
344
|
+
priv.$$certificateChain = null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private _checkCertificateSanMismatch(): void {
|
|
348
|
+
try {
|
|
349
|
+
const missing = this.checkCertificateSAN();
|
|
350
|
+
if (missing.length > 0) {
|
|
351
|
+
warningLog(
|
|
352
|
+
`[NODE-OPCUA-W26] Certificate SAN is missing the following configured hostnames/IPs: ${missing.join(", ")}. ` +
|
|
353
|
+
"Clients with strict certificate validation may reject connections for these entries. " +
|
|
354
|
+
"Use server.regenerateSelfSignedCertificate() to fix this."
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
} catch (_err) {
|
|
358
|
+
// ignore errors during SAN check (e.g. cert not yet loaded)
|
|
359
|
+
}
|
|
223
360
|
}
|
|
224
361
|
|
|
225
362
|
/**
|
|
226
363
|
* start all registered endPoint, in parallel, and call done when all endPoints are listening.
|
|
227
364
|
*/
|
|
228
|
-
public start(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
365
|
+
public start(): Promise<void>;
|
|
366
|
+
public start(done: () => void): void;
|
|
367
|
+
public start(...args: [((err?: Error) => void)?]): Promise<void> | void {
|
|
368
|
+
const callback = args[0];
|
|
369
|
+
if (!callback || args.length === 0) {
|
|
370
|
+
return this.startAsync();
|
|
371
|
+
} else {
|
|
372
|
+
this.startAsync()
|
|
373
|
+
.then(() => {
|
|
374
|
+
callback();
|
|
375
|
+
})
|
|
376
|
+
.catch((err) => callback(err));
|
|
377
|
+
}
|
|
233
378
|
}
|
|
234
379
|
|
|
235
380
|
protected async performPreInitialization(): Promise<void> {
|
|
@@ -294,7 +439,7 @@ export class OPCUABaseServer extends OPCUASecureObject {
|
|
|
294
439
|
/**
|
|
295
440
|
* shutdown all server endPoints
|
|
296
441
|
*/
|
|
297
|
-
public shutdown(done: (err?: Error) => void): void {
|
|
442
|
+
public shutdown(done: (err?: Error | null) => void): void {
|
|
298
443
|
assert(typeof done === "function");
|
|
299
444
|
uninstallPeriodicClockAdjustment();
|
|
300
445
|
this.serverCertificateManager.dispose().then(() => {
|
|
@@ -307,7 +452,7 @@ export class OPCUABaseServer extends OPCUASecureObject {
|
|
|
307
452
|
},
|
|
308
453
|
(err?: Error | null) => {
|
|
309
454
|
debugLog("shutdown completed");
|
|
310
|
-
done(err
|
|
455
|
+
done(err);
|
|
311
456
|
}
|
|
312
457
|
);
|
|
313
458
|
});
|
|
@@ -317,6 +462,8 @@ export class OPCUABaseServer extends OPCUASecureObject {
|
|
|
317
462
|
public shutdownChannels(callback: (err?: Error | null) => void): void;
|
|
318
463
|
public shutdownChannels(callback?: (err?: Error | null) => void): Promise<void> | void {
|
|
319
464
|
assert(typeof callback === "function");
|
|
465
|
+
// c8 ignore next
|
|
466
|
+
if (!callback) throw new Error("thenify is not available");
|
|
320
467
|
debugLog("OPCUABaseServer#shutdownChannels");
|
|
321
468
|
async.forEach(
|
|
322
469
|
this.endpoints,
|
|
@@ -338,7 +485,7 @@ export class OPCUABaseServer extends OPCUASecureObject {
|
|
|
338
485
|
inner_callback
|
|
339
486
|
);
|
|
340
487
|
},
|
|
341
|
-
callback
|
|
488
|
+
callback
|
|
342
489
|
);
|
|
343
490
|
}
|
|
344
491
|
|
|
@@ -352,7 +499,7 @@ export class OPCUABaseServer extends OPCUASecureObject {
|
|
|
352
499
|
|
|
353
500
|
// install channel._on_response so we can intercept its call and emit the "response" event.
|
|
354
501
|
if (!channel._on_response) {
|
|
355
|
-
channel._on_response = (
|
|
502
|
+
channel._on_response = (_msg: string, response1: Response /*, inner_message: Message*/) => {
|
|
356
503
|
this.emit("response", response1, channel);
|
|
357
504
|
};
|
|
358
505
|
}
|
|
@@ -375,12 +522,11 @@ export class OPCUABaseServer extends OPCUASecureObject {
|
|
|
375
522
|
|
|
376
523
|
try {
|
|
377
524
|
// handler must be named _on_ActionRequest()
|
|
378
|
-
const handler = (this as
|
|
525
|
+
const handler = (this as unknown as Record<string, unknown>)[`_on_${request.schema.name}`];
|
|
379
526
|
if (typeof handler === "function") {
|
|
380
|
-
|
|
381
|
-
handler.apply(this, arguments);
|
|
527
|
+
handler.call(this, message, channel);
|
|
382
528
|
} else {
|
|
383
|
-
errMessage =
|
|
529
|
+
errMessage = `[NODE-OPCUA-W07] Unsupported Service : ${request.schema.name}`;
|
|
384
530
|
warningLog(errMessage);
|
|
385
531
|
debugLog(chalk.red.bold(errMessage));
|
|
386
532
|
response = makeServiceFault(StatusCodes.BadServiceUnsupported, [errMessage]);
|
|
@@ -388,14 +534,14 @@ export class OPCUABaseServer extends OPCUASecureObject {
|
|
|
388
534
|
}
|
|
389
535
|
} catch (err) {
|
|
390
536
|
/* c8 ignore next */
|
|
391
|
-
const errMessage1 =
|
|
537
|
+
const errMessage1 = `[NODE-OPCUA-W08] EXCEPTION CAUGHT WHILE PROCESSING REQUEST !! ${request.schema.name}`;
|
|
392
538
|
warningLog(chalk.red.bold(errMessage1));
|
|
393
539
|
warningLog(request.toString());
|
|
394
540
|
displayTraceFromThisProjectOnly(err as Error);
|
|
395
541
|
|
|
396
542
|
let additional_messages = [];
|
|
397
|
-
additional_messages.push(
|
|
398
|
-
if (
|
|
543
|
+
additional_messages.push(`EXCEPTION CAUGHT WHILE PROCESSING REQUEST !!! ${request.schema.name}`);
|
|
544
|
+
if (err instanceof Error) {
|
|
399
545
|
additional_messages.push(err.message);
|
|
400
546
|
if (err.stack) {
|
|
401
547
|
additional_messages = additional_messages.concat(err.stack.split("\n"));
|
|
@@ -408,9 +554,18 @@ export class OPCUABaseServer extends OPCUASecureObject {
|
|
|
408
554
|
}
|
|
409
555
|
|
|
410
556
|
/**
|
|
411
|
-
*
|
|
557
|
+
* Find endpoint descriptions matching a given endpoint URL.
|
|
558
|
+
*
|
|
559
|
+
* When `endpointUrl` is provided, only endpoints whose URL matches
|
|
560
|
+
* (case-insensitive) are returned. When `null` or omitted, all
|
|
561
|
+
* endpoints from every `OPCUAServerEndPoint` are returned.
|
|
562
|
+
*
|
|
563
|
+
* This is the shared resolution path used by both `GetEndpoints`
|
|
564
|
+
* and `CreateSession` (`validate_security_endpoint`).
|
|
565
|
+
*
|
|
566
|
+
* @internal (was _get_endpoints)
|
|
412
567
|
*/
|
|
413
|
-
public
|
|
568
|
+
public findMatchingEndpoints(endpointUrl?: string | null): EndpointDescription[] {
|
|
414
569
|
let endpoints: EndpointDescription[] = [];
|
|
415
570
|
for (const endPoint of this.endpoints) {
|
|
416
571
|
const ep = endPoint.endpointDescriptions();
|
|
@@ -423,17 +578,17 @@ export class OPCUABaseServer extends OPCUASecureObject {
|
|
|
423
578
|
* get one of the possible endpointUrl
|
|
424
579
|
*/
|
|
425
580
|
public getEndpointUrl(): string {
|
|
426
|
-
return this.
|
|
581
|
+
return this.findMatchingEndpoints()[0].endpointUrl || "";
|
|
427
582
|
}
|
|
428
583
|
|
|
429
584
|
public getDiscoveryUrls(): string[] {
|
|
430
585
|
const discoveryUrls = this.endpoints.map((e: OPCUAServerEndPoint) => {
|
|
431
|
-
return e.endpointDescriptions()[0].endpointUrl
|
|
586
|
+
return e.endpointDescriptions()[0].endpointUrl || "";
|
|
432
587
|
});
|
|
433
588
|
return discoveryUrls;
|
|
434
589
|
}
|
|
435
590
|
|
|
436
|
-
public getServers(
|
|
591
|
+
public getServers(_channel: ServerSecureChannelLayer): ApplicationDescription[] {
|
|
437
592
|
this.serverInfo.discoveryUrls = this.getDiscoveryUrls();
|
|
438
593
|
const servers = [this.serverInfo];
|
|
439
594
|
return servers;
|
|
@@ -447,8 +602,8 @@ export class OPCUABaseServer extends OPCUASecureObject {
|
|
|
447
602
|
*
|
|
448
603
|
*/
|
|
449
604
|
public async suspendEndPoints(): Promise<void>;
|
|
450
|
-
public suspendEndPoints(callback: (err?: Error) => void): void;
|
|
451
|
-
public suspendEndPoints(callback?: (err?: Error) => void): void | Promise<void> {
|
|
605
|
+
public suspendEndPoints(callback: (err?: Error | null) => void): void;
|
|
606
|
+
public suspendEndPoints(callback?: (err?: Error | null) => void): void | Promise<void> {
|
|
452
607
|
/* c8 ignore next */
|
|
453
608
|
if (!callback) {
|
|
454
609
|
throw new Error("Internal Error");
|
|
@@ -469,7 +624,7 @@ export class OPCUABaseServer extends OPCUASecureObject {
|
|
|
469
624
|
_inner_callback(err);
|
|
470
625
|
});
|
|
471
626
|
},
|
|
472
|
-
(err?: Error | null) => callback(err
|
|
627
|
+
(err?: Error | null) => callback(err)
|
|
473
628
|
);
|
|
474
629
|
}
|
|
475
630
|
|
|
@@ -479,18 +634,20 @@ export class OPCUABaseServer extends OPCUASecureObject {
|
|
|
479
634
|
* this method is useful for testing purpose
|
|
480
635
|
*/
|
|
481
636
|
public async resumeEndPoints(): Promise<void>;
|
|
482
|
-
public resumeEndPoints(callback: (err?: Error) => void): void;
|
|
483
|
-
public resumeEndPoints(callback?: (err?: Error) => void): void | Promise<void> {
|
|
637
|
+
public resumeEndPoints(callback: (err?: Error | null) => void): void;
|
|
638
|
+
public resumeEndPoints(callback?: (err?: Error | null) => void): void | Promise<void> {
|
|
639
|
+
// c8 ignore next
|
|
640
|
+
if (!callback) throw new Error("thenify is not available");
|
|
484
641
|
async.forEach(
|
|
485
642
|
this.endpoints,
|
|
486
643
|
(ep: OPCUAServerEndPoint, _inner_callback) => {
|
|
487
644
|
ep.restoreConnection(_inner_callback);
|
|
488
645
|
},
|
|
489
|
-
(err?: Error | null) => callback
|
|
646
|
+
(err?: Error | null) => callback(err)
|
|
490
647
|
);
|
|
491
648
|
}
|
|
492
649
|
|
|
493
|
-
protected prepare(
|
|
650
|
+
protected prepare(_message: Message, _channel: ServerSecureChannelLayer): void {
|
|
494
651
|
/* empty */
|
|
495
652
|
}
|
|
496
653
|
|
|
@@ -516,23 +673,26 @@ export class OPCUABaseServer extends OPCUASecureObject {
|
|
|
516
673
|
* If the URI is a URL, this URL may have a query string appended.
|
|
517
674
|
* The Transport Profiles that support query strings are defined in OPC 10000-7.
|
|
518
675
|
*/
|
|
519
|
-
response.endpoints = this.
|
|
520
|
-
const
|
|
676
|
+
response.endpoints = this.findMatchingEndpoints(null);
|
|
677
|
+
const _e = response.endpoints.map((e) => e.endpointUrl);
|
|
521
678
|
if (request.endpointUrl) {
|
|
522
|
-
const filtered = response.endpoints.filter(
|
|
523
|
-
(endpoint
|
|
679
|
+
const filtered = response.endpoints.filter((endpoint: EndpointDescription) =>
|
|
680
|
+
matchUri(endpoint.endpointUrl, request.endpointUrl)
|
|
524
681
|
);
|
|
525
682
|
if (filtered.length > 0) {
|
|
526
683
|
response.endpoints = filtered;
|
|
527
684
|
}
|
|
528
685
|
}
|
|
529
|
-
response.endpoints = response.endpoints.filter(
|
|
686
|
+
response.endpoints = response.endpoints.filter(
|
|
687
|
+
(endpoint: EndpointDescription) => !(endpoint as unknown as { restricted: boolean }).restricted
|
|
688
|
+
);
|
|
530
689
|
|
|
531
690
|
// apply filters
|
|
532
691
|
if (request.profileUris && request.profileUris.length > 0) {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
692
|
+
const profileUris = request.profileUris;
|
|
693
|
+
response.endpoints = response.endpoints.filter(
|
|
694
|
+
(endpoint: EndpointDescription) => profileUris.indexOf(endpoint.transportProfileUri) >= 0
|
|
695
|
+
);
|
|
536
696
|
}
|
|
537
697
|
|
|
538
698
|
// adjust locale on ApplicationName to match requested local or provide
|
|
@@ -567,9 +727,10 @@ export class OPCUABaseServer extends OPCUASecureObject {
|
|
|
567
727
|
// apply filters
|
|
568
728
|
// TODO /
|
|
569
729
|
if (request.serverUris && request.serverUris.length > 0) {
|
|
730
|
+
const serverUris = request.serverUris;
|
|
570
731
|
// A serverUri matches the applicationUri from the ApplicationDescription define
|
|
571
732
|
servers = servers.filter((inner_Server: ApplicationDescription) => {
|
|
572
|
-
return
|
|
733
|
+
return serverUris.indexOf(inner_Server.applicationUri) >= 0;
|
|
573
734
|
});
|
|
574
735
|
}
|
|
575
736
|
|
|
@@ -626,6 +787,7 @@ function makeServiceFault(statusCode: StatusCode, messages: string[]): ServiceFa
|
|
|
626
787
|
|
|
627
788
|
// tslint:disable:no-var-requires
|
|
628
789
|
import { withCallback } from "thenify-ex";
|
|
790
|
+
|
|
629
791
|
const opts = { multiArgs: false };
|
|
630
792
|
OPCUABaseServer.prototype.resumeEndPoints = withCallback(OPCUABaseServer.prototype.resumeEndPoints, opts);
|
|
631
793
|
OPCUABaseServer.prototype.suspendEndPoints = withCallback(OPCUABaseServer.prototype.suspendEndPoints, opts);
|