node-opcua-server 2.51.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/.mocharc.yml +10 -0
- package/LICENSE +20 -0
- package/dist/base_server.d.ts +110 -0
- package/dist/base_server.js +476 -0
- package/dist/base_server.js.map +1 -0
- package/dist/factory.d.ts +10 -0
- package/dist/factory.js +24 -0
- package/dist/factory.js.map +1 -0
- package/dist/history_server_capabilities.d.ts +35 -0
- package/dist/history_server_capabilities.js +44 -0
- package/dist/history_server_capabilities.js.map +1 -0
- package/dist/i_channel_data.d.ts +13 -0
- package/dist/i_channel_data.js +3 -0
- package/dist/i_channel_data.js.map +1 -0
- package/dist/i_register_server_manager.d.ts +16 -0
- package/dist/i_register_server_manager.js +3 -0
- package/dist/i_register_server_manager.js.map +1 -0
- package/dist/i_server_side_publish_engine.d.ts +36 -0
- package/dist/i_server_side_publish_engine.js +50 -0
- package/dist/i_server_side_publish_engine.js.map +1 -0
- package/dist/i_socket_data.d.ts +11 -0
- package/dist/i_socket_data.js +3 -0
- package/dist/i_socket_data.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/monitored_item.d.ts +173 -0
- package/dist/monitored_item.js +1006 -0
- package/dist/monitored_item.js.map +1 -0
- package/dist/node_sampler.d.ts +3 -0
- package/dist/node_sampler.js +76 -0
- package/dist/node_sampler.js.map +1 -0
- package/dist/opcua_server.d.ts +668 -0
- package/dist/opcua_server.js +2407 -0
- package/dist/opcua_server.js.map +1 -0
- package/dist/queue.d.ts +11 -0
- package/dist/queue.js +71 -0
- package/dist/queue.js.map +1 -0
- package/dist/register_server_manager.d.ts +92 -0
- package/dist/register_server_manager.js +574 -0
- package/dist/register_server_manager.js.map +1 -0
- package/dist/register_server_manager_hidden.d.ts +17 -0
- package/dist/register_server_manager_hidden.js +28 -0
- package/dist/register_server_manager_hidden.js.map +1 -0
- package/dist/register_server_manager_mdns_only.d.ts +19 -0
- package/dist/register_server_manager_mdns_only.js +58 -0
- package/dist/register_server_manager_mdns_only.js.map +1 -0
- package/dist/server_capabilities.d.ts +61 -0
- package/dist/server_capabilities.js +109 -0
- package/dist/server_capabilities.js.map +1 -0
- package/dist/server_end_point.d.ts +180 -0
- package/dist/server_end_point.js +825 -0
- package/dist/server_end_point.js.map +1 -0
- package/dist/server_engine.d.ts +311 -0
- package/dist/server_engine.js +1659 -0
- package/dist/server_engine.js.map +1 -0
- package/dist/server_publish_engine.d.ts +109 -0
- package/dist/server_publish_engine.js +531 -0
- package/dist/server_publish_engine.js.map +1 -0
- package/dist/server_publish_engine_for_orphan_subscriptions.d.ts +16 -0
- package/dist/server_publish_engine_for_orphan_subscriptions.js +50 -0
- package/dist/server_publish_engine_for_orphan_subscriptions.js.map +1 -0
- package/dist/server_session.d.ts +176 -0
- package/dist/server_session.js +734 -0
- package/dist/server_session.js.map +1 -0
- package/dist/server_subscription.d.ts +393 -0
- package/dist/server_subscription.js +1313 -0
- package/dist/server_subscription.js.map +1 -0
- package/dist/sessions_compatible_for_transfer.d.ts +2 -0
- package/dist/sessions_compatible_for_transfer.js +36 -0
- package/dist/sessions_compatible_for_transfer.js.map +1 -0
- package/dist/validate_filter.d.ts +5 -0
- package/dist/validate_filter.js +64 -0
- package/dist/validate_filter.js.map +1 -0
- package/package.json +88 -0
- package/source/base_server.ts +617 -0
- package/source/factory.ts +25 -0
- package/source/history_server_capabilities.ts +75 -0
- package/source/i_channel_data.ts +17 -0
- package/source/i_register_server_manager.ts +24 -0
- package/source/i_server_side_publish_engine.ts +77 -0
- package/source/i_socket_data.ts +11 -0
- package/source/index.ts +14 -0
- package/source/monitored_item.ts +1303 -0
- package/source/node_sampler.ts +82 -0
- package/source/opcua_server.ts +3742 -0
- package/source/queue.ts +73 -0
- package/source/register_server_manager.ts +744 -0
- package/source/register_server_manager_hidden.ts +33 -0
- package/source/register_server_manager_mdns_only.ts +69 -0
- package/source/server_capabilities.ts +177 -0
- package/source/server_end_point.ts +1182 -0
- package/source/server_engine.ts +2167 -0
- package/source/server_publish_engine.ts +657 -0
- package/source/server_publish_engine_for_orphan_subscriptions.ts +52 -0
- package/source/server_session.ts +931 -0
- package/source/server_subscription.ts +1792 -0
- package/source/sessions_compatible_for_transfer.ts +33 -0
- package/source/validate_filter.ts +86 -0
- package/test_helpers/create_certificates.js +1 -0
|
@@ -0,0 +1,3742 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-server
|
|
3
|
+
*/
|
|
4
|
+
// tslint:disable:no-console
|
|
5
|
+
// tslint:disable:max-line-length
|
|
6
|
+
// tslint:disable:unified-signatures
|
|
7
|
+
|
|
8
|
+
import * as crypto from "crypto";
|
|
9
|
+
import { EventEmitter } from "events";
|
|
10
|
+
import { callbackify } from "util";
|
|
11
|
+
|
|
12
|
+
import * as async from "async";
|
|
13
|
+
import * as chalk from "chalk";
|
|
14
|
+
|
|
15
|
+
import { extractFullyQualifiedDomainName, getFullyQualifiedDomainName } from "node-opcua-hostname";
|
|
16
|
+
|
|
17
|
+
import { assert } from "node-opcua-assert";
|
|
18
|
+
import * as utils from "node-opcua-utils";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
AddressSpace,
|
|
22
|
+
callMethodHelper,
|
|
23
|
+
ContinuationPoint,
|
|
24
|
+
IUserManager,
|
|
25
|
+
PseudoVariantBoolean,
|
|
26
|
+
PseudoVariantByteString,
|
|
27
|
+
PseudoVariantDateTime,
|
|
28
|
+
PseudoVariantDuration,
|
|
29
|
+
PseudoVariantExtensionObject,
|
|
30
|
+
PseudoVariantExtensionObjectArray,
|
|
31
|
+
PseudoVariantLocalizedText,
|
|
32
|
+
PseudoVariantNodeId,
|
|
33
|
+
PseudoVariantString,
|
|
34
|
+
RaiseEventData,
|
|
35
|
+
SessionContext,
|
|
36
|
+
UAObject,
|
|
37
|
+
UAVariable,
|
|
38
|
+
ISessionContext,
|
|
39
|
+
UAView
|
|
40
|
+
} from "node-opcua-address-space";
|
|
41
|
+
import { getDefaultCertificateManager, OPCUACertificateManager } from "node-opcua-certificate-manager";
|
|
42
|
+
import { ServerState } from "node-opcua-common";
|
|
43
|
+
import { Certificate, exploreCertificate, makeSHA1Thumbprint, Nonce, toPem } from "node-opcua-crypto";
|
|
44
|
+
import { AttributeIds, LocalizedText, NodeClass } from "node-opcua-data-model";
|
|
45
|
+
import { DataValue } from "node-opcua-data-value";
|
|
46
|
+
import { dump, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
|
|
47
|
+
import { NodeId } from "node-opcua-nodeid";
|
|
48
|
+
import { ObjectRegistry } from "node-opcua-object-registry";
|
|
49
|
+
import {
|
|
50
|
+
AsymmetricAlgorithmSecurityHeader,
|
|
51
|
+
computeSignature,
|
|
52
|
+
fromURI,
|
|
53
|
+
getCryptoFactory,
|
|
54
|
+
Message,
|
|
55
|
+
MessageSecurityMode,
|
|
56
|
+
nonceAlreadyBeenUsed,
|
|
57
|
+
Request,
|
|
58
|
+
Response,
|
|
59
|
+
SecurityPolicy,
|
|
60
|
+
ServerSecureChannelLayer,
|
|
61
|
+
SignatureData,
|
|
62
|
+
verifySignature
|
|
63
|
+
} from "node-opcua-secure-channel";
|
|
64
|
+
import { BrowseNextRequest, BrowseNextResponse, BrowseRequest, BrowseResponse } from "node-opcua-service-browse";
|
|
65
|
+
import { CallRequest, CallResponse } from "node-opcua-service-call";
|
|
66
|
+
import { ApplicationType, UserTokenType } from "node-opcua-service-endpoints";
|
|
67
|
+
import { HistoryReadRequest, HistoryReadResponse, HistoryReadResult, HistoryUpdateResponse } from "node-opcua-service-history";
|
|
68
|
+
import {
|
|
69
|
+
AddNodesResponse,
|
|
70
|
+
AddReferencesResponse,
|
|
71
|
+
DeleteNodesResponse,
|
|
72
|
+
DeleteReferencesResponse
|
|
73
|
+
} from "node-opcua-service-node-management";
|
|
74
|
+
import { QueryFirstResponse, QueryNextResponse } from "node-opcua-service-query";
|
|
75
|
+
import { ReadRequest, ReadResponse, ReadValueId, TimestampsToReturn } from "node-opcua-service-read";
|
|
76
|
+
import {
|
|
77
|
+
RegisterNodesRequest,
|
|
78
|
+
RegisterNodesResponse,
|
|
79
|
+
UnregisterNodesRequest,
|
|
80
|
+
UnregisterNodesResponse
|
|
81
|
+
} from "node-opcua-service-register-node";
|
|
82
|
+
import {
|
|
83
|
+
ActivateSessionRequest,
|
|
84
|
+
ActivateSessionResponse,
|
|
85
|
+
AnonymousIdentityToken,
|
|
86
|
+
CloseSessionRequest,
|
|
87
|
+
CloseSessionResponse,
|
|
88
|
+
CreateSessionRequest,
|
|
89
|
+
CreateSessionResponse,
|
|
90
|
+
UserNameIdentityToken,
|
|
91
|
+
X509IdentityToken
|
|
92
|
+
} from "node-opcua-service-session";
|
|
93
|
+
import {
|
|
94
|
+
CreateMonitoredItemsRequest,
|
|
95
|
+
CreateMonitoredItemsResponse,
|
|
96
|
+
CreateSubscriptionRequest,
|
|
97
|
+
CreateSubscriptionResponse,
|
|
98
|
+
DeleteMonitoredItemsRequest,
|
|
99
|
+
DeleteMonitoredItemsResponse,
|
|
100
|
+
DeleteSubscriptionsRequest,
|
|
101
|
+
DeleteSubscriptionsResponse,
|
|
102
|
+
ModifyMonitoredItemsRequest,
|
|
103
|
+
ModifyMonitoredItemsResponse,
|
|
104
|
+
ModifySubscriptionRequest,
|
|
105
|
+
ModifySubscriptionResponse,
|
|
106
|
+
MonitoredItemModifyResult,
|
|
107
|
+
PublishRequest,
|
|
108
|
+
PublishResponse,
|
|
109
|
+
RepublishRequest,
|
|
110
|
+
RepublishResponse,
|
|
111
|
+
SetMonitoringModeRequest,
|
|
112
|
+
SetMonitoringModeResponse,
|
|
113
|
+
SetPublishingModeRequest,
|
|
114
|
+
SetPublishingModeResponse,
|
|
115
|
+
SetTriggeringRequestOptions,
|
|
116
|
+
SetTriggeringRequest,
|
|
117
|
+
SetTriggeringResponse,
|
|
118
|
+
TransferSubscriptionsRequest,
|
|
119
|
+
TransferSubscriptionsResponse
|
|
120
|
+
} from "node-opcua-service-subscription";
|
|
121
|
+
import {
|
|
122
|
+
TranslateBrowsePathsToNodeIdsRequest,
|
|
123
|
+
TranslateBrowsePathsToNodeIdsResponse
|
|
124
|
+
} from "node-opcua-service-translate-browse-path";
|
|
125
|
+
import { WriteRequest, WriteResponse } from "node-opcua-service-write";
|
|
126
|
+
import { ErrorCallback, StatusCode, StatusCodes } from "node-opcua-status-code";
|
|
127
|
+
import {
|
|
128
|
+
ApplicationDescriptionOptions,
|
|
129
|
+
BrowseResult,
|
|
130
|
+
BuildInfo,
|
|
131
|
+
CallMethodResultOptions,
|
|
132
|
+
CancelResponse,
|
|
133
|
+
EndpointDescription,
|
|
134
|
+
MonitoredItemModifyRequest,
|
|
135
|
+
MonitoringMode,
|
|
136
|
+
UserIdentityToken,
|
|
137
|
+
UserTokenPolicy,
|
|
138
|
+
BrowseDescription,
|
|
139
|
+
BuildInfoOptions,
|
|
140
|
+
MonitoredItemCreateResult,
|
|
141
|
+
IssuedIdentityToken
|
|
142
|
+
} from "node-opcua-types";
|
|
143
|
+
import { DataType } from "node-opcua-variant";
|
|
144
|
+
import { VariantArrayType } from "node-opcua-variant";
|
|
145
|
+
import { matchUri } from "node-opcua-utils";
|
|
146
|
+
|
|
147
|
+
import { OPCUABaseServer, OPCUABaseServerOptions } from "./base_server";
|
|
148
|
+
import { Factory } from "./factory";
|
|
149
|
+
import { IRegisterServerManager } from "./i_register_server_manager";
|
|
150
|
+
import { MonitoredItem } from "./monitored_item";
|
|
151
|
+
import { RegisterServerManager } from "./register_server_manager";
|
|
152
|
+
import { RegisterServerManagerHidden } from "./register_server_manager_hidden";
|
|
153
|
+
import { RegisterServerManagerMDNSONLY } from "./register_server_manager_mdns_only";
|
|
154
|
+
import { ServerCapabilitiesOptions } from "./server_capabilities";
|
|
155
|
+
import { OPCUAServerEndPoint } from "./server_end_point";
|
|
156
|
+
import { ClosingReason, ServerEngine } from "./server_engine";
|
|
157
|
+
import { ServerSession } from "./server_session";
|
|
158
|
+
import { CreateMonitoredItemHook, DeleteMonitoredItemHook, Subscription } from "./server_subscription";
|
|
159
|
+
import { ISocketData } from "./i_socket_data";
|
|
160
|
+
import { IChannelData } from "./i_channel_data";
|
|
161
|
+
|
|
162
|
+
function isSubscriptionIdInvalid(subscriptionId: number): boolean {
|
|
163
|
+
return subscriptionId < 0 || subscriptionId >= 0xffffffff;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export type ValidUserFunc = (this: ServerSession, username: string, password: string) => boolean;
|
|
167
|
+
export type ValidUserAsyncFunc = (
|
|
168
|
+
this: ServerSession,
|
|
169
|
+
username: string,
|
|
170
|
+
password: string,
|
|
171
|
+
callback: (err: Error | null, isAuthorized?: boolean) => void
|
|
172
|
+
) => void;
|
|
173
|
+
|
|
174
|
+
export interface UserManagerOptions extends IUserManager {
|
|
175
|
+
/** synchronous function to check the credentials - can be overruled by isValidUserAsync */
|
|
176
|
+
isValidUser?: ValidUserFunc;
|
|
177
|
+
/** asynchronous function to check if the credentials - overrules isValidUser */
|
|
178
|
+
isValidUserAsync?: ValidUserAsyncFunc;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// tslint:disable-next-line:no-var-requires
|
|
182
|
+
const package_info = require("../package.json");
|
|
183
|
+
const debugLog = make_debugLog(__filename);
|
|
184
|
+
const errorLog = make_errorLog(__filename);
|
|
185
|
+
const warningLog = make_warningLog(__filename);
|
|
186
|
+
|
|
187
|
+
const default_maxAllowedSessionNumber = 10;
|
|
188
|
+
const default_maxConnectionsPerEndpoint = 10;
|
|
189
|
+
|
|
190
|
+
function g_sendError(channel: ServerSecureChannelLayer, message: Message, ResponseClass: any, statusCode: StatusCode): void {
|
|
191
|
+
const response = new ResponseClass({
|
|
192
|
+
responseHeader: { serviceResult: statusCode }
|
|
193
|
+
});
|
|
194
|
+
return channel.send_response("MSG", response, message);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const default_build_info: BuildInfoOptions = {
|
|
198
|
+
manufacturerName: "NodeOPCUA : MIT Licence ( see http://node-opcua.github.io/)",
|
|
199
|
+
productName: "NodeOPCUA-Server",
|
|
200
|
+
productUri: null, // << should be same as default_server_info.productUri?
|
|
201
|
+
softwareVersion: package_info.version,
|
|
202
|
+
buildNumber: "0",
|
|
203
|
+
buildDate: new Date(2020, 1, 1)
|
|
204
|
+
// xx buildDate: fs.statSync(package_json_file).mtime
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const minSessionTimeout = 100; // 100 milliseconds
|
|
208
|
+
const defaultSessionTimeout = 1000 * 30; // 30 seconds
|
|
209
|
+
const maxSessionTimeout = 1000 * 60 * 50; // 50 minutes
|
|
210
|
+
|
|
211
|
+
function _adjust_session_timeout(sessionTimeout: number) {
|
|
212
|
+
let revisedSessionTimeout = sessionTimeout || defaultSessionTimeout;
|
|
213
|
+
revisedSessionTimeout = Math.min(revisedSessionTimeout, maxSessionTimeout);
|
|
214
|
+
revisedSessionTimeout = Math.max(revisedSessionTimeout, minSessionTimeout);
|
|
215
|
+
return revisedSessionTimeout;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function channel_has_session(channel: ServerSecureChannelLayer, session: ServerSession): boolean {
|
|
219
|
+
if (session.channel === channel) {
|
|
220
|
+
assert(channel.sessionTokens.hasOwnProperty(session.authenticationToken.toString()));
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function moveSessionToChannel(session: ServerSession, channel: ServerSecureChannelLayer) {
|
|
227
|
+
debugLog("moveSessionToChannel sessionId", session.nodeId, " channelId=", channel.channelId);
|
|
228
|
+
if (session.publishEngine) {
|
|
229
|
+
session.publishEngine.cancelPendingPublishRequestBeforeChannelChange();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
session._detach_channel();
|
|
233
|
+
session._attach_channel(channel);
|
|
234
|
+
|
|
235
|
+
assert(session.channel!.channelId === channel.channelId);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function _attempt_to_close_some_old_unactivated_session(server: OPCUAServer) {
|
|
239
|
+
const session = server.engine!.getOldestUnactivatedSession();
|
|
240
|
+
if (session) {
|
|
241
|
+
await server.engine!.closeSession(session.authenticationToken, false, "Forcing");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getRequiredEndpointInfo(endpoint: EndpointDescription) {
|
|
246
|
+
assert(endpoint instanceof EndpointDescription);
|
|
247
|
+
// It is recommended that Servers only include the server.applicationUri, endpointUrl, securityMode,
|
|
248
|
+
// securityPolicyUri, userIdentityTokens, transportProfileUri and securityLevel with all
|
|
249
|
+
// other parameters set to null. Only the recommended parameters shall be verified by
|
|
250
|
+
// the client.
|
|
251
|
+
|
|
252
|
+
const e = new EndpointDescription({
|
|
253
|
+
endpointUrl: endpoint.endpointUrl,
|
|
254
|
+
securityLevel: endpoint.securityLevel,
|
|
255
|
+
securityMode: endpoint.securityMode,
|
|
256
|
+
securityPolicyUri: endpoint.securityPolicyUri,
|
|
257
|
+
server: {
|
|
258
|
+
applicationUri: endpoint.server.applicationUri,
|
|
259
|
+
applicationType: endpoint.server.applicationType,
|
|
260
|
+
applicationName: endpoint.server.applicationName
|
|
261
|
+
// ... to be continued after verifying what fields are actually needed
|
|
262
|
+
},
|
|
263
|
+
transportProfileUri: endpoint.transportProfileUri,
|
|
264
|
+
userIdentityTokens: endpoint.userIdentityTokens
|
|
265
|
+
});
|
|
266
|
+
// reduce even further by explicitly setting unwanted members to null
|
|
267
|
+
e.server.productUri = null;
|
|
268
|
+
e.server.applicationName = null as any;
|
|
269
|
+
// xx e.server.applicationType = null as any;
|
|
270
|
+
e.server.gatewayServerUri = null;
|
|
271
|
+
e.server.discoveryProfileUri = null;
|
|
272
|
+
e.server.discoveryUrls = null;
|
|
273
|
+
e.serverCertificate = null as any;
|
|
274
|
+
return e;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// serverUri String This value is only specified if the EndpointDescription has a gatewayServerUri.
|
|
278
|
+
// This value is the applicationUri from the EndpointDescription which is the applicationUri for the
|
|
279
|
+
// underlying Server. The type EndpointDescription is defined in 7.10.
|
|
280
|
+
|
|
281
|
+
function _serverEndpointsForCreateSessionResponse(server: OPCUAServer, endpointUrl: string | null, serverUri: string | null) {
|
|
282
|
+
serverUri = null; // unused then
|
|
283
|
+
|
|
284
|
+
// The Server shall return a set of EndpointDescriptions available for the serverUri specified in the request.
|
|
285
|
+
// It is recommended that Servers only include the endpointUrl, securityMode,
|
|
286
|
+
// securityPolicyUri, userIdentityTokens, transportProfileUri and securityLevel with all other parameters
|
|
287
|
+
// set to null. Only the recommended parameters shall be verified by the client.
|
|
288
|
+
return server
|
|
289
|
+
._get_endpoints(endpointUrl)
|
|
290
|
+
.filter((e) => !(e as any).restricted) // remove restricted endpoints
|
|
291
|
+
.filter((e) => matchUri(e.endpointUrl, endpointUrl))
|
|
292
|
+
.map(getRequiredEndpointInfo);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function adjustSecurityPolicy(
|
|
296
|
+
channel: ServerSecureChannelLayer,
|
|
297
|
+
userTokenPolicy_securityPolicyUri: SecurityPolicy
|
|
298
|
+
): SecurityPolicy {
|
|
299
|
+
// check that userIdentityToken
|
|
300
|
+
let securityPolicy = fromURI(userTokenPolicy_securityPolicyUri);
|
|
301
|
+
|
|
302
|
+
// if the security policy is not specified we use the session security policy
|
|
303
|
+
if (securityPolicy === SecurityPolicy.Invalid) {
|
|
304
|
+
securityPolicy = fromURI((channel.clientSecurityHeader! as AsymmetricAlgorithmSecurityHeader).securityPolicyUri);
|
|
305
|
+
assert(securityPolicy !== SecurityPolicy.Invalid);
|
|
306
|
+
}
|
|
307
|
+
return securityPolicy;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function findUserTokenByPolicy(
|
|
311
|
+
endpoint_description: EndpointDescription,
|
|
312
|
+
userTokenType: UserTokenType,
|
|
313
|
+
policyId: SecurityPolicy | string | null
|
|
314
|
+
): UserTokenPolicy | null {
|
|
315
|
+
assert(endpoint_description instanceof EndpointDescription);
|
|
316
|
+
const r = endpoint_description.userIdentityTokens!.filter(
|
|
317
|
+
(userIdentity: UserTokenPolicy) =>
|
|
318
|
+
userIdentity.tokenType === userTokenType && (!policyId || userIdentity.policyId === policyId)
|
|
319
|
+
);
|
|
320
|
+
return r.length === 0 ? null : r[0];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function findUserTokenPolicy(endpoint_description: EndpointDescription, userTokenType: UserTokenType): UserTokenPolicy | null {
|
|
324
|
+
assert(endpoint_description instanceof EndpointDescription);
|
|
325
|
+
const r = endpoint_description.userIdentityTokens!.filter((userIdentity: UserTokenPolicy) => {
|
|
326
|
+
assert(userIdentity.tokenType !== undefined);
|
|
327
|
+
return userIdentity.tokenType === userTokenType;
|
|
328
|
+
});
|
|
329
|
+
return r.length === 0 ? null : r[0];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function createAnonymousIdentityToken(endpoint_desc: EndpointDescription) {
|
|
333
|
+
assert(endpoint_desc instanceof EndpointDescription);
|
|
334
|
+
const userTokenPolicy = findUserTokenPolicy(endpoint_desc, UserTokenType.Anonymous);
|
|
335
|
+
if (!userTokenPolicy) {
|
|
336
|
+
throw new Error("Cannot find ANONYMOUS user token policy in end point description");
|
|
337
|
+
}
|
|
338
|
+
return new AnonymousIdentityToken({ policyId: userTokenPolicy.policyId });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function sameIdentityToken(token1: UserIdentityToken, token2: UserIdentityToken): boolean {
|
|
342
|
+
if (token1 instanceof UserNameIdentityToken) {
|
|
343
|
+
if (!(token2 instanceof UserNameIdentityToken)) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
if (token1.userName !== token2.userName) {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
if (token1.password.toString("hex") !== token2.password.toString("hex")) {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
} else if (token1 instanceof AnonymousIdentityToken) {
|
|
353
|
+
if (!(token2 instanceof AnonymousIdentityToken)) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
if (token1.policyId !== token2.policyId) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
assert(false, " Not implemented yet");
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
function getTokenType(userIdentityToken: UserIdentityToken): UserTokenType {
|
|
365
|
+
if (userIdentityToken instanceof AnonymousIdentityToken) {
|
|
366
|
+
return UserTokenType.Anonymous;
|
|
367
|
+
} else if (userIdentityToken instanceof UserNameIdentityToken) {
|
|
368
|
+
return UserTokenType.UserName;
|
|
369
|
+
} else if (userIdentityToken instanceof IssuedIdentityToken) {
|
|
370
|
+
return UserTokenType.IssuedToken;
|
|
371
|
+
} else if (userIdentityToken instanceof X509IdentityToken) {
|
|
372
|
+
return UserTokenType.Certificate;
|
|
373
|
+
}
|
|
374
|
+
return UserTokenType.Invalid;
|
|
375
|
+
}
|
|
376
|
+
function thumbprint(certificate?: Certificate): string {
|
|
377
|
+
return certificate ? certificate.toString("base64") : "";
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/*=== private
|
|
381
|
+
*
|
|
382
|
+
* perform the read operation on a given node for a monitored item.
|
|
383
|
+
* this method DOES NOT apply to Variable Values attribute
|
|
384
|
+
*
|
|
385
|
+
* @param self
|
|
386
|
+
* @param oldValue
|
|
387
|
+
* @param node
|
|
388
|
+
* @param itemToMonitor
|
|
389
|
+
* @private
|
|
390
|
+
*/
|
|
391
|
+
function monitoredItem_read_and_record_value(
|
|
392
|
+
self: MonitoredItem,
|
|
393
|
+
context: ISessionContext | null,
|
|
394
|
+
oldValue: DataValue,
|
|
395
|
+
node: UAVariable,
|
|
396
|
+
itemToMonitor: any,
|
|
397
|
+
callback: (err: Error | null, dataValue?: DataValue) => void
|
|
398
|
+
) {
|
|
399
|
+
assert(self instanceof MonitoredItem);
|
|
400
|
+
assert(oldValue instanceof DataValue);
|
|
401
|
+
assert(itemToMonitor.attributeId === AttributeIds.Value);
|
|
402
|
+
|
|
403
|
+
const dataValue = node.readAttribute(context, itemToMonitor.attributeId, itemToMonitor.indexRange, itemToMonitor.dataEncoding);
|
|
404
|
+
|
|
405
|
+
callback(null, dataValue);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/*== private
|
|
409
|
+
* @method monitoredItem_read_and_record_value_async
|
|
410
|
+
* this method applies to Variable Values attribute
|
|
411
|
+
* @param self
|
|
412
|
+
* @param oldValue
|
|
413
|
+
* @param node
|
|
414
|
+
* @param itemToMonitor
|
|
415
|
+
* @private
|
|
416
|
+
*/
|
|
417
|
+
function monitoredItem_read_and_record_value_async(
|
|
418
|
+
self: MonitoredItem,
|
|
419
|
+
context: ISessionContext,
|
|
420
|
+
oldValue: DataValue,
|
|
421
|
+
node: UAVariable,
|
|
422
|
+
itemToMonitor: any,
|
|
423
|
+
callback: (err: Error | null, dataValue?: DataValue) => void
|
|
424
|
+
) {
|
|
425
|
+
assert(context instanceof SessionContext);
|
|
426
|
+
assert(itemToMonitor.attributeId === AttributeIds.Value);
|
|
427
|
+
assert(self instanceof MonitoredItem);
|
|
428
|
+
assert(oldValue instanceof DataValue);
|
|
429
|
+
// do it asynchronously ( this is only valid for value attributes )
|
|
430
|
+
assert(itemToMonitor.attributeId === AttributeIds.Value);
|
|
431
|
+
|
|
432
|
+
node.readValueAsync(context, (err: Error | null, dataValue?: DataValue) => {
|
|
433
|
+
callback(err, dataValue);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function build_scanning_node_function(
|
|
438
|
+
context: ISessionContext,
|
|
439
|
+
addressSpace: AddressSpace,
|
|
440
|
+
monitoredItem: MonitoredItem,
|
|
441
|
+
itemToMonitor: any
|
|
442
|
+
): (dataValue: DataValue, callback: (err: Error | null, dataValue?: DataValue) => void) => void {
|
|
443
|
+
assert(context instanceof SessionContext);
|
|
444
|
+
assert(itemToMonitor instanceof ReadValueId);
|
|
445
|
+
|
|
446
|
+
const node = addressSpace.findNode(itemToMonitor.nodeId) as UAVariable;
|
|
447
|
+
|
|
448
|
+
/* istanbul ignore next */
|
|
449
|
+
if (!node) {
|
|
450
|
+
errorLog(" INVALID NODE ID , ", itemToMonitor.nodeId.toString());
|
|
451
|
+
dump(itemToMonitor);
|
|
452
|
+
return (oldData: DataValue, callback: (err: Error | null, dataValue?: DataValue) => void) => {
|
|
453
|
+
callback(
|
|
454
|
+
null,
|
|
455
|
+
new DataValue({
|
|
456
|
+
statusCode: StatusCodes.BadNodeIdUnknown,
|
|
457
|
+
value: { dataType: DataType.Null, value: 0 }
|
|
458
|
+
})
|
|
459
|
+
);
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
///// !!monitoredItem.setNode(node);
|
|
464
|
+
|
|
465
|
+
if (itemToMonitor.attributeId === AttributeIds.Value) {
|
|
466
|
+
const monitoredItem_read_and_record_value_func =
|
|
467
|
+
itemToMonitor.attributeId === AttributeIds.Value && typeof node.readValueAsync === "function"
|
|
468
|
+
? monitoredItem_read_and_record_value_async
|
|
469
|
+
: monitoredItem_read_and_record_value;
|
|
470
|
+
|
|
471
|
+
return function func(
|
|
472
|
+
this: MonitoredItem,
|
|
473
|
+
oldDataValue: DataValue,
|
|
474
|
+
callback: (err: Error | null, dataValue?: DataValue) => void
|
|
475
|
+
) {
|
|
476
|
+
assert(this instanceof MonitoredItem);
|
|
477
|
+
assert(oldDataValue instanceof DataValue);
|
|
478
|
+
assert(typeof callback === "function");
|
|
479
|
+
monitoredItem_read_and_record_value_func(this, context, oldDataValue, node, itemToMonitor, callback);
|
|
480
|
+
};
|
|
481
|
+
} else {
|
|
482
|
+
// Attributes, other than the Value Attribute, are only monitored for a change in value.
|
|
483
|
+
// The filter is not used for these Attributes. Any change in value for these Attributes
|
|
484
|
+
// causes a Notification to be generated.
|
|
485
|
+
|
|
486
|
+
// only record value when it has changed
|
|
487
|
+
return function func(
|
|
488
|
+
this: MonitoredItem,
|
|
489
|
+
oldDataValue: DataValue,
|
|
490
|
+
callback: (err: Error | null, dataValue?: DataValue) => void
|
|
491
|
+
) {
|
|
492
|
+
const self = this;
|
|
493
|
+
assert(self instanceof MonitoredItem);
|
|
494
|
+
assert(oldDataValue instanceof DataValue);
|
|
495
|
+
assert(typeof callback === "function");
|
|
496
|
+
const newDataValue = node.readAttribute(null, itemToMonitor.attributeId);
|
|
497
|
+
callback(null, newDataValue);
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function prepareMonitoredItem(context: ISessionContext, addressSpace: AddressSpace, monitoredItem: MonitoredItem) {
|
|
503
|
+
const itemToMonitor = monitoredItem.itemToMonitor;
|
|
504
|
+
const readNodeFunc = build_scanning_node_function(context, addressSpace, monitoredItem, itemToMonitor);
|
|
505
|
+
monitoredItem.samplingFunc = readNodeFunc;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function isMonitoringModeValid(monitoringMode: MonitoringMode): boolean {
|
|
509
|
+
assert(MonitoringMode.Invalid !== undefined);
|
|
510
|
+
return monitoringMode !== MonitoringMode.Invalid && monitoringMode <= MonitoringMode.Reporting;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function _installRegisterServerManager(self: OPCUAServer) {
|
|
514
|
+
assert(self instanceof OPCUAServer);
|
|
515
|
+
assert(!self.registerServerManager);
|
|
516
|
+
|
|
517
|
+
/* istanbul ignore next */
|
|
518
|
+
if (!self.registerServerMethod) {
|
|
519
|
+
throw new Error("Internal Error");
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
switch (self.registerServerMethod) {
|
|
523
|
+
case RegisterServerMethod.HIDDEN:
|
|
524
|
+
self.registerServerManager = new RegisterServerManagerHidden({
|
|
525
|
+
server: self
|
|
526
|
+
});
|
|
527
|
+
break;
|
|
528
|
+
case RegisterServerMethod.MDNS:
|
|
529
|
+
self.registerServerManager = new RegisterServerManagerMDNSONLY({
|
|
530
|
+
server: self
|
|
531
|
+
});
|
|
532
|
+
break;
|
|
533
|
+
case RegisterServerMethod.LDS:
|
|
534
|
+
self.registerServerManager = new RegisterServerManager({
|
|
535
|
+
discoveryServerEndpointUrl: self.discoveryServerEndpointUrl,
|
|
536
|
+
server: self
|
|
537
|
+
});
|
|
538
|
+
break;
|
|
539
|
+
/* istanbul ignore next */
|
|
540
|
+
default:
|
|
541
|
+
throw new Error("Invalid switch");
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
self.registerServerManager.on("serverRegistrationPending", () => {
|
|
545
|
+
/**
|
|
546
|
+
* emitted when the server is trying to registered the LDS
|
|
547
|
+
* but when the connection to the lds has failed
|
|
548
|
+
* serverRegistrationPending is sent when the backoff signal of the
|
|
549
|
+
* connection process is raised
|
|
550
|
+
* @event serverRegistrationPending
|
|
551
|
+
*/
|
|
552
|
+
debugLog("serverRegistrationPending");
|
|
553
|
+
self.emit("serverRegistrationPending");
|
|
554
|
+
});
|
|
555
|
+
self.registerServerManager.on("serverRegistered", () => {
|
|
556
|
+
/**
|
|
557
|
+
* emitted when the server is successfully registered to the LDS
|
|
558
|
+
* @event serverRegistered
|
|
559
|
+
*/
|
|
560
|
+
debugLog("serverRegistered");
|
|
561
|
+
self.emit("serverRegistered");
|
|
562
|
+
});
|
|
563
|
+
self.registerServerManager.on("serverRegistrationRenewed", () => {
|
|
564
|
+
/**
|
|
565
|
+
* emitted when the server has successfully renewed its registration to the LDS
|
|
566
|
+
* @event serverRegistrationRenewed
|
|
567
|
+
*/
|
|
568
|
+
debugLog("serverRegistrationRenewed");
|
|
569
|
+
self.emit("serverRegistrationRenewed");
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
self.registerServerManager.on("serverUnregistered", () => {
|
|
573
|
+
debugLog("serverUnregistered");
|
|
574
|
+
/**
|
|
575
|
+
* emitted when the server is successfully unregistered to the LDS
|
|
576
|
+
* ( for instance during shutdown)
|
|
577
|
+
* @event serverUnregistered
|
|
578
|
+
*/
|
|
579
|
+
self.emit("serverUnregistered");
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export enum RegisterServerMethod {
|
|
584
|
+
HIDDEN = 1, // the server doesn't expose itself to the external world
|
|
585
|
+
MDNS = 2, // the server publish itself to the mDNS Multicast network directly
|
|
586
|
+
LDS = 3 // the server registers itself to the LDS or LDS-ME (Local Discovery Server)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export interface OPCUAServerEndpointOptions {
|
|
590
|
+
/**
|
|
591
|
+
* the primary hostname of the endpoint.
|
|
592
|
+
* @default getFullyQualifiedDomainName()
|
|
593
|
+
*/
|
|
594
|
+
hostname?: string;
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* the TCP port to listen to.
|
|
598
|
+
* @default 26543
|
|
599
|
+
*/
|
|
600
|
+
port?: number;
|
|
601
|
+
/**
|
|
602
|
+
* the possible security policies that the server will expose
|
|
603
|
+
* @default [SecurityPolicy.None, SecurityPolicy.Basic128Rsa15, SecurityPolicy.Basic256Sha256]
|
|
604
|
+
*/
|
|
605
|
+
securityPolicies?: SecurityPolicy[];
|
|
606
|
+
/**
|
|
607
|
+
* the possible security mode that the server will expose
|
|
608
|
+
* @default [MessageSecurityMode.None, MessageSecurityMode.Sign, MessageSecurityMode.SignAndEncrypt]
|
|
609
|
+
*/
|
|
610
|
+
securityModes?: MessageSecurityMode[];
|
|
611
|
+
/**
|
|
612
|
+
* tells if the server default endpoints should allow anonymous connection.
|
|
613
|
+
* @default true
|
|
614
|
+
*/
|
|
615
|
+
allowAnonymous?: boolean;
|
|
616
|
+
|
|
617
|
+
/** alternate hostname or IP to use */
|
|
618
|
+
alternateHostname?: string | string[];
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* true, if discovery service on secure channel shall be disabled
|
|
622
|
+
*/
|
|
623
|
+
disableDiscovery?: boolean;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
export interface OPCUAServerOptions extends OPCUABaseServerOptions, OPCUAServerEndpointOptions {
|
|
627
|
+
alternateEndpoints?: OPCUAServerEndpointOptions[];
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* the server certificate full path filename
|
|
631
|
+
*
|
|
632
|
+
* the certificate should be in PEM format
|
|
633
|
+
*/
|
|
634
|
+
certificateFile?: string;
|
|
635
|
+
/**
|
|
636
|
+
* the server private key full path filename
|
|
637
|
+
*
|
|
638
|
+
* This file should contains the private key that has been used to generate
|
|
639
|
+
* the server certificate file.
|
|
640
|
+
*
|
|
641
|
+
* the private key should be in PEM format
|
|
642
|
+
*
|
|
643
|
+
*/
|
|
644
|
+
privateKeyFile?: string;
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* the default secure token life time in ms.
|
|
648
|
+
*/
|
|
649
|
+
defaultSecureTokenLifetime?: number;
|
|
650
|
+
/**
|
|
651
|
+
* the HEL/ACK transaction timeout in ms.
|
|
652
|
+
*
|
|
653
|
+
* Use a large value ( i.e 15000 ms) for slow connections or embedded devices.
|
|
654
|
+
* @default 10000
|
|
655
|
+
*/
|
|
656
|
+
timeout?: number;
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* the maximum number of simultaneous sessions allowed.
|
|
660
|
+
* @default 10
|
|
661
|
+
*/
|
|
662
|
+
maxAllowedSessionNumber?: number;
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* the maximum number authorized simultaneous connections per endpoint
|
|
666
|
+
* @default 10
|
|
667
|
+
*/
|
|
668
|
+
maxConnectionsPerEndpoint?: number;
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* the nodeset.xml file(s) to load
|
|
672
|
+
*
|
|
673
|
+
* node-opcua comes with pre-installed node-set files that can be used
|
|
674
|
+
*
|
|
675
|
+
* example:
|
|
676
|
+
*
|
|
677
|
+
* ``` javascript
|
|
678
|
+
*
|
|
679
|
+
* ```
|
|
680
|
+
*/
|
|
681
|
+
nodeset_filename?: string[] | string;
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* the server Info
|
|
685
|
+
*
|
|
686
|
+
* this object contains the value that will populate the
|
|
687
|
+
* Root/ObjectS/Server/ServerInfo OPCUA object in the address space.
|
|
688
|
+
*/
|
|
689
|
+
serverInfo?: ApplicationDescriptionOptions;
|
|
690
|
+
/*{
|
|
691
|
+
applicationUri?: string;
|
|
692
|
+
productUri?: string;
|
|
693
|
+
applicationName?: LocalizedTextLike | string;
|
|
694
|
+
gatewayServerUri?: string | null;
|
|
695
|
+
discoveryProfileUri?: string | null;
|
|
696
|
+
discoveryUrls?: string[];
|
|
697
|
+
};
|
|
698
|
+
*/
|
|
699
|
+
buildInfo?: {
|
|
700
|
+
productName?: string;
|
|
701
|
+
productUri?: string | null; // << should be same as default_server_info.productUri?
|
|
702
|
+
manufacturerName?: string;
|
|
703
|
+
softwareVersion?: string;
|
|
704
|
+
buildNumber?: string;
|
|
705
|
+
buildDate?: Date;
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* an object that implements user authentication methods
|
|
710
|
+
*/
|
|
711
|
+
userManager?: UserManagerOptions;
|
|
712
|
+
|
|
713
|
+
/** resource Path is a string added at the end of the url such as "/UA/Server" */
|
|
714
|
+
resourcePath?: string;
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
*
|
|
718
|
+
*/
|
|
719
|
+
serverCapabilities?: ServerCapabilitiesOptions;
|
|
720
|
+
/**
|
|
721
|
+
* if server shall raise AuditingEvent
|
|
722
|
+
* @default true
|
|
723
|
+
*/
|
|
724
|
+
isAuditing?: boolean;
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* strategy used by the server to declare itself to a discovery server
|
|
728
|
+
*
|
|
729
|
+
* - HIDDEN: the server doesn't expose itself to the external world
|
|
730
|
+
* - MDNS: the server publish itself to the mDNS Multicast network directly
|
|
731
|
+
* - LDS: the server registers itself to the LDS or LDS-ME (Local Discovery Server)
|
|
732
|
+
*
|
|
733
|
+
* @default RegisterServerMethod.HIDDEN - by default the server
|
|
734
|
+
* will not register itself to the local discovery server
|
|
735
|
+
*
|
|
736
|
+
*/
|
|
737
|
+
registerServerMethod?: RegisterServerMethod;
|
|
738
|
+
/**
|
|
739
|
+
*
|
|
740
|
+
* @default "opc.tcp://localhost:4840"]
|
|
741
|
+
*/
|
|
742
|
+
discoveryServerEndpointUrl?: string;
|
|
743
|
+
/**
|
|
744
|
+
*
|
|
745
|
+
* supported server capabilities for the Multicast (mDNS)
|
|
746
|
+
* @default ["NA"]
|
|
747
|
+
* the possible values are any of node-opcua-discovery.serverCapabilities)
|
|
748
|
+
*
|
|
749
|
+
*/
|
|
750
|
+
capabilitiesForMDNS?: string[];
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* user Certificate Manager
|
|
754
|
+
* this certificate manager holds the X509 certificates used
|
|
755
|
+
* by client that uses X509 certificate token to impersonate a user
|
|
756
|
+
*/
|
|
757
|
+
userCertificateManager?: OPCUACertificateManager;
|
|
758
|
+
/**
|
|
759
|
+
* Server Certificate Manager
|
|
760
|
+
*
|
|
761
|
+
* this certificate manager will be used by the server to access
|
|
762
|
+
* and store certificates from the connecting clients
|
|
763
|
+
*/
|
|
764
|
+
serverCertificateManager?: OPCUACertificateManager;
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
*
|
|
768
|
+
*/
|
|
769
|
+
onCreateMonitoredItem?: CreateMonitoredItemHook;
|
|
770
|
+
onDeleteMonitoredItem?: DeleteMonitoredItemHook;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
export interface OPCUAServer {
|
|
774
|
+
/**
|
|
775
|
+
*
|
|
776
|
+
*/
|
|
777
|
+
engine: ServerEngine;
|
|
778
|
+
/**
|
|
779
|
+
*
|
|
780
|
+
*/
|
|
781
|
+
registerServerMethod: RegisterServerMethod;
|
|
782
|
+
/**
|
|
783
|
+
*
|
|
784
|
+
*/
|
|
785
|
+
discoveryServerEndpointUrl: string;
|
|
786
|
+
/**
|
|
787
|
+
*
|
|
788
|
+
*/
|
|
789
|
+
registerServerManager?: IRegisterServerManager;
|
|
790
|
+
/**
|
|
791
|
+
*
|
|
792
|
+
*/
|
|
793
|
+
capabilitiesForMDNS: string[];
|
|
794
|
+
/**
|
|
795
|
+
*
|
|
796
|
+
*/
|
|
797
|
+
userCertificateManager: OPCUACertificateManager;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const g_requestExactEndpointUrl: boolean = !!process.env.NODEOPCUA_SERVER_REQUEST_EXACT_ENDPOINT_URL;
|
|
801
|
+
/**
|
|
802
|
+
*
|
|
803
|
+
*/
|
|
804
|
+
export class OPCUAServer extends OPCUABaseServer {
|
|
805
|
+
static defaultShutdownTimeout: number = 100; // 250 ms
|
|
806
|
+
/**
|
|
807
|
+
* if requestExactEndpointUrl is set to true the server will only accept createSession that have a endpointUrl that strictly matches
|
|
808
|
+
* one of the provided endpoint.
|
|
809
|
+
* This mean that if the server expose a endpoint with url such as opc.tcp://MYHOSTNAME:1234, client will not be able to reach the server
|
|
810
|
+
* with the ip address of the server.
|
|
811
|
+
* requestExactEndpointUrl = true => emulates the Prosys Server behavior
|
|
812
|
+
* requestExactEndpointUrl = false => emulates the Unified Automation behavior.
|
|
813
|
+
*/
|
|
814
|
+
static requestExactEndpointUrl: boolean = g_requestExactEndpointUrl;
|
|
815
|
+
/**
|
|
816
|
+
* total number of bytes written by the server since startup
|
|
817
|
+
*/
|
|
818
|
+
public get bytesWritten(): number {
|
|
819
|
+
return this.endpoints.reduce((accumulated: number, endpoint: OPCUAServerEndPoint) => {
|
|
820
|
+
return accumulated + endpoint.bytesWritten;
|
|
821
|
+
}, 0);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* total number of bytes read by the server since startup
|
|
826
|
+
*/
|
|
827
|
+
public get bytesRead(): number {
|
|
828
|
+
return this.endpoints.reduce((accumulated: number, endpoint: OPCUAServerEndPoint) => {
|
|
829
|
+
return accumulated + endpoint.bytesRead;
|
|
830
|
+
}, 0);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Number of transactions processed by the server since startup
|
|
835
|
+
*/
|
|
836
|
+
public get transactionsCount(): number {
|
|
837
|
+
return this.endpoints.reduce((accumulated: number, endpoint: OPCUAServerEndPoint) => {
|
|
838
|
+
return accumulated + endpoint.transactionsCount;
|
|
839
|
+
}, 0);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* The server build info
|
|
844
|
+
*/
|
|
845
|
+
public get buildInfo(): BuildInfo {
|
|
846
|
+
return this.engine.buildInfo;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* the number of connected channel on all existing end points
|
|
851
|
+
*/
|
|
852
|
+
public get currentChannelCount(): number {
|
|
853
|
+
// TODO : move to base
|
|
854
|
+
const self = this;
|
|
855
|
+
return self.endpoints.reduce((currentValue: number, endPoint: OPCUAServerEndPoint) => {
|
|
856
|
+
return currentValue + endPoint.currentChannelCount;
|
|
857
|
+
}, 0);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* The number of active subscriptions from all sessions
|
|
862
|
+
*/
|
|
863
|
+
public get currentSubscriptionCount(): number {
|
|
864
|
+
return this.engine ? this.engine.currentSubscriptionCount : 0;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* the number of session activation requests that have been rejected
|
|
869
|
+
*/
|
|
870
|
+
public get rejectedSessionCount(): number {
|
|
871
|
+
return this.engine ? this.engine.rejectedSessionCount : 0;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* the number of request that have been rejected
|
|
876
|
+
*/
|
|
877
|
+
public get rejectedRequestsCount(): number {
|
|
878
|
+
return this.engine ? this.engine.rejectedRequestsCount : 0;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* the number of sessions that have been aborted
|
|
883
|
+
*/
|
|
884
|
+
public get sessionAbortCount(): number {
|
|
885
|
+
return this.engine ? this.engine.sessionAbortCount : 0;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* the publishing interval count
|
|
890
|
+
*/
|
|
891
|
+
public get publishingIntervalCount(): number {
|
|
892
|
+
return this.engine ? this.engine.publishingIntervalCount : 0;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* the number of sessions currently active
|
|
897
|
+
*/
|
|
898
|
+
public get currentSessionCount(): number {
|
|
899
|
+
return this.engine ? this.engine.currentSessionCount : 0;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* true if the server has been initialized
|
|
904
|
+
*
|
|
905
|
+
*/
|
|
906
|
+
public get initialized(): boolean {
|
|
907
|
+
return this.engine && this.engine.addressSpace !== null;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* is the server auditing ?
|
|
912
|
+
*/
|
|
913
|
+
public get isAuditing(): boolean {
|
|
914
|
+
return this.engine ? this.engine.isAuditing : false;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
public static registry = new ObjectRegistry();
|
|
918
|
+
public static fallbackSessionName = "Client didn't provide a meaningful sessionName ...";
|
|
919
|
+
/**
|
|
920
|
+
* the maximum number of subscription that can be created per server
|
|
921
|
+
*/
|
|
922
|
+
public static MAX_SUBSCRIPTION = 50;
|
|
923
|
+
/**
|
|
924
|
+
* the maximum number of concurrent sessions allowed on the server
|
|
925
|
+
*/
|
|
926
|
+
public maxAllowedSessionNumber: number;
|
|
927
|
+
/**
|
|
928
|
+
* the maximum number for concurrent connection per end point
|
|
929
|
+
*/
|
|
930
|
+
public maxConnectionsPerEndpoint: number;
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* false if anonymous connection are not allowed
|
|
934
|
+
*/
|
|
935
|
+
public allowAnonymous: boolean = false;
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* the user manager
|
|
939
|
+
*/
|
|
940
|
+
public userManager: UserManagerOptions;
|
|
941
|
+
public readonly options: OPCUAServerOptions;
|
|
942
|
+
|
|
943
|
+
private objectFactory?: Factory;
|
|
944
|
+
|
|
945
|
+
private _delayInit?: () => Promise<void>;
|
|
946
|
+
|
|
947
|
+
constructor(options?: OPCUAServerOptions) {
|
|
948
|
+
super(options);
|
|
949
|
+
|
|
950
|
+
options = options || {};
|
|
951
|
+
|
|
952
|
+
this.options = options;
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* @property maxAllowedSessionNumber
|
|
956
|
+
*/
|
|
957
|
+
this.maxAllowedSessionNumber = options.maxAllowedSessionNumber || default_maxAllowedSessionNumber;
|
|
958
|
+
/**
|
|
959
|
+
* @property maxConnectionsPerEndpoint
|
|
960
|
+
*/
|
|
961
|
+
this.maxConnectionsPerEndpoint = options.maxConnectionsPerEndpoint || default_maxConnectionsPerEndpoint;
|
|
962
|
+
|
|
963
|
+
// build Info
|
|
964
|
+
const buildInfo: BuildInfoOptions = {
|
|
965
|
+
...default_build_info,
|
|
966
|
+
...options.buildInfo
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
// repair product name
|
|
970
|
+
buildInfo.productUri = buildInfo.productUri || this.serverInfo.productUri;
|
|
971
|
+
this.serverInfo.productUri = this.serverInfo.productUri || buildInfo.productUri;
|
|
972
|
+
|
|
973
|
+
this.userManager = options.userManager || {};
|
|
974
|
+
if (typeof this.userManager.isValidUser !== "function") {
|
|
975
|
+
this.userManager.isValidUser = (/*userName,password*/) => {
|
|
976
|
+
return false;
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
options.allowAnonymous = options.allowAnonymous === undefined ? true : !!options.allowAnonymous;
|
|
981
|
+
/**
|
|
982
|
+
* @property allowAnonymous
|
|
983
|
+
*/
|
|
984
|
+
this.allowAnonymous = options.allowAnonymous;
|
|
985
|
+
|
|
986
|
+
this.discoveryServerEndpointUrl = options.discoveryServerEndpointUrl || "opc.tcp://%FQDN%:4840";
|
|
987
|
+
assert(typeof this.discoveryServerEndpointUrl === "string");
|
|
988
|
+
|
|
989
|
+
this.serverInfo.applicationType = ApplicationType.Server;
|
|
990
|
+
this.capabilitiesForMDNS = options.capabilitiesForMDNS || ["NA"];
|
|
991
|
+
this.registerServerMethod = options.registerServerMethod || RegisterServerMethod.HIDDEN;
|
|
992
|
+
_installRegisterServerManager(this);
|
|
993
|
+
|
|
994
|
+
if (!options.userCertificateManager) {
|
|
995
|
+
this.userCertificateManager = getDefaultCertificateManager("UserPKI");
|
|
996
|
+
} else {
|
|
997
|
+
this.userCertificateManager = options.userCertificateManager;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// note: we need to delay initialization of endpoint as certain resources
|
|
1001
|
+
// such as %FQDN% might not be ready yet at this stage
|
|
1002
|
+
this._delayInit = async () => {
|
|
1003
|
+
/* istanbul ignore next */
|
|
1004
|
+
if (!options) {
|
|
1005
|
+
throw new Error("Internal Error");
|
|
1006
|
+
}
|
|
1007
|
+
// to check => this.serverInfo.applicationName = this.serverInfo.productName || buildInfo.productName;
|
|
1008
|
+
|
|
1009
|
+
// note: applicationUri is handled in a special way
|
|
1010
|
+
this.engine = new ServerEngine({
|
|
1011
|
+
applicationUri: () => this.serverInfo.applicationUri!,
|
|
1012
|
+
buildInfo,
|
|
1013
|
+
isAuditing: options.isAuditing,
|
|
1014
|
+
serverCapabilities: options.serverCapabilities
|
|
1015
|
+
});
|
|
1016
|
+
this.objectFactory = new Factory(this.engine);
|
|
1017
|
+
|
|
1018
|
+
const endpointDefinitions = options.alternateEndpoints || [];
|
|
1019
|
+
const hostname = getFullyQualifiedDomainName();
|
|
1020
|
+
|
|
1021
|
+
endpointDefinitions.push({
|
|
1022
|
+
port: options.port || 26543,
|
|
1023
|
+
|
|
1024
|
+
allowAnonymous: options.allowAnonymous,
|
|
1025
|
+
alternateHostname: options.alternateHostname,
|
|
1026
|
+
disableDiscovery: options.disableDiscovery,
|
|
1027
|
+
hostname: options.hostname || hostname,
|
|
1028
|
+
securityModes: options.securityModes,
|
|
1029
|
+
securityPolicies: options.securityPolicies
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
// todo should self.serverInfo.productUri match self.engine.buildInfo.productUri ?
|
|
1033
|
+
for (const endpointOptions of endpointDefinitions) {
|
|
1034
|
+
const endPoint = this.createEndpointDescriptions(options!, endpointOptions);
|
|
1035
|
+
this.endpoints.push(endPoint);
|
|
1036
|
+
endPoint.on("message", (message: Message, channel: ServerSecureChannelLayer) => {
|
|
1037
|
+
this.on_request(message, channel);
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
endPoint.on("error", (err: Error) => {
|
|
1041
|
+
errorLog("OPCUAServer endpoint error", err);
|
|
1042
|
+
// set serverState to ServerState.Failed;
|
|
1043
|
+
this.engine.setServerState(ServerState.Failed);
|
|
1044
|
+
this.shutdown(() => {
|
|
1045
|
+
/* empty */
|
|
1046
|
+
});
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Initialize the server by installing default node set.
|
|
1054
|
+
*
|
|
1055
|
+
* and instruct the server to listen to its endpoints.
|
|
1056
|
+
*
|
|
1057
|
+
* ```javascript
|
|
1058
|
+
* const server = new OPCUAServer();
|
|
1059
|
+
* await server.initialize();
|
|
1060
|
+
*
|
|
1061
|
+
* // default server namespace is now initialized
|
|
1062
|
+
* // it is a good time to create life instance objects
|
|
1063
|
+
* const namespace = server.engine.addressSpace.getOwnNamespace();
|
|
1064
|
+
* namespace.addObject({
|
|
1065
|
+
* browseName: "SomeObject",
|
|
1066
|
+
* organizedBy: server.engine.addressSpace.rootFolder.objects
|
|
1067
|
+
* });
|
|
1068
|
+
*
|
|
1069
|
+
* // the addressSpace is now complete
|
|
1070
|
+
* // let's now start listening to clients
|
|
1071
|
+
* await server.start();
|
|
1072
|
+
* ```
|
|
1073
|
+
*/
|
|
1074
|
+
public initialize(): Promise<void>;
|
|
1075
|
+
public initialize(done: () => void): void;
|
|
1076
|
+
public initialize(...args: [any?, ...any[]]): any {
|
|
1077
|
+
const done = args[0] as (err?: Error) => void;
|
|
1078
|
+
assert(!this.initialized, "server is already initialized"); // already initialized ?
|
|
1079
|
+
|
|
1080
|
+
this._preInitTask.push(async () => {
|
|
1081
|
+
/* istanbul ignore else */
|
|
1082
|
+
if (this._delayInit) {
|
|
1083
|
+
await this._delayInit();
|
|
1084
|
+
this._delayInit = undefined;
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
this.performPreInitialization()
|
|
1089
|
+
.then(() => {
|
|
1090
|
+
OPCUAServer.registry.register(this);
|
|
1091
|
+
this.engine.initialize(this.options, () => {
|
|
1092
|
+
setImmediate(() => {
|
|
1093
|
+
this.emit("post_initialize");
|
|
1094
|
+
done();
|
|
1095
|
+
});
|
|
1096
|
+
});
|
|
1097
|
+
})
|
|
1098
|
+
.catch((err) => {
|
|
1099
|
+
done(err);
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
/**
|
|
1104
|
+
* Initiate the server by starting all its endpoints
|
|
1105
|
+
* @async
|
|
1106
|
+
*/
|
|
1107
|
+
public start(): Promise<void>;
|
|
1108
|
+
public start(done: () => void): void;
|
|
1109
|
+
public start(...args: [any?, ...any[]]): any {
|
|
1110
|
+
const done = args[0] as () => void;
|
|
1111
|
+
const tasks: any[] = [];
|
|
1112
|
+
|
|
1113
|
+
tasks.push(callbackify(extractFullyQualifiedDomainName));
|
|
1114
|
+
|
|
1115
|
+
if (!this.initialized) {
|
|
1116
|
+
tasks.push((callback: ErrorCallback) => {
|
|
1117
|
+
this.initialize(callback);
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
tasks.push((callback: ErrorCallback) => {
|
|
1121
|
+
super.start((err?: Error | null) => {
|
|
1122
|
+
if (err) {
|
|
1123
|
+
this.shutdown((/*err2*/ err2?: Error) => {
|
|
1124
|
+
callback(err);
|
|
1125
|
+
});
|
|
1126
|
+
} else {
|
|
1127
|
+
// we start the registration process asynchronously
|
|
1128
|
+
// as we want to make server immediately available
|
|
1129
|
+
this.registerServerManager!.start(() => {
|
|
1130
|
+
/* empty */
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
setImmediate(callback);
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
async.series(tasks, done);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* shutdown all server endpoints
|
|
1143
|
+
* @method shutdown
|
|
1144
|
+
* @async
|
|
1145
|
+
* @param timeout the timeout (in ms) before the server is actually shutdown
|
|
1146
|
+
*
|
|
1147
|
+
* @example
|
|
1148
|
+
*
|
|
1149
|
+
* ```javascript
|
|
1150
|
+
* // shutdown immediately
|
|
1151
|
+
* server.shutdown(function(err) {
|
|
1152
|
+
* });
|
|
1153
|
+
* ```
|
|
1154
|
+
* ```ts
|
|
1155
|
+
* // in typescript with promises
|
|
1156
|
+
* server.shutdown(10000).then(()=>{
|
|
1157
|
+
* console.log("Server has shutdown");
|
|
1158
|
+
* });
|
|
1159
|
+
* ```
|
|
1160
|
+
* ```javascript
|
|
1161
|
+
* // shutdown within 10 seconds
|
|
1162
|
+
* server.engine .shutdownReason = coerceLocalizedText("Shutdown for maintenance");
|
|
1163
|
+
* server.shutdown(10000,function(err) {
|
|
1164
|
+
* });
|
|
1165
|
+
* ```
|
|
1166
|
+
*/
|
|
1167
|
+
public shutdown(timeout?: number): Promise<void>;
|
|
1168
|
+
public shutdown(callback: (err?: Error) => void): void;
|
|
1169
|
+
public shutdown(timeout: number, callback: (err?: Error) => void): void;
|
|
1170
|
+
public shutdown(...args: [any?, ...any[]]): any {
|
|
1171
|
+
const timeout = args.length === 1 ? OPCUAServer.defaultShutdownTimeout : (args[0] as number);
|
|
1172
|
+
const callback = (args.length === 1 ? args[0] : args[1]) as (err?: Error) => void;
|
|
1173
|
+
assert(typeof callback === "function");
|
|
1174
|
+
debugLog("OPCUAServer#shutdown (timeout = ", timeout, ")");
|
|
1175
|
+
|
|
1176
|
+
/* istanbul ignore next */
|
|
1177
|
+
if (!this.engine) {
|
|
1178
|
+
return callback();
|
|
1179
|
+
}
|
|
1180
|
+
assert(this.engine);
|
|
1181
|
+
if (!this.engine.isStarted()) {
|
|
1182
|
+
// server may have been shot down already , or may have fail to start !!
|
|
1183
|
+
const err = new Error("OPCUAServer#shutdown failure ! server doesn't seems to be started yet");
|
|
1184
|
+
return callback(err);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
this.userCertificateManager.dispose();
|
|
1188
|
+
|
|
1189
|
+
this.engine.setServerState(ServerState.Shutdown);
|
|
1190
|
+
|
|
1191
|
+
const shutdownTime = new Date(Date.now() + timeout);
|
|
1192
|
+
this.engine.setShutdownTime(shutdownTime);
|
|
1193
|
+
|
|
1194
|
+
debugLog("OPCUAServer is now unregistering itself from the discovery server " + this.buildInfo);
|
|
1195
|
+
this.registerServerManager!.stop((err?: Error | null) => {
|
|
1196
|
+
debugLog("OPCUAServer unregistered from discovery server", err);
|
|
1197
|
+
setTimeout(() => {
|
|
1198
|
+
this.engine.shutdown();
|
|
1199
|
+
|
|
1200
|
+
debugLog("OPCUAServer#shutdown: started");
|
|
1201
|
+
OPCUABaseServer.prototype.shutdown.call(this, (err1?: Error) => {
|
|
1202
|
+
debugLog("OPCUAServer#shutdown: completed");
|
|
1203
|
+
|
|
1204
|
+
this.dispose();
|
|
1205
|
+
callback(err1);
|
|
1206
|
+
});
|
|
1207
|
+
}, timeout);
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
public dispose() {
|
|
1212
|
+
for (const endpoint of this.endpoints) {
|
|
1213
|
+
endpoint.dispose();
|
|
1214
|
+
}
|
|
1215
|
+
this.endpoints = [];
|
|
1216
|
+
|
|
1217
|
+
this.removeAllListeners();
|
|
1218
|
+
|
|
1219
|
+
if (this.registerServerManager) {
|
|
1220
|
+
this.registerServerManager.dispose();
|
|
1221
|
+
this.registerServerManager = undefined;
|
|
1222
|
+
}
|
|
1223
|
+
OPCUAServer.registry.unregister(this);
|
|
1224
|
+
|
|
1225
|
+
/* istanbul ignore next */
|
|
1226
|
+
if (this.engine) {
|
|
1227
|
+
this.engine.dispose();
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
public raiseEvent(eventType: any, options: any): void {
|
|
1232
|
+
/* istanbul ignore next */
|
|
1233
|
+
if (!this.engine.addressSpace) {
|
|
1234
|
+
errorLog("addressSpace missing");
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
const server = this.engine.addressSpace.findNode("Server") as UAObject;
|
|
1239
|
+
|
|
1240
|
+
/* istanbul ignore next */
|
|
1241
|
+
if (!server) {
|
|
1242
|
+
// xx throw new Error("OPCUAServer#raiseEvent : cannot find Server object");
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
let eventTypeNode = eventType;
|
|
1247
|
+
if (typeof eventType === "string") {
|
|
1248
|
+
eventTypeNode = this.engine.addressSpace.findEventType(eventType);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/* istanbul ignore else */
|
|
1252
|
+
if (eventTypeNode) {
|
|
1253
|
+
return server.raiseEvent(eventTypeNode, options);
|
|
1254
|
+
} else {
|
|
1255
|
+
console.warn(" cannot find event type ", eventType);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/**
|
|
1260
|
+
* create and register a new session
|
|
1261
|
+
* @internal
|
|
1262
|
+
*/
|
|
1263
|
+
protected createSession(options: any): ServerSession {
|
|
1264
|
+
/* istanbul ignore next */
|
|
1265
|
+
if (!this.engine) {
|
|
1266
|
+
throw new Error("Internal Error");
|
|
1267
|
+
}
|
|
1268
|
+
return this.engine.createSession(options);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* retrieve a session by authentication token
|
|
1273
|
+
* @internal
|
|
1274
|
+
*/
|
|
1275
|
+
protected getSession(authenticationToken: NodeId, activeOnly?: boolean): ServerSession | null {
|
|
1276
|
+
return this.engine ? this.engine.getSession(authenticationToken, activeOnly) : null;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
*
|
|
1281
|
+
* @param channel
|
|
1282
|
+
* @param clientCertificate
|
|
1283
|
+
* @param clientNonce
|
|
1284
|
+
* @internal
|
|
1285
|
+
*/
|
|
1286
|
+
protected computeServerSignature(
|
|
1287
|
+
channel: ServerSecureChannelLayer,
|
|
1288
|
+
clientCertificate: Certificate,
|
|
1289
|
+
clientNonce: Nonce
|
|
1290
|
+
): SignatureData | undefined {
|
|
1291
|
+
return computeSignature(clientCertificate, clientNonce, this.getPrivateKey(), channel.messageBuilder.securityPolicy);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
*
|
|
1296
|
+
* @param session
|
|
1297
|
+
* @param channel
|
|
1298
|
+
* @param clientSignature
|
|
1299
|
+
* @internal
|
|
1300
|
+
*/
|
|
1301
|
+
protected verifyClientSignature(
|
|
1302
|
+
session: ServerSession,
|
|
1303
|
+
channel: ServerSecureChannelLayer,
|
|
1304
|
+
clientSignature: SignatureData
|
|
1305
|
+
): boolean {
|
|
1306
|
+
const clientCertificate = channel.receiverCertificate!;
|
|
1307
|
+
const securityPolicy = channel.messageBuilder.securityPolicy;
|
|
1308
|
+
const serverCertificate = this.getCertificate();
|
|
1309
|
+
|
|
1310
|
+
const result = verifySignature(serverCertificate, session.nonce!, clientSignature, clientCertificate, securityPolicy);
|
|
1311
|
+
|
|
1312
|
+
return result;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
protected isValidUserNameIdentityToken(
|
|
1316
|
+
channel: ServerSecureChannelLayer,
|
|
1317
|
+
session: ServerSession,
|
|
1318
|
+
userTokenPolicy: any,
|
|
1319
|
+
userIdentityToken: UserNameIdentityToken,
|
|
1320
|
+
userTokenSignature: any,
|
|
1321
|
+
callback: (err: Error | null, statusCode?: StatusCode) => void
|
|
1322
|
+
) {
|
|
1323
|
+
assert(userIdentityToken instanceof UserNameIdentityToken);
|
|
1324
|
+
|
|
1325
|
+
const securityPolicy = adjustSecurityPolicy(channel, userTokenPolicy.securityPolicyUri);
|
|
1326
|
+
if (securityPolicy === SecurityPolicy.None) {
|
|
1327
|
+
return callback(null, StatusCodes.Good);
|
|
1328
|
+
}
|
|
1329
|
+
const cryptoFactory = getCryptoFactory(securityPolicy);
|
|
1330
|
+
|
|
1331
|
+
/* istanbul ignore next */
|
|
1332
|
+
if (!cryptoFactory) {
|
|
1333
|
+
return callback(null, StatusCodes.BadSecurityPolicyRejected);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/* istanbul ignore next */
|
|
1337
|
+
if (userIdentityToken.encryptionAlgorithm !== cryptoFactory.asymmetricEncryptionAlgorithm) {
|
|
1338
|
+
errorLog("invalid encryptionAlgorithm");
|
|
1339
|
+
errorLog("userTokenPolicy", userTokenPolicy.toString());
|
|
1340
|
+
errorLog("userTokenPolicy", userIdentityToken.toString());
|
|
1341
|
+
return callback(null, StatusCodes.BadIdentityTokenInvalid);
|
|
1342
|
+
}
|
|
1343
|
+
const userName = userIdentityToken.userName;
|
|
1344
|
+
const password = userIdentityToken.password;
|
|
1345
|
+
if (!userName || !password) {
|
|
1346
|
+
return callback(null, StatusCodes.BadIdentityTokenInvalid);
|
|
1347
|
+
}
|
|
1348
|
+
return callback(null, StatusCodes.Good);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
protected isValidX509IdentityToken(
|
|
1352
|
+
channel: ServerSecureChannelLayer,
|
|
1353
|
+
session: ServerSession,
|
|
1354
|
+
userTokenPolicy: any,
|
|
1355
|
+
userIdentityToken: X509IdentityToken,
|
|
1356
|
+
userTokenSignature: any,
|
|
1357
|
+
callback: (err: Error | null, statusCode?: StatusCode) => void
|
|
1358
|
+
) {
|
|
1359
|
+
assert(userIdentityToken instanceof X509IdentityToken);
|
|
1360
|
+
assert(callback instanceof Function);
|
|
1361
|
+
|
|
1362
|
+
const securityPolicy = adjustSecurityPolicy(channel, userTokenPolicy.securityPolicyUri);
|
|
1363
|
+
|
|
1364
|
+
const cryptoFactory = getCryptoFactory(securityPolicy);
|
|
1365
|
+
/* istanbul ignore next */
|
|
1366
|
+
if (!cryptoFactory) {
|
|
1367
|
+
return callback(null, StatusCodes.BadSecurityPolicyRejected);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
if (!userTokenSignature || !userTokenSignature.signature) {
|
|
1371
|
+
return callback(null, StatusCodes.BadUserSignatureInvalid);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
if (userIdentityToken.policyId !== userTokenPolicy.policyId) {
|
|
1375
|
+
errorLog("invalid encryptionAlgorithm");
|
|
1376
|
+
errorLog("userTokenPolicy", userTokenPolicy.toString());
|
|
1377
|
+
errorLog("userTokenPolicy", userIdentityToken.toString());
|
|
1378
|
+
return callback(null, StatusCodes.BadSecurityPolicyRejected);
|
|
1379
|
+
}
|
|
1380
|
+
const certificate = userIdentityToken.certificateData; /* as Certificate*/
|
|
1381
|
+
const nonce = session.nonce!;
|
|
1382
|
+
const serverCertificate = this.getCertificate();
|
|
1383
|
+
|
|
1384
|
+
assert(serverCertificate instanceof Buffer);
|
|
1385
|
+
assert(certificate instanceof Buffer, "expecting certificate to be a Buffer");
|
|
1386
|
+
assert(nonce instanceof Buffer, "expecting nonce to be a Buffer");
|
|
1387
|
+
assert(userTokenSignature.signature instanceof Buffer, "expecting userTokenSignature to be a Buffer");
|
|
1388
|
+
|
|
1389
|
+
// verify proof of possession by checking certificate signature & server nonce correctness
|
|
1390
|
+
if (!verifySignature(serverCertificate, nonce, userTokenSignature, certificate, securityPolicy)) {
|
|
1391
|
+
return callback(null, StatusCodes.BadUserSignatureInvalid);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// verify if certificate is Valid
|
|
1395
|
+
this.userCertificateManager!.checkCertificate(certificate, (err, certificateStatus) => {
|
|
1396
|
+
/* istanbul ignore next */
|
|
1397
|
+
if (err) {
|
|
1398
|
+
return callback(err);
|
|
1399
|
+
}
|
|
1400
|
+
if (
|
|
1401
|
+
StatusCodes.BadCertificateUntrusted === certificateStatus ||
|
|
1402
|
+
StatusCodes.BadCertificateTimeInvalid === certificateStatus ||
|
|
1403
|
+
StatusCodes.BadCertificateIssuerTimeInvalid === certificateStatus ||
|
|
1404
|
+
StatusCodes.BadCertificateIssuerUseNotAllowed === certificateStatus ||
|
|
1405
|
+
StatusCodes.BadCertificateIssuerRevocationUnknown === certificateStatus ||
|
|
1406
|
+
StatusCodes.BadCertificateRevocationUnknown === certificateStatus ||
|
|
1407
|
+
StatusCodes.BadCertificateRevoked === certificateStatus ||
|
|
1408
|
+
StatusCodes.BadCertificateUseNotAllowed === certificateStatus ||
|
|
1409
|
+
StatusCodes.BadSecurityChecksFailed === certificateStatus ||
|
|
1410
|
+
StatusCodes.Good !== certificateStatus
|
|
1411
|
+
) {
|
|
1412
|
+
debugLog("isValidX509IdentityToken => certificateStatus = ", certificateStatus?.toString());
|
|
1413
|
+
return callback(null, StatusCodes.BadIdentityTokenRejected);
|
|
1414
|
+
}
|
|
1415
|
+
if (StatusCodes.Good !== certificateStatus) {
|
|
1416
|
+
assert(certificateStatus instanceof StatusCode);
|
|
1417
|
+
return callback(null, certificateStatus);
|
|
1418
|
+
// return callback(null, StatusCodes.BadIdentityTokenInvalid);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// verify if certificate is truster or rejected
|
|
1422
|
+
// todo: StatusCodes.BadCertificateUntrusted
|
|
1423
|
+
|
|
1424
|
+
// store untrusted certificate to rejected folder
|
|
1425
|
+
// todo:
|
|
1426
|
+
return callback(null, StatusCodes.Good);
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
/**
|
|
1431
|
+
* @internal
|
|
1432
|
+
*/
|
|
1433
|
+
protected userNameIdentityTokenAuthenticateUser(
|
|
1434
|
+
channel: ServerSecureChannelLayer,
|
|
1435
|
+
session: ServerSession,
|
|
1436
|
+
userTokenPolicy: any,
|
|
1437
|
+
userIdentityToken: UserNameIdentityToken,
|
|
1438
|
+
callback: (err: Error | null, isAuthorized?: boolean) => void
|
|
1439
|
+
): void {
|
|
1440
|
+
assert(userIdentityToken instanceof UserNameIdentityToken);
|
|
1441
|
+
// assert(this.isValidUserNameIdentityToken(channel, session, userTokenPolicy, userIdentityToken));
|
|
1442
|
+
|
|
1443
|
+
const securityPolicy = adjustSecurityPolicy(channel, userTokenPolicy.securityPolicyUri);
|
|
1444
|
+
|
|
1445
|
+
const userName = userIdentityToken.userName!;
|
|
1446
|
+
let password: any = userIdentityToken.password;
|
|
1447
|
+
|
|
1448
|
+
// decrypt password if necessary
|
|
1449
|
+
if (securityPolicy === SecurityPolicy.None) {
|
|
1450
|
+
// not good, password was sent in clear text ...
|
|
1451
|
+
password = password.toString();
|
|
1452
|
+
} else {
|
|
1453
|
+
const serverPrivateKey = this.getPrivateKey();
|
|
1454
|
+
|
|
1455
|
+
const serverNonce = session.nonce!;
|
|
1456
|
+
assert(serverNonce instanceof Buffer);
|
|
1457
|
+
|
|
1458
|
+
const cryptoFactory = getCryptoFactory(securityPolicy);
|
|
1459
|
+
/* istanbul ignore next */
|
|
1460
|
+
if (!cryptoFactory) {
|
|
1461
|
+
return callback(new Error(" Unsupported security Policy"));
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
const buff = cryptoFactory.asymmetricDecrypt(password, serverPrivateKey);
|
|
1465
|
+
|
|
1466
|
+
// server certificate may be invalid and asymmetricDecrypt may fail
|
|
1467
|
+
if (!buff || buff.length < 4) {
|
|
1468
|
+
async.setImmediate(() => callback(null, false));
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
const length = buff.readUInt32LE(0) - serverNonce.length;
|
|
1473
|
+
password = buff.slice(4, 4 + length).toString("utf-8");
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
if (typeof this.userManager.isValidUserAsync === "function") {
|
|
1477
|
+
this.userManager.isValidUserAsync.call(session, userName, password, callback);
|
|
1478
|
+
} else {
|
|
1479
|
+
const authorized = this.userManager.isValidUser!.call(session, userName, password);
|
|
1480
|
+
async.setImmediate(() => callback(null, authorized));
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
/**
|
|
1485
|
+
* @internal
|
|
1486
|
+
*/
|
|
1487
|
+
protected isValidUserIdentityToken(
|
|
1488
|
+
channel: ServerSecureChannelLayer,
|
|
1489
|
+
session: ServerSession,
|
|
1490
|
+
userIdentityToken: UserIdentityToken,
|
|
1491
|
+
userTokenSignature: any,
|
|
1492
|
+
endpointDescription: EndpointDescription,
|
|
1493
|
+
callback: (err: Error | null, statusCode?: StatusCode) => void
|
|
1494
|
+
): void {
|
|
1495
|
+
assert(callback instanceof Function);
|
|
1496
|
+
/* istanbul ignore next */
|
|
1497
|
+
if (!userIdentityToken) {
|
|
1498
|
+
throw new Error("Invalid token");
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
const userTokenType = getTokenType(userIdentityToken);
|
|
1502
|
+
|
|
1503
|
+
const userTokenPolicy = findUserTokenByPolicy(endpointDescription, userTokenType, userIdentityToken.policyId!);
|
|
1504
|
+
if (!userTokenPolicy) {
|
|
1505
|
+
// cannot find token with this policyId
|
|
1506
|
+
return callback(null, StatusCodes.BadIdentityTokenInvalid);
|
|
1507
|
+
}
|
|
1508
|
+
//
|
|
1509
|
+
if (userIdentityToken instanceof UserNameIdentityToken) {
|
|
1510
|
+
return this.isValidUserNameIdentityToken(
|
|
1511
|
+
channel,
|
|
1512
|
+
session,
|
|
1513
|
+
userTokenPolicy,
|
|
1514
|
+
userIdentityToken,
|
|
1515
|
+
userTokenSignature,
|
|
1516
|
+
callback
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
if (userIdentityToken instanceof X509IdentityToken) {
|
|
1520
|
+
return this.isValidX509IdentityToken(
|
|
1521
|
+
channel,
|
|
1522
|
+
session,
|
|
1523
|
+
userTokenPolicy,
|
|
1524
|
+
userIdentityToken,
|
|
1525
|
+
userTokenSignature,
|
|
1526
|
+
callback
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
return callback(null, StatusCodes.Good);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
*
|
|
1535
|
+
* @internal
|
|
1536
|
+
* @param channel
|
|
1537
|
+
* @param session
|
|
1538
|
+
* @param userIdentityToken
|
|
1539
|
+
* @param callback
|
|
1540
|
+
* @returns {*}
|
|
1541
|
+
*/
|
|
1542
|
+
protected isUserAuthorized(
|
|
1543
|
+
channel: ServerSecureChannelLayer,
|
|
1544
|
+
session: ServerSession,
|
|
1545
|
+
userIdentityToken: UserIdentityToken,
|
|
1546
|
+
callback: (err: Error | null, isAuthorized?: boolean) => void
|
|
1547
|
+
) {
|
|
1548
|
+
assert(userIdentityToken);
|
|
1549
|
+
assert(typeof callback === "function");
|
|
1550
|
+
|
|
1551
|
+
const userTokenType = getTokenType(userIdentityToken);
|
|
1552
|
+
const userTokenPolicy = findUserTokenByPolicy(session.getEndpointDescription(), userTokenType, userIdentityToken.policyId!);
|
|
1553
|
+
assert(userTokenPolicy);
|
|
1554
|
+
// find if a userToken exists
|
|
1555
|
+
if (userIdentityToken instanceof UserNameIdentityToken) {
|
|
1556
|
+
return this.userNameIdentityTokenAuthenticateUser(channel, session, userTokenPolicy, userIdentityToken, callback);
|
|
1557
|
+
}
|
|
1558
|
+
async.setImmediate(callback.bind(null, null, true));
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
protected makeServerNonce(): Nonce {
|
|
1562
|
+
return crypto.randomBytes(32);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// session services
|
|
1566
|
+
protected async _on_CreateSessionRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
1567
|
+
const server = this;
|
|
1568
|
+
const request = message.request as CreateSessionRequest;
|
|
1569
|
+
assert(request instanceof CreateSessionRequest);
|
|
1570
|
+
|
|
1571
|
+
function rejectConnection(statusCode: StatusCode): void {
|
|
1572
|
+
server.engine.incrementSecurityRejectedSessionCount();
|
|
1573
|
+
|
|
1574
|
+
const response1 = new CreateSessionResponse({
|
|
1575
|
+
responseHeader: { serviceResult: statusCode }
|
|
1576
|
+
});
|
|
1577
|
+
channel.send_response("MSG", response1, message);
|
|
1578
|
+
// and close !
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// From OPCUA V1.03 Part 4 5.6.2 CreateSession
|
|
1582
|
+
// A Server application should limit the number of Sessions. To protect against misbehaving Clients and denial
|
|
1583
|
+
// of service attacks, the Server shall close the oldest Session that is not activated before reaching the
|
|
1584
|
+
// maximum number of supported Sessions
|
|
1585
|
+
if (server.currentSessionCount >= server.maxAllowedSessionNumber) {
|
|
1586
|
+
await _attempt_to_close_some_old_unactivated_session(server);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// check if session count hasn't reach the maximum allowed sessions
|
|
1590
|
+
if (server.currentSessionCount >= server.maxAllowedSessionNumber) {
|
|
1591
|
+
return rejectConnection(StatusCodes.BadTooManySessions);
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// Release 1.03 OPC Unified Architecture, Part 4 page 24 - CreateSession Parameters
|
|
1595
|
+
// client should prove a sessionName
|
|
1596
|
+
// Session name is a Human readable string that identifies the Session. The Server makes this name and the
|
|
1597
|
+
// sessionId visible in its AddressSpace for diagnostic purposes. The Client should provide a name that is
|
|
1598
|
+
// unique for the instance of the Client.
|
|
1599
|
+
// If this parameter is not specified the Server shall assign a value.
|
|
1600
|
+
|
|
1601
|
+
if (utils.isNullOrUndefined(request.sessionName)) {
|
|
1602
|
+
// see also #198
|
|
1603
|
+
// let's the server assign a sessionName for this lazy client.
|
|
1604
|
+
|
|
1605
|
+
debugLog(
|
|
1606
|
+
"assigning OPCUAServer.fallbackSessionName because client's sessionName is null ",
|
|
1607
|
+
OPCUAServer.fallbackSessionName
|
|
1608
|
+
);
|
|
1609
|
+
|
|
1610
|
+
request.sessionName = OPCUAServer.fallbackSessionName;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// Duration Requested maximum number of milliseconds that a Session should remain open without activity.
|
|
1614
|
+
// If the Client fails to issue a Service request within this interval, then the Server shall automatically
|
|
1615
|
+
// terminate the Client Session.
|
|
1616
|
+
const revisedSessionTimeout = _adjust_session_timeout(request.requestedSessionTimeout);
|
|
1617
|
+
|
|
1618
|
+
// Release 1.02 page 27 OPC Unified Architecture, Part 4: CreateSession.clientNonce
|
|
1619
|
+
// A random number that should never be used in any other request. This number shall have a minimum length of 32
|
|
1620
|
+
// bytes. Profiles may increase the required length. The Server shall use this value to prove possession of
|
|
1621
|
+
// its application instance Certificate in the response.
|
|
1622
|
+
if (!request.clientNonce || request.clientNonce.length < 32) {
|
|
1623
|
+
if (channel.securityMode !== MessageSecurityMode.None) {
|
|
1624
|
+
errorLog(
|
|
1625
|
+
chalk.red("SERVER with secure connection: Missing or invalid client Nonce "),
|
|
1626
|
+
request.clientNonce && request.clientNonce.toString("hex")
|
|
1627
|
+
);
|
|
1628
|
+
|
|
1629
|
+
return rejectConnection(StatusCodes.BadNonceInvalid);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
if (nonceAlreadyBeenUsed(request.clientNonce)) {
|
|
1633
|
+
errorLog(
|
|
1634
|
+
chalk.red("SERVER with secure connection: None has already been used"),
|
|
1635
|
+
request.clientNonce && request.clientNonce.toString("hex")
|
|
1636
|
+
);
|
|
1637
|
+
|
|
1638
|
+
return rejectConnection(StatusCodes.BadNonceInvalid);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
function validate_applicationUri(applicationUri: string, clientCertificate: Certificate): boolean {
|
|
1642
|
+
// if session is insecure there is no need to check certificate information
|
|
1643
|
+
if (channel.securityMode === MessageSecurityMode.None) {
|
|
1644
|
+
return true; // assume correct
|
|
1645
|
+
}
|
|
1646
|
+
if (!clientCertificate || clientCertificate.length === 0) {
|
|
1647
|
+
return true; // can't check
|
|
1648
|
+
}
|
|
1649
|
+
const e = exploreCertificate(clientCertificate);
|
|
1650
|
+
const applicationUriFromCert = e.tbsCertificate.extensions!.subjectAltName.uniformResourceIdentifier[0];
|
|
1651
|
+
|
|
1652
|
+
/* istanbul ignore next */
|
|
1653
|
+
if (applicationUriFromCert !== applicationUri) {
|
|
1654
|
+
errorLog("BadCertificateUriInvalid!");
|
|
1655
|
+
errorLog("applicationUri = ", applicationUri);
|
|
1656
|
+
errorLog("applicationUriFromCert = ", applicationUriFromCert);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
return applicationUriFromCert === applicationUri;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// check application spoofing
|
|
1663
|
+
// check if applicationUri in createSessionRequest matches applicationUri in client Certificate
|
|
1664
|
+
if (!validate_applicationUri(request.clientDescription.applicationUri!, request.clientCertificate)) {
|
|
1665
|
+
return rejectConnection(StatusCodes.BadCertificateUriInvalid);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
function validate_security_endpoint(channel1: ServerSecureChannelLayer): {
|
|
1669
|
+
errCode: StatusCode;
|
|
1670
|
+
endpoint?: EndpointDescription;
|
|
1671
|
+
} {
|
|
1672
|
+
debugLog("validate_security_endpoint = ", request.endpointUrl);
|
|
1673
|
+
let endpoints = server._get_endpoints(request.endpointUrl);
|
|
1674
|
+
// endpointUrl String The network address that the Client used to access the Session Endpoint.
|
|
1675
|
+
// The HostName portion of the URL should be one of the HostNames for the application that are
|
|
1676
|
+
// specified in the Server’s ApplicationInstanceCertificate (see 7.2). The Server shall raise an
|
|
1677
|
+
// AuditUrlMismatchEventType event if the URL does not match the Server’s HostNames.
|
|
1678
|
+
// AuditUrlMismatchEventType event type is defined in Part 5.
|
|
1679
|
+
// The Server uses this information for diagnostics and to determine the set of
|
|
1680
|
+
// EndpointDescriptions to return in the response.
|
|
1681
|
+
// ToDo: check endpointUrl validity and emit an AuditUrlMismatchEventType event if not
|
|
1682
|
+
if (endpoints.length === 0) {
|
|
1683
|
+
// we have a UrlMismatch here
|
|
1684
|
+
const ua_server = server.engine.addressSpace!.rootFolder.objects.server;
|
|
1685
|
+
ua_server.raiseEvent("AuditUrlMismatchEventType", {
|
|
1686
|
+
endpointUrl: { dataType: DataType.String, value: request.endpointUrl }
|
|
1687
|
+
});
|
|
1688
|
+
debugLog("Cannot find endpoint in available endpoints with endpointUri", request.endpointUrl);
|
|
1689
|
+
if (OPCUAServer.requestExactEndpointUrl) {
|
|
1690
|
+
return { errCode: StatusCodes.BadServiceUnsupported };
|
|
1691
|
+
} else {
|
|
1692
|
+
endpoints = server._get_endpoints(null);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
// ignore restricted endpoints
|
|
1696
|
+
endpoints = endpoints.filter((e: EndpointDescription) => !(e as any).restricted);
|
|
1697
|
+
|
|
1698
|
+
const endpoints_matching_security_mode = endpoints.filter((e: EndpointDescription) => {
|
|
1699
|
+
return e.securityMode === channel1.securityMode;
|
|
1700
|
+
});
|
|
1701
|
+
|
|
1702
|
+
if (endpoints_matching_security_mode.length === 0) {
|
|
1703
|
+
return { errCode: StatusCodes.BadSecurityModeRejected };
|
|
1704
|
+
}
|
|
1705
|
+
const endpoints_matching_security_policy = endpoints_matching_security_mode.filter((e: EndpointDescription) => {
|
|
1706
|
+
return e.securityPolicyUri === channel1.securityHeader!.securityPolicyUri;
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
if (endpoints_matching_security_policy.length === 0) {
|
|
1710
|
+
return { errCode: StatusCodes.BadSecurityPolicyRejected };
|
|
1711
|
+
}
|
|
1712
|
+
if (endpoints_matching_security_policy.length !== 1) {
|
|
1713
|
+
debugLog("endpoints_matching_security_policy= ", endpoints_matching_security_policy.length);
|
|
1714
|
+
}
|
|
1715
|
+
return { errCode: StatusCodes.Good, endpoint: endpoints_matching_security_policy[0] };
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
const { errCode, endpoint } = validate_security_endpoint(channel);
|
|
1719
|
+
if (errCode !== StatusCodes.Good) {
|
|
1720
|
+
return rejectConnection(errCode);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// see Release 1.02 27 OPC Unified Architecture, Part 4
|
|
1724
|
+
const session = server.createSession({
|
|
1725
|
+
clientDescription: request.clientDescription,
|
|
1726
|
+
sessionTimeout: revisedSessionTimeout
|
|
1727
|
+
});
|
|
1728
|
+
session.endpoint = endpoint;
|
|
1729
|
+
|
|
1730
|
+
assert(session);
|
|
1731
|
+
assert(session.sessionTimeout === revisedSessionTimeout);
|
|
1732
|
+
|
|
1733
|
+
session.clientDescription = request.clientDescription;
|
|
1734
|
+
session.sessionName = request.sessionName || "<unknown session name>";
|
|
1735
|
+
|
|
1736
|
+
// Depending upon on the SecurityPolicy and the SecurityMode of the SecureChannel, the exchange of
|
|
1737
|
+
// ApplicationInstanceCertificates and Nonces may be optional and the signatures may be empty. See
|
|
1738
|
+
// Part 7 for the definition of SecurityPolicies and the handling of these parameters
|
|
1739
|
+
|
|
1740
|
+
// serverNonce:
|
|
1741
|
+
// A random number that should never be used in any other request.
|
|
1742
|
+
// This number shall have a minimum length of 32 bytes.
|
|
1743
|
+
// The Client shall use this value to prove possession of its application instance
|
|
1744
|
+
// Certificate in the ActivateSession request.
|
|
1745
|
+
// This value may also be used to prove possession of the userIdentityToken it
|
|
1746
|
+
// specified in the ActivateSession request.
|
|
1747
|
+
//
|
|
1748
|
+
// ( this serverNonce will only be used up to the _on_ActivateSessionRequest
|
|
1749
|
+
// where a new nonce will be created)
|
|
1750
|
+
session.nonce = server.makeServerNonce();
|
|
1751
|
+
session.channelId = channel.channelId;
|
|
1752
|
+
|
|
1753
|
+
session._attach_channel(channel);
|
|
1754
|
+
|
|
1755
|
+
const serverCertificateChain = server.getCertificateChain();
|
|
1756
|
+
|
|
1757
|
+
const hasEncryption = true;
|
|
1758
|
+
// If the securityPolicyUri is None and none of the UserTokenPolicies requires encryption
|
|
1759
|
+
if (session.channel!.securityMode === MessageSecurityMode.None) {
|
|
1760
|
+
// ToDo: Check that none of our insecure endpoint has a a UserTokenPolicy that require encryption
|
|
1761
|
+
// and set hasEncryption = false under this condition
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
const response = new CreateSessionResponse({
|
|
1765
|
+
// A identifier which uniquely identifies the session.
|
|
1766
|
+
sessionId: session.nodeId,
|
|
1767
|
+
|
|
1768
|
+
// A unique identifier assigned by the Server to the Session.
|
|
1769
|
+
// The token used to authenticate the client in subsequent requests.
|
|
1770
|
+
authenticationToken: session.authenticationToken,
|
|
1771
|
+
|
|
1772
|
+
revisedSessionTimeout,
|
|
1773
|
+
|
|
1774
|
+
serverNonce: session.nonce,
|
|
1775
|
+
|
|
1776
|
+
// serverCertificate: type ApplicationServerCertificate
|
|
1777
|
+
// The application instance Certificate issued to the Server.
|
|
1778
|
+
// A Server shall prove possession by using the private key to sign the Nonce provided
|
|
1779
|
+
// by the Client in the request. The Client shall verify that this Certificate is the same as
|
|
1780
|
+
// the one it used to create the SecureChannel.
|
|
1781
|
+
// The ApplicationInstanceCertificate type is defined in OpCUA 1.03 part 4 - $7.2 page 108
|
|
1782
|
+
// If the securityPolicyUri is None and none of the UserTokenPolicies requires
|
|
1783
|
+
// encryption, the Server shall not send an ApplicationInstanceCertificate and the Client
|
|
1784
|
+
// shall ignore the ApplicationInstanceCertificate.
|
|
1785
|
+
serverCertificate: hasEncryption ? serverCertificateChain : undefined,
|
|
1786
|
+
|
|
1787
|
+
// The endpoints provided by the server.
|
|
1788
|
+
// The Server shall return a set of EndpointDescriptions available for the serverUri
|
|
1789
|
+
// specified in the request.[...]
|
|
1790
|
+
// The Client shall verify this list with the list from a Discovery Endpoint if it used a Discovery
|
|
1791
|
+
// Endpoint to fetch the EndpointDescriptions.
|
|
1792
|
+
// It is recommended that Servers only include the endpointUrl, securityMode,
|
|
1793
|
+
// securityPolicyUri, userIdentityTokens, transportProfileUri and securityLevel with all
|
|
1794
|
+
// other parameters set to null. Only the recommended parameters shall be verified by
|
|
1795
|
+
// the client.
|
|
1796
|
+
serverEndpoints: _serverEndpointsForCreateSessionResponse(server, session.endpoint!.endpointUrl, request.serverUri),
|
|
1797
|
+
|
|
1798
|
+
// This parameter is deprecated and the array shall be empty.
|
|
1799
|
+
serverSoftwareCertificates: null,
|
|
1800
|
+
|
|
1801
|
+
// This is a signature generated with the private key associated with the
|
|
1802
|
+
// serverCertificate. This parameter is calculated by appending the clientNonce to the
|
|
1803
|
+
// clientCertificate and signing the resulting sequence of bytes.
|
|
1804
|
+
// The SignatureAlgorithm shall be the AsymmetricSignatureAlgorithm specified in the
|
|
1805
|
+
// SecurityPolicy for the Endpoint.
|
|
1806
|
+
// The SignatureData type is defined in 7.30.
|
|
1807
|
+
serverSignature: server.computeServerSignature(channel, request.clientCertificate, request.clientNonce),
|
|
1808
|
+
|
|
1809
|
+
// The maximum message size accepted by the server
|
|
1810
|
+
// The Client Communication Stack should return a Bad_RequestTooLarge error to the
|
|
1811
|
+
// application if a request message exceeds this limit.
|
|
1812
|
+
// The value zero indicates that this parameter is not used.
|
|
1813
|
+
maxRequestMessageSize: 0x4000000
|
|
1814
|
+
});
|
|
1815
|
+
|
|
1816
|
+
server.emit("create_session", session);
|
|
1817
|
+
|
|
1818
|
+
session.on("session_closed", (session1: ServerSession, deleteSubscriptions: boolean, reason: string) => {
|
|
1819
|
+
assert(typeof reason === "string");
|
|
1820
|
+
if (server.isAuditing) {
|
|
1821
|
+
assert(reason === "Timeout" || reason === "Terminated" || reason === "CloseSession" || reason === "Forcing");
|
|
1822
|
+
const sourceName = "Session/" + reason;
|
|
1823
|
+
|
|
1824
|
+
server.raiseEvent("AuditSessionEventType", {
|
|
1825
|
+
/* part 5 - 6.4.3 AuditEventType */
|
|
1826
|
+
actionTimeStamp: { dataType: "DateTime", value: new Date() },
|
|
1827
|
+
status: { dataType: "Boolean", value: true },
|
|
1828
|
+
|
|
1829
|
+
serverId: { dataType: "String", value: "" },
|
|
1830
|
+
|
|
1831
|
+
// ClientAuditEntryId contains the human-readable AuditEntryId defined in Part 3.
|
|
1832
|
+
clientAuditEntryId: { dataType: "String", value: "" },
|
|
1833
|
+
|
|
1834
|
+
// The ClientUserId identifies the user of the client requesting an action. The ClientUserId can be
|
|
1835
|
+
// obtained from the UserIdentityToken passed in the ActivateSession call.
|
|
1836
|
+
clientUserId: { dataType: "String", value: "" },
|
|
1837
|
+
|
|
1838
|
+
sourceName: { dataType: "String", value: sourceName },
|
|
1839
|
+
|
|
1840
|
+
/* part 5 - 6.4.7 AuditSessionEventType */
|
|
1841
|
+
sessionId: { dataType: "NodeId", value: session1.nodeId }
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
server.emit("session_closed", session1, deleteSubscriptions);
|
|
1846
|
+
});
|
|
1847
|
+
|
|
1848
|
+
if (server.isAuditing) {
|
|
1849
|
+
// ------------------------------------------------------------------------------------------------------
|
|
1850
|
+
server.raiseEvent("AuditCreateSessionEventType", {
|
|
1851
|
+
/* part 5 - 6.4.3 AuditEventType */
|
|
1852
|
+
actionTimeStamp: { dataType: "DateTime", value: new Date() },
|
|
1853
|
+
status: { dataType: "Boolean", value: true },
|
|
1854
|
+
|
|
1855
|
+
serverId: { dataType: "String", value: "" },
|
|
1856
|
+
|
|
1857
|
+
// ClientAuditEntryId contains the human-readable AuditEntryId defined in Part 3.
|
|
1858
|
+
clientAuditEntryId: { dataType: "String", value: "" },
|
|
1859
|
+
|
|
1860
|
+
// The ClientUserId identifies the user of the client requesting an action. The ClientUserId can be
|
|
1861
|
+
// obtained from the UserIdentityToken passed in the ActivateSession call.
|
|
1862
|
+
clientUserId: { dataType: "String", value: "" },
|
|
1863
|
+
|
|
1864
|
+
sourceName: { dataType: "String", value: "Session/CreateSession" },
|
|
1865
|
+
|
|
1866
|
+
/* part 5 - 6.4.7 AuditSessionEventType */
|
|
1867
|
+
sessionId: { dataType: "NodeId", value: session.nodeId },
|
|
1868
|
+
|
|
1869
|
+
/* part 5 - 6.4.8 AuditCreateSessionEventType */
|
|
1870
|
+
// SecureChannelId shall uniquely identify the SecureChannel. The application shall use the same
|
|
1871
|
+
// identifier in all AuditEvents related to the Session Service Set (AuditCreateSessionEventType,
|
|
1872
|
+
// AuditActivateSessionEventType and their subtypes) and the SecureChannel Service Set
|
|
1873
|
+
// (AuditChannelEventType and its subtypes
|
|
1874
|
+
secureChannelId: { dataType: "String", value: session.channel!.channelId!.toString() },
|
|
1875
|
+
|
|
1876
|
+
// Duration
|
|
1877
|
+
revisedSessionTimeout: { dataType: "Duration", value: session.sessionTimeout },
|
|
1878
|
+
|
|
1879
|
+
// clientCertificate
|
|
1880
|
+
clientCertificate: { dataType: "ByteString", value: session.channel!.clientCertificate },
|
|
1881
|
+
|
|
1882
|
+
// clientCertificateThumbprint
|
|
1883
|
+
clientCertificateThumbprint: {
|
|
1884
|
+
dataType: "ByteString",
|
|
1885
|
+
value: thumbprint(session.channel!.clientCertificate!)
|
|
1886
|
+
}
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
// -----------------------------------------------------------------------------------------------------------
|
|
1890
|
+
|
|
1891
|
+
assert(response.authenticationToken);
|
|
1892
|
+
channel.send_response("MSG", response, message);
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
// TODO : implement this:
|
|
1896
|
+
//
|
|
1897
|
+
// When the ActivateSession Service is called for the first time then the Server shall reject the request
|
|
1898
|
+
// if the SecureChannel is not same as the one associated with the CreateSession request.
|
|
1899
|
+
// Subsequent calls to ActivateSession may be associated with different SecureChannels. If this is the
|
|
1900
|
+
// case then the Server shall verify that the Certificate the Client used to create the new
|
|
1901
|
+
// SecureChannel is the same as the Certificate used to create the original SecureChannel. In addition,
|
|
1902
|
+
// the Server shall verify that the Client supplied a UserIdentityToken that is identical to the token
|
|
1903
|
+
// currently associated with the Session. Once the Server accepts the new SecureChannel it shall
|
|
1904
|
+
// reject requests sent via the old SecureChannel.
|
|
1905
|
+
/**
|
|
1906
|
+
*
|
|
1907
|
+
* @method _on_ActivateSessionRequest
|
|
1908
|
+
* @private
|
|
1909
|
+
*
|
|
1910
|
+
*
|
|
1911
|
+
*/
|
|
1912
|
+
protected _on_ActivateSessionRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
1913
|
+
const server = this;
|
|
1914
|
+
const request = message.request as ActivateSessionRequest;
|
|
1915
|
+
assert(request instanceof ActivateSessionRequest);
|
|
1916
|
+
|
|
1917
|
+
// get session from authenticationToken
|
|
1918
|
+
const authenticationToken = request.requestHeader.authenticationToken;
|
|
1919
|
+
|
|
1920
|
+
const session = server.getSession(authenticationToken);
|
|
1921
|
+
|
|
1922
|
+
function rejectConnection(statusCode: StatusCode): void {
|
|
1923
|
+
if (statusCode === StatusCodes.BadSessionIdInvalid) {
|
|
1924
|
+
server.engine.incrementRejectedSessionCount();
|
|
1925
|
+
} else {
|
|
1926
|
+
server.engine.incrementRejectedSessionCount();
|
|
1927
|
+
server.engine.incrementSecurityRejectedSessionCount();
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
const response1 = new ActivateSessionResponse({ responseHeader: { serviceResult: statusCode } });
|
|
1931
|
+
|
|
1932
|
+
channel.send_response("MSG", response1, message);
|
|
1933
|
+
// and close !
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
let response;
|
|
1937
|
+
|
|
1938
|
+
/* istanbul ignore next */
|
|
1939
|
+
if (!session) {
|
|
1940
|
+
// this may happen when the server has been restarted and a client tries to reconnect, thinking
|
|
1941
|
+
// that the previous session may still be active
|
|
1942
|
+
debugLog(chalk.yellow.bold(" Bad Session in _on_ActivateSessionRequest"), authenticationToken.toString());
|
|
1943
|
+
|
|
1944
|
+
return rejectConnection(StatusCodes.BadSessionIdInvalid);
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
// tslint:disable-next-line: no-unused-expression
|
|
1948
|
+
session.keepAlive ? session.keepAlive() : void 0;
|
|
1949
|
+
|
|
1950
|
+
// OpcUA 1.02 part 3 $5.6.3.1 ActiveSession Set page 29
|
|
1951
|
+
// When the ActivateSession Service is called f or the first time then the Server shall reject the request
|
|
1952
|
+
// if the SecureChannel is not same as the one associated with the CreateSession request.
|
|
1953
|
+
if (session.status === "new") {
|
|
1954
|
+
// xx if (channel.session_nonce !== session.nonce) {
|
|
1955
|
+
if (!channel_has_session(channel, session)) {
|
|
1956
|
+
// it looks like session activation is being using a channel that is not the
|
|
1957
|
+
// one that have been used to create the session
|
|
1958
|
+
errorLog(" channel.sessionTokens === " + Object.keys(channel.sessionTokens).join(" "));
|
|
1959
|
+
return rejectConnection(StatusCodes.BadSessionNotActivated);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// OpcUA 1.02 part 3 $5.6.3.1 ActiveSession Set page 29
|
|
1964
|
+
// ... Subsequent calls to ActivateSession may be associated with different SecureChannels. If this is the
|
|
1965
|
+
// case then the Server shall verify that the Certificate the Client used to create the new
|
|
1966
|
+
// SecureChannel is the same as the Certificate used to create the original SecureChannel.
|
|
1967
|
+
|
|
1968
|
+
if (session.status === "active") {
|
|
1969
|
+
if (session.channel!.channelId !== channel.channelId) {
|
|
1970
|
+
warningLog(
|
|
1971
|
+
" Session ",
|
|
1972
|
+
session.sessionName,
|
|
1973
|
+
" is being transferred from channel",
|
|
1974
|
+
chalk.cyan(session.channel!.channelId!.toString()),
|
|
1975
|
+
" to channel ",
|
|
1976
|
+
chalk.cyan(channel.channelId!.toString())
|
|
1977
|
+
);
|
|
1978
|
+
|
|
1979
|
+
// session is being reassigned to a new Channel,
|
|
1980
|
+
// we shall verify that the certificate used to create the Session is the same as the current
|
|
1981
|
+
// channel certificate.
|
|
1982
|
+
const old_channel_cert_thumbprint = thumbprint(session.channel!.clientCertificate!);
|
|
1983
|
+
const new_channel_cert_thumbprint = thumbprint(channel.clientCertificate!);
|
|
1984
|
+
|
|
1985
|
+
if (old_channel_cert_thumbprint !== new_channel_cert_thumbprint) {
|
|
1986
|
+
return rejectConnection(StatusCodes.BadNoValidCertificates); // not sure about this code !
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
// ... In addition the Server shall verify that the Client supplied a UserIdentityToken that is
|
|
1990
|
+
// identical to the token currently associated with the Session reassign session to new channel.
|
|
1991
|
+
if (!sameIdentityToken(session.userIdentityToken!, request.userIdentityToken as UserIdentityToken)) {
|
|
1992
|
+
return rejectConnection(StatusCodes.BadIdentityChangeNotSupported); // not sure about this code !
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
moveSessionToChannel(session, channel);
|
|
1997
|
+
} else if (session.status === "screwed") {
|
|
1998
|
+
// session has been used before being activated => this should be detected and session should be dismissed.
|
|
1999
|
+
return rejectConnection(StatusCodes.BadSessionClosed);
|
|
2000
|
+
} else if (session.status === "closed") {
|
|
2001
|
+
warningLog(chalk.yellow.bold(" Bad Session Closed in _on_ActivateSessionRequest"), authenticationToken.toString());
|
|
2002
|
+
return rejectConnection(StatusCodes.BadSessionClosed);
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
// verify clientSignature provided by the client
|
|
2006
|
+
if (!server.verifyClientSignature(session, channel, request.clientSignature)) {
|
|
2007
|
+
return rejectConnection(StatusCodes.BadApplicationSignatureInvalid);
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
// userIdentityToken may be missing , assume anonymous access then
|
|
2011
|
+
request.userIdentityToken = request.userIdentityToken || createAnonymousIdentityToken(session.endpoint!);
|
|
2012
|
+
|
|
2013
|
+
// check request.userIdentityToken is correct ( expected type and correctly formed)
|
|
2014
|
+
server.isValidUserIdentityToken(
|
|
2015
|
+
channel,
|
|
2016
|
+
session,
|
|
2017
|
+
request.userIdentityToken as UserIdentityToken,
|
|
2018
|
+
request.userTokenSignature,
|
|
2019
|
+
session.endpoint!,
|
|
2020
|
+
(err: Error | null, statusCode?: StatusCode) => {
|
|
2021
|
+
if (statusCode !== StatusCodes.Good) {
|
|
2022
|
+
/* istanbul ignore next */
|
|
2023
|
+
if (!(statusCode && statusCode instanceof StatusCode)) {
|
|
2024
|
+
const a = 23;
|
|
2025
|
+
}
|
|
2026
|
+
assert(statusCode && statusCode instanceof StatusCode, "expecting statusCode");
|
|
2027
|
+
return rejectConnection(statusCode!);
|
|
2028
|
+
}
|
|
2029
|
+
session.userIdentityToken = request.userIdentityToken as UserIdentityToken;
|
|
2030
|
+
|
|
2031
|
+
// check if user access is granted
|
|
2032
|
+
server.isUserAuthorized(
|
|
2033
|
+
channel,
|
|
2034
|
+
session,
|
|
2035
|
+
request.userIdentityToken as UserIdentityToken,
|
|
2036
|
+
(err1: Error | null, authorized?: boolean) => {
|
|
2037
|
+
/* istanbul ignore next */
|
|
2038
|
+
if (err1) {
|
|
2039
|
+
return rejectConnection(StatusCodes.BadInternalError);
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
if (!authorized) {
|
|
2043
|
+
return rejectConnection(StatusCodes.BadUserAccessDenied);
|
|
2044
|
+
} else {
|
|
2045
|
+
// extract : OPC UA part 4 - 5.6.3
|
|
2046
|
+
// Once used, a serverNonce cannot be used again. For that reason, the Server returns a new
|
|
2047
|
+
// serverNonce each time the ActivateSession Service is called.
|
|
2048
|
+
session.nonce = server.makeServerNonce();
|
|
2049
|
+
|
|
2050
|
+
session.status = "active";
|
|
2051
|
+
|
|
2052
|
+
response = new ActivateSessionResponse({ serverNonce: session.nonce });
|
|
2053
|
+
channel.send_response("MSG", response, message);
|
|
2054
|
+
|
|
2055
|
+
const userIdentityTokenPasswordRemoved = (userIdentityToken: any) => {
|
|
2056
|
+
const a = userIdentityToken.clone();
|
|
2057
|
+
// remove password
|
|
2058
|
+
a.password = "*************";
|
|
2059
|
+
return a;
|
|
2060
|
+
};
|
|
2061
|
+
|
|
2062
|
+
// send OPCUA Event Notification
|
|
2063
|
+
// see part 5 : 6.4.3 AuditEventType
|
|
2064
|
+
// 6.4.7 AuditSessionEventType
|
|
2065
|
+
// 6.4.10 AuditActivateSessionEventType
|
|
2066
|
+
assert(session.nodeId); // sessionId
|
|
2067
|
+
// xx assert(session.channel.clientCertificate instanceof Buffer);
|
|
2068
|
+
assert(session.sessionTimeout > 0);
|
|
2069
|
+
|
|
2070
|
+
if (server.isAuditing) {
|
|
2071
|
+
server.raiseEvent("AuditActivateSessionEventType", {
|
|
2072
|
+
/* part 5 - 6.4.3 AuditEventType */
|
|
2073
|
+
actionTimeStamp: { dataType: "DateTime", value: new Date() },
|
|
2074
|
+
status: { dataType: "Boolean", value: true },
|
|
2075
|
+
|
|
2076
|
+
serverId: { dataType: "String", value: "" },
|
|
2077
|
+
|
|
2078
|
+
// ClientAuditEntryId contains the human-readable AuditEntryId defined in Part 3.
|
|
2079
|
+
clientAuditEntryId: { dataType: "String", value: "" },
|
|
2080
|
+
|
|
2081
|
+
// The ClientUserId identifies the user of the client requesting an action.
|
|
2082
|
+
// The ClientUserId can be obtained from the UserIdentityToken passed in the
|
|
2083
|
+
// ActivateSession call.
|
|
2084
|
+
clientUserId: { dataType: "String", value: "cc" },
|
|
2085
|
+
|
|
2086
|
+
sourceName: { dataType: "String", value: "Session/ActivateSession" },
|
|
2087
|
+
|
|
2088
|
+
/* part 5 - 6.4.7 AuditSessionEventType */
|
|
2089
|
+
sessionId: { dataType: "NodeId", value: session.nodeId },
|
|
2090
|
+
|
|
2091
|
+
/* part 5 - 6.4.10 AuditActivateSessionEventType */
|
|
2092
|
+
clientSoftwareCertificates: {
|
|
2093
|
+
arrayType: VariantArrayType.Array,
|
|
2094
|
+
dataType: "ExtensionObject" /* SignedSoftwareCertificate */,
|
|
2095
|
+
value: []
|
|
2096
|
+
},
|
|
2097
|
+
// UserIdentityToken reflects the userIdentityToken parameter of the ActivateSession
|
|
2098
|
+
// Service call.
|
|
2099
|
+
// For Username/Password tokens the password should NOT be included.
|
|
2100
|
+
userIdentityToken: {
|
|
2101
|
+
dataType: "ExtensionObject" /* UserIdentityToken */,
|
|
2102
|
+
value: userIdentityTokenPasswordRemoved(session.userIdentityToken)
|
|
2103
|
+
},
|
|
2104
|
+
|
|
2105
|
+
// SecureChannelId shall uniquely identify the SecureChannel. The application shall
|
|
2106
|
+
// use the same identifier in all AuditEvents related to the Session Service Set
|
|
2107
|
+
// (AuditCreateSessionEventType, AuditActivateSessionEventType and their subtypes) and
|
|
2108
|
+
// the SecureChannel Service Set (AuditChannelEventType and its subtypes).
|
|
2109
|
+
secureChannelId: { dataType: "String", value: session.channel!.channelId!.toString() }
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
server.emit("session_activated", session, userIdentityTokenPasswordRemoved);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
protected prepare(message: Message, channel: ServerSecureChannelLayer) {
|
|
2121
|
+
const server = this;
|
|
2122
|
+
const request = message.request;
|
|
2123
|
+
|
|
2124
|
+
// --- check that session is correct
|
|
2125
|
+
const authenticationToken = request.requestHeader.authenticationToken;
|
|
2126
|
+
const session = server.getSession(authenticationToken, /*activeOnly*/ true);
|
|
2127
|
+
if (!session) {
|
|
2128
|
+
message.session_statusCode = StatusCodes.BadSessionIdInvalid;
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
message.session = session;
|
|
2132
|
+
|
|
2133
|
+
// --- check that provided session matches session attached to channel
|
|
2134
|
+
if (channel.channelId !== session.channelId) {
|
|
2135
|
+
if (!(request instanceof ActivateSessionRequest)) {
|
|
2136
|
+
errorLog(chalk.red.bgWhite("ERROR: channel.channelId !== session.channelId"), channel.channelId, session.channelId);
|
|
2137
|
+
}
|
|
2138
|
+
message.session_statusCode = StatusCodes.BadSecureChannelIdInvalid;
|
|
2139
|
+
} else if (channel_has_session(channel, session)) {
|
|
2140
|
+
message.session_statusCode = StatusCodes.Good;
|
|
2141
|
+
} else {
|
|
2142
|
+
// session ma y have been moved to a different channel
|
|
2143
|
+
message.session_statusCode = StatusCodes.BadSecureChannelIdInvalid;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
/**
|
|
2148
|
+
* ensure that action is performed on a valid session object,
|
|
2149
|
+
* @method _apply_on_SessionObject
|
|
2150
|
+
* @param ResponseClass the constructor of the response Class
|
|
2151
|
+
* @param message
|
|
2152
|
+
* @param channel
|
|
2153
|
+
* @param action_to_perform
|
|
2154
|
+
* @param action_to_perform.session {ServerSession}
|
|
2155
|
+
* @param action_to_perform.sendResponse
|
|
2156
|
+
* @param action_to_perform.sendResponse.response
|
|
2157
|
+
* @param action_to_perform.sendError
|
|
2158
|
+
* @param action_to_perform.sendError.statusCode
|
|
2159
|
+
* @param action_to_perform.sendError.diagnostics
|
|
2160
|
+
*
|
|
2161
|
+
* @private
|
|
2162
|
+
*/
|
|
2163
|
+
protected async _apply_on_SessionObject(
|
|
2164
|
+
ResponseClass: any,
|
|
2165
|
+
message: Message,
|
|
2166
|
+
channel: ServerSecureChannelLayer,
|
|
2167
|
+
action_to_perform: (
|
|
2168
|
+
session: ServerSession,
|
|
2169
|
+
sendResponse: (response: Response) => void,
|
|
2170
|
+
sendError: (statusCode: StatusCode) => void
|
|
2171
|
+
) => void | Promise<void>
|
|
2172
|
+
) {
|
|
2173
|
+
assert(typeof action_to_perform === "function");
|
|
2174
|
+
|
|
2175
|
+
function sendResponse(response1: Response) {
|
|
2176
|
+
try {
|
|
2177
|
+
assert(response1 instanceof ResponseClass);
|
|
2178
|
+
if (message.session) {
|
|
2179
|
+
const counterName = ResponseClass.name.replace("Response", "");
|
|
2180
|
+
message.session.incrementRequestTotalCounter(counterName);
|
|
2181
|
+
}
|
|
2182
|
+
return channel.send_response("MSG", response1, message);
|
|
2183
|
+
} catch (err) {
|
|
2184
|
+
if (err instanceof Error) {
|
|
2185
|
+
// istanbul ignore next
|
|
2186
|
+
errorLog(
|
|
2187
|
+
"Internal error in issuing response\nplease contact support@sterfive.com",
|
|
2188
|
+
message.request.toString(),
|
|
2189
|
+
"\n",
|
|
2190
|
+
response1.toString()
|
|
2191
|
+
);
|
|
2192
|
+
}
|
|
2193
|
+
// istanbul ignore next
|
|
2194
|
+
throw err;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
function sendError(statusCode: StatusCode) {
|
|
2199
|
+
if (message.session) {
|
|
2200
|
+
message.session.incrementRequestErrorCounter(ResponseClass.name.replace("Response", ""));
|
|
2201
|
+
}
|
|
2202
|
+
return g_sendError(channel, message, ResponseClass, statusCode);
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
let response: any;
|
|
2206
|
+
/* istanbul ignore next */
|
|
2207
|
+
if (!message.session || message.session_statusCode !== StatusCodes.Good) {
|
|
2208
|
+
const errMessage = "INVALID SESSION !! ";
|
|
2209
|
+
response = new ResponseClass({ responseHeader: { serviceResult: message.session_statusCode } });
|
|
2210
|
+
debugLog(chalk.red.bold(errMessage), chalk.yellow(message.session_statusCode!.toString()), response.constructor.name);
|
|
2211
|
+
return sendResponse(response);
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
assert(message.session_statusCode === StatusCodes.Good);
|
|
2215
|
+
|
|
2216
|
+
// OPC UA Specification 1.02 part 4 page 26
|
|
2217
|
+
// When a Session is terminated, all outstanding requests on the Session are aborted and
|
|
2218
|
+
// Bad_SessionClosed StatusCodes are returned to the Client. In addition, the Server deletes the entry
|
|
2219
|
+
// for the Client from its SessionDiagnostics Array Variable and notifies any other Clients who were
|
|
2220
|
+
// subscribed to this entry.
|
|
2221
|
+
if (message.session.status === "closed") {
|
|
2222
|
+
// note : use StatusCodes.BadSessionClosed , for pending message for this session
|
|
2223
|
+
return sendError(StatusCodes.BadSessionIdInvalid);
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
if (message.session.status === "new") {
|
|
2227
|
+
// mark session as being screwed ! so it cannot be activated anymore
|
|
2228
|
+
message.session.status = "screwed";
|
|
2229
|
+
|
|
2230
|
+
return sendError(StatusCodes.BadSessionNotActivated);
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
if (message.session.status !== "active") {
|
|
2234
|
+
// mark session as being screwed ! so it cannot be activated anymore
|
|
2235
|
+
message.session.status = "screwed";
|
|
2236
|
+
|
|
2237
|
+
// note : use StatusCodes.BadSessionClosed , for pending message for this session
|
|
2238
|
+
return sendError(StatusCodes.BadSessionIdInvalid);
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
// lets also reset the session watchdog so it doesn't
|
|
2242
|
+
// (Sessions are terminated by the Server automatically if the Client fails to issue a Service
|
|
2243
|
+
// request on the Session within the timeout period negotiated by the Server in the
|
|
2244
|
+
// CreateSession Service response. )
|
|
2245
|
+
if (message.session.keepAlive) {
|
|
2246
|
+
assert(typeof message.session.keepAlive === "function");
|
|
2247
|
+
message.session.keepAlive();
|
|
2248
|
+
}
|
|
2249
|
+
message.session.incrementTotalRequestCount();
|
|
2250
|
+
await action_to_perform(message.session as ServerSession, sendResponse, sendError);
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
/**
|
|
2254
|
+
* @method _apply_on_Subscription
|
|
2255
|
+
* @param ResponseClass
|
|
2256
|
+
* @param message
|
|
2257
|
+
* @param channel
|
|
2258
|
+
* @param action_to_perform
|
|
2259
|
+
* @private
|
|
2260
|
+
*/
|
|
2261
|
+
protected async _apply_on_Subscription(
|
|
2262
|
+
ResponseClass: any,
|
|
2263
|
+
message: Message,
|
|
2264
|
+
channel: ServerSecureChannelLayer,
|
|
2265
|
+
action_to_perform: (
|
|
2266
|
+
session: ServerSession,
|
|
2267
|
+
subscription: Subscription,
|
|
2268
|
+
sendResponse: (response: Response) => void,
|
|
2269
|
+
sendError: (statusCode: StatusCode) => void
|
|
2270
|
+
) => Promise<void>
|
|
2271
|
+
) {
|
|
2272
|
+
assert(typeof action_to_perform === "function");
|
|
2273
|
+
const request = message.request as unknown as { subscriptionId: number };
|
|
2274
|
+
assert(request.hasOwnProperty("subscriptionId"));
|
|
2275
|
+
|
|
2276
|
+
this._apply_on_SessionObject(
|
|
2277
|
+
ResponseClass,
|
|
2278
|
+
message,
|
|
2279
|
+
channel,
|
|
2280
|
+
async (
|
|
2281
|
+
session: ServerSession,
|
|
2282
|
+
sendResponse: (response: Response) => void,
|
|
2283
|
+
sendError: (statusCode: StatusCode) => void
|
|
2284
|
+
) => {
|
|
2285
|
+
const subscription = session.getSubscription(request.subscriptionId);
|
|
2286
|
+
if (!subscription) {
|
|
2287
|
+
return sendError(StatusCodes.BadSubscriptionIdInvalid);
|
|
2288
|
+
}
|
|
2289
|
+
subscription.resetLifeTimeAndKeepAliveCounters();
|
|
2290
|
+
await action_to_perform(session, subscription, sendResponse, sendError);
|
|
2291
|
+
}
|
|
2292
|
+
);
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
/**
|
|
2296
|
+
* @method _apply_on_SubscriptionIds
|
|
2297
|
+
* @param ResponseClass
|
|
2298
|
+
* @param message
|
|
2299
|
+
* @param channel
|
|
2300
|
+
* @param action_to_perform
|
|
2301
|
+
* @private
|
|
2302
|
+
*/
|
|
2303
|
+
protected _apply_on_SubscriptionIds<T>(
|
|
2304
|
+
ResponseClass: typeof SetPublishingModeResponse | typeof TransferSubscriptionsResponse | typeof DeleteSubscriptionsResponse,
|
|
2305
|
+
message: Message,
|
|
2306
|
+
channel: ServerSecureChannelLayer,
|
|
2307
|
+
action_to_perform: (session: ServerSession, subscriptionId: number) => Promise<T>
|
|
2308
|
+
) {
|
|
2309
|
+
assert(typeof action_to_perform === "function");
|
|
2310
|
+
const request = message.request as unknown as { subscriptionIds: number[] };
|
|
2311
|
+
assert(request.hasOwnProperty("subscriptionIds"));
|
|
2312
|
+
|
|
2313
|
+
this._apply_on_SessionObject(
|
|
2314
|
+
ResponseClass,
|
|
2315
|
+
message,
|
|
2316
|
+
channel,
|
|
2317
|
+
async (
|
|
2318
|
+
session: ServerSession,
|
|
2319
|
+
sendResponse: (response: Response) => void,
|
|
2320
|
+
sendError: (statusCode: StatusCode) => void
|
|
2321
|
+
) => {
|
|
2322
|
+
const subscriptionIds = request.subscriptionIds;
|
|
2323
|
+
|
|
2324
|
+
if (!request.subscriptionIds || request.subscriptionIds.length === 0) {
|
|
2325
|
+
return sendError(StatusCodes.BadNothingToDo);
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
// if (request.subscriptionIds.length > OPCUAServer.MAX_SUBSCRIPTION) {
|
|
2329
|
+
// return sendError(StatusCodes.BadTooManyOperations);
|
|
2330
|
+
// }
|
|
2331
|
+
|
|
2332
|
+
const results: any[] = subscriptionIds.map((subscriptionId: number) => action_to_perform(session, subscriptionId));
|
|
2333
|
+
|
|
2334
|
+
// resolve potential pending promises ....
|
|
2335
|
+
for (let i = 0; i < results.length; i++) {
|
|
2336
|
+
if (results[i].then) {
|
|
2337
|
+
results[i] = await results[i];
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
const response = new ResponseClass({
|
|
2342
|
+
responseHeader: {
|
|
2343
|
+
serviceResult:
|
|
2344
|
+
request.subscriptionIds.length > OPCUAServer.MAX_SUBSCRIPTION
|
|
2345
|
+
? StatusCodes.BadTooManyOperations
|
|
2346
|
+
: StatusCodes.Good
|
|
2347
|
+
},
|
|
2348
|
+
results
|
|
2349
|
+
});
|
|
2350
|
+
sendResponse(response);
|
|
2351
|
+
}
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
/**
|
|
2356
|
+
* @method _apply_on_Subscriptions
|
|
2357
|
+
* @param ResponseClass
|
|
2358
|
+
* @param message
|
|
2359
|
+
* @param channel
|
|
2360
|
+
* @param action_to_perform
|
|
2361
|
+
* @private
|
|
2362
|
+
*/
|
|
2363
|
+
protected _apply_on_Subscriptions(
|
|
2364
|
+
ResponseClass: typeof SetPublishingModeResponse,
|
|
2365
|
+
message: Message,
|
|
2366
|
+
channel: ServerSecureChannelLayer,
|
|
2367
|
+
action_to_perform: (session: ServerSession, subscription: Subscription) => Promise<StatusCode>
|
|
2368
|
+
) {
|
|
2369
|
+
this._apply_on_SubscriptionIds<StatusCode>(
|
|
2370
|
+
ResponseClass,
|
|
2371
|
+
message,
|
|
2372
|
+
channel,
|
|
2373
|
+
async (session: ServerSession, subscriptionId: number) => {
|
|
2374
|
+
/* istanbul ignore next */
|
|
2375
|
+
if (isSubscriptionIdInvalid(subscriptionId)) {
|
|
2376
|
+
return StatusCodes.BadSubscriptionIdInvalid;
|
|
2377
|
+
}
|
|
2378
|
+
const subscription = session.getSubscription(subscriptionId);
|
|
2379
|
+
if (!subscription) {
|
|
2380
|
+
return StatusCodes.BadSubscriptionIdInvalid;
|
|
2381
|
+
}
|
|
2382
|
+
return action_to_perform(session, subscription);
|
|
2383
|
+
}
|
|
2384
|
+
);
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
private async _closeSession(authenticationToken: NodeId, deleteSubscriptions: boolean, reason: ClosingReason) {
|
|
2388
|
+
const server = this;
|
|
2389
|
+
|
|
2390
|
+
//
|
|
2391
|
+
if (deleteSubscriptions && this.options.onDeleteMonitoredItem) {
|
|
2392
|
+
const session = this.getSession(authenticationToken);
|
|
2393
|
+
if (session) {
|
|
2394
|
+
const subscriptions = session.publishEngine.subscriptions;
|
|
2395
|
+
for (const subscription of subscriptions) {
|
|
2396
|
+
await subscription.applyOnMonitoredItem(this.options.onDeleteMonitoredItem.bind(null, subscription) as any);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
await server.engine.closeSession(authenticationToken, deleteSubscriptions, reason);
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* @method _on_CloseSessionRequest
|
|
2404
|
+
* @param message
|
|
2405
|
+
* @param channel
|
|
2406
|
+
* @private
|
|
2407
|
+
*/
|
|
2408
|
+
protected _on_CloseSessionRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
2409
|
+
const server = this;
|
|
2410
|
+
|
|
2411
|
+
const request = message.request as CloseSessionRequest;
|
|
2412
|
+
assert(request instanceof CloseSessionRequest);
|
|
2413
|
+
|
|
2414
|
+
let response;
|
|
2415
|
+
|
|
2416
|
+
message.session_statusCode = StatusCodes.Good;
|
|
2417
|
+
|
|
2418
|
+
function sendError(statusCode: StatusCode) {
|
|
2419
|
+
return g_sendError(channel, message, CloseSessionResponse, statusCode);
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
function sendResponse(response1: CloseSessionResponse) {
|
|
2423
|
+
channel.send_response("MSG", response1, message);
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
// do not use _apply_on_SessionObject
|
|
2427
|
+
// this._apply_on_SessionObject(CloseSessionResponse, message, channel, function (session) {
|
|
2428
|
+
// });
|
|
2429
|
+
|
|
2430
|
+
const session = message.session;
|
|
2431
|
+
if (!session) {
|
|
2432
|
+
return sendError(StatusCodes.BadSessionIdInvalid);
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
// session has been created but not activated !
|
|
2436
|
+
const wasNotActivated = session.status === "new";
|
|
2437
|
+
|
|
2438
|
+
(async () => {
|
|
2439
|
+
await this._closeSession(request.requestHeader.authenticationToken, request.deleteSubscriptions, "CloseSession");
|
|
2440
|
+
|
|
2441
|
+
// if (false && wasNotActivated) {
|
|
2442
|
+
// return sendError(StatusCodes.BadSessionNotActivated);
|
|
2443
|
+
// }
|
|
2444
|
+
|
|
2445
|
+
response = new CloseSessionResponse({});
|
|
2446
|
+
sendResponse(response);
|
|
2447
|
+
})();
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// browse services
|
|
2451
|
+
/**
|
|
2452
|
+
* @method _on_BrowseRequest
|
|
2453
|
+
* @param message
|
|
2454
|
+
* @param channel
|
|
2455
|
+
* @private
|
|
2456
|
+
*/
|
|
2457
|
+
protected _on_BrowseRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
2458
|
+
const server = this;
|
|
2459
|
+
const request = message.request as BrowseRequest;
|
|
2460
|
+
assert(request instanceof BrowseRequest);
|
|
2461
|
+
const diagnostic: any = {};
|
|
2462
|
+
|
|
2463
|
+
this._apply_on_SessionObject(
|
|
2464
|
+
BrowseResponse,
|
|
2465
|
+
message,
|
|
2466
|
+
channel,
|
|
2467
|
+
(session: ServerSession, sendResponse: (response: Response) => void, sendError: (statusCode: StatusCode) => void) => {
|
|
2468
|
+
let response: BrowseResponse;
|
|
2469
|
+
// test view
|
|
2470
|
+
if (request.view && !request.view.viewId.isEmpty()) {
|
|
2471
|
+
let theView: UAView | null = server.engine!.addressSpace!.findNode(request.view.viewId) as UAView;
|
|
2472
|
+
if (theView && theView.nodeClass !== NodeClass.View) {
|
|
2473
|
+
// Error: theView is not a View
|
|
2474
|
+
diagnostic.localizedText = { text: "Expecting a view here" };
|
|
2475
|
+
theView = null;
|
|
2476
|
+
}
|
|
2477
|
+
if (!theView) {
|
|
2478
|
+
return sendError(StatusCodes.BadViewIdUnknown);
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
if (!request.nodesToBrowse || request.nodesToBrowse.length === 0) {
|
|
2483
|
+
return sendError(StatusCodes.BadNothingToDo);
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
if (server.engine.serverCapabilities.operationLimits.maxNodesPerBrowse > 0) {
|
|
2487
|
+
if (request.nodesToBrowse.length > server.engine.serverCapabilities.operationLimits.maxNodesPerBrowse) {
|
|
2488
|
+
return sendError(StatusCodes.BadTooManyOperations);
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
// limit results to requestedMaxReferencesPerNode further so it never exceed a too big number
|
|
2493
|
+
const requestedMaxReferencesPerNode = Math.min(9876, request.requestedMaxReferencesPerNode);
|
|
2494
|
+
assert(request.nodesToBrowse[0].schema.name === "BrowseDescription");
|
|
2495
|
+
|
|
2496
|
+
const context = new SessionContext({ session, server });
|
|
2497
|
+
|
|
2498
|
+
const f = callbackify(server.engine.browseWithAutomaticExpansion).bind(server.engine);
|
|
2499
|
+
(f as any)(request.nodesToBrowse, context, (err?: Error | null, results?: BrowseResult[]) => {
|
|
2500
|
+
// istanbul ignore next
|
|
2501
|
+
if (!results) {
|
|
2502
|
+
throw new Error("internal error : " + err?.message);
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
assert(results[0].schema.name === "BrowseResult");
|
|
2506
|
+
|
|
2507
|
+
// handle continuation point and requestedMaxReferencesPerNode
|
|
2508
|
+
const maxBrowseContinuationPoints = server.engine.serverCapabilities.maxBrowseContinuationPoints;
|
|
2509
|
+
results = results.map((result: BrowseResult) => {
|
|
2510
|
+
assert(!result.continuationPoint);
|
|
2511
|
+
|
|
2512
|
+
// istanbul ignore next
|
|
2513
|
+
if (!session.continuationPointManager) {
|
|
2514
|
+
return new BrowseResult({ statusCode: StatusCodes.BadNoContinuationPoints });
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
if (session.continuationPointManager.hasReachMaximum(maxBrowseContinuationPoints)) {
|
|
2518
|
+
return new BrowseResult({ statusCode: StatusCodes.BadNoContinuationPoints });
|
|
2519
|
+
}
|
|
2520
|
+
const truncatedResult = session.continuationPointManager.register(
|
|
2521
|
+
requestedMaxReferencesPerNode,
|
|
2522
|
+
result.references || []
|
|
2523
|
+
);
|
|
2524
|
+
assert(truncatedResult.statusCode === StatusCodes.Good);
|
|
2525
|
+
truncatedResult.statusCode = result.statusCode;
|
|
2526
|
+
return new BrowseResult(truncatedResult);
|
|
2527
|
+
});
|
|
2528
|
+
|
|
2529
|
+
response = new BrowseResponse({
|
|
2530
|
+
diagnosticInfos: undefined,
|
|
2531
|
+
results
|
|
2532
|
+
});
|
|
2533
|
+
sendResponse(response);
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
);
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
/**
|
|
2540
|
+
* @method _on_BrowseNextRequest
|
|
2541
|
+
* @param message
|
|
2542
|
+
* @param channel
|
|
2543
|
+
* @private
|
|
2544
|
+
*/
|
|
2545
|
+
protected _on_BrowseNextRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
2546
|
+
const request = message.request as BrowseNextRequest;
|
|
2547
|
+
assert(request instanceof BrowseNextRequest);
|
|
2548
|
+
this._apply_on_SessionObject(
|
|
2549
|
+
BrowseNextResponse,
|
|
2550
|
+
message,
|
|
2551
|
+
channel,
|
|
2552
|
+
(session: ServerSession, sendResponse: (response: Response) => void, sendError: (statusCode: StatusCode) => void) => {
|
|
2553
|
+
let response;
|
|
2554
|
+
|
|
2555
|
+
if (!request.continuationPoints || request.continuationPoints.length === 0) {
|
|
2556
|
+
return sendError(StatusCodes.BadNothingToDo);
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// A Boolean parameter with the following values:
|
|
2560
|
+
|
|
2561
|
+
let results;
|
|
2562
|
+
if (request.releaseContinuationPoints) {
|
|
2563
|
+
// releaseContinuationPoints = TRUE
|
|
2564
|
+
// passed continuationPoints shall be reset to free resources in
|
|
2565
|
+
// the Server. The continuation points are released and the results
|
|
2566
|
+
// and diagnosticInfos arrays are empty.
|
|
2567
|
+
results = request.continuationPoints.map((continuationPoint: ContinuationPoint) => {
|
|
2568
|
+
return session.continuationPointManager.cancel(continuationPoint);
|
|
2569
|
+
});
|
|
2570
|
+
} else {
|
|
2571
|
+
// let extract data from continuation points
|
|
2572
|
+
|
|
2573
|
+
// releaseContinuationPoints = FALSE
|
|
2574
|
+
// passed continuationPoints shall be used to get the next set of
|
|
2575
|
+
// browse information.
|
|
2576
|
+
results = request.continuationPoints.map((continuationPoint: ContinuationPoint) => {
|
|
2577
|
+
return session.continuationPointManager.getNext(continuationPoint);
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
response = new BrowseNextResponse({
|
|
2582
|
+
diagnosticInfos: undefined,
|
|
2583
|
+
results
|
|
2584
|
+
});
|
|
2585
|
+
sendResponse(response);
|
|
2586
|
+
}
|
|
2587
|
+
);
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
// read services
|
|
2591
|
+
protected _on_ReadRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
2592
|
+
const server = this;
|
|
2593
|
+
const request = message.request as ReadRequest;
|
|
2594
|
+
assert(request instanceof ReadRequest);
|
|
2595
|
+
|
|
2596
|
+
this._apply_on_SessionObject(
|
|
2597
|
+
ReadResponse,
|
|
2598
|
+
message,
|
|
2599
|
+
channel,
|
|
2600
|
+
(session: ServerSession, sendResponse: (response: Response) => void, sendError: (statusCode: StatusCode) => void) => {
|
|
2601
|
+
const context = new SessionContext({ session, server });
|
|
2602
|
+
|
|
2603
|
+
let response;
|
|
2604
|
+
|
|
2605
|
+
let results = [];
|
|
2606
|
+
|
|
2607
|
+
const timestampsToReturn = request.timestampsToReturn;
|
|
2608
|
+
|
|
2609
|
+
if (timestampsToReturn === TimestampsToReturn.Invalid) {
|
|
2610
|
+
return sendError(StatusCodes.BadTimestampsToReturnInvalid);
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
if (request.maxAge < 0) {
|
|
2614
|
+
return sendError(StatusCodes.BadMaxAgeInvalid);
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
request.nodesToRead = request.nodesToRead || [];
|
|
2618
|
+
|
|
2619
|
+
if (!request.nodesToRead || request.nodesToRead.length <= 0) {
|
|
2620
|
+
return sendError(StatusCodes.BadNothingToDo);
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
assert(request.nodesToRead[0].schema.name === "ReadValueId");
|
|
2624
|
+
|
|
2625
|
+
// limit size of nodesToRead array to maxNodesPerRead
|
|
2626
|
+
if (server.engine.serverCapabilities.operationLimits.maxNodesPerRead > 0) {
|
|
2627
|
+
if (request.nodesToRead.length > server.engine.serverCapabilities.operationLimits.maxNodesPerRead) {
|
|
2628
|
+
return sendError(StatusCodes.BadTooManyOperations);
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
// proceed with registered nodes alias resolution
|
|
2633
|
+
for (const nodeToRead of request.nodesToRead) {
|
|
2634
|
+
nodeToRead.nodeId = session.resolveRegisteredNode(nodeToRead.nodeId);
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
// ask for a refresh of asynchronous variables
|
|
2638
|
+
server.engine.refreshValues(request.nodesToRead, request.maxAge, (err?: Error | null) => {
|
|
2639
|
+
assert(!err, " error not handled here , fix me");
|
|
2640
|
+
|
|
2641
|
+
results = server.engine.read(context, request);
|
|
2642
|
+
|
|
2643
|
+
assert(results[0].schema.name === "DataValue");
|
|
2644
|
+
assert(results.length === request.nodesToRead!.length);
|
|
2645
|
+
|
|
2646
|
+
response = new ReadResponse({
|
|
2647
|
+
diagnosticInfos: undefined,
|
|
2648
|
+
results: undefined
|
|
2649
|
+
});
|
|
2650
|
+
// set it here for performance
|
|
2651
|
+
response.results = results;
|
|
2652
|
+
assert(response.diagnosticInfos!.length === 0);
|
|
2653
|
+
sendResponse(response);
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
);
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
// read services
|
|
2660
|
+
protected _on_HistoryReadRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
2661
|
+
const server = this;
|
|
2662
|
+
const request = message.request as HistoryReadRequest;
|
|
2663
|
+
|
|
2664
|
+
assert(request instanceof HistoryReadRequest);
|
|
2665
|
+
|
|
2666
|
+
this._apply_on_SessionObject(
|
|
2667
|
+
HistoryReadResponse,
|
|
2668
|
+
message,
|
|
2669
|
+
channel,
|
|
2670
|
+
(session: ServerSession, sendResponse: (response: Response) => void, sendError: (statsCode: StatusCode) => void) => {
|
|
2671
|
+
let response;
|
|
2672
|
+
|
|
2673
|
+
const timestampsToReturn = request.timestampsToReturn;
|
|
2674
|
+
|
|
2675
|
+
if (timestampsToReturn === TimestampsToReturn.Invalid) {
|
|
2676
|
+
return sendError(StatusCodes.BadTimestampsToReturnInvalid);
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
request.nodesToRead = request.nodesToRead || [];
|
|
2680
|
+
|
|
2681
|
+
if (!request.nodesToRead || request.nodesToRead.length <= 0) {
|
|
2682
|
+
return sendError(StatusCodes.BadNothingToDo);
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
assert(request.nodesToRead[0].schema.name === "HistoryReadValueId");
|
|
2686
|
+
|
|
2687
|
+
// limit size of nodesToRead array to maxNodesPerRead
|
|
2688
|
+
if (server.engine.serverCapabilities.operationLimits.maxNodesPerRead > 0) {
|
|
2689
|
+
if (request.nodesToRead.length > server.engine.serverCapabilities.operationLimits.maxNodesPerRead) {
|
|
2690
|
+
return sendError(StatusCodes.BadTooManyOperations);
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
// todo : handle
|
|
2694
|
+
if (server.engine.serverCapabilities.operationLimits.maxNodesPerHistoryReadData > 0) {
|
|
2695
|
+
if (request.nodesToRead.length > server.engine.serverCapabilities.operationLimits.maxNodesPerHistoryReadData) {
|
|
2696
|
+
return sendError(StatusCodes.BadTooManyOperations);
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
if (server.engine.serverCapabilities.operationLimits.maxNodesPerHistoryReadEvents > 0) {
|
|
2700
|
+
if (
|
|
2701
|
+
request.nodesToRead.length > server.engine.serverCapabilities.operationLimits.maxNodesPerHistoryReadEvents
|
|
2702
|
+
) {
|
|
2703
|
+
return sendError(StatusCodes.BadTooManyOperations);
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
const context = new SessionContext({ session, server });
|
|
2708
|
+
|
|
2709
|
+
// ask for a refresh of asynchronous variables
|
|
2710
|
+
server.engine.refreshValues(request.nodesToRead, 0, (err?: Error | null) => {
|
|
2711
|
+
assert(!err, " error not handled here , fix me"); // TODO
|
|
2712
|
+
|
|
2713
|
+
server.engine.historyRead(context, request, (err1: Error | null, results?: HistoryReadResult[]) => {
|
|
2714
|
+
if (err1) {
|
|
2715
|
+
return sendError(StatusCodes.BadHistoryOperationInvalid);
|
|
2716
|
+
}
|
|
2717
|
+
if (!results) {
|
|
2718
|
+
return sendError(StatusCodes.BadHistoryOperationInvalid);
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
assert(results[0].schema.name === "HistoryReadResult");
|
|
2722
|
+
assert(results.length === request.nodesToRead!.length);
|
|
2723
|
+
|
|
2724
|
+
response = new HistoryReadResponse({
|
|
2725
|
+
diagnosticInfos: undefined,
|
|
2726
|
+
results
|
|
2727
|
+
});
|
|
2728
|
+
|
|
2729
|
+
assert(response.diagnosticInfos!.length === 0);
|
|
2730
|
+
sendResponse(response);
|
|
2731
|
+
});
|
|
2732
|
+
});
|
|
2733
|
+
}
|
|
2734
|
+
);
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
/*
|
|
2738
|
+
// write services
|
|
2739
|
+
// OPCUA Specification 1.02 Part 3 : 5.10.4 Write
|
|
2740
|
+
// This Service is used to write values to one or more Attributes of one or more Nodes. For constructed
|
|
2741
|
+
// Attribute values whose elements are indexed, such as an array, this Service allows Clients to write
|
|
2742
|
+
// the entire set of indexed values as a composite, to write individual elements or to write ranges of
|
|
2743
|
+
// elements of the composite.
|
|
2744
|
+
// The values are written to the data source, such as a device, and the Service does not return until it writes
|
|
2745
|
+
// the values or determines that the value cannot be written. In certain cases, the Server will successfully
|
|
2746
|
+
// to an intermediate system or Server, and will not know if the data source was updated properly. In these cases,
|
|
2747
|
+
// the Server should report a success code that indicates that the write was not verified.
|
|
2748
|
+
// In the cases where the Server is able to verify that it has successfully written to the data source,
|
|
2749
|
+
// it reports an unconditional success.
|
|
2750
|
+
*/
|
|
2751
|
+
protected _on_WriteRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
2752
|
+
const server = this;
|
|
2753
|
+
const request = message.request as WriteRequest;
|
|
2754
|
+
assert(request instanceof WriteRequest);
|
|
2755
|
+
assert(!request.nodesToWrite || Array.isArray(request.nodesToWrite));
|
|
2756
|
+
|
|
2757
|
+
this._apply_on_SessionObject(
|
|
2758
|
+
WriteResponse,
|
|
2759
|
+
message,
|
|
2760
|
+
channel,
|
|
2761
|
+
(session: ServerSession, sendResponse: (response: Response) => void, sendError: (statusCode: StatusCode) => void) => {
|
|
2762
|
+
let response;
|
|
2763
|
+
|
|
2764
|
+
if (!request.nodesToWrite || request.nodesToWrite.length === 0) {
|
|
2765
|
+
return sendError(StatusCodes.BadNothingToDo);
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
if (server.engine.serverCapabilities.operationLimits.maxNodesPerWrite > 0) {
|
|
2769
|
+
if (request.nodesToWrite.length > server.engine.serverCapabilities.operationLimits.maxNodesPerWrite) {
|
|
2770
|
+
return sendError(StatusCodes.BadTooManyOperations);
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
// proceed with registered nodes alias resolution
|
|
2775
|
+
for (const nodeToWrite of request.nodesToWrite) {
|
|
2776
|
+
nodeToWrite.nodeId = session.resolveRegisteredNode(nodeToWrite.nodeId);
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
const context = new SessionContext({ session, server });
|
|
2780
|
+
|
|
2781
|
+
assert(request.nodesToWrite[0].schema.name === "WriteValue");
|
|
2782
|
+
server.engine.write(context, request.nodesToWrite, (err: Error | null, results?: StatusCode[]) => {
|
|
2783
|
+
assert(!err);
|
|
2784
|
+
assert(Array.isArray(results));
|
|
2785
|
+
assert(results!.length === request.nodesToWrite!.length);
|
|
2786
|
+
response = new WriteResponse({
|
|
2787
|
+
diagnosticInfos: undefined,
|
|
2788
|
+
results
|
|
2789
|
+
});
|
|
2790
|
+
sendResponse(response);
|
|
2791
|
+
});
|
|
2792
|
+
}
|
|
2793
|
+
);
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
// subscription services
|
|
2797
|
+
protected _on_CreateSubscriptionRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
2798
|
+
const server = this;
|
|
2799
|
+
const engine = server.engine;
|
|
2800
|
+
const addressSpace = engine.addressSpace!;
|
|
2801
|
+
|
|
2802
|
+
const request = message.request as CreateSubscriptionRequest;
|
|
2803
|
+
assert(request instanceof CreateSubscriptionRequest);
|
|
2804
|
+
|
|
2805
|
+
this._apply_on_SessionObject(
|
|
2806
|
+
CreateSubscriptionResponse,
|
|
2807
|
+
message,
|
|
2808
|
+
channel,
|
|
2809
|
+
(session: ServerSession, sendResponse: (response: Response) => void, sendError: (statusCode: StatusCode) => void) => {
|
|
2810
|
+
const context = new SessionContext({ session, server });
|
|
2811
|
+
|
|
2812
|
+
if (session.currentSubscriptionCount >= OPCUAServer.MAX_SUBSCRIPTION) {
|
|
2813
|
+
return sendError(StatusCodes.BadTooManySubscriptions);
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
const subscription = session.createSubscription(request);
|
|
2817
|
+
|
|
2818
|
+
subscription.on("monitoredItem", (monitoredItem: MonitoredItem) => {
|
|
2819
|
+
prepareMonitoredItem(context, addressSpace, monitoredItem);
|
|
2820
|
+
});
|
|
2821
|
+
|
|
2822
|
+
const response = new CreateSubscriptionResponse({
|
|
2823
|
+
revisedLifetimeCount: subscription.lifeTimeCount,
|
|
2824
|
+
revisedMaxKeepAliveCount: subscription.maxKeepAliveCount,
|
|
2825
|
+
revisedPublishingInterval: subscription.publishingInterval,
|
|
2826
|
+
subscriptionId: subscription.id
|
|
2827
|
+
});
|
|
2828
|
+
sendResponse(response);
|
|
2829
|
+
}
|
|
2830
|
+
);
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
protected _on_DeleteSubscriptionsRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
2834
|
+
const server = this;
|
|
2835
|
+
const request = message.request as DeleteSubscriptionsRequest;
|
|
2836
|
+
assert(request instanceof DeleteSubscriptionsRequest);
|
|
2837
|
+
this._apply_on_SubscriptionIds(
|
|
2838
|
+
DeleteSubscriptionsResponse,
|
|
2839
|
+
message,
|
|
2840
|
+
channel,
|
|
2841
|
+
async (session: ServerSession, subscriptionId: number) => {
|
|
2842
|
+
let subscription = server.engine.findOrphanSubscription(subscriptionId);
|
|
2843
|
+
// istanbul ignore next
|
|
2844
|
+
if (subscription) {
|
|
2845
|
+
warningLog("Deleting an orphan subscription", subscriptionId);
|
|
2846
|
+
|
|
2847
|
+
await this._beforeDeleteSubscription(subscription);
|
|
2848
|
+
return server.engine.deleteOrphanSubscription(subscription);
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
subscription = session.getSubscription(subscriptionId);
|
|
2852
|
+
if (subscription) {
|
|
2853
|
+
await this._beforeDeleteSubscription(subscription);
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
return session.deleteSubscription(subscriptionId);
|
|
2857
|
+
}
|
|
2858
|
+
);
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
protected _on_TransferSubscriptionsRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
2862
|
+
//
|
|
2863
|
+
// sendInitialValue Boolean
|
|
2864
|
+
// A Boolean parameter with the following values:
|
|
2865
|
+
// TRUE the first Publish response(s) after the TransferSubscriptions call shall
|
|
2866
|
+
// contain the current values of all Monitored Items in the Subscription where
|
|
2867
|
+
// the Monitoring Mode is set to Reporting.
|
|
2868
|
+
// FALSE the first Publish response after the TransferSubscriptions call shall contain only the value
|
|
2869
|
+
// changes since the last Publish response was sent.
|
|
2870
|
+
// This parameter only applies to MonitoredItems used for monitoring Attribute changes.
|
|
2871
|
+
//
|
|
2872
|
+
|
|
2873
|
+
const server = this;
|
|
2874
|
+
const engine = server.engine;
|
|
2875
|
+
|
|
2876
|
+
const request = message.request as TransferSubscriptionsRequest;
|
|
2877
|
+
assert(request instanceof TransferSubscriptionsRequest);
|
|
2878
|
+
this._apply_on_SubscriptionIds(
|
|
2879
|
+
TransferSubscriptionsResponse,
|
|
2880
|
+
message,
|
|
2881
|
+
channel,
|
|
2882
|
+
async (session: ServerSession, subscriptionId: number) =>
|
|
2883
|
+
await engine.transferSubscription(session, subscriptionId, request.sendInitialValues)
|
|
2884
|
+
);
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
protected _on_CreateMonitoredItemsRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
2888
|
+
const server = this;
|
|
2889
|
+
const engine = server.engine;
|
|
2890
|
+
const addressSpace = engine.addressSpace!;
|
|
2891
|
+
|
|
2892
|
+
const request = message.request as CreateMonitoredItemsRequest;
|
|
2893
|
+
assert(request instanceof CreateMonitoredItemsRequest);
|
|
2894
|
+
|
|
2895
|
+
this._apply_on_Subscription(
|
|
2896
|
+
CreateMonitoredItemsResponse,
|
|
2897
|
+
message,
|
|
2898
|
+
channel,
|
|
2899
|
+
async (
|
|
2900
|
+
session: ServerSession,
|
|
2901
|
+
subscription: Subscription,
|
|
2902
|
+
sendResponse: (response: Response) => void,
|
|
2903
|
+
sendError: (statusCode: StatusCode) => void
|
|
2904
|
+
): Promise<void> => {
|
|
2905
|
+
const timestampsToReturn = request.timestampsToReturn;
|
|
2906
|
+
if (timestampsToReturn === TimestampsToReturn.Invalid) {
|
|
2907
|
+
return sendError(StatusCodes.BadTimestampsToReturnInvalid);
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
if (!request.itemsToCreate || request.itemsToCreate.length === 0) {
|
|
2911
|
+
return sendError(StatusCodes.BadNothingToDo);
|
|
2912
|
+
}
|
|
2913
|
+
if (server.engine.serverCapabilities.operationLimits.maxMonitoredItemsPerCall > 0) {
|
|
2914
|
+
if (request.itemsToCreate.length > server.engine.serverCapabilities.operationLimits.maxMonitoredItemsPerCall) {
|
|
2915
|
+
return sendError(StatusCodes.BadTooManyOperations);
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
const options = this.options as OPCUAServerOptions;
|
|
2920
|
+
let results: MonitoredItemCreateResult[] = [];
|
|
2921
|
+
if (options.onCreateMonitoredItem) {
|
|
2922
|
+
const resultsPromise = request.itemsToCreate.map(async (monitoredItemCreateRequest) => {
|
|
2923
|
+
const { monitoredItem, createResult } = subscription.preCreateMonitoredItem(
|
|
2924
|
+
addressSpace,
|
|
2925
|
+
timestampsToReturn,
|
|
2926
|
+
monitoredItemCreateRequest
|
|
2927
|
+
);
|
|
2928
|
+
if (monitoredItem) {
|
|
2929
|
+
await options.onCreateMonitoredItem!(subscription, monitoredItem);
|
|
2930
|
+
subscription.postCreateMonitoredItem(monitoredItem, monitoredItemCreateRequest, createResult);
|
|
2931
|
+
}
|
|
2932
|
+
return createResult;
|
|
2933
|
+
});
|
|
2934
|
+
results = await Promise.all(resultsPromise);
|
|
2935
|
+
} else {
|
|
2936
|
+
results = request.itemsToCreate.map((monitoredItemCreateRequest) => {
|
|
2937
|
+
const { monitoredItem, createResult } = subscription.preCreateMonitoredItem(
|
|
2938
|
+
addressSpace,
|
|
2939
|
+
timestampsToReturn,
|
|
2940
|
+
monitoredItemCreateRequest
|
|
2941
|
+
);
|
|
2942
|
+
if (monitoredItem) {
|
|
2943
|
+
subscription.postCreateMonitoredItem(monitoredItem, monitoredItemCreateRequest, createResult);
|
|
2944
|
+
}
|
|
2945
|
+
return createResult;
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2948
|
+
const response = new CreateMonitoredItemsResponse({
|
|
2949
|
+
responseHeader: { serviceResult: StatusCodes.Good },
|
|
2950
|
+
results
|
|
2951
|
+
// ,diagnosticInfos: []
|
|
2952
|
+
});
|
|
2953
|
+
sendResponse(response);
|
|
2954
|
+
}
|
|
2955
|
+
);
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
protected _on_ModifySubscriptionRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
2959
|
+
const request = message.request as ModifySubscriptionRequest;
|
|
2960
|
+
assert(request instanceof ModifySubscriptionRequest);
|
|
2961
|
+
|
|
2962
|
+
this._apply_on_Subscription(
|
|
2963
|
+
ModifySubscriptionResponse,
|
|
2964
|
+
message,
|
|
2965
|
+
channel,
|
|
2966
|
+
async (
|
|
2967
|
+
session: ServerSession,
|
|
2968
|
+
subscription: Subscription,
|
|
2969
|
+
sendResponse: (response: ModifySubscriptionResponse) => void,
|
|
2970
|
+
sendError: (statusCode: StatusCode) => void
|
|
2971
|
+
) => {
|
|
2972
|
+
subscription.modify(request);
|
|
2973
|
+
|
|
2974
|
+
const response = new ModifySubscriptionResponse({
|
|
2975
|
+
revisedLifetimeCount: subscription.lifeTimeCount,
|
|
2976
|
+
revisedMaxKeepAliveCount: subscription.maxKeepAliveCount,
|
|
2977
|
+
revisedPublishingInterval: subscription.publishingInterval
|
|
2978
|
+
});
|
|
2979
|
+
|
|
2980
|
+
sendResponse(response);
|
|
2981
|
+
}
|
|
2982
|
+
);
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
protected _on_ModifyMonitoredItemsRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
2986
|
+
const server = this;
|
|
2987
|
+
const request = message.request as ModifyMonitoredItemsRequest;
|
|
2988
|
+
|
|
2989
|
+
assert(request instanceof ModifyMonitoredItemsRequest);
|
|
2990
|
+
this._apply_on_Subscription(
|
|
2991
|
+
ModifyMonitoredItemsResponse,
|
|
2992
|
+
message,
|
|
2993
|
+
channel,
|
|
2994
|
+
async (
|
|
2995
|
+
session: ServerSession,
|
|
2996
|
+
subscription: Subscription,
|
|
2997
|
+
sendResponse: (response: ModifyMonitoredItemsResponse) => void,
|
|
2998
|
+
sendError: (statusCode: StatusCode) => void
|
|
2999
|
+
) => {
|
|
3000
|
+
const timestampsToReturn = request.timestampsToReturn;
|
|
3001
|
+
if (timestampsToReturn === TimestampsToReturn.Invalid) {
|
|
3002
|
+
return sendError(StatusCodes.BadTimestampsToReturnInvalid);
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
if (!request.itemsToModify || request.itemsToModify.length === 0) {
|
|
3006
|
+
return sendError(StatusCodes.BadNothingToDo);
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
/* istanbul ignore next */
|
|
3010
|
+
if (server.engine.serverCapabilities.operationLimits.maxMonitoredItemsPerCall > 0) {
|
|
3011
|
+
if (request.itemsToModify.length > server.engine.serverCapabilities.operationLimits.maxMonitoredItemsPerCall) {
|
|
3012
|
+
return sendError(StatusCodes.BadTooManyOperations);
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
const itemsToModify = request.itemsToModify; // MonitoredItemModifyRequest
|
|
3017
|
+
|
|
3018
|
+
function modifyMonitoredItem(item: MonitoredItemModifyRequest) {
|
|
3019
|
+
const monitoredItemId = item.monitoredItemId;
|
|
3020
|
+
const monitoredItem = subscription.getMonitoredItem(monitoredItemId);
|
|
3021
|
+
if (!monitoredItem) {
|
|
3022
|
+
return new MonitoredItemModifyResult({ statusCode: StatusCodes.BadMonitoredItemIdInvalid });
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
// adjust samplingInterval if === -1
|
|
3026
|
+
if (item.requestedParameters.samplingInterval === -1) {
|
|
3027
|
+
item.requestedParameters.samplingInterval = subscription.publishingInterval;
|
|
3028
|
+
}
|
|
3029
|
+
return monitoredItem.modify(timestampsToReturn, item.requestedParameters);
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
const results = itemsToModify.map(modifyMonitoredItem);
|
|
3033
|
+
|
|
3034
|
+
const response = new ModifyMonitoredItemsResponse({
|
|
3035
|
+
results
|
|
3036
|
+
});
|
|
3037
|
+
sendResponse(response);
|
|
3038
|
+
}
|
|
3039
|
+
);
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
protected _on_PublishRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
3043
|
+
const request = message.request as PublishRequest;
|
|
3044
|
+
assert(request instanceof PublishRequest);
|
|
3045
|
+
|
|
3046
|
+
this._apply_on_SessionObject(
|
|
3047
|
+
PublishResponse,
|
|
3048
|
+
message,
|
|
3049
|
+
channel,
|
|
3050
|
+
(session: ServerSession, sendResponse: (response: Response) => void, sendError: (statusCode: StatusCode) => void) => {
|
|
3051
|
+
assert(session);
|
|
3052
|
+
assert(session.publishEngine); // server.publishEngine doesn't exists, OPCUAServer has probably shut down already
|
|
3053
|
+
session.publishEngine._on_PublishRequest(request, (request1: any, response: any) => {
|
|
3054
|
+
sendResponse(response);
|
|
3055
|
+
});
|
|
3056
|
+
}
|
|
3057
|
+
);
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
protected _on_SetPublishingModeRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
3061
|
+
const request = message.request as SetPublishingModeRequest;
|
|
3062
|
+
assert(request instanceof SetPublishingModeRequest);
|
|
3063
|
+
const publishingEnabled = request.publishingEnabled;
|
|
3064
|
+
this._apply_on_Subscriptions(
|
|
3065
|
+
SetPublishingModeResponse,
|
|
3066
|
+
message,
|
|
3067
|
+
channel,
|
|
3068
|
+
async (session: ServerSession, subscription: Subscription) => {
|
|
3069
|
+
return subscription.setPublishingMode(publishingEnabled);
|
|
3070
|
+
}
|
|
3071
|
+
);
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
protected _on_DeleteMonitoredItemsRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
3075
|
+
const server = this;
|
|
3076
|
+
const request = message.request as DeleteMonitoredItemsRequest;
|
|
3077
|
+
assert(request instanceof DeleteMonitoredItemsRequest);
|
|
3078
|
+
|
|
3079
|
+
this._apply_on_Subscription(
|
|
3080
|
+
DeleteMonitoredItemsResponse,
|
|
3081
|
+
message,
|
|
3082
|
+
channel,
|
|
3083
|
+
async (
|
|
3084
|
+
session: ServerSession,
|
|
3085
|
+
subscription: Subscription,
|
|
3086
|
+
sendResponse: (response: Response) => void,
|
|
3087
|
+
sendError: (statusCode: StatusCode) => void
|
|
3088
|
+
) => {
|
|
3089
|
+
/* istanbul ignore next */
|
|
3090
|
+
if (!request.monitoredItemIds || request.monitoredItemIds.length === 0) {
|
|
3091
|
+
return sendError(StatusCodes.BadNothingToDo);
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
/* istanbul ignore next */
|
|
3095
|
+
if (server.engine.serverCapabilities.operationLimits.maxMonitoredItemsPerCall > 0) {
|
|
3096
|
+
if (
|
|
3097
|
+
request.monitoredItemIds.length > server.engine.serverCapabilities.operationLimits.maxMonitoredItemsPerCall
|
|
3098
|
+
) {
|
|
3099
|
+
return sendError(StatusCodes.BadTooManyOperations);
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
const resultsPromises = request.monitoredItemIds.map(async (monitoredItemId: number) => {
|
|
3104
|
+
if (this.options.onDeleteMonitoredItem) {
|
|
3105
|
+
const monitoredItem = subscription.getMonitoredItem(monitoredItemId);
|
|
3106
|
+
if (monitoredItem) {
|
|
3107
|
+
await this.options.onDeleteMonitoredItem(subscription, monitoredItem);
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
return subscription.removeMonitoredItem(monitoredItemId);
|
|
3111
|
+
});
|
|
3112
|
+
|
|
3113
|
+
try {
|
|
3114
|
+
const results = await Promise.all(resultsPromises);
|
|
3115
|
+
|
|
3116
|
+
const response = new DeleteMonitoredItemsResponse({
|
|
3117
|
+
diagnosticInfos: undefined,
|
|
3118
|
+
results
|
|
3119
|
+
});
|
|
3120
|
+
|
|
3121
|
+
sendResponse(response);
|
|
3122
|
+
} catch (err) {
|
|
3123
|
+
console.log(err);
|
|
3124
|
+
return sendError(StatusCodes.BadInternalError);
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
);
|
|
3128
|
+
}
|
|
3129
|
+
protected _on_SetTriggeringRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
3130
|
+
const server = this;
|
|
3131
|
+
const request = message.request as SetTriggeringRequest;
|
|
3132
|
+
assert(request instanceof SetTriggeringRequest);
|
|
3133
|
+
|
|
3134
|
+
this._apply_on_Subscription(
|
|
3135
|
+
SetTriggeringResponse,
|
|
3136
|
+
message,
|
|
3137
|
+
channel,
|
|
3138
|
+
async (
|
|
3139
|
+
session: ServerSession,
|
|
3140
|
+
subscription: Subscription,
|
|
3141
|
+
sendResponse: (response: Response) => void,
|
|
3142
|
+
sendError: (statusCode: StatusCode) => void
|
|
3143
|
+
) => {
|
|
3144
|
+
/* */
|
|
3145
|
+
const { triggeringItemId, linksToAdd, linksToRemove } = request;
|
|
3146
|
+
|
|
3147
|
+
/**
|
|
3148
|
+
* The MaxMonitoredItemsPerCall Property indicates
|
|
3149
|
+
* [...]
|
|
3150
|
+
* • the maximum size of the sum of the linksToAdd and linksToRemove arrays when a
|
|
3151
|
+
* Client calls the SetTriggering Service.
|
|
3152
|
+
*
|
|
3153
|
+
*/
|
|
3154
|
+
const maxElements = (linksToAdd ? linksToAdd.length : 0) + (linksToRemove ? linksToRemove.length : 0);
|
|
3155
|
+
if (server.engine.serverCapabilities.operationLimits.maxMonitoredItemsPerCall > 0) {
|
|
3156
|
+
if (maxElements > server.engine.serverCapabilities.operationLimits.maxMonitoredItemsPerCall) {
|
|
3157
|
+
return sendError(StatusCodes.BadTooManyOperations);
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
const { addResults, removeResults, statusCode } = subscription.setTriggering(
|
|
3162
|
+
triggeringItemId,
|
|
3163
|
+
linksToAdd,
|
|
3164
|
+
linksToRemove
|
|
3165
|
+
);
|
|
3166
|
+
const response = new SetTriggeringResponse({
|
|
3167
|
+
responseHeader: { serviceResult: statusCode },
|
|
3168
|
+
|
|
3169
|
+
addResults,
|
|
3170
|
+
removeResults,
|
|
3171
|
+
addDiagnosticInfos: null,
|
|
3172
|
+
removeDiagnosticInfos: null
|
|
3173
|
+
});
|
|
3174
|
+
|
|
3175
|
+
sendResponse(response);
|
|
3176
|
+
}
|
|
3177
|
+
);
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
protected async _beforeDeleteSubscription(subscription: Subscription) {
|
|
3181
|
+
if (!this.options.onDeleteMonitoredItem) {
|
|
3182
|
+
return;
|
|
3183
|
+
}
|
|
3184
|
+
await subscription.applyOnMonitoredItem(this.options.onDeleteMonitoredItem.bind(null, subscription) as any);
|
|
3185
|
+
}
|
|
3186
|
+
protected _on_RepublishRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
3187
|
+
const request = message.request as RepublishRequest;
|
|
3188
|
+
assert(request instanceof RepublishRequest);
|
|
3189
|
+
|
|
3190
|
+
this._apply_on_Subscription(
|
|
3191
|
+
RepublishResponse,
|
|
3192
|
+
message,
|
|
3193
|
+
channel,
|
|
3194
|
+
async (
|
|
3195
|
+
session: ServerSession,
|
|
3196
|
+
subscription: Subscription,
|
|
3197
|
+
sendResponse: (response: Response) => void,
|
|
3198
|
+
sendError: (statusCode: StatusCode) => void
|
|
3199
|
+
) => {
|
|
3200
|
+
// update diagnostic counter
|
|
3201
|
+
subscription.subscriptionDiagnostics.republishRequestCount += 1;
|
|
3202
|
+
subscription.subscriptionDiagnostics.republishMessageRequestCount += 1;
|
|
3203
|
+
|
|
3204
|
+
const retransmitSequenceNumber = request.retransmitSequenceNumber;
|
|
3205
|
+
const notificationMessage = subscription.getMessageForSequenceNumber(retransmitSequenceNumber);
|
|
3206
|
+
|
|
3207
|
+
if (!notificationMessage) {
|
|
3208
|
+
return sendError(StatusCodes.BadMessageNotAvailable);
|
|
3209
|
+
}
|
|
3210
|
+
const response = new RepublishResponse({
|
|
3211
|
+
notificationMessage,
|
|
3212
|
+
responseHeader: {
|
|
3213
|
+
serviceResult: StatusCodes.Good
|
|
3214
|
+
}
|
|
3215
|
+
});
|
|
3216
|
+
// update diagnostic counter
|
|
3217
|
+
subscription.subscriptionDiagnostics.republishMessageCount += 1;
|
|
3218
|
+
|
|
3219
|
+
sendResponse(response);
|
|
3220
|
+
}
|
|
3221
|
+
);
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
// Bad_NothingToDo
|
|
3225
|
+
// Bad_TooManyOperations
|
|
3226
|
+
// Bad_SubscriptionIdInvalid
|
|
3227
|
+
// Bad_MonitoringModeInvalid
|
|
3228
|
+
protected _on_SetMonitoringModeRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
3229
|
+
const server = this;
|
|
3230
|
+
const request = message.request as SetMonitoringModeRequest;
|
|
3231
|
+
assert(request instanceof SetMonitoringModeRequest);
|
|
3232
|
+
|
|
3233
|
+
this._apply_on_Subscription(
|
|
3234
|
+
SetMonitoringModeResponse,
|
|
3235
|
+
message,
|
|
3236
|
+
channel,
|
|
3237
|
+
async (
|
|
3238
|
+
session: ServerSession,
|
|
3239
|
+
subscription: Subscription,
|
|
3240
|
+
sendResponse: (response: Response) => void,
|
|
3241
|
+
sendError: (statusCode: StatusCode) => void
|
|
3242
|
+
) => {
|
|
3243
|
+
/* istanbul ignore next */
|
|
3244
|
+
if (!request.monitoredItemIds || request.monitoredItemIds.length === 0) {
|
|
3245
|
+
return sendError(StatusCodes.BadNothingToDo);
|
|
3246
|
+
}
|
|
3247
|
+
|
|
3248
|
+
/* istanbul ignore next */
|
|
3249
|
+
if (server.engine.serverCapabilities.operationLimits.maxMonitoredItemsPerCall > 0) {
|
|
3250
|
+
if (
|
|
3251
|
+
request.monitoredItemIds.length > server.engine.serverCapabilities.operationLimits.maxMonitoredItemsPerCall
|
|
3252
|
+
) {
|
|
3253
|
+
return sendError(StatusCodes.BadTooManyOperations);
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
const monitoringMode = request.monitoringMode;
|
|
3257
|
+
|
|
3258
|
+
if (!isMonitoringModeValid(monitoringMode)) {
|
|
3259
|
+
return sendError(StatusCodes.BadMonitoringModeInvalid);
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
const results = request.monitoredItemIds.map((monitoredItemId) => {
|
|
3263
|
+
const monitoredItem = subscription.getMonitoredItem(monitoredItemId);
|
|
3264
|
+
if (!monitoredItem) {
|
|
3265
|
+
return StatusCodes.BadMonitoredItemIdInvalid;
|
|
3266
|
+
}
|
|
3267
|
+
monitoredItem.setMonitoringMode(monitoringMode);
|
|
3268
|
+
return StatusCodes.Good;
|
|
3269
|
+
});
|
|
3270
|
+
|
|
3271
|
+
const response = new SetMonitoringModeResponse({
|
|
3272
|
+
results
|
|
3273
|
+
});
|
|
3274
|
+
sendResponse(response);
|
|
3275
|
+
}
|
|
3276
|
+
);
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
// _on_TranslateBrowsePathsToNodeIds service
|
|
3280
|
+
protected _on_TranslateBrowsePathsToNodeIdsRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
3281
|
+
const request = message.request as TranslateBrowsePathsToNodeIdsRequest;
|
|
3282
|
+
assert(request instanceof TranslateBrowsePathsToNodeIdsRequest);
|
|
3283
|
+
const server = this;
|
|
3284
|
+
|
|
3285
|
+
this._apply_on_SessionObject(
|
|
3286
|
+
TranslateBrowsePathsToNodeIdsResponse,
|
|
3287
|
+
message,
|
|
3288
|
+
channel,
|
|
3289
|
+
async (
|
|
3290
|
+
session: ServerSession,
|
|
3291
|
+
sendResponse: (response: Response) => void,
|
|
3292
|
+
sendError: (statusCode: StatusCode) => void
|
|
3293
|
+
) => {
|
|
3294
|
+
if (!request.browsePaths || request.browsePaths.length === 0) {
|
|
3295
|
+
return sendError(StatusCodes.BadNothingToDo);
|
|
3296
|
+
}
|
|
3297
|
+
if (server.engine.serverCapabilities.operationLimits.maxNodesPerTranslateBrowsePathsToNodeIds > 0) {
|
|
3298
|
+
if (
|
|
3299
|
+
request.browsePaths.length >
|
|
3300
|
+
server.engine.serverCapabilities.operationLimits.maxNodesPerTranslateBrowsePathsToNodeIds
|
|
3301
|
+
) {
|
|
3302
|
+
return sendError(StatusCodes.BadTooManyOperations);
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
const browsePathsResults = request.browsePaths.map((browsePath) => server.engine.browsePath(browsePath));
|
|
3307
|
+
|
|
3308
|
+
const response = new TranslateBrowsePathsToNodeIdsResponse({
|
|
3309
|
+
diagnosticInfos: null,
|
|
3310
|
+
results: browsePathsResults
|
|
3311
|
+
});
|
|
3312
|
+
|
|
3313
|
+
sendResponse(response);
|
|
3314
|
+
}
|
|
3315
|
+
);
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
// Call Service Result Codes
|
|
3319
|
+
// Symbolic Id Description
|
|
3320
|
+
// Bad_NothingToDo See Table 165 for the description of this result code.
|
|
3321
|
+
// Bad_TooManyOperations See Table 165 for the description of this result code.
|
|
3322
|
+
//
|
|
3323
|
+
protected _on_CallRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
3324
|
+
const server = this;
|
|
3325
|
+
const request = message.request as CallRequest;
|
|
3326
|
+
assert(request instanceof CallRequest);
|
|
3327
|
+
|
|
3328
|
+
this._apply_on_SessionObject(
|
|
3329
|
+
CallResponse,
|
|
3330
|
+
message,
|
|
3331
|
+
channel,
|
|
3332
|
+
(session: ServerSession, sendResponse: (response: Response) => void, sendError: (statusCode: StatusCode) => void) => {
|
|
3333
|
+
let response;
|
|
3334
|
+
|
|
3335
|
+
if (!request.methodsToCall || request.methodsToCall.length === 0) {
|
|
3336
|
+
return sendError(StatusCodes.BadNothingToDo);
|
|
3337
|
+
}
|
|
3338
|
+
|
|
3339
|
+
// the MaxNodesPerMethodCall Property indicates the maximum size of the methodsToCall array when
|
|
3340
|
+
// a Client calls the Call Service.
|
|
3341
|
+
let maxNodesPerMethodCall = server.engine.serverCapabilities.operationLimits.maxNodesPerMethodCall;
|
|
3342
|
+
maxNodesPerMethodCall = maxNodesPerMethodCall <= 0 ? 1000 : maxNodesPerMethodCall;
|
|
3343
|
+
if (request.methodsToCall.length > maxNodesPerMethodCall) {
|
|
3344
|
+
return sendError(StatusCodes.BadTooManyOperations);
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
/* jshint validthis: true */
|
|
3348
|
+
const addressSpace = server.engine.addressSpace!;
|
|
3349
|
+
|
|
3350
|
+
const context = new SessionContext({ session, server });
|
|
3351
|
+
|
|
3352
|
+
async.map(
|
|
3353
|
+
request.methodsToCall,
|
|
3354
|
+
callMethodHelper.bind(null, context, addressSpace),
|
|
3355
|
+
(err?: Error | null, results?: (CallMethodResultOptions | undefined)[]) => {
|
|
3356
|
+
/* istanbul ignore next */
|
|
3357
|
+
if (err) {
|
|
3358
|
+
errorLog("ERROR in method Call !! ", err);
|
|
3359
|
+
}
|
|
3360
|
+
assert(Array.isArray(results));
|
|
3361
|
+
response = new CallResponse({
|
|
3362
|
+
results: results as CallMethodResultOptions[]
|
|
3363
|
+
});
|
|
3364
|
+
sendResponse(response);
|
|
3365
|
+
}
|
|
3366
|
+
);
|
|
3367
|
+
}
|
|
3368
|
+
);
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
protected _on_RegisterNodesRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
3372
|
+
const server = this;
|
|
3373
|
+
const request = message.request as RegisterNodesRequest;
|
|
3374
|
+
assert(request instanceof RegisterNodesRequest);
|
|
3375
|
+
|
|
3376
|
+
this._apply_on_SessionObject(
|
|
3377
|
+
RegisterNodesResponse,
|
|
3378
|
+
message,
|
|
3379
|
+
channel,
|
|
3380
|
+
(session: ServerSession, sendResponse: (response: Response) => void, sendError: (statusCode: StatusCode) => void) => {
|
|
3381
|
+
let response;
|
|
3382
|
+
|
|
3383
|
+
if (!request.nodesToRegister || request.nodesToRegister.length === 0) {
|
|
3384
|
+
response = new RegisterNodesResponse({ responseHeader: { serviceResult: StatusCodes.BadNothingToDo } });
|
|
3385
|
+
return sendResponse(response);
|
|
3386
|
+
}
|
|
3387
|
+
if (server.engine.serverCapabilities.operationLimits.maxNodesPerRegisterNodes > 0) {
|
|
3388
|
+
if (
|
|
3389
|
+
request.nodesToRegister.length > server.engine.serverCapabilities.operationLimits.maxNodesPerRegisterNodes
|
|
3390
|
+
) {
|
|
3391
|
+
return sendError(StatusCodes.BadTooManyOperations);
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
// A list of NodeIds which the Client shall use for subsequent access operations. The
|
|
3395
|
+
// size and order of this list matches the size and order of the nodesToRegister
|
|
3396
|
+
// request parameter.
|
|
3397
|
+
// The Server may return the NodeId from the request or a new (an alias) NodeId. It
|
|
3398
|
+
// is recommended that the Server return a numeric NodeIds for aliasing.
|
|
3399
|
+
// In case no optimization is supported for a Node, the Server shall return the
|
|
3400
|
+
// NodeId from the request.
|
|
3401
|
+
const registeredNodeIds = request.nodesToRegister.map((nodeId) => session.registerNode(nodeId));
|
|
3402
|
+
|
|
3403
|
+
response = new RegisterNodesResponse({
|
|
3404
|
+
registeredNodeIds
|
|
3405
|
+
});
|
|
3406
|
+
sendResponse(response);
|
|
3407
|
+
}
|
|
3408
|
+
);
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3411
|
+
protected _on_UnregisterNodesRequest(message: Message, channel: ServerSecureChannelLayer) {
|
|
3412
|
+
const server = this;
|
|
3413
|
+
const request = message.request as UnregisterNodesRequest;
|
|
3414
|
+
assert(request instanceof UnregisterNodesRequest);
|
|
3415
|
+
|
|
3416
|
+
this._apply_on_SessionObject(
|
|
3417
|
+
UnregisterNodesResponse,
|
|
3418
|
+
message,
|
|
3419
|
+
channel,
|
|
3420
|
+
(session: ServerSession, sendResponse: (response: Response) => void, sendError: (statusCode: StatusCode) => void) => {
|
|
3421
|
+
let response;
|
|
3422
|
+
|
|
3423
|
+
request.nodesToUnregister = request.nodesToUnregister || [];
|
|
3424
|
+
|
|
3425
|
+
if (!request.nodesToUnregister || request.nodesToUnregister.length === 0) {
|
|
3426
|
+
response = new UnregisterNodesResponse({ responseHeader: { serviceResult: StatusCodes.BadNothingToDo } });
|
|
3427
|
+
return sendResponse(response);
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
if (server.engine.serverCapabilities.operationLimits.maxNodesPerRegisterNodes > 0) {
|
|
3431
|
+
if (
|
|
3432
|
+
request.nodesToUnregister.length > server.engine.serverCapabilities.operationLimits.maxNodesPerRegisterNodes
|
|
3433
|
+
) {
|
|
3434
|
+
return sendError(StatusCodes.BadTooManyOperations);
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
request.nodesToUnregister.map((nodeId: NodeId) => session.unRegisterNode(nodeId));
|
|
3439
|
+
|
|
3440
|
+
response = new UnregisterNodesResponse({});
|
|
3441
|
+
sendResponse(response);
|
|
3442
|
+
}
|
|
3443
|
+
);
|
|
3444
|
+
}
|
|
3445
|
+
|
|
3446
|
+
/* istanbul ignore next */
|
|
3447
|
+
protected _on_Cancel(message: Message, channel: ServerSecureChannelLayer) {
|
|
3448
|
+
return g_sendError(channel, message, CancelResponse, StatusCodes.BadServiceUnsupported);
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
// NodeManagement Service Set Overview
|
|
3452
|
+
// This Service Set defines Services to add and delete AddressSpace Nodes and References between them. All added
|
|
3453
|
+
// Nodes continue to exist in the AddressSpace even if the Client that created them disconnects from the Server.
|
|
3454
|
+
//
|
|
3455
|
+
/* istanbul ignore next */
|
|
3456
|
+
protected _on_AddNodes(message: Message, channel: ServerSecureChannelLayer) {
|
|
3457
|
+
return g_sendError(channel, message, AddNodesResponse, StatusCodes.BadServiceUnsupported);
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
/* istanbul ignore next */
|
|
3461
|
+
protected _on_AddReferences(message: Message, channel: ServerSecureChannelLayer) {
|
|
3462
|
+
return g_sendError(channel, message, AddReferencesResponse, StatusCodes.BadServiceUnsupported);
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3465
|
+
/* istanbul ignore next */
|
|
3466
|
+
protected _on_DeleteNodes(message: Message, channel: ServerSecureChannelLayer) {
|
|
3467
|
+
return g_sendError(channel, message, DeleteNodesResponse, StatusCodes.BadServiceUnsupported);
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
/* istanbul ignore next */
|
|
3471
|
+
protected _on_DeleteReferences(message: Message, channel: ServerSecureChannelLayer) {
|
|
3472
|
+
return g_sendError(channel, message, DeleteReferencesResponse, StatusCodes.BadServiceUnsupported);
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
// Query Service
|
|
3476
|
+
/* istanbul ignore next */
|
|
3477
|
+
protected _on_QueryFirst(message: Message, channel: ServerSecureChannelLayer) {
|
|
3478
|
+
return g_sendError(channel, message, QueryFirstResponse, StatusCodes.BadServiceUnsupported);
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
/* istanbul ignore next */
|
|
3482
|
+
protected _on_QueryNext(message: Message, channel: ServerSecureChannelLayer) {
|
|
3483
|
+
return g_sendError(channel, message, QueryNextResponse, StatusCodes.BadServiceUnsupported);
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
/* istanbul ignore next */
|
|
3487
|
+
protected _on_HistoryUpdate(message: Message, channel: ServerSecureChannelLayer) {
|
|
3488
|
+
return g_sendError(channel, message, HistoryUpdateResponse, StatusCodes.BadServiceUnsupported);
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
private createEndpoint(port1: number, serverOptions: OPCUAServerOptions): OPCUAServerEndPoint {
|
|
3492
|
+
// add the tcp/ip endpoint with no security
|
|
3493
|
+
const endPoint = new OPCUAServerEndPoint({
|
|
3494
|
+
port: port1,
|
|
3495
|
+
|
|
3496
|
+
certificateManager: this.serverCertificateManager,
|
|
3497
|
+
|
|
3498
|
+
certificateChain: this.getCertificateChain(),
|
|
3499
|
+
privateKey: this.getPrivateKey(),
|
|
3500
|
+
|
|
3501
|
+
defaultSecureTokenLifetime: serverOptions.defaultSecureTokenLifetime || 600000,
|
|
3502
|
+
timeout: serverOptions.timeout || 3 * 60 * 1000,
|
|
3503
|
+
|
|
3504
|
+
maxConnections: this.maxConnectionsPerEndpoint,
|
|
3505
|
+
objectFactory: this.objectFactory,
|
|
3506
|
+
serverInfo: this.serverInfo
|
|
3507
|
+
});
|
|
3508
|
+
return endPoint;
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
private createEndpointDescriptions(
|
|
3512
|
+
serverOption: OPCUAServerOptions,
|
|
3513
|
+
endpointOptions: OPCUAServerEndpointOptions
|
|
3514
|
+
): OPCUAServerEndPoint {
|
|
3515
|
+
/* istanbul ignore next */
|
|
3516
|
+
if (!endpointOptions) {
|
|
3517
|
+
throw new Error("internal error");
|
|
3518
|
+
}
|
|
3519
|
+
const hostname = getFullyQualifiedDomainName();
|
|
3520
|
+
endpointOptions.hostname = endpointOptions.hostname || hostname;
|
|
3521
|
+
endpointOptions.port = endpointOptions.port || 26543;
|
|
3522
|
+
|
|
3523
|
+
/* istanbul ignore next */
|
|
3524
|
+
if (
|
|
3525
|
+
!Object.prototype.hasOwnProperty.call(endpointOptions,"port") ||
|
|
3526
|
+
!isFinite(endpointOptions.port!) ||
|
|
3527
|
+
typeof endpointOptions.port !== "number"
|
|
3528
|
+
) {
|
|
3529
|
+
throw new Error("expecting a valid port (number)");
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3532
|
+
const port = Number(endpointOptions.port || 0);
|
|
3533
|
+
|
|
3534
|
+
const endPoint = this.createEndpoint(port, serverOption);
|
|
3535
|
+
|
|
3536
|
+
endpointOptions.alternateHostname = endpointOptions.alternateHostname || [];
|
|
3537
|
+
const alternateHostname =
|
|
3538
|
+
endpointOptions.alternateHostname instanceof Array
|
|
3539
|
+
? endpointOptions.alternateHostname
|
|
3540
|
+
: [endpointOptions.alternateHostname];
|
|
3541
|
+
const allowAnonymous = endpointOptions.allowAnonymous === undefined ? true : !!endpointOptions.allowAnonymous;
|
|
3542
|
+
|
|
3543
|
+
endPoint.addStandardEndpointDescriptions({
|
|
3544
|
+
allowAnonymous,
|
|
3545
|
+
securityModes: endpointOptions.securityModes,
|
|
3546
|
+
securityPolicies: endpointOptions.securityPolicies,
|
|
3547
|
+
|
|
3548
|
+
hostname: endpointOptions.hostname,
|
|
3549
|
+
|
|
3550
|
+
alternateHostname,
|
|
3551
|
+
|
|
3552
|
+
disableDiscovery: !!endpointOptions.disableDiscovery,
|
|
3553
|
+
// xx hostname,
|
|
3554
|
+
resourcePath: serverOption.resourcePath || ""
|
|
3555
|
+
});
|
|
3556
|
+
return endPoint;
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
protected async initializeCM(): Promise<void> {
|
|
3560
|
+
await super.initializeCM();
|
|
3561
|
+
await this.userCertificateManager.initialize();
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
export interface RaiseEventAuditEventData extends RaiseEventData {
|
|
3566
|
+
actionTimeStamp: PseudoVariantDateTime;
|
|
3567
|
+
status: PseudoVariantBoolean;
|
|
3568
|
+
serverId: PseudoVariantString;
|
|
3569
|
+
/**
|
|
3570
|
+
* ClientAuditEntryId contains the human-readable AuditEntryId defined in Part 3.
|
|
3571
|
+
*/
|
|
3572
|
+
clientAuditEntryId: PseudoVariantString;
|
|
3573
|
+
/**
|
|
3574
|
+
* The ClientUserId identifies the user of the client requesting an action. The ClientUserId can be
|
|
3575
|
+
* obtained from the UserIdentityToken passed in the ActivateSession call.
|
|
3576
|
+
*/
|
|
3577
|
+
clientUserId: PseudoVariantString;
|
|
3578
|
+
sourceName: PseudoVariantString;
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
export interface RaiseEventAuditUpdateMethodEventData extends RaiseEventAuditEventData {
|
|
3582
|
+
methodId: PseudoVariantNodeId;
|
|
3583
|
+
inputArguments: any;
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
export interface RaiseEventAuditConditionCommentEventData extends RaiseEventAuditUpdateMethodEventData {
|
|
3587
|
+
eventId: PseudoVariantByteString;
|
|
3588
|
+
comment: PseudoVariantLocalizedText;
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
export interface RaiseEventAuditSessionEventData extends RaiseEventAuditEventData {
|
|
3592
|
+
/**
|
|
3593
|
+
* part 5 - 6.4.7 AuditSessionEventType
|
|
3594
|
+
*/
|
|
3595
|
+
sessionId: PseudoVariantNodeId;
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
export interface RaiseEventAuditCreateSessionEventData extends RaiseEventAuditSessionEventData {
|
|
3599
|
+
/**
|
|
3600
|
+
* part 5 - 6.4.8 AuditCreateSessionEventType
|
|
3601
|
+
* SecureChannelId shall uniquely identify the SecureChannel.
|
|
3602
|
+
* The application shall use the same identifier in
|
|
3603
|
+
* all AuditEvents related to the Session Service Set (AuditCreateSessionEventType, AuditActivateSessionEventType
|
|
3604
|
+
* and their subtypes) and the SecureChannel Service Set (AuditChannelEventType and its subtype
|
|
3605
|
+
*/
|
|
3606
|
+
secureChannelId: PseudoVariantString;
|
|
3607
|
+
revisedSessionTimeout: PseudoVariantDuration;
|
|
3608
|
+
clientCertificate: PseudoVariantByteString;
|
|
3609
|
+
clientCertificateThumbprint: PseudoVariantByteString;
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
export interface RaiseEventAuditActivateSessionEventData extends RaiseEventAuditSessionEventData {
|
|
3613
|
+
/**
|
|
3614
|
+
* part 5 - 6.4.10 AuditActivateSessionEventType
|
|
3615
|
+
*/
|
|
3616
|
+
clientSoftwareCertificates: PseudoVariantExtensionObjectArray;
|
|
3617
|
+
/**
|
|
3618
|
+
* UserIdentityToken reflects the userIdentityToken parameter of the ActivateSession Service call.
|
|
3619
|
+
* For Username/Password tokens the password should NOT be included.
|
|
3620
|
+
*/
|
|
3621
|
+
userIdentityToken: PseudoVariantExtensionObject;
|
|
3622
|
+
/**
|
|
3623
|
+
* SecureChannelId shall uniquely identify the SecureChannel. The application shall use the same identifier
|
|
3624
|
+
* in all AuditEvents related to the Session Service Set (AuditCreateSessionEventType,
|
|
3625
|
+
* AuditActivateSessionEventType and their subtypes) and the SecureChannel Service Set
|
|
3626
|
+
* (AuditChannelEventType and its subtypes).
|
|
3627
|
+
*/
|
|
3628
|
+
secureChannelId: PseudoVariantString;
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
// tslint:disable:no-empty-interface
|
|
3632
|
+
export interface RaiseEventTransitionEventData extends RaiseEventData {}
|
|
3633
|
+
|
|
3634
|
+
export interface RaiseEventAuditUrlMismatchEventTypeData extends RaiseEventData {
|
|
3635
|
+
endpointUrl: PseudoVariantString;
|
|
3636
|
+
}
|
|
3637
|
+
export interface OPCUAServer {
|
|
3638
|
+
/**
|
|
3639
|
+
* @internal
|
|
3640
|
+
* @param eventType
|
|
3641
|
+
* @param options
|
|
3642
|
+
*/
|
|
3643
|
+
raiseEvent(eventType: "AuditSessionEventType", options: RaiseEventAuditSessionEventData): void;
|
|
3644
|
+
|
|
3645
|
+
raiseEvent(eventType: "AuditCreateSessionEventType", options: RaiseEventAuditCreateSessionEventData): void;
|
|
3646
|
+
|
|
3647
|
+
raiseEvent(eventType: "AuditActivateSessionEventType", options: RaiseEventAuditActivateSessionEventData): void;
|
|
3648
|
+
|
|
3649
|
+
raiseEvent(eventType: "AuditCreateSessionEventType", options: RaiseEventData): void;
|
|
3650
|
+
|
|
3651
|
+
raiseEvent(eventType: "AuditConditionCommentEventType", options: RaiseEventAuditConditionCommentEventData): void;
|
|
3652
|
+
|
|
3653
|
+
raiseEvent(eventType: "AuditUrlMismatchEventType", options: RaiseEventAuditUrlMismatchEventTypeData): void;
|
|
3654
|
+
|
|
3655
|
+
raiseEvent(eventType: "TransitionEventType", options: RaiseEventTransitionEventData): void;
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
export interface OPCUAServer extends EventEmitter {
|
|
3659
|
+
on(event: "create_session", eventHandler: (session: ServerSession) => void): this;
|
|
3660
|
+
|
|
3661
|
+
on(event: "session_activated", eventHandler: (session: ServerSession) => void): this;
|
|
3662
|
+
|
|
3663
|
+
on(event: "session_closed", eventHandler: (session: ServerSession, reason: string) => void): this;
|
|
3664
|
+
|
|
3665
|
+
on(event: "post_initialize", eventHandler: () => void): this;
|
|
3666
|
+
|
|
3667
|
+
/**
|
|
3668
|
+
* emitted when the server is trying to registered the LDS
|
|
3669
|
+
* but when the connection to the lds has failed
|
|
3670
|
+
* serverRegistrationPending is sent when the backoff signal of the
|
|
3671
|
+
* connection process is raised
|
|
3672
|
+
* @event serverRegistrationPending
|
|
3673
|
+
*/
|
|
3674
|
+
on(event: "serverRegistrationPending", eventHandler: () => void): this;
|
|
3675
|
+
|
|
3676
|
+
/**
|
|
3677
|
+
* event raised when server has been successfully registered on the local discovery server
|
|
3678
|
+
* @event serverRegistered
|
|
3679
|
+
*/
|
|
3680
|
+
on(event: "serverRegistered", eventHandler: () => void): this;
|
|
3681
|
+
|
|
3682
|
+
/**
|
|
3683
|
+
* event raised when server registration has been successfully renewed on the local discovery server
|
|
3684
|
+
* @event serverRegistered
|
|
3685
|
+
*/
|
|
3686
|
+
on(event: "serverRegistrationRenewed", eventHandler: () => void): this;
|
|
3687
|
+
|
|
3688
|
+
/**
|
|
3689
|
+
* event raised when server has been successfully unregistered from the local discovery server
|
|
3690
|
+
* @event serverUnregistered
|
|
3691
|
+
*/
|
|
3692
|
+
on(event: "serverUnregistered", eventHandler: () => void): this;
|
|
3693
|
+
|
|
3694
|
+
/**
|
|
3695
|
+
* event raised after the server has raised an OPCUA event toward a client
|
|
3696
|
+
*/
|
|
3697
|
+
on(event: "event", eventHandler: (eventData: any) => void): this;
|
|
3698
|
+
|
|
3699
|
+
/**
|
|
3700
|
+
* event raised when the server received a request from one of its connected client.
|
|
3701
|
+
* useful for trace purpose.
|
|
3702
|
+
*/
|
|
3703
|
+
on(event: "request", eventHandler: (request: Request, channel: ServerSecureChannelLayer) => void): this;
|
|
3704
|
+
|
|
3705
|
+
/**
|
|
3706
|
+
* event raised when the server send an response to a request to one of its connected client.
|
|
3707
|
+
* useful for trace purpose.
|
|
3708
|
+
*/
|
|
3709
|
+
on(event: "response", eventHandler: (request: Response, channel: ServerSecureChannelLayer) => void): this;
|
|
3710
|
+
|
|
3711
|
+
/**
|
|
3712
|
+
* event raised when a new secure channel is opened
|
|
3713
|
+
*/
|
|
3714
|
+
on(event: "newChannel", eventHandler: (channel: ServerSecureChannelLayer, endpoint: OPCUAServerEndPoint) => void): this;
|
|
3715
|
+
|
|
3716
|
+
/**
|
|
3717
|
+
* event raised when a new secure channel is closed
|
|
3718
|
+
*/
|
|
3719
|
+
on(event: "closeChannel", eventHandler: (channel: ServerSecureChannelLayer, endpoint: OPCUAServerEndPoint) => void): this;
|
|
3720
|
+
|
|
3721
|
+
/**
|
|
3722
|
+
* event raised when the server refused a tcp connection from a client. ( for instance because too any connections)
|
|
3723
|
+
*/
|
|
3724
|
+
on(event: "connectionRefused", eventHandler: (socketData: ISocketData, endpoint: OPCUAServerEndPoint) => void): this;
|
|
3725
|
+
|
|
3726
|
+
/**
|
|
3727
|
+
* event raised when a OpenSecureChannel has failed, it could be a invalid certificate or malformed message
|
|
3728
|
+
*/
|
|
3729
|
+
on(
|
|
3730
|
+
event: "openSecureChannelFailure",
|
|
3731
|
+
eventHandler: (socketData: ISocketData, channelData: IChannelData, endpoint: OPCUAServerEndPoint) => void
|
|
3732
|
+
): this;
|
|
3733
|
+
|
|
3734
|
+
on(event: string, eventHandler: (...args: [any?, ...any[]]) => void): this;
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
// tslint:disable:no-var-requires
|
|
3738
|
+
const thenify = require("thenify");
|
|
3739
|
+
const opts = { multiArgs: false };
|
|
3740
|
+
OPCUAServer.prototype.start = thenify.withCallback(OPCUAServer.prototype.start, opts);
|
|
3741
|
+
OPCUAServer.prototype.initialize = thenify.withCallback(OPCUAServer.prototype.initialize, opts);
|
|
3742
|
+
OPCUAServer.prototype.shutdown = thenify.withCallback(OPCUAServer.prototype.shutdown, opts);
|