node-opcua-server 2.81.0 → 2.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/base_server.js +10 -1
- package/dist/base_server.js.map +1 -1
- package/dist/monitored_item.d.ts +2 -3
- package/dist/monitored_item.js +69 -32
- package/dist/monitored_item.js.map +1 -1
- package/dist/node_sampler.js +4 -0
- package/dist/node_sampler.js.map +1 -1
- package/dist/opcua_server.js +1 -1
- package/dist/opcua_server.js.map +1 -1
- package/dist/server_end_point.d.ts +9 -7
- package/dist/server_end_point.js +82 -61
- package/dist/server_end_point.js.map +1 -1
- package/dist/server_engine.js +38 -21
- package/dist/server_engine.js.map +1 -1
- package/dist/server_subscription.js +5 -4
- package/dist/server_subscription.js.map +1 -1
- package/dist/validate_filter.js +26 -6
- package/dist/validate_filter.js.map +1 -1
- package/package.json +44 -44
- package/source/base_server.ts +15 -3
- package/source/monitored_item.ts +81 -44
- package/source/node_sampler.ts +6 -0
- package/source/opcua_server.ts +3 -4
- package/source/server_end_point.ts +116 -79
- package/source/server_engine.ts +62 -27
- package/source/server_subscription.ts +11 -8
- package/source/validate_filter.ts +28 -10
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
import { UserTokenType } from "node-opcua-service-endpoints";
|
|
28
28
|
import { EndpointDescription } from "node-opcua-service-endpoints";
|
|
29
29
|
import { ApplicationDescription } from "node-opcua-service-endpoints";
|
|
30
|
+
import { UserTokenPolicyOptions } from "node-opcua-types";
|
|
30
31
|
import { IChannelData } from "./i_channel_data";
|
|
31
32
|
import { ISocketData } from "./i_socket_data";
|
|
32
33
|
|
|
@@ -155,26 +156,27 @@ export interface OPCUAServerEndPointOptions {
|
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
export interface EndpointDescriptionParams {
|
|
158
|
-
allowAnonymous?: boolean;
|
|
159
159
|
restricted?: boolean;
|
|
160
160
|
allowUnsecurePassword?: boolean;
|
|
161
161
|
resourcePath?: string;
|
|
162
162
|
alternateHostname?: string[];
|
|
163
163
|
hostname: string;
|
|
164
164
|
securityPolicies: SecurityPolicy[];
|
|
165
|
+
userTokenTypes: UserTokenType[];
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
export interface AddStandardEndpointDescriptionsParam {
|
|
168
|
-
securityModes?: MessageSecurityMode[];
|
|
169
|
-
securityPolicies?: SecurityPolicy[];
|
|
170
|
-
disableDiscovery?: boolean;
|
|
171
|
-
|
|
172
169
|
allowAnonymous?: boolean;
|
|
170
|
+
disableDiscovery?: boolean;
|
|
171
|
+
securityModes?: MessageSecurityMode[];
|
|
172
|
+
|
|
173
173
|
restricted?: boolean;
|
|
174
|
-
hostname?: string;
|
|
175
|
-
alternateHostname?: string[];
|
|
176
174
|
allowUnsecurePassword?: boolean;
|
|
177
175
|
resourcePath?: string;
|
|
176
|
+
alternateHostname?: string[];
|
|
177
|
+
hostname?: string;
|
|
178
|
+
securityPolicies?: SecurityPolicy[];
|
|
179
|
+
userTokenTypes?: UserTokenType[];
|
|
178
180
|
}
|
|
179
181
|
|
|
180
182
|
function getUniqueName(name: string, collection: { [key: string]: number }) {
|
|
@@ -367,17 +369,8 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
367
369
|
public addEndpointDescription(
|
|
368
370
|
securityMode: MessageSecurityMode,
|
|
369
371
|
securityPolicy: SecurityPolicy,
|
|
370
|
-
options
|
|
372
|
+
options: EndpointDescriptionParams
|
|
371
373
|
): void {
|
|
372
|
-
if (!options) {
|
|
373
|
-
options = {
|
|
374
|
-
hostname: getFullyQualifiedDomainName(),
|
|
375
|
-
securityPolicies: [SecurityPolicy.Basic256Sha256]
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
options.allowAnonymous = options.allowAnonymous === undefined ? true : options.allowAnonymous;
|
|
380
|
-
|
|
381
374
|
// istanbul ignore next
|
|
382
375
|
if (securityMode === MessageSecurityMode.None && securityPolicy !== SecurityPolicy.None) {
|
|
383
376
|
throw new Error(" invalid security ");
|
|
@@ -405,6 +398,8 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
405
398
|
throw new Error(" endpoint already exist");
|
|
406
399
|
}
|
|
407
400
|
|
|
401
|
+
const userTokenTypes = options.userTokenTypes;
|
|
402
|
+
|
|
408
403
|
// now build endpointUrl
|
|
409
404
|
this._endpoints.push(
|
|
410
405
|
_makeEndpointDescription({
|
|
@@ -419,12 +414,13 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
419
414
|
securityMode,
|
|
420
415
|
securityPolicy,
|
|
421
416
|
|
|
422
|
-
allowAnonymous: options.allowAnonymous,
|
|
423
417
|
allowUnsecurePassword: options.allowUnsecurePassword,
|
|
424
418
|
resourcePath: options.resourcePath,
|
|
425
419
|
|
|
426
420
|
restricted: !!options.restricted,
|
|
427
|
-
securityPolicies: options
|
|
421
|
+
securityPolicies: options.securityPolicies || [],
|
|
422
|
+
|
|
423
|
+
userTokenTypes
|
|
428
424
|
})
|
|
429
425
|
);
|
|
430
426
|
}
|
|
@@ -440,6 +436,14 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
440
436
|
|
|
441
437
|
options.securityModes = options.securityModes || defaultSecurityModes;
|
|
442
438
|
options.securityPolicies = options.securityPolicies || defaultSecurityPolicies;
|
|
439
|
+
options.userTokenTypes = options.userTokenTypes || defaultUserTokenTypes;
|
|
440
|
+
|
|
441
|
+
options.allowAnonymous = options.allowAnonymous === undefined ? true : options.allowAnonymous;
|
|
442
|
+
// make sure we do not have anonymous
|
|
443
|
+
if (!options.allowAnonymous) {
|
|
444
|
+
options.userTokenTypes = options.userTokenTypes.filter((r) => r !== UserTokenType.Anonymous);
|
|
445
|
+
}
|
|
446
|
+
|
|
443
447
|
|
|
444
448
|
const defaultHostname = options.hostname || getFullyQualifiedDomainName();
|
|
445
449
|
|
|
@@ -450,11 +454,17 @@ export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureCha
|
|
|
450
454
|
options.alternateHostname = [options.alternateHostname];
|
|
451
455
|
}
|
|
452
456
|
// remove duplicates if any (uniq)
|
|
453
|
-
hostnames = [...new Set(hostnames.concat(options.alternateHostname
|
|
457
|
+
hostnames = [...new Set(hostnames.concat(options.alternateHostname))];
|
|
454
458
|
|
|
455
459
|
for (const alternateHostname of hostnames) {
|
|
456
|
-
const optionsE =
|
|
457
|
-
|
|
460
|
+
const optionsE: EndpointDescriptionParams = {
|
|
461
|
+
hostname: alternateHostname,
|
|
462
|
+
securityPolicies: options.securityPolicies,
|
|
463
|
+
userTokenTypes: options.userTokenTypes,
|
|
464
|
+
allowUnsecurePassword: options.allowUnsecurePassword,
|
|
465
|
+
alternateHostname: options.alternateHostname,
|
|
466
|
+
resourcePath: options.resourcePath
|
|
467
|
+
};
|
|
458
468
|
|
|
459
469
|
if (options.securityModes.indexOf(MessageSecurityMode.None) >= 0) {
|
|
460
470
|
this.addEndpointDescription(MessageSecurityMode.None, SecurityPolicy.None, optionsE);
|
|
@@ -966,7 +976,6 @@ interface MakeEndpointDescriptionOptions {
|
|
|
966
976
|
};
|
|
967
977
|
*/
|
|
968
978
|
resourcePath?: string;
|
|
969
|
-
allowAnonymous?: boolean; // default true
|
|
970
979
|
|
|
971
980
|
// allow un-encrypted password in userNameIdentity
|
|
972
981
|
allowUnsecurePassword?: boolean; // default false
|
|
@@ -981,6 +990,15 @@ interface MakeEndpointDescriptionOptions {
|
|
|
981
990
|
collection: { [key: string]: number };
|
|
982
991
|
|
|
983
992
|
securityPolicies: SecurityPolicy[];
|
|
993
|
+
|
|
994
|
+
userTokenTypes: UserTokenType[];
|
|
995
|
+
/**
|
|
996
|
+
*
|
|
997
|
+
* default value: false;
|
|
998
|
+
*
|
|
999
|
+
* note: setting noUserIdentityTokens=true is useful for pure local discovery servers
|
|
1000
|
+
*/
|
|
1001
|
+
noUserIdentityTokens?: boolean;
|
|
984
1002
|
}
|
|
985
1003
|
|
|
986
1004
|
interface EndpointDescriptionEx extends EndpointDescription {
|
|
@@ -1040,72 +1058,85 @@ function _makeEndpointDescription(options: MakeEndpointDescriptionOptions): Endp
|
|
|
1040
1058
|
|
|
1041
1059
|
const securityPolicyUri = toURI(options.securityPolicy);
|
|
1042
1060
|
|
|
1043
|
-
const userIdentityTokens = [];
|
|
1044
|
-
|
|
1045
|
-
if (options.securityPolicy === SecurityPolicy.None) {
|
|
1046
|
-
if (options.allowUnsecurePassword) {
|
|
1047
|
-
userIdentityTokens.push({
|
|
1048
|
-
policyId: u("username_unsecure"),
|
|
1049
|
-
tokenType: UserTokenType.UserName,
|
|
1061
|
+
const userIdentityTokens: UserTokenPolicyOptions[] = [];
|
|
1050
1062
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1063
|
+
const registerIdentity2 = (tokenType: UserTokenType, securityPolicy: SecurityPolicy, name: string) => {
|
|
1064
|
+
return registerIdentity({
|
|
1065
|
+
policyId: u(name),
|
|
1066
|
+
tokenType,
|
|
1067
|
+
issuedTokenType: null,
|
|
1068
|
+
issuerEndpointUrl: null,
|
|
1069
|
+
securityPolicyUri: securityPolicy
|
|
1070
|
+
});
|
|
1071
|
+
};
|
|
1072
|
+
const registerIdentity = (r: UserTokenPolicyOptions) => {
|
|
1073
|
+
const tokenType = r.tokenType === undefined ? UserTokenType.Invalid : r.tokenType;
|
|
1074
|
+
const securityPolicy = (r.securityPolicyUri || "") as SecurityPolicy;
|
|
1075
|
+
if (!securityPolicy && options.userTokenTypes.indexOf(tokenType) >= 0) {
|
|
1076
|
+
userIdentityTokens.push(r);
|
|
1077
|
+
return;
|
|
1055
1078
|
}
|
|
1079
|
+
if (options.securityPolicies.indexOf(securityPolicy) >= 0 && options.userTokenTypes.indexOf(tokenType) >= 0) {
|
|
1080
|
+
userIdentityTokens.push(r);
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
if (!options.noUserIdentityTokens) {
|
|
1085
|
+
if (options.securityPolicy === SecurityPolicy.None) {
|
|
1086
|
+
if (options.allowUnsecurePassword) {
|
|
1087
|
+
registerIdentity({
|
|
1088
|
+
policyId: u("username_unsecure"),
|
|
1089
|
+
tokenType: UserTokenType.UserName,
|
|
1056
1090
|
|
|
1057
|
-
const a = (tokenType: UserTokenType, securityPolicy: SecurityPolicy, name: string) => {
|
|
1058
|
-
if (options.securityPolicies.indexOf(securityPolicy) >= 0) {
|
|
1059
|
-
userIdentityTokens.push({
|
|
1060
|
-
policyId: u(name),
|
|
1061
|
-
tokenType,
|
|
1062
1091
|
issuedTokenType: null,
|
|
1063
1092
|
issuerEndpointUrl: null,
|
|
1064
|
-
securityPolicyUri:
|
|
1093
|
+
securityPolicyUri: null
|
|
1065
1094
|
});
|
|
1066
1095
|
}
|
|
1067
|
-
};
|
|
1068
|
-
const onlyCertificateLessConnection =
|
|
1069
|
-
options.onlyCertificateLessConnection === undefined ? false : options.onlyCertificateLessConnection;
|
|
1070
|
-
|
|
1071
|
-
if (!onlyCertificateLessConnection) {
|
|
1072
|
-
a(UserTokenType.UserName, SecurityPolicy.Basic256, "username_basic256");
|
|
1073
|
-
a(UserTokenType.UserName, SecurityPolicy.Basic128Rsa15, "username_basic128Rsa15");
|
|
1074
|
-
a(UserTokenType.UserName, SecurityPolicy.Basic256Sha256, "username_basic256Sha256");
|
|
1075
|
-
a(UserTokenType.UserName, SecurityPolicy.Aes128_Sha256_RsaOaep, "username_aes128Sha256RsaOaep");
|
|
1076
|
-
|
|
1077
|
-
// X509
|
|
1078
|
-
a(UserTokenType.Certificate, SecurityPolicy.Basic256, "certificate_basic256");
|
|
1079
|
-
a(UserTokenType.Certificate, SecurityPolicy.Basic128Rsa15, "certificate_basic128Rsa15");
|
|
1080
|
-
a(UserTokenType.Certificate, SecurityPolicy.Basic256Sha256, "certificate_basic256Sha256");
|
|
1081
|
-
a(UserTokenType.Certificate, SecurityPolicy.Aes128_Sha256_RsaOaep, "certificate_aes128Sha256RsaOaep");
|
|
1082
|
-
}
|
|
1083
|
-
} else {
|
|
1084
|
-
// note:
|
|
1085
|
-
// when channel session security is not "None",
|
|
1086
|
-
// userIdentityTokens can be left to null.
|
|
1087
|
-
// in this case this mean that secure policy will be the same as connection security policy
|
|
1088
|
-
userIdentityTokens.push({
|
|
1089
|
-
policyId: u("usernamePassword"),
|
|
1090
|
-
tokenType: UserTokenType.UserName,
|
|
1091
1096
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1097
|
+
const onlyCertificateLessConnection =
|
|
1098
|
+
options.onlyCertificateLessConnection === undefined ? false : options.onlyCertificateLessConnection;
|
|
1099
|
+
|
|
1100
|
+
if (!onlyCertificateLessConnection) {
|
|
1101
|
+
registerIdentity2(UserTokenType.UserName, SecurityPolicy.Basic256, "username_basic256");
|
|
1102
|
+
registerIdentity2(UserTokenType.UserName, SecurityPolicy.Basic128Rsa15, "username_basic128Rsa15");
|
|
1103
|
+
registerIdentity2(UserTokenType.UserName, SecurityPolicy.Basic256Sha256, "username_basic256Sha256");
|
|
1104
|
+
registerIdentity2(UserTokenType.UserName, SecurityPolicy.Aes128_Sha256_RsaOaep, "username_aes128Sha256RsaOaep");
|
|
1105
|
+
|
|
1106
|
+
// X509
|
|
1107
|
+
registerIdentity2(UserTokenType.Certificate, SecurityPolicy.Basic256, "certificate_basic256");
|
|
1108
|
+
registerIdentity2(UserTokenType.Certificate, SecurityPolicy.Basic128Rsa15, "certificate_basic128Rsa15");
|
|
1109
|
+
registerIdentity2(UserTokenType.Certificate, SecurityPolicy.Basic256Sha256, "certificate_basic256Sha256");
|
|
1110
|
+
registerIdentity2(
|
|
1111
|
+
UserTokenType.Certificate,
|
|
1112
|
+
SecurityPolicy.Aes128_Sha256_RsaOaep,
|
|
1113
|
+
"certificate_aes128Sha256RsaOaep"
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
} else {
|
|
1117
|
+
// note:
|
|
1118
|
+
// when channel session security is not "None",
|
|
1119
|
+
// userIdentityTokens can be left to null.
|
|
1120
|
+
// in this case this mean that secure policy will be the same as connection security policy
|
|
1121
|
+
registerIdentity({
|
|
1122
|
+
policyId: u("usernamePassword"),
|
|
1123
|
+
tokenType: UserTokenType.UserName,
|
|
1124
|
+
issuedTokenType: null,
|
|
1125
|
+
issuerEndpointUrl: null,
|
|
1126
|
+
securityPolicyUri: null
|
|
1127
|
+
});
|
|
1096
1128
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1129
|
+
registerIdentity({
|
|
1130
|
+
policyId: u("certificateX509"),
|
|
1131
|
+
tokenType: UserTokenType.Certificate,
|
|
1100
1132
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1133
|
+
issuedTokenType: null,
|
|
1134
|
+
issuerEndpointUrl: null,
|
|
1135
|
+
securityPolicyUri: null
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1106
1138
|
|
|
1107
|
-
|
|
1108
|
-
userIdentityTokens.push({
|
|
1139
|
+
registerIdentity({
|
|
1109
1140
|
policyId: u("anonymous"),
|
|
1110
1141
|
tokenType: UserTokenType.Anonymous,
|
|
1111
1142
|
|
|
@@ -1114,7 +1145,6 @@ function _makeEndpointDescription(options: MakeEndpointDescriptionOptions): Endp
|
|
|
1114
1145
|
securityPolicyUri: null
|
|
1115
1146
|
});
|
|
1116
1147
|
}
|
|
1117
|
-
|
|
1118
1148
|
// return the endpoint object
|
|
1119
1149
|
const endpoint = new EndpointDescription({
|
|
1120
1150
|
endpointUrl: options.endpointUrl,
|
|
@@ -1172,3 +1202,10 @@ const defaultSecurityPolicies = [
|
|
|
1172
1202
|
SecurityPolicy.Aes128_Sha256_RsaOaep
|
|
1173
1203
|
// NO USED YET SecurityPolicy.Aes256_Sha256_RsaPss
|
|
1174
1204
|
];
|
|
1205
|
+
|
|
1206
|
+
const defaultUserTokenTypes = [
|
|
1207
|
+
UserTokenType.Anonymous,
|
|
1208
|
+
UserTokenType.UserName,
|
|
1209
|
+
UserTokenType.Certificate
|
|
1210
|
+
// NOT USED YET : UserTokenType.IssuedToken
|
|
1211
|
+
];
|
package/source/server_engine.ts
CHANGED
|
@@ -96,7 +96,6 @@ import { ServerSession } from "./server_session";
|
|
|
96
96
|
import { Subscription } from "./server_subscription";
|
|
97
97
|
import { sessionsCompatibleForTransfer } from "./sessions_compatible_for_transfer";
|
|
98
98
|
import { OPCUAServerOptions } from "./opcua_server";
|
|
99
|
-
import { IUserManager } from "node-opcua-address-space/source";
|
|
100
99
|
|
|
101
100
|
const debugLog = make_debugLog(__filename);
|
|
102
101
|
const errorLog = make_errorLog(__filename);
|
|
@@ -123,25 +122,13 @@ function setSubscriptionDurable(
|
|
|
123
122
|
) {
|
|
124
123
|
// see https://reference.opcfoundation.org/v104/Core/docs/Part5/9.3/
|
|
125
124
|
// https://reference.opcfoundation.org/v104/Core/docs/Part4/6.8/
|
|
126
|
-
assert(Array.isArray(inputArguments));
|
|
127
125
|
assert(typeof callback === "function");
|
|
128
|
-
assert(Object.prototype.hasOwnProperty.call(context, "session"), " expecting a session id in the context object");
|
|
129
|
-
const session = context.session as ServerSession;
|
|
130
|
-
if (!session) {
|
|
131
|
-
return callback(null, { statusCode: StatusCodes.BadInternalError });
|
|
132
|
-
}
|
|
133
|
-
const subscriptionId = inputArguments[0].value as UInt32;
|
|
134
|
-
const lifetimeInHours = inputArguments[1].value as UInt32;
|
|
135
126
|
|
|
136
|
-
const
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return callback(null, { statusCode: StatusCodes.BadUserAccessDenied });
|
|
142
|
-
}
|
|
143
|
-
return callback(null, { statusCode: StatusCodes.BadSubscriptionIdInvalid });
|
|
144
|
-
}
|
|
127
|
+
const data = _getSubscription.call(this, inputArguments, context);
|
|
128
|
+
if (data.statusCode) return callback(null, { statusCode: data.statusCode });
|
|
129
|
+
const { subscription } = data;
|
|
130
|
+
|
|
131
|
+
const lifetimeInHours = inputArguments[1].value as UInt32;
|
|
145
132
|
if (subscription.monitoredItemCount > 0) {
|
|
146
133
|
// This is returned when a Subscription already contains MonitoredItems.
|
|
147
134
|
return callback(null, { statusCode: StatusCodes.BadInvalidState });
|
|
@@ -199,8 +186,7 @@ function setSubscriptionDurable(
|
|
|
199
186
|
callback(null, callMethodResult);
|
|
200
187
|
}
|
|
201
188
|
|
|
202
|
-
|
|
203
|
-
function getMonitoredItemsId(
|
|
189
|
+
function requestServerStateChange(
|
|
204
190
|
this: ServerEngine,
|
|
205
191
|
inputArguments: Variant[],
|
|
206
192
|
context: ISessionContext,
|
|
@@ -209,23 +195,70 @@ function getMonitoredItemsId(
|
|
|
209
195
|
assert(Array.isArray(inputArguments));
|
|
210
196
|
assert(typeof callback === "function");
|
|
211
197
|
assert(Object.prototype.hasOwnProperty.call(context, "session"), " expecting a session id in the context object");
|
|
212
|
-
|
|
213
198
|
const session = context.session as ServerSession;
|
|
214
199
|
if (!session) {
|
|
215
200
|
return callback(null, { statusCode: StatusCodes.BadInternalError });
|
|
216
201
|
}
|
|
217
202
|
|
|
203
|
+
return callback(null, { statusCode: StatusCodes.BadNotImplemented });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function _getSubscription(
|
|
207
|
+
this: ServerEngine,
|
|
208
|
+
inputArguments: Variant[],
|
|
209
|
+
context: ISessionContext
|
|
210
|
+
): { subscription: Subscription, statusCode?: never } | { statusCode: StatusCode, subscription?: never } {
|
|
211
|
+
assert(Array.isArray(inputArguments));
|
|
212
|
+
assert(Object.prototype.hasOwnProperty.call(context, "session"), " expecting a session id in the context object");
|
|
213
|
+
const session = context.session as ServerSession;
|
|
214
|
+
if (!session) {
|
|
215
|
+
return { statusCode: StatusCodes.BadInternalError };
|
|
216
|
+
}
|
|
218
217
|
const subscriptionId = inputArguments[0].value;
|
|
219
218
|
const subscription = session.getSubscription(subscriptionId);
|
|
220
219
|
if (!subscription) {
|
|
221
220
|
// subscription may belongs to a different session that ours
|
|
222
221
|
if (this.findSubscription(subscriptionId)) {
|
|
223
222
|
// if yes, then access to Subscription data should be denied
|
|
224
|
-
return
|
|
223
|
+
return { statusCode: StatusCodes.BadUserAccessDenied };
|
|
225
224
|
}
|
|
226
|
-
|
|
227
|
-
return callback(null, { statusCode: StatusCodes.BadSubscriptionIdInvalid });
|
|
225
|
+
return { statusCode: StatusCodes.BadSubscriptionIdInvalid };
|
|
228
226
|
}
|
|
227
|
+
return { subscription };
|
|
228
|
+
|
|
229
|
+
}
|
|
230
|
+
function resendData(
|
|
231
|
+
this: ServerEngine,
|
|
232
|
+
inputArguments: Variant[],
|
|
233
|
+
context: ISessionContext,
|
|
234
|
+
callback: CallbackT<CallMethodResultOptions>
|
|
235
|
+
): void {
|
|
236
|
+
assert(typeof callback === "function");
|
|
237
|
+
|
|
238
|
+
const data = _getSubscription.call(this, inputArguments, context);
|
|
239
|
+
if (data.statusCode) return callback(null, { statusCode: data.statusCode });
|
|
240
|
+
const { subscription } = data;
|
|
241
|
+
|
|
242
|
+
subscription.resendInitialValues().then(() => {
|
|
243
|
+
callback(null, { statusCode: StatusCodes.Good });
|
|
244
|
+
}).catch((err) => callback(err));
|
|
245
|
+
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
// binding methods
|
|
250
|
+
function getMonitoredItemsId(
|
|
251
|
+
this: ServerEngine,
|
|
252
|
+
inputArguments: Variant[],
|
|
253
|
+
context: ISessionContext,
|
|
254
|
+
callback: CallbackT<CallMethodResultOptions>
|
|
255
|
+
) {
|
|
256
|
+
assert(typeof callback === "function");
|
|
257
|
+
|
|
258
|
+
const data = _getSubscription.call(this, inputArguments, context);
|
|
259
|
+
if (data.statusCode) return callback(null, { statusCode: data.statusCode });
|
|
260
|
+
const { subscription } = data;
|
|
261
|
+
|
|
229
262
|
const result = subscription.getMonitoredItems();
|
|
230
263
|
assert(result.statusCode);
|
|
231
264
|
assert(result.serverHandles.length === result.clientHandles.length);
|
|
@@ -1202,6 +1235,8 @@ export class ServerEngine extends EventEmitter {
|
|
|
1202
1235
|
|
|
1203
1236
|
this.__internal_bindMethod(makeNodeId(MethodIds.Server_GetMonitoredItems), getMonitoredItemsId.bind(this));
|
|
1204
1237
|
this.__internal_bindMethod(makeNodeId(MethodIds.Server_SetSubscriptionDurable), setSubscriptionDurable.bind(this));
|
|
1238
|
+
this.__internal_bindMethod(makeNodeId(MethodIds.Server_ResendData), resendData.bind(this));
|
|
1239
|
+
this.__internal_bindMethod(makeNodeId(MethodIds.Server_RequestServerStateChange), requestServerStateChange.bind(this));
|
|
1205
1240
|
|
|
1206
1241
|
// fix getMonitoredItems.outputArguments arrayDimensions
|
|
1207
1242
|
const fixGetMonitoredItemArgs = () => {
|
|
@@ -1415,7 +1450,7 @@ export class ServerEngine extends EventEmitter {
|
|
|
1415
1450
|
(!dataValue.serverTimestamp || dataValue.serverTimestamp.getTime() === minOPCUADate.getTime())
|
|
1416
1451
|
) {
|
|
1417
1452
|
dataValue.serverTimestamp = context.currentTime.timestamp;
|
|
1418
|
-
dataValue.
|
|
1453
|
+
dataValue.serverPicoseconds = 0; // context.currentTime.picoseconds;
|
|
1419
1454
|
}
|
|
1420
1455
|
dataValues.push(dataValue);
|
|
1421
1456
|
}
|
|
@@ -2246,8 +2281,8 @@ export class ServerEngine extends EventEmitter {
|
|
|
2246
2281
|
} else {
|
|
2247
2282
|
warningLog(
|
|
2248
2283
|
chalk.yellow("WARNING: cannot bind a method with id ") +
|
|
2249
|
-
|
|
2250
|
-
|
|
2284
|
+
chalk.cyan(nodeId.toString()) +
|
|
2285
|
+
chalk.yellow(". please check your nodeset.xml file or add this node programmatically")
|
|
2251
2286
|
);
|
|
2252
2287
|
warningLog(traceFromThisProjectOnly());
|
|
2253
2288
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { EventEmitter } from "events";
|
|
7
7
|
import * as chalk from "chalk";
|
|
8
8
|
|
|
9
|
-
import { SessionContext,AddressSpace, BaseNode, Duration, UAObjectType } from "node-opcua-address-space";
|
|
9
|
+
import { SessionContext, AddressSpace, BaseNode, Duration, UAObjectType } from "node-opcua-address-space";
|
|
10
10
|
import { assert } from "node-opcua-assert";
|
|
11
11
|
import { Byte, UInt32 } from "node-opcua-basic-types";
|
|
12
12
|
import { SubscriptionDiagnosticsDataType } from "node-opcua-common";
|
|
@@ -81,19 +81,22 @@ function _adjust_maxKeepAliveCount(maxKeepAliveCount?: number /*,publishingInter
|
|
|
81
81
|
return maxKeepAliveCount;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
const MaxUint32 = 0xFFFFFFFF;
|
|
85
|
+
|
|
84
86
|
function _adjust_lifeTimeCount(lifeTimeCount: number, maxKeepAliveCount: number, publishingInterval: number): number {
|
|
85
87
|
lifeTimeCount = lifeTimeCount || 1;
|
|
86
88
|
|
|
87
|
-
// let's make sure that lifeTimeCount is at least three time maxKeepAliveCount
|
|
88
|
-
// Note : the specs say ( part 3 - CreateSubscriptionParameter )
|
|
89
|
-
// "The lifetime count shall be a minimum of three times the keep keep-alive count."
|
|
90
|
-
lifeTimeCount = Math.max(lifeTimeCount, maxKeepAliveCount * 3);
|
|
91
|
-
|
|
92
89
|
const minTicks = Math.ceil(Subscription.minimumLifetimeDuration / publishingInterval);
|
|
93
90
|
const maxTicks = Math.floor(Subscription.maximumLifetimeDuration / publishingInterval);
|
|
94
91
|
|
|
95
92
|
lifeTimeCount = Math.max(minTicks, lifeTimeCount);
|
|
96
93
|
lifeTimeCount = Math.min(maxTicks, lifeTimeCount);
|
|
94
|
+
|
|
95
|
+
// let's make sure that lifeTimeCount is at least three time maxKeepAliveCount
|
|
96
|
+
// Note : the specs say ( part 3 - CreateSubscriptionParameter )
|
|
97
|
+
// "The lifetime count shall be a minimum of three times the keep keep-alive count."
|
|
98
|
+
lifeTimeCount = Math.max(lifeTimeCount, Math.min(maxKeepAliveCount * 3, MaxUint32));
|
|
99
|
+
|
|
97
100
|
return lifeTimeCount;
|
|
98
101
|
}
|
|
99
102
|
|
|
@@ -1362,7 +1365,7 @@ export class Subscription extends EventEmitter {
|
|
|
1362
1365
|
const availableSequenceNumbers = this.getAvailableSequenceNumbers();
|
|
1363
1366
|
assert(
|
|
1364
1367
|
!response.notificationMessage ||
|
|
1365
|
-
|
|
1368
|
+
availableSequenceNumbers[availableSequenceNumbers.length - 1] === response.notificationMessage.sequenceNumber
|
|
1366
1369
|
);
|
|
1367
1370
|
response.availableSequenceNumbers = availableSequenceNumbers;
|
|
1368
1371
|
|
|
@@ -1439,7 +1442,7 @@ export class Subscription extends EventEmitter {
|
|
|
1439
1442
|
} else {
|
|
1440
1443
|
debugLog(
|
|
1441
1444
|
" -> subscription.state === LATE , " +
|
|
1442
|
-
|
|
1445
|
+
"because keepAlive Response cannot be send due to lack of PublishRequest"
|
|
1443
1446
|
);
|
|
1444
1447
|
if (this.messageSent || this.keepAliveCounterHasExpired) {
|
|
1445
1448
|
this.state = SubscriptionState.LATE;
|
|
@@ -7,28 +7,46 @@ import { BaseNode, UAVariable } from "node-opcua-address-space";
|
|
|
7
7
|
import { AttributeIds } from "node-opcua-data-model";
|
|
8
8
|
import { NodeClass } from "node-opcua-data-model";
|
|
9
9
|
import { ExtensionObject } from "node-opcua-extension-object";
|
|
10
|
-
import { NodeId } from "node-opcua-nodeid";
|
|
10
|
+
import { NodeId, NodeIdType } from "node-opcua-nodeid";
|
|
11
11
|
import { DataChangeFilter, EventFilter } from "node-opcua-service-filter";
|
|
12
12
|
import { DeadbandType } from "node-opcua-service-subscription";
|
|
13
13
|
import { StatusCode, StatusCodes } from "node-opcua-status-code";
|
|
14
14
|
import { ReadValueIdOptions } from "node-opcua-types";
|
|
15
|
+
import { DataType } from "node-opcua-basic-types";
|
|
16
|
+
|
|
17
|
+
function isNumberDataType(node: UAVariable): boolean {
|
|
18
|
+
if (node.dataType.namespace === 0 && node.dataType.identifierType === NodeIdType.NUMERIC && node.dataType.value < 22) {
|
|
19
|
+
switch (node.dataType.identifierType === NodeIdType.NUMERIC && node.dataType.value) {
|
|
20
|
+
case DataType.Float:
|
|
21
|
+
case DataType.Double:
|
|
22
|
+
case DataType.Byte:
|
|
23
|
+
case DataType.SByte:
|
|
24
|
+
case DataType.Int16:
|
|
25
|
+
case DataType.Int32:
|
|
26
|
+
case DataType.Int64:
|
|
27
|
+
case DataType.UInt16:
|
|
28
|
+
case DataType.UInt32:
|
|
29
|
+
case DataType.UInt64:
|
|
30
|
+
return true;
|
|
31
|
+
default:
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const dataType = node.addressSpace.findDataType(node.dataType)!;
|
|
36
|
+
const dataTypeNumber = node.addressSpace.findDataType("Number")!;
|
|
37
|
+
return dataType.isSupertypeOf(dataTypeNumber);
|
|
38
|
+
}
|
|
15
39
|
|
|
16
40
|
function __validateDataChangeFilter(filter: DataChangeFilter, itemToMonitor: ReadValueIdOptions, node: UAVariable): StatusCode {
|
|
17
41
|
assert(itemToMonitor.attributeId === AttributeIds.Value);
|
|
18
|
-
|
|
19
42
|
if (node.nodeClass !== NodeClass.Variable) {
|
|
20
43
|
return StatusCodes.BadNodeIdInvalid;
|
|
21
44
|
}
|
|
22
45
|
|
|
23
|
-
assert(node.nodeClass === NodeClass.Variable);
|
|
24
|
-
|
|
25
|
-
// if node is not Numerical=> DataChangeFilter
|
|
26
|
-
assert(node.dataType instanceof NodeId);
|
|
27
|
-
const dataType = node.addressSpace.findDataType(node.dataType)!;
|
|
28
|
-
|
|
29
|
-
const dataTypeNumber = node.addressSpace.findDataType("Number")!;
|
|
30
46
|
if (filter.deadbandType !== DeadbandType.None) {
|
|
31
|
-
if
|
|
47
|
+
// if node is not Numerical=> DataChangeFilter
|
|
48
|
+
assert(node.dataType instanceof NodeId);
|
|
49
|
+
if (!isNumberDataType(node)) {
|
|
32
50
|
return StatusCodes.BadFilterNotAllowed;
|
|
33
51
|
}
|
|
34
52
|
}
|