• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

node-opcua / node-opcua / 23974043205

04 Apr 2026 07:17AM UTC coverage: 92.589% (+0.01%) from 92.576%
23974043205

push

github

erossignon
chore: fix Mocha.Suite.settimeout misused

18408 of 21832 branches covered (84.32%)

161708 of 174651 relevant lines covered (92.59%)

461089.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.69
/packages/node-opcua-server/source/base_server.ts
1
/**
37✔
2
 * @module node-opcua-server
2✔
3
 */
2✔
4
// tslint:disable:no-console
2✔
5

2✔
6
import fs from "node:fs";
2✔
7
import { isIP } from "node:net";
2✔
8
import os from "node:os";
2✔
9
import path from "node:path";
2✔
10
import { withLock } from "@ster5/global-mutex";
2✔
11

2✔
12
import chalk from "chalk";
2✔
13
import { assert } from "node-opcua-assert";
2✔
14
import { getDefaultCertificateManager, makeSubject, type OPCUACertificateManager } from "node-opcua-certificate-manager";
2✔
15
import { performCertificateSanityCheck } from "node-opcua-client";
2✔
16
import { type IOPCUASecureObjectOptions, invalidateCachedSecrets, makeApplicationUrn, OPCUASecureObject } from "node-opcua-common";
2✔
17
import { exploreCertificate } from "node-opcua-crypto/web";
2✔
18
import { coerceLocalizedText } from "node-opcua-data-model";
2✔
19
import { installPeriodicClockAdjustment, uninstallPeriodicClockAdjustment } from "node-opcua-date-time";
2✔
20
import { checkDebugFlag, displayTraceFromThisProjectOnly, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
2✔
21
import {
2✔
22
    extractFullyQualifiedDomainName,
2✔
23
    getFullyQualifiedDomainName,
2✔
24
    getHostname,
2✔
25
    getIpAddresses,
2✔
26
    ipv4ToHex,
2✔
27
    resolveFullyQualifiedDomainName
2✔
28
} from "node-opcua-hostname";
2✔
29
import type { Message, Request, Response, ServerSecureChannelLayer } from "node-opcua-secure-channel";
2✔
30
import { FindServersRequest, FindServersResponse } from "node-opcua-service-discovery";
2✔
31
import { ApplicationDescription, ApplicationType, GetEndpointsResponse } from "node-opcua-service-endpoints";
2✔
32
import { ServiceFault } from "node-opcua-service-secure-channel";
2✔
33
import { type StatusCode, StatusCodes } from "node-opcua-status-code";
2✔
34
import type { ApplicationDescriptionOptions, EndpointDescription, GetEndpointsRequest } from "node-opcua-types";
2✔
35
import { checkFileExistsAndIsNotEmpty, matchUri } from "node-opcua-utils";
2✔
36
import type { IChannelData } from "./i_channel_data";
2✔
37
import type { ISocketData } from "./i_socket_data";
2✔
38
import type { OPCUAServerEndPoint } from "./server_end_point";
2✔
39

2✔
40
const doDebug = checkDebugFlag(__filename);
2✔
41
const debugLog = make_debugLog(__filename);
2✔
42
const errorLog = make_errorLog(__filename);
2✔
43
const warningLog = make_warningLog(__filename);
2✔
44

2✔
45
const default_server_info = {
2✔
46
    // The globally unique identifier for the application instance. This URI is used as
2✔
47
    // ServerUri in Services if the application is a Server.
2✔
48
    applicationUri: makeApplicationUrn(os.hostname(), "NodeOPCUA-Server"),
2✔
49

2✔
50
    // The globally unique identifier for the product.
2✔
51
    productUri: "NodeOPCUA-Server",
2✔
52

2✔
53
    // A localized descriptive name for the application.
2✔
54
    applicationName: { text: "NodeOPCUA", locale: "en" },
2✔
55
    applicationType: ApplicationType.Server,
2✔
56
    gatewayServerUri: "",
2✔
57

2✔
58
    discoveryProfileUri: "",
2✔
59

2✔
60
    discoveryUrls: []
2✔
61
};
2✔
62

2✔
63
function cleanupEndpoint(endpoint: OPCUAServerEndPoint) {
317✔
64
    if (endpoint._on_new_channel) {
317✔
65
        assert(typeof endpoint._on_new_channel === "function");
293✔
66
        endpoint.removeListener("newChannel", endpoint._on_new_channel);
293✔
67
        endpoint._on_new_channel = undefined;
293✔
68
    }
293✔
69

317✔
70
    if (endpoint._on_close_channel) {
317✔
71
        assert(typeof endpoint._on_close_channel === "function");
293✔
72
        endpoint.removeListener("closeChannel", endpoint._on_close_channel);
293✔
73
        endpoint._on_close_channel = undefined;
293✔
74
    }
293✔
75
    if (endpoint._on_connectionRefused) {
317✔
76
        assert(typeof endpoint._on_connectionRefused === "function");
293✔
77
        endpoint.removeListener("connectionRefused", endpoint._on_connectionRefused);
293✔
78
        endpoint._on_connectionRefused = undefined;
293✔
79
    }
293✔
80
    if (endpoint._on_openSecureChannelFailure) {
317✔
81
        assert(typeof endpoint._on_openSecureChannelFailure === "function");
293✔
82
        endpoint.removeListener("openSecureChannelFailure", endpoint._on_openSecureChannelFailure);
293✔
83
        endpoint._on_openSecureChannelFailure = undefined;
293✔
84
    }
293✔
85
    if (endpoint._on_channel_secured) {
317✔
86
        assert(typeof endpoint._on_channel_secured === "function");
293✔
87
        endpoint.removeListener("channelSecured", endpoint._on_channel_secured);
293✔
88
        endpoint._on_channel_secured = undefined;
293✔
89
    }
293✔
90
}
317✔
91

2✔
92
/**
2✔
93
 *
2✔
94
 */
2✔
95
export interface OPCUABaseServerOptions extends IOPCUASecureObjectOptions {
2✔
96
    /**
2✔
97
     * the information used in the end point description
2✔
98
     */
2✔
99
    serverInfo?: ApplicationDescriptionOptions;
2✔
100
    /**
2✔
101
     * the server Certificate Manager
2✔
102
     */
2✔
103
    serverCertificateManager?: OPCUACertificateManager;
2✔
104
}
2✔
105

2✔
106
const emptyCallback = () => {
2✔
107
    /* empty */
1,561✔
108
};
1,561✔
109

2✔
110
export interface OPCUABaseServerEvents {
2✔
111
    request: [request: Request, channel: ServerSecureChannelLayer];
2✔
112
    response: [response: Response, channel: ServerSecureChannelLayer];
2✔
113
    newChannel: [channel: ServerSecureChannelLayer, endpoint: OPCUAServerEndPoint];
2✔
114
    channelSecured: [channel: ServerSecureChannelLayer, endpoint: OPCUAServerEndPoint];
2✔
115
    closeChannel: [channel: ServerSecureChannelLayer, endpoint: OPCUAServerEndPoint];
2✔
116
    connectionRefused: [socketData: ISocketData, endpoint: OPCUAServerEndPoint];
2✔
117
    openSecureChannelFailure: [socketData: ISocketData, channelData: IChannelData, endpoint: OPCUAServerEndPoint];
2✔
118
}
2✔
119

2✔
120
// biome-ignore lint/suspicious/noExplicitAny: must propagate EventEmitter generic
2✔
121
export class OPCUABaseServer<T extends OPCUABaseServerEvents = any> extends OPCUASecureObject<T> {
2✔
122
    public static makeServiceFault = makeServiceFault;
35✔
123

35✔
124
    /**
35✔
125
     * The type of server
35✔
126
     */
35✔
127
    get serverType(): ApplicationType {
35✔
128
        return this.serverInfo.applicationType;
455✔
129
    }
455✔
130

35✔
131
    public serverInfo: ApplicationDescription;
35✔
132
    public endpoints: OPCUAServerEndPoint[];
35✔
133
    public readonly serverCertificateManager: OPCUACertificateManager;
35✔
134
    public capabilitiesForMDNS: string[];
35✔
135
    protected _preInitTask: (() => Promise<void>)[];
35✔
136

35✔
137
    protected options: OPCUABaseServerOptions;
35✔
138

35✔
139
    constructor(options?: OPCUABaseServerOptions) {
35✔
140
        options = options || ({} as OPCUABaseServerOptions);
318✔
141

318✔
142
        if (!options.serverCertificateManager) {
318✔
143
            options.serverCertificateManager = getDefaultCertificateManager("PKI");
93✔
144
        }
93✔
145
        options.privateKeyFile = options.privateKeyFile || options.serverCertificateManager.privateKey;
318✔
146
        options.certificateFile =
318✔
147
            options.certificateFile || path.join(options.serverCertificateManager.rootDir, "own/certs/certificate.pem");
318✔
148

318✔
149
        super(options);
318✔
150

318✔
151
        this.serverCertificateManager = options.serverCertificateManager;
318✔
152
        this.capabilitiesForMDNS = [];
318✔
153
        this.endpoints = [];
318✔
154
        this.options = options;
318✔
155
        this._preInitTask = [];
318✔
156

318✔
157
        const serverInfo: ApplicationDescriptionOptions = {
318✔
158
            ...default_server_info,
318✔
159
            ...options.serverInfo
318✔
160
        };
318✔
161
        serverInfo.applicationName = coerceLocalizedText(serverInfo.applicationName);
318✔
162
        this.serverInfo = new ApplicationDescription(serverInfo);
318✔
163

318✔
164
        if (this.serverInfo.applicationName.toString().match(/urn:/)) {
318!
165
            errorLog("[NODE-OPCUA-E06] application name cannot be a urn", this.serverInfo.applicationName.toString());
×
166
        }
×
167

318✔
168
        this.serverInfo.applicationName.locale = this.serverInfo.applicationName.locale || "en";
318✔
169

318✔
170
        if (!this.serverInfo.applicationName.locale) {
318!
171
            warningLog(
×
172
                "[NODE-OPCUA-W24] the server applicationName must have a valid locale : ",
×
173
                this.serverInfo.applicationName.toString()
×
174
            );
×
175
        }
×
176

318✔
177
        const __applicationUri = serverInfo.applicationUri || "";
318!
178

318✔
179
        Object.defineProperty(this.serverInfo, "applicationUri", {
318✔
180
            get: () => resolveFullyQualifiedDomainName(__applicationUri),
318✔
181
            configurable: true
318✔
182
        });
318✔
183

318✔
184
        this._preInitTask.push(async () => {
318✔
185
            await extractFullyQualifiedDomainName();
317✔
186
        });
318✔
187

318✔
188
        this._preInitTask.push(async () => {
318✔
189
            await this.initializeCM();
317✔
190
        });
318✔
191
    }
318✔
192

35✔
193
    /**
35✔
194
     * Return additional DNS hostnames to include in the self-signed
35✔
195
     * certificate's SubjectAlternativeName (SAN).
35✔
196
     *
35✔
197
     * The base implementation returns an empty array. Subclasses
35✔
198
     * (e.g. `OPCUAServer`) override this to include hostnames from
35✔
199
     * `alternateHostname` and `advertisedEndpoints`.
35✔
200
     *
35✔
201
     * @internal
35✔
202
     */
35✔
203
    protected getConfiguredHostnames(): string[] {
35✔
204
        return [];
27✔
205
    }
27✔
206

35✔
207
    /**
35✔
208
     * Return additional IP addresses to include in the self-signed
35✔
209
     * certificate's SubjectAlternativeName (SAN) iPAddress entries.
35✔
210
     *
35✔
211
     * The base implementation returns an empty array. Subclasses
35✔
212
     * (e.g. `OPCUAServer`) override this to include IP literals
35✔
213
     * found in `alternateHostname` and `advertisedEndpoints`.
35✔
214
     *
35✔
215
     * These IPs are considered **explicitly configured** by the
35✔
216
     * user and are therefore checked by `checkCertificateSAN()`.
35✔
217
     * In contrast, auto-detected IPs from `getIpAddresses()` are
35✔
218
     * included in the certificate at creation time but are NOT
35✔
219
     * checked later — see `checkCertificateSAN()` for rationale.
35✔
220
     *
35✔
221
     * @internal
35✔
222
     */
35✔
223
    protected getConfiguredIPs(): string[] {
35✔
224
        return [];
27✔
225
    }
27✔
226

35✔
227
    public async createDefaultCertificate(): Promise<void> {
35✔
228
        if (fs.existsSync(this.certificateFile)) {
336✔
229
            return;
266✔
230
        }
266✔
231

84✔
232
        if (!checkFileExistsAndIsNotEmpty(this.certificateFile)) {
84✔
233
            await withLock({ fileToLock: `${this.certificateFile}.mutex` }, async () => {
70✔
234
                if (checkFileExistsAndIsNotEmpty(this.certificateFile)) {
70!
235
                    return;
×
236
                }
×
237
                const applicationUri = this.serverInfo.applicationUri || "<missing application uri>";
70!
238
                const fqdn = getFullyQualifiedDomainName();
70✔
239
                const hostname = getHostname();
70✔
240
                const dns = [...new Set([fqdn, hostname, ...this.getConfiguredHostnames()])].sort();
70✔
241

70✔
242
                // Include both auto-detected IPs and explicitly configured IPs.
70✔
243
                // Auto-detected IPs (getIpAddresses) are ephemeral — they depend on
70✔
244
                // the current network state (WiFi, tethering, VPN, roaming) and may
70✔
245
                // change between reboots. They are included here so that the initial
70✔
246
                // certificate covers the current network configuration, but they are
70✔
247
                // NOT checked by checkCertificateSAN() to avoid noisy warnings when
70✔
248
                // the network changes. Only explicitly configured IPs (from
70✔
249
                // alternateHostname / advertisedEndpoints) are checked at startup.
70✔
250
                const ip = [...new Set([...getIpAddresses(), ...this.getConfiguredIPs()])].sort();
70✔
251

70✔
252
                await this.serverCertificateManager.createSelfSignedCertificate({
70✔
253
                    applicationUri,
70✔
254
                    dns,
70✔
255
                    ip,
70✔
256
                    outputFile: this.certificateFile,
70✔
257

70✔
258
                    subject: makeSubject(this.serverInfo.applicationName.text || "<missing application name>", hostname),
70!
259

70✔
260
                    startDate: new Date(),
70✔
261
                    validity: 365 * 10 // 10 years
70✔
262
                });
70✔
263
            });
70✔
264
        }
70✔
265
    }
336✔
266

35✔
267
    public async initializeCM(): Promise<void> {
35✔
268
        await this.serverCertificateManager.initialize();
327✔
269
        await this.createDefaultCertificate();
327✔
270
        debugLog("privateKey      = ", this.privateKeyFile, this.serverCertificateManager.privateKey);
327✔
271
        debugLog("certificateFile = ", this.certificateFile);
327✔
272
        this._checkCertificateSanMismatch();
327✔
273
        await performCertificateSanityCheck(this, "server", this.serverCertificateManager, this.serverInfo.applicationUri || "");
327!
274
    }
327✔
275

35✔
276
    /**
35✔
277
     * Compare the current certificate's SAN entries against all
35✔
278
     * explicitly configured hostnames and IPs, and return any
35✔
279
     * that are missing.
35✔
280
     *
35✔
281
     * Returns an empty array when the certificate covers every
35✔
282
     * configured hostname and IP.
35✔
283
     *
35✔
284
     * **Important — ephemeral IP mitigation:**
35✔
285
     * Auto-detected IPs (from `getIpAddresses()`) are deliberately
35✔
286
     * NOT included in this check. Network interfaces are transient
35✔
287
     * — WiFi IPs change on reconnect, tethering IPs appear/disappear,
35✔
288
     * VPN adapters come and go. Including them would cause the
35✔
289
     * `[NODE-OPCUA-W26]` warning to fire on every server restart
35✔
290
     * whenever the network state differs from when the certificate
35✔
291
     * was originally created.
35✔
292
     *
35✔
293
     * Only **explicitly configured** values are checked:
35✔
294
     * - Hostnames: FQDN, os.hostname(), `alternateHostname` (non-IP),
35✔
295
     *   hostnames from `advertisedEndpoints` URLs
35✔
296
     * - IPs: IP literals from `alternateHostname`, IP literals
35✔
297
     *   from `advertisedEndpoints` URLs
35✔
298
     *
35✔
299
     * The certificate itself still includes auto-detected IPs at
35✔
300
     * creation time — this is fine because it captures the network
35✔
301
     * state at that moment. But the *mismatch warning* only fires
35✔
302
     * for things the user explicitly asked for.
35✔
303
     */
35✔
304
    public checkCertificateSAN(): string[] {
35✔
305
        const certDer = this.getCertificate();
329✔
306
        const info = exploreCertificate(certDer);
329✔
307
        const sanDns: string[] = info.tbsCertificate.extensions?.subjectAltName?.dNSName || [];
329✔
308
        const sanIpsHex: string[] = info.tbsCertificate.extensions?.subjectAltName?.iPAddress || [];
329✔
309

329✔
310
        const fqdn = getFullyQualifiedDomainName();
329✔
311
        const hostname = getHostname();
329✔
312
        const expectedDns = [...new Set([fqdn, hostname, ...this.getConfiguredHostnames()])].sort();
329✔
313

329✔
314
        // Only check explicitly configured IPs — NOT auto-detected ones.
329✔
315
        // See JSDoc above for the rationale (ephemeral network interfaces).
329✔
316
        const expectedIps = [...new Set(this.getConfiguredIPs())].sort();
329✔
317

329✔
318
        const missingDns = expectedDns.filter((name) => !sanDns.includes(name));
329✔
319
        // exploreCertificate returns iPAddress entries as hex strings
329✔
320
        // Only IPv4 addresses can be converted with ipv4ToHex here; IPv6 (and invalid) IPs are skipped.
329✔
321
        const missingIps = expectedIps.filter((ip) => {
329✔
322
            const family = isIP(ip);
6✔
323
            if (family === 4) {
6✔
324
                return !sanIpsHex.includes(ipv4ToHex(ip));
6✔
325
            }
6✔
326
            // IPv6 or invalid literals are currently not matched against SAN iPAddress entries here.
5✔
327
            return false;
5!
328
        });
329✔
329

329✔
330
        return [...missingDns, ...missingIps];
329✔
331
    }
329✔
332

35✔
333
    /**
35✔
334
     * Delete the existing self-signed certificate and create a new
35✔
335
     * one that includes all currently configured hostnames.
35✔
336
     *
35✔
337
     * @throws if the current certificate was NOT self-signed
35✔
338
     *         (i.e. issued by a CA or GDS)
35✔
339
     */
35✔
340
    public async regenerateSelfSignedCertificate(): Promise<void> {
35✔
341
        // guard: only allow regeneration of self-signed certs
2✔
342
        const certDer = this.getCertificate();
2✔
343
        const info = exploreCertificate(certDer);
2✔
344
        const issuer = info.tbsCertificate.issuer;
2✔
345
        const subject = info.tbsCertificate.subject;
2✔
346
        const isSelfSigned = issuer.commonName === subject.commonName && issuer.organizationName === subject.organizationName;
2✔
347
        if (!isSelfSigned) {
2!
348
            throw new Error("Cannot regenerate certificate: current certificate is not self-signed (issued by a CA or GDS)");
×
349
        }
×
350

2✔
351
        // delete old cert
2✔
352
        if (fs.existsSync(this.certificateFile)) {
2✔
353
            fs.unlinkSync(this.certificateFile);
2✔
354
        }
2✔
355
        // recreate with current hostnames
2✔
356
        await this.createDefaultCertificate();
2✔
357
        // invalidate cached cert so next getCertificate() reloads from disk
2✔
358
        invalidateCachedSecrets(this);
2✔
359
    }
2✔
360

35✔
361
    private _checkCertificateSanMismatch(): void {
35✔
362
        try {
327✔
363
            const missing = this.checkCertificateSAN();
327✔
364
            if (missing.length > 0) {
327✔
365
                warningLog(
133✔
366
                    `[NODE-OPCUA-W26] Certificate SAN is missing the following configured hostnames/IPs: ${missing.join(", ")}. ` +
133✔
367
                    "Clients with strict certificate validation may reject connections for these entries. " +
133✔
368
                    "Use server.regenerateSelfSignedCertificate() to fix this."
133✔
369
                );
133✔
370
            }
133✔
371
        } catch (_err) {
327!
372
            // ignore errors during SAN check (e.g. cert not yet loaded)
×
373
        }
×
374
    }
327✔
375

35✔
376
    /**
35✔
377
     * start all registered endPoint, in parallel, and call done when all endPoints are listening.
35✔
378
     */
35✔
379
    public start(): Promise<void>;
35✔
380
    public start(done: () => void): void;
35✔
381
    public start(...args: [((err?: Error) => void)?]): Promise<void> | void {
35✔
382
        const callback = args[0];
293✔
383
        if (!callback || args.length === 0) {
293✔
384
            return this.startAsync();
274✔
385
        } else {
293✔
386
            this.startAsync()
19✔
387
                .then(() => {
19✔
388
                    callback();
19✔
389
                })
19✔
390
                .catch((err) => callback(err));
19✔
391
        }
19✔
392
    }
293✔
393

35✔
394
    protected async performPreInitialization(): Promise<void> {
35✔
395
        const tasks = this._preInitTask;
592✔
396
        this._preInitTask = [];
592✔
397
        for (const task of tasks) {
592✔
398
            await task();
950✔
399
        }
950✔
400
    }
592✔
401

35✔
402
    protected async startAsync(): Promise<void> {
35✔
403
        await this.performPreInitialization();
292✔
404

292✔
405
        assert(Array.isArray(this.endpoints));
292✔
406
        assert(this.endpoints.length > 0, "We need at least one end point");
292✔
407

292✔
408
        installPeriodicClockAdjustment();
292✔
409
        // eslint-disable-next-line @typescript-eslint/no-this-alias
292✔
410
        const server: OPCUABaseServer<OPCUABaseServerEvents> = this;
292✔
411
        const _on_new_channel = function (this: OPCUAServerEndPoint, channel: ServerSecureChannelLayer) {
292✔
412
            server.emit("newChannel", channel, this);
1,496✔
413
        };
281✔
414

292✔
415
        const _on_channel_secured = function (this: OPCUAServerEndPoint, channel: ServerSecureChannelLayer) {
292✔
416
            // Install a response interceptor once per channel so the
1,448✔
417
            // server can emit "response" events for diagnostics.
1,448✔
418
            // Done here (after OpenSecureChannel) rather than at
1,448✔
419
            // newChannel time, so the interceptor can rely on
1,448✔
420
            // securityPolicy/securityMode being populated.
1,448✔
421
            channel.setResponseInterceptor((_msg, response1) => {
1,448✔
422
                server.emit("response", response1, channel);
46,681✔
423
            });
1,448✔
424
            server.emit("channelSecured", channel, this);
1,448✔
425
        };
281✔
426

292✔
427
        const _on_close_channel = function (this: OPCUAServerEndPoint, channel: ServerSecureChannelLayer) {
292✔
428
            server.emit("closeChannel", channel, this);
1,478✔
429
        };
281✔
430

292✔
431
        const _on_connectionRefused = function (this: OPCUAServerEndPoint, socketData: ISocketData) {
292✔
432
            server.emit("connectionRefused", socketData, this);
19✔
433
        };
279✔
434

292✔
435
        const _on_openSecureChannelFailure = function (
292✔
436
            this: OPCUAServerEndPoint,
8✔
437
            socketData: ISocketData,
8✔
438
            channelData: IChannelData
8✔
439
        ) {
8✔
440
            server.emit("openSecureChannelFailure", socketData, channelData, this);
8✔
441
        };
285✔
442

292✔
443
        const promises: Promise<void>[] = [];
292✔
444

292✔
445
        for (const endpoint of this.endpoints) {
292✔
446
            assert(!endpoint._on_close_channel);
293✔
447

293✔
448
            endpoint._on_new_channel = _on_new_channel;
293✔
449
            endpoint.on("newChannel", endpoint._on_new_channel);
293✔
450

293✔
451
            endpoint._on_channel_secured = _on_channel_secured;
293✔
452
            endpoint.on("channelSecured", endpoint._on_channel_secured);
293✔
453

293✔
454
            endpoint._on_close_channel = _on_close_channel;
293✔
455
            endpoint.on("closeChannel", endpoint._on_close_channel);
293✔
456

293✔
457
            endpoint._on_connectionRefused = _on_connectionRefused;
293✔
458
            endpoint.on("connectionRefused", endpoint._on_connectionRefused);
293✔
459

293✔
460
            endpoint._on_openSecureChannelFailure = _on_openSecureChannelFailure;
293✔
461
            endpoint.on("openSecureChannelFailure", endpoint._on_openSecureChannelFailure);
293✔
462

293✔
463
            promises.push(new Promise<void>((resolve, reject) => endpoint.start((err) => (err ? reject(err) : resolve()))));
293✔
464
        }
293✔
465
        await Promise.all(promises);
292✔
466
    }
291✔
467

35✔
468
    /**
35✔
469
     * shutdown all server endPoints
35✔
470
     */
35✔
471
    public shutdown(done: (err?: Error | null) => void): void {
35✔
472
        assert(typeof done === "function");
316✔
473
        uninstallPeriodicClockAdjustment();
316✔
474
        this.serverCertificateManager.dispose().then(() => {
316✔
475
            debugLog("OPCUABaseServer#shutdown starting");
316✔
476
            const promises = this.endpoints.map((endpoint) => {
316✔
477
                return new Promise<void>((resolve, reject) => {
317✔
478
                    cleanupEndpoint(endpoint);
317✔
479
                    endpoint.shutdown((err) => (err ? reject(err) : resolve()));
317!
480
                });
317✔
481
            });
316✔
482
            Promise.all(promises)
316✔
483
                .then(() => {
316✔
484
                    debugLog("shutdown completed");
316✔
485
                    done();
316✔
486
                })
316✔
487
                .catch((err) => done(err));
316✔
488
        });
316✔
489
    }
316✔
490

35✔
491
    public async shutdownChannels(): Promise<void>;
35✔
492
    public shutdownChannels(callback: (err?: Error | null) => void): void;
35✔
493
    public shutdownChannels(callback?: (err?: Error | null) => void): Promise<void> | void {
35✔
494
        assert(typeof callback === "function");
4✔
495
        // c8 ignore next
4✔
496
        if (!callback) throw new Error("thenify is not available");
4✔
497
        debugLog("OPCUABaseServer#shutdownChannels");
4✔
498
        const promises = this.endpoints.map((endpoint) => {
4✔
499
            return new Promise<void>((resolve, reject) => {
4✔
500
                debugLog(" shutting down endpoint ", endpoint.endpointDescriptions()[0].endpointUrl);
4✔
501
                endpoint.abruptlyInterruptChannels();
4✔
502
                endpoint.shutdown((err) => (err ? reject(err) : resolve()));
4✔
503
            });
4✔
504
        });
4✔
505
        Promise.all(promises)
4✔
506
            .then(() => callback())
4✔
507
            .catch((err) => callback?.(err));
4✔
508
    }
4✔
509

35✔
510
    /**
35✔
511
     * @private
35✔
512
     */
35✔
513
    public on_request(message: Message, channel: ServerSecureChannelLayer): void {
35✔
514
        assert(message.request);
46,463✔
515
        assert(message.requestId !== 0);
46,463✔
516
        const request = message.request;
46,463✔
517

46,463✔
518
        // prepare request
46,463✔
519
        this.prepare(message, channel);
46,463✔
520

46,463✔
521
        if (doDebug) {
46,463!
522
            debugLog(
×
523
                chalk.green.bold("--------------------------------------------------------"),
×
524
                channel.channelId,
×
525
                request.schema.name
×
526
            );
×
527
        }
×
528

46,463✔
529
        let errMessage: string;
46,463✔
530
        let response: Response;
46,463✔
531

46,463✔
532
        (this as OPCUABaseServer<OPCUABaseServerEvents>).emit("request", request, channel);
46,463✔
533

46,463✔
534
        try {
46,463✔
535
            // handler must be named _on_ActionRequest()
46,463✔
536
            const handler = (this as unknown as Record<string, unknown>)[`_on_${request.schema.name}`];
46,463✔
537
            if (typeof handler === "function") {
46,463✔
538
                handler.call(this, message, channel);
46,463✔
539
            } else {
46,463!
540
                errMessage = `[NODE-OPCUA-W07] Unsupported Service : ${request.schema.name}`;
×
541
                warningLog(errMessage);
×
542
                debugLog(chalk.red.bold(errMessage));
×
543
                response = makeServiceFault(StatusCodes.BadServiceUnsupported, [errMessage]);
×
544
                channel.send_response("MSG", response, message, emptyCallback);
×
545
            }
×
546
        } catch (err) {
46,463!
547
            /* c8 ignore next */
2✔
548
            const errMessage1 = `[NODE-OPCUA-W08] EXCEPTION CAUGHT WHILE PROCESSING REQUEST !! ${request.schema.name}`;
2✔
549
            warningLog(chalk.red.bold(errMessage1));
×
550
            warningLog(request.toString());
×
551
            displayTraceFromThisProjectOnly(err as Error);
×
552

×
553
            let additional_messages = [];
×
554
            additional_messages.push(`EXCEPTION CAUGHT WHILE PROCESSING REQUEST !!! ${request.schema.name}`);
×
555
            if (err instanceof Error) {
×
556
                additional_messages.push(err.message);
×
557
                if (err.stack) {
×
558
                    additional_messages = additional_messages.concat(err.stack.split("\n"));
×
559
                }
×
560
            }
×
561
            response = makeServiceFault(StatusCodes.BadInternalError, additional_messages);
×
562

×
563
            channel.send_response("MSG", response, message, emptyCallback);
×
564
        }
×
565
    }
46,463✔
566

35✔
567
    /**
35✔
568
     * Find endpoint descriptions matching a given endpoint URL.
35✔
569
     *
35✔
570
     * When `endpointUrl` is provided, only endpoints whose URL matches
35✔
571
     * (case-insensitive) are returned. When `null` or omitted, all
35✔
572
     * endpoints from every `OPCUAServerEndPoint` are returned.
35✔
573
     *
35✔
574
     * This is the shared resolution path used by both `GetEndpoints`
35✔
575
     * and `CreateSession` (`validate_security_endpoint`).
35✔
576
     *
35✔
577
     * @internal (was _get_endpoints)
35✔
578
     */
35✔
579
    public findMatchingEndpoints(endpointUrl?: string | null): EndpointDescription[] {
35✔
580
        let endpoints: EndpointDescription[] = [];
4,323✔
581
        for (const endPoint of this.endpoints) {
4,323✔
582
            const ep = endPoint.endpointDescriptions();
4,327✔
583
            const epFiltered = endpointUrl ? ep.filter((e) => matchUri(e.endpointUrl, endpointUrl)) : ep;
4,327✔
584
            endpoints = endpoints.concat(epFiltered);
4,327✔
585
        }
4,327✔
586
        return endpoints;
4,323✔
587
    }
4,323✔
588
    /**
35✔
589
     * get one of the possible endpointUrl
35✔
590
     */
35✔
591
    public getEndpointUrl(): string {
35✔
592
        return this.findMatchingEndpoints()[0].endpointUrl || "";
670✔
593
    }
670✔
594

35✔
595
    public getDiscoveryUrls(): string[] {
35✔
596
        const discoveryUrls = this.endpoints.map((e: OPCUAServerEndPoint) => {
239✔
597
            return e.endpointDescriptions()[0].endpointUrl || "";
239✔
598
        });
239✔
599
        return discoveryUrls;
239✔
600
    }
239✔
601

35✔
602
    public getServers(_channel: ServerSecureChannelLayer): ApplicationDescription[] {
35✔
603
        this.serverInfo.discoveryUrls = this.getDiscoveryUrls();
4✔
604
        const servers = [this.serverInfo];
4✔
605
        return servers;
4✔
606
    }
4✔
607

35✔
608
    /**
35✔
609
     * set all the end point into a state where they do not accept further connections
35✔
610
     *
35✔
611
     * note:
35✔
612
     *     this method is useful for testing purpose
35✔
613
     *
35✔
614
     */
35✔
615
    public async suspendEndPoints(): Promise<void>;
35✔
616
    public suspendEndPoints(callback: (err?: Error | null) => void): void;
35✔
617
    public suspendEndPoints(callback?: (err?: Error | null) => void): void | Promise<void> {
35✔
618
        /* c8 ignore next */
2✔
619
        if (!callback) {
2✔
620
            throw new Error("Internal Error");
×
621
        }
×
622
        const promises = this.endpoints.map((ep) => {
4✔
623
            return new Promise<void>((resolve, reject) => {
4✔
624
                /* c8 ignore next */
2✔
625
                if (doDebug) {
2✔
626
                    debugLog("Suspending ", ep.endpointDescriptions()[0].endpointUrl);
×
627
                }
×
628
                ep.suspendConnection((err?: Error | null) => {
4✔
629
                    /* c8 ignore next */
2✔
630
                    if (doDebug) {
2✔
631
                        debugLog("Suspended ", ep.endpointDescriptions()[0].endpointUrl);
×
632
                    }
×
633
                    err ? reject(err) : resolve();
4✔
634
                });
4✔
635
            });
4✔
636
        });
4✔
637
        Promise.all(promises)
4✔
638
            .then(() => callback())
4✔
639
            .catch((err) => callback(err));
4✔
640
    }
4✔
641

35✔
642
    /**
35✔
643
     * set all the end point into a state where they do accept connections
35✔
644
     * note:
35✔
645
     *    this method is useful for testing purpose
35✔
646
     */
35✔
647
    public async resumeEndPoints(): Promise<void>;
35✔
648
    public resumeEndPoints(callback: (err?: Error | null) => void): void;
35✔
649
    public resumeEndPoints(callback?: (err?: Error | null) => void): void | Promise<void> {
35✔
650
        // c8 ignore next
4✔
651
        if (!callback) throw new Error("thenify is not available");
4✔
652
        const promises = this.endpoints.map((ep) => {
4✔
653
            return new Promise<void>((resolve, reject) => {
4✔
654
                ep.restoreConnection((err) => (err ? reject(err) : resolve()));
4✔
655
            });
4✔
656
        });
4✔
657
        Promise.all(promises)
4✔
658
            .then(() => callback())
4✔
659
            .catch((err) => callback(err));
4✔
660
    }
4✔
661

35✔
662
    protected prepare(_message: Message, _channel: ServerSecureChannelLayer): void {
35✔
663
        /* empty */
707✔
664
    }
707✔
665

35✔
666
    /**
35✔
667
     * @private
35✔
668
     */
35✔
669
    protected _on_GetEndpointsRequest(message: Message, channel: ServerSecureChannelLayer): void {
35✔
670
        const request = message.request as GetEndpointsRequest;
1,546✔
671

1,546✔
672
        assert(request.schema.name === "GetEndpointsRequest");
1,546✔
673

1,546✔
674
        const response = new GetEndpointsResponse({});
1,546✔
675

1,546✔
676
        /**
1,546✔
677
         * endpointUrl        String        The network address that the Client used to access the DiscoveryEndpoint.
1,546✔
678
         *                      The Server uses this information for diagnostics and to determine what URLs to return in the response.
1,546✔
679
         *                      The Server should return a suitable default URL if it does not recognize the HostName in the URL
1,546✔
680
         * localeIds   []LocaleId        List of locales to use.
1,546✔
681
         *                          Specifies the locale to use when returning human readable strings.
1,546✔
682
         * profileUris []        String        List of Transport Profile that the returned Endpoints shall support.
1,546✔
683
         *                          OPC 10000-7 defines URIs for the Transport Profiles.
1,546✔
684
         *                          All Endpoints are returned if the list is empty.
1,546✔
685
         *                          If the URI is a URL, this URL may have a query string appended.
1,546✔
686
         *                          The Transport Profiles that support query strings are defined in OPC 10000-7.
1,546✔
687
         */
1,546✔
688
        response.endpoints = this.findMatchingEndpoints(null);
1,546✔
689
        const _e = response.endpoints.map((e) => e.endpointUrl);
1,546✔
690
        if (request.endpointUrl) {
1,546✔
691
            const filtered = response.endpoints.filter((endpoint: EndpointDescription) =>
1,546✔
692
                matchUri(endpoint.endpointUrl, request.endpointUrl)
11,952✔
693
            );
1,546✔
694
            if (filtered.length > 0) {
1,546✔
695
                response.endpoints = filtered;
1,493✔
696
            }
1,493✔
697
        }
1,546✔
698
        response.endpoints = response.endpoints.filter(
1,546✔
699
            (endpoint: EndpointDescription) => !(endpoint as unknown as { restricted: boolean }).restricted
1,546✔
700
        );
1,546✔
701

1,546✔
702
        // apply filters
1,546✔
703
        if (request.profileUris && request.profileUris.length > 0) {
1,546!
704
            const profileUris = request.profileUris;
×
705
            response.endpoints = response.endpoints.filter(
×
706
                (endpoint: EndpointDescription) => profileUris.indexOf(endpoint.transportProfileUri) >= 0
×
707
            );
×
708
        }
×
709

1,546✔
710
        // adjust locale on ApplicationName to match requested local or provide
1,546✔
711
        // a string with neutral locale (locale === null)
1,546✔
712
        // TODO: find a better way to handle this
1,546✔
713
        response.endpoints.forEach((endpoint: EndpointDescription) => {
1,546✔
714
            endpoint.server.applicationName.locale = "en-US";
11,919✔
715
        });
1,546✔
716

1,546✔
717
        channel.send_response("MSG", response, message, emptyCallback);
1,546✔
718
    }
1,546✔
719

35✔
720
    /**
35✔
721
     * @private
35✔
722
     */
35✔
723
    protected _on_FindServersRequest(message: Message, channel: ServerSecureChannelLayer): void {
35✔
724
        // Release 1.02  13  OPC Unified Architecture, Part 4 :
15✔
725
        //   This  Service  can be used without security and it is therefore vulnerable to Denial Of Service (DOS)
15✔
726
        //   attacks. A  Server  should minimize the amount of processing required to send the response for this
15✔
727
        //   Service.  This can be achieved by preparing the result in advance.   The  Server  should  also add a
15✔
728
        //   short delay before starting processing of a request during high traffic conditions.
15✔
729

15✔
730
        const shortDelay = 100; // milliseconds
15✔
731
        setTimeout(() => {
15✔
732
            const request = message.request;
15✔
733
            assert(request.schema.name === "FindServersRequest");
15✔
734
            if (!(request instanceof FindServersRequest)) {
15✔
735
                throw new Error("Invalid request type");
×
736
            }
×
737

15✔
738
            let servers = this.getServers(channel);
15✔
739
            // apply filters
15✔
740
            // TODO /
15✔
741
            if (request.serverUris && request.serverUris.length > 0) {
15✔
742
                const serverUris = request.serverUris;
2✔
743
                // A serverUri matches the applicationUri from the ApplicationDescription define
2✔
744
                servers = servers.filter((inner_Server: ApplicationDescription) => {
2✔
745
                    return serverUris.indexOf(inner_Server.applicationUri) >= 0;
2✔
746
                });
2✔
747
            }
2✔
748

15✔
749
            function adapt(applicationDescription: ApplicationDescription): ApplicationDescription {
15✔
750
                return new ApplicationDescription({
24✔
751
                    applicationName: applicationDescription.applicationName,
24✔
752
                    applicationType: applicationDescription.applicationType,
24✔
753
                    applicationUri: applicationDescription.applicationUri,
24✔
754
                    discoveryProfileUri: applicationDescription.discoveryProfileUri,
24✔
755
                    discoveryUrls: applicationDescription.discoveryUrls,
24✔
756
                    gatewayServerUri: applicationDescription.gatewayServerUri,
24✔
757
                    productUri: applicationDescription.productUri
24✔
758
                });
24✔
759
            }
24✔
760

15✔
761
            const response = new FindServersResponse({
15✔
762
                servers: servers.map(adapt)
15✔
763
            });
15✔
764

15✔
765
            channel.send_response("MSG", response, message, emptyCallback);
15✔
766
        }, shortDelay);
15✔
767
    }
15✔
768

35✔
769
    /**
35✔
770
     * returns a array of currently active channels
35✔
771
     */
35✔
772
    protected getChannels(): ServerSecureChannelLayer[] {
35✔
773
        let channels: ServerSecureChannelLayer[] = [];
8✔
774

8✔
775
        for (const endpoint of this.endpoints) {
8✔
776
            const c = endpoint.getChannels();
7✔
777
            channels = channels.concat(c);
7✔
778
        }
7✔
779
        return channels;
8✔
780
    }
8✔
781
}
35✔
782

2✔
783
/**
2✔
784
 * construct a service Fault response
2✔
785
 */
2✔
786
function makeServiceFault(statusCode: StatusCode, messages: string[]): ServiceFault {
×
787
    const response = new ServiceFault();
×
788
    response.responseHeader.serviceResult = statusCode;
×
789
    // xx response.serviceDiagnostics.push( new DiagnosticInfo({ additionalInfo: messages.join("\n")}));
×
790

×
791
    assert(Array.isArray(messages));
×
792
    assert(typeof messages[0] === "string");
×
793

×
794
    response.responseHeader.stringTable = messages;
×
795
    // tslint:disable:no-console
×
796
    warningLog(chalk.cyan(" messages "), messages.join("\n"));
×
797
    return response;
×
798
}
×
799

2✔
800
// tslint:disable:no-var-requires
2✔
801
import { withCallback } from "thenify-ex";
2✔
802

2✔
803
const opts = { multiArgs: false };
2✔
804
OPCUABaseServer.prototype.resumeEndPoints = withCallback(OPCUABaseServer.prototype.resumeEndPoints, opts);
2✔
805
OPCUABaseServer.prototype.suspendEndPoints = withCallback(OPCUABaseServer.prototype.suspendEndPoints, opts);
2✔
806
OPCUABaseServer.prototype.shutdownChannels = withCallback(OPCUABaseServer.prototype.shutdownChannels, opts);
2!
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc