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,1182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-server
|
|
3
|
+
*/
|
|
4
|
+
// tslint:disable:no-console
|
|
5
|
+
import * as async from "async";
|
|
6
|
+
import * as chalk from "chalk";
|
|
7
|
+
import { EventEmitter } from "events";
|
|
8
|
+
import * as net from "net";
|
|
9
|
+
import { Server, Socket } from "net";
|
|
10
|
+
|
|
11
|
+
import { assert } from "node-opcua-assert";
|
|
12
|
+
import { ICertificateManager, OPCUACertificateManager } from "node-opcua-certificate-manager";
|
|
13
|
+
import { Certificate, convertPEMtoDER, makeSHA1Thumbprint, PrivateKeyPEM, split_der } from "node-opcua-crypto";
|
|
14
|
+
import { checkDebugFlag, make_debugLog, make_errorLog } from "node-opcua-debug";
|
|
15
|
+
import { getFullyQualifiedDomainName, resolveFullyQualifiedDomainName } from "node-opcua-hostname";
|
|
16
|
+
import {
|
|
17
|
+
fromURI,
|
|
18
|
+
MessageSecurityMode,
|
|
19
|
+
SecurityPolicy,
|
|
20
|
+
ServerSecureChannelLayer,
|
|
21
|
+
ServerSecureChannelParent,
|
|
22
|
+
toURI,
|
|
23
|
+
AsymmetricAlgorithmSecurityHeader
|
|
24
|
+
} from "node-opcua-secure-channel";
|
|
25
|
+
import { UserTokenType } from "node-opcua-service-endpoints";
|
|
26
|
+
import { EndpointDescription } from "node-opcua-service-endpoints";
|
|
27
|
+
import { ApplicationDescription } from "node-opcua-service-endpoints";
|
|
28
|
+
import { IChannelData } from "./i_channel_data";
|
|
29
|
+
import { ISocketData } from "./i_socket_data";
|
|
30
|
+
|
|
31
|
+
const debugLog = make_debugLog(__filename);
|
|
32
|
+
const errorLog = make_errorLog(__filename);
|
|
33
|
+
const doDebug = checkDebugFlag(__filename);
|
|
34
|
+
|
|
35
|
+
const default_transportProfileUri = "http://opcfoundation.org/UA-Profile/Transport/uatcp-uasc-uabinary";
|
|
36
|
+
|
|
37
|
+
function extractSocketData(socket: net.Socket, reason: string): ISocketData {
|
|
38
|
+
const { bytesRead, bytesWritten, remoteAddress, remoteFamily, remotePort, localAddress, localPort } = socket;
|
|
39
|
+
const data: ISocketData = {
|
|
40
|
+
bytesRead,
|
|
41
|
+
bytesWritten,
|
|
42
|
+
localAddress,
|
|
43
|
+
localPort,
|
|
44
|
+
remoteAddress,
|
|
45
|
+
remoteFamily,
|
|
46
|
+
remotePort,
|
|
47
|
+
timestamp: new Date(),
|
|
48
|
+
reason
|
|
49
|
+
};
|
|
50
|
+
return data;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function extractChannelData(channel: ServerSecureChannelLayer): IChannelData {
|
|
54
|
+
const {
|
|
55
|
+
channelId,
|
|
56
|
+
clientCertificate,
|
|
57
|
+
clientNonce,
|
|
58
|
+
clientSecurityHeader,
|
|
59
|
+
securityHeader,
|
|
60
|
+
securityMode,
|
|
61
|
+
securityPolicy,
|
|
62
|
+
timeout,
|
|
63
|
+
transactionsCount
|
|
64
|
+
} = channel;
|
|
65
|
+
|
|
66
|
+
const channelData: IChannelData = {
|
|
67
|
+
channelId,
|
|
68
|
+
clientCertificate,
|
|
69
|
+
clientNonce,
|
|
70
|
+
clientSecurityHeader,
|
|
71
|
+
securityHeader,
|
|
72
|
+
securityMode,
|
|
73
|
+
securityPolicy,
|
|
74
|
+
timeout,
|
|
75
|
+
transactionsCount
|
|
76
|
+
};
|
|
77
|
+
return channelData;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function dumpChannelInfo(channels: ServerSecureChannelLayer[]): void {
|
|
81
|
+
function dumpChannel(channel: ServerSecureChannelLayer): void {
|
|
82
|
+
console.log("------------------------------------------------------");
|
|
83
|
+
console.log(" channelId = ", channel.channelId);
|
|
84
|
+
console.log(" timeout = ", channel.timeout);
|
|
85
|
+
console.log(" remoteAddress = ", channel.remoteAddress);
|
|
86
|
+
console.log(" remotePort = ", channel.remotePort);
|
|
87
|
+
console.log("");
|
|
88
|
+
console.log(" bytesWritten = ", channel.bytesWritten);
|
|
89
|
+
console.log(" bytesRead = ", channel.bytesRead);
|
|
90
|
+
|
|
91
|
+
const socket = (channel as any).transport?._socket;
|
|
92
|
+
if (!socket) {
|
|
93
|
+
console.log(" SOCKET IS CLOSED");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const channel of channels) {
|
|
98
|
+
dumpChannel(channel);
|
|
99
|
+
}
|
|
100
|
+
console.log("------------------------------------------------------");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const emptyCertificate = Buffer.alloc(0);
|
|
104
|
+
const emptyPrivateKeyPEM = "";
|
|
105
|
+
|
|
106
|
+
let OPCUAServerEndPointCounter = 0;
|
|
107
|
+
|
|
108
|
+
export interface OPCUAServerEndPointOptions {
|
|
109
|
+
/**
|
|
110
|
+
* the tcp port
|
|
111
|
+
*/
|
|
112
|
+
port: number;
|
|
113
|
+
/**
|
|
114
|
+
* the DER certificate chain
|
|
115
|
+
*/
|
|
116
|
+
certificateChain: Certificate;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* privateKey
|
|
120
|
+
*/
|
|
121
|
+
privateKey: PrivateKeyPEM;
|
|
122
|
+
|
|
123
|
+
certificateManager: OPCUACertificateManager;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* the default secureToken lifetime @default=60000
|
|
127
|
+
*/
|
|
128
|
+
defaultSecureTokenLifetime?: number;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* the maximum number of connection allowed on the TCP server socket
|
|
132
|
+
* @default 20
|
|
133
|
+
*/
|
|
134
|
+
maxConnections?: number;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* the timeout for the TCP HEL/ACK transaction (in ms)
|
|
138
|
+
* @default 30000
|
|
139
|
+
*/
|
|
140
|
+
timeout?: number;
|
|
141
|
+
|
|
142
|
+
serverInfo: ApplicationDescription;
|
|
143
|
+
|
|
144
|
+
objectFactory?: any;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface EndpointDescriptionParams {
|
|
148
|
+
allowAnonymous?: boolean;
|
|
149
|
+
restricted?: boolean;
|
|
150
|
+
allowUnsecurePassword?: boolean;
|
|
151
|
+
resourcePath?: string;
|
|
152
|
+
alternateHostname?: string[];
|
|
153
|
+
hostname: string;
|
|
154
|
+
securityPolicies: SecurityPolicy[];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface AddStandardEndpointDescriptionsParam {
|
|
158
|
+
securityModes?: MessageSecurityMode[];
|
|
159
|
+
securityPolicies?: SecurityPolicy[];
|
|
160
|
+
disableDiscovery?: boolean;
|
|
161
|
+
|
|
162
|
+
allowAnonymous?: boolean;
|
|
163
|
+
restricted?: boolean;
|
|
164
|
+
hostname?: string;
|
|
165
|
+
alternateHostname?: string[];
|
|
166
|
+
allowUnsecurePassword?: boolean;
|
|
167
|
+
resourcePath?: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getUniqueName(name: string, collection: { [key: string]: number }) {
|
|
171
|
+
if (collection[name]) {
|
|
172
|
+
let counter = 0;
|
|
173
|
+
while (collection[name + "_" + counter.toString()]) {
|
|
174
|
+
counter++;
|
|
175
|
+
}
|
|
176
|
+
name = name + "_" + counter.toString();
|
|
177
|
+
collection[name] = 1;
|
|
178
|
+
return name;
|
|
179
|
+
} else {
|
|
180
|
+
collection[name] = 1;
|
|
181
|
+
return name;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* OPCUAServerEndPoint a Server EndPoint.
|
|
186
|
+
* A sever end point is listening to one port
|
|
187
|
+
* note:
|
|
188
|
+
* see OPCUA Release 1.03 part 4 page 108 7.1 ApplicationDescription
|
|
189
|
+
*/
|
|
190
|
+
export class OPCUAServerEndPoint extends EventEmitter implements ServerSecureChannelParent {
|
|
191
|
+
/**
|
|
192
|
+
* the tcp port
|
|
193
|
+
*/
|
|
194
|
+
public port: number;
|
|
195
|
+
public certificateManager: OPCUACertificateManager;
|
|
196
|
+
public defaultSecureTokenLifetime: number;
|
|
197
|
+
public maxConnections: number;
|
|
198
|
+
public timeout: number;
|
|
199
|
+
public bytesWrittenInOldChannels: number;
|
|
200
|
+
public bytesReadInOldChannels: number;
|
|
201
|
+
public transactionsCountOldChannels: number;
|
|
202
|
+
public securityTokenCountOldChannels: number;
|
|
203
|
+
public serverInfo: ApplicationDescription;
|
|
204
|
+
public objectFactory: any;
|
|
205
|
+
|
|
206
|
+
public _on_new_channel?: (channel: ServerSecureChannelLayer) => void;
|
|
207
|
+
public _on_close_channel?: (channel: ServerSecureChannelLayer) => void;
|
|
208
|
+
public _on_connectionRefused?: (socketData: any) => void;
|
|
209
|
+
public _on_openSecureChannelFailure?: (socketData: any, channelData: any) => void;
|
|
210
|
+
|
|
211
|
+
private _certificateChain: Certificate;
|
|
212
|
+
private _privateKey: PrivateKeyPEM;
|
|
213
|
+
private _channels: { [key: string]: ServerSecureChannelLayer };
|
|
214
|
+
private _server?: Server;
|
|
215
|
+
private _endpoints: EndpointDescription[];
|
|
216
|
+
private _listen_callback: any;
|
|
217
|
+
private _started: boolean = false;
|
|
218
|
+
private _counter = OPCUAServerEndPointCounter++;
|
|
219
|
+
private _policy_deduplicator: { [key: string]: number } = {};
|
|
220
|
+
constructor(options: OPCUAServerEndPointOptions) {
|
|
221
|
+
super();
|
|
222
|
+
|
|
223
|
+
assert(!Object.prototype.hasOwnProperty.call(options,"certificate"), "expecting a certificateChain instead");
|
|
224
|
+
assert(Object.prototype.hasOwnProperty.call(options,"certificateChain"), "expecting a certificateChain");
|
|
225
|
+
assert(Object.prototype.hasOwnProperty.call(options,"privateKey"));
|
|
226
|
+
|
|
227
|
+
this.certificateManager = options.certificateManager;
|
|
228
|
+
|
|
229
|
+
options.port = options.port || 0;
|
|
230
|
+
|
|
231
|
+
this.port = parseInt(options.port.toString(), 10);
|
|
232
|
+
assert(typeof this.port === "number");
|
|
233
|
+
|
|
234
|
+
this._certificateChain = options.certificateChain;
|
|
235
|
+
this._privateKey = options.privateKey;
|
|
236
|
+
|
|
237
|
+
this._channels = {};
|
|
238
|
+
|
|
239
|
+
this.defaultSecureTokenLifetime = options.defaultSecureTokenLifetime || 600000;
|
|
240
|
+
|
|
241
|
+
this.maxConnections = options.maxConnections || 20;
|
|
242
|
+
|
|
243
|
+
this.timeout = options.timeout || 30000;
|
|
244
|
+
|
|
245
|
+
this._server = undefined;
|
|
246
|
+
|
|
247
|
+
this._setup_server();
|
|
248
|
+
|
|
249
|
+
this._endpoints = [];
|
|
250
|
+
|
|
251
|
+
this.objectFactory = options.objectFactory;
|
|
252
|
+
|
|
253
|
+
this.bytesWrittenInOldChannels = 0;
|
|
254
|
+
this.bytesReadInOldChannels = 0;
|
|
255
|
+
this.transactionsCountOldChannels = 0;
|
|
256
|
+
this.securityTokenCountOldChannels = 0;
|
|
257
|
+
|
|
258
|
+
this.serverInfo = options.serverInfo;
|
|
259
|
+
assert(this.serverInfo !== null && typeof this.serverInfo === "object");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
public dispose() {
|
|
263
|
+
this._certificateChain = emptyCertificate;
|
|
264
|
+
this._privateKey = emptyPrivateKeyPEM;
|
|
265
|
+
|
|
266
|
+
assert(Object.keys(this._channels).length === 0, "OPCUAServerEndPoint channels must have been deleted");
|
|
267
|
+
this._channels = {};
|
|
268
|
+
this.serverInfo = new ApplicationDescription({});
|
|
269
|
+
|
|
270
|
+
this._endpoints = [];
|
|
271
|
+
assert(this._endpoints.length === 0, "endpoints must have been deleted");
|
|
272
|
+
this._endpoints = [];
|
|
273
|
+
|
|
274
|
+
this._server = undefined;
|
|
275
|
+
this._listen_callback = null;
|
|
276
|
+
|
|
277
|
+
this.removeAllListeners();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
public toString(): string {
|
|
281
|
+
const privateKey1 = convertPEMtoDER(this.getPrivateKey());
|
|
282
|
+
|
|
283
|
+
const txt =
|
|
284
|
+
" end point" +
|
|
285
|
+
this._counter +
|
|
286
|
+
" port = " +
|
|
287
|
+
this.port +
|
|
288
|
+
" l = " +
|
|
289
|
+
this._endpoints.length +
|
|
290
|
+
" " +
|
|
291
|
+
makeSHA1Thumbprint(this.getCertificateChain()).toString("hex") +
|
|
292
|
+
" " +
|
|
293
|
+
makeSHA1Thumbprint(privateKey1).toString("hex");
|
|
294
|
+
return txt;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
public getChannels(): ServerSecureChannelLayer[] {
|
|
298
|
+
return Object.values(this._channels);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Returns the X509 DER form of the server certificate
|
|
303
|
+
*/
|
|
304
|
+
public getCertificate(): Certificate {
|
|
305
|
+
return split_der(this.getCertificateChain())[0];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Returns the X509 DER form of the server certificate
|
|
310
|
+
*/
|
|
311
|
+
public getCertificateChain(): Certificate {
|
|
312
|
+
return this._certificateChain;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* the private key
|
|
317
|
+
*/
|
|
318
|
+
public getPrivateKey(): PrivateKeyPEM {
|
|
319
|
+
return this._privateKey;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* The number of active channel on this end point.
|
|
324
|
+
*/
|
|
325
|
+
public get currentChannelCount(): number {
|
|
326
|
+
return Object.keys(this._channels).length;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* @method getEndpointDescription
|
|
331
|
+
* @param securityMode
|
|
332
|
+
* @param securityPolicy
|
|
333
|
+
* @return endpoint_description {EndpointDescription|null}
|
|
334
|
+
*/
|
|
335
|
+
public getEndpointDescription(
|
|
336
|
+
securityMode: MessageSecurityMode,
|
|
337
|
+
securityPolicy: SecurityPolicy,
|
|
338
|
+
endpointUrl: string | null
|
|
339
|
+
): EndpointDescription | null {
|
|
340
|
+
const endpoints = this.endpointDescriptions();
|
|
341
|
+
const arr = endpoints.filter(matching_endpoint.bind(this, securityMode, securityPolicy, endpointUrl));
|
|
342
|
+
|
|
343
|
+
if (endpointUrl && endpointUrl.length > 0 && !(arr.length === 0 || arr.length === 1)) {
|
|
344
|
+
errorLog("Several matching endpoints have been found : ");
|
|
345
|
+
for (const a of arr) {
|
|
346
|
+
errorLog(" ", a.endpointUrl, MessageSecurityMode[securityMode], securityPolicy);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return arr.length === 0 ? null : arr[0];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
public addEndpointDescription(
|
|
353
|
+
securityMode: MessageSecurityMode,
|
|
354
|
+
securityPolicy: SecurityPolicy,
|
|
355
|
+
options?: EndpointDescriptionParams
|
|
356
|
+
) {
|
|
357
|
+
if (!options) {
|
|
358
|
+
options = {
|
|
359
|
+
hostname: getFullyQualifiedDomainName(),
|
|
360
|
+
securityPolicies: [SecurityPolicy.Basic256Sha256]
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
options.allowAnonymous = options.allowAnonymous === undefined ? true : options.allowAnonymous;
|
|
365
|
+
|
|
366
|
+
// istanbul ignore next
|
|
367
|
+
if (securityMode === MessageSecurityMode.None && securityPolicy !== SecurityPolicy.None) {
|
|
368
|
+
throw new Error(" invalid security ");
|
|
369
|
+
}
|
|
370
|
+
// istanbul ignore next
|
|
371
|
+
if (securityMode !== MessageSecurityMode.None && securityPolicy === SecurityPolicy.None) {
|
|
372
|
+
throw new Error(" invalid security ");
|
|
373
|
+
}
|
|
374
|
+
//
|
|
375
|
+
|
|
376
|
+
const port = this.port;
|
|
377
|
+
|
|
378
|
+
// resource Path is a string added at the end of the url such as "/UA/Server"
|
|
379
|
+
const resourcePath = (options.resourcePath || "").replace(/\\/g, "/");
|
|
380
|
+
|
|
381
|
+
assert(resourcePath.length === 0 || resourcePath.charAt(0) === "/", "resourcePath should start with /");
|
|
382
|
+
|
|
383
|
+
const hostname = options.hostname || getFullyQualifiedDomainName();
|
|
384
|
+
const endpointUrl = `opc.tcp://${hostname}:${port}${resourcePath}`;
|
|
385
|
+
|
|
386
|
+
const endpoint_desc = this.getEndpointDescription(securityMode, securityPolicy, endpointUrl);
|
|
387
|
+
|
|
388
|
+
// istanbul ignore next
|
|
389
|
+
if (endpoint_desc) {
|
|
390
|
+
throw new Error(" endpoint already exist");
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// now build endpointUrl
|
|
394
|
+
this._endpoints.push(
|
|
395
|
+
_makeEndpointDescription({
|
|
396
|
+
collection: this._policy_deduplicator,
|
|
397
|
+
endpointUrl,
|
|
398
|
+
hostname,
|
|
399
|
+
port,
|
|
400
|
+
|
|
401
|
+
server: this.serverInfo,
|
|
402
|
+
serverCertificateChain: this.getCertificateChain(),
|
|
403
|
+
|
|
404
|
+
securityMode,
|
|
405
|
+
securityPolicy,
|
|
406
|
+
|
|
407
|
+
allowAnonymous: options.allowAnonymous,
|
|
408
|
+
allowUnsecurePassword: options.allowUnsecurePassword,
|
|
409
|
+
resourcePath: options.resourcePath,
|
|
410
|
+
|
|
411
|
+
restricted: !!options.restricted,
|
|
412
|
+
securityPolicies: options?.securityPolicies || []
|
|
413
|
+
})
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
public addRestrictedEndpointDescription(options: EndpointDescriptionParams) {
|
|
418
|
+
options = { ...options };
|
|
419
|
+
options.restricted = true;
|
|
420
|
+
return this.addEndpointDescription(MessageSecurityMode.None, SecurityPolicy.None, options);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
public addStandardEndpointDescriptions(options?: AddStandardEndpointDescriptionsParam) {
|
|
424
|
+
options = options || {};
|
|
425
|
+
|
|
426
|
+
options.securityModes = options.securityModes || defaultSecurityModes;
|
|
427
|
+
options.securityPolicies = options.securityPolicies || defaultSecurityPolicies;
|
|
428
|
+
|
|
429
|
+
const defaultHostname = options.hostname || getFullyQualifiedDomainName();
|
|
430
|
+
|
|
431
|
+
let hostnames: string[] = [defaultHostname];
|
|
432
|
+
|
|
433
|
+
options.alternateHostname = options.alternateHostname || [];
|
|
434
|
+
if (typeof options.alternateHostname === "string") {
|
|
435
|
+
options.alternateHostname = [options.alternateHostname];
|
|
436
|
+
}
|
|
437
|
+
// remove duplicates if any (uniq)
|
|
438
|
+
hostnames = [...new Set(hostnames.concat(options.alternateHostname as string[]))];
|
|
439
|
+
|
|
440
|
+
for (const alternateHostname of hostnames) {
|
|
441
|
+
const optionsE = options as EndpointDescriptionParams;
|
|
442
|
+
optionsE.hostname = alternateHostname;
|
|
443
|
+
|
|
444
|
+
if (options.securityModes.indexOf(MessageSecurityMode.None) >= 0) {
|
|
445
|
+
this.addEndpointDescription(MessageSecurityMode.None, SecurityPolicy.None, optionsE);
|
|
446
|
+
} else {
|
|
447
|
+
if (!options.disableDiscovery) {
|
|
448
|
+
this.addRestrictedEndpointDescription(optionsE);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
for (const securityMode of options.securityModes) {
|
|
452
|
+
if (securityMode === MessageSecurityMode.None) {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
for (const securityPolicy of options.securityPolicies) {
|
|
456
|
+
if (securityPolicy === SecurityPolicy.None) {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
this.addEndpointDescription(securityMode, securityPolicy, optionsE);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* returns the list of end point descriptions.
|
|
467
|
+
*/
|
|
468
|
+
public endpointDescriptions(): EndpointDescription[] {
|
|
469
|
+
return this._endpoints;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* @method listen
|
|
474
|
+
* @async
|
|
475
|
+
*/
|
|
476
|
+
public listen(callback: (err?: Error) => void) {
|
|
477
|
+
assert(typeof callback === "function");
|
|
478
|
+
assert(!this._started, "OPCUAServerEndPoint is already listening");
|
|
479
|
+
|
|
480
|
+
this._listen_callback = callback;
|
|
481
|
+
|
|
482
|
+
this._server!.on("error", (err: Error) => {
|
|
483
|
+
debugLog(chalk.red.bold(" error") + " port = " + this.port, err);
|
|
484
|
+
this._started = false;
|
|
485
|
+
this._end_listen(err);
|
|
486
|
+
});
|
|
487
|
+
this._server!.on("listening", () => {
|
|
488
|
+
debugLog("server is listening");
|
|
489
|
+
});
|
|
490
|
+
this._server!.listen(
|
|
491
|
+
this.port,
|
|
492
|
+
/*"::",*/ (err?: Error) => {
|
|
493
|
+
// 'listening' listener
|
|
494
|
+
debugLog(chalk.green.bold("LISTENING TO PORT "), this.port, "err ", err);
|
|
495
|
+
assert(!err, " cannot listen to port ");
|
|
496
|
+
this._started = true;
|
|
497
|
+
this._end_listen();
|
|
498
|
+
}
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
public killClientSockets(callback: (err?: Error) => void) {
|
|
503
|
+
for (const channel of this.getChannels()) {
|
|
504
|
+
const hacked_channel = channel as any;
|
|
505
|
+
if (hacked_channel.transport && hacked_channel.transport._socket) {
|
|
506
|
+
// hacked_channel.transport._socket.close();
|
|
507
|
+
hacked_channel.transport._socket.destroy();
|
|
508
|
+
hacked_channel.transport._socket.emit("error", new Error("EPIPE"));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
callback();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
public suspendConnection(callback: (err?: Error) => void) {
|
|
515
|
+
if (!this._started) {
|
|
516
|
+
return callback(new Error("Connection already suspended !!"));
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Stops the server from accepting new connections and keeps existing connections.
|
|
520
|
+
// (note from nodejs doc: This function is asynchronous, the server is finally closed
|
|
521
|
+
// when all connections are ended and the server emits a 'close' event.
|
|
522
|
+
// The optional callback will be called once the 'close' event occurs.
|
|
523
|
+
// Unlike that event, it will be called with an Error as its only argument
|
|
524
|
+
// if the server was not open when it was closed.
|
|
525
|
+
this._server!.close(() => {
|
|
526
|
+
this._started = false;
|
|
527
|
+
debugLog("Connection has been closed !" + this.port);
|
|
528
|
+
});
|
|
529
|
+
this._started = false;
|
|
530
|
+
callback();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
public restoreConnection(callback: (err?: Error) => void) {
|
|
534
|
+
this.listen(callback);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
public abruptlyInterruptChannels() {
|
|
538
|
+
for (const channel of Object.values(this._channels)) {
|
|
539
|
+
channel.abruptlyInterrupt();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* @method shutdown
|
|
545
|
+
* @async
|
|
546
|
+
*/
|
|
547
|
+
public shutdown(callback: (err?: Error) => void) {
|
|
548
|
+
debugLog("OPCUAServerEndPoint#shutdown ");
|
|
549
|
+
|
|
550
|
+
if (this._started) {
|
|
551
|
+
// make sure we don't accept new connection any more ...
|
|
552
|
+
this.suspendConnection(() => {
|
|
553
|
+
// shutdown all opened channels ...
|
|
554
|
+
const _channels = Object.values(this._channels);
|
|
555
|
+
async.each(
|
|
556
|
+
_channels,
|
|
557
|
+
(channel: ServerSecureChannelLayer, callback1: (err?: Error) => void) => {
|
|
558
|
+
this.shutdown_channel(channel, callback1);
|
|
559
|
+
},
|
|
560
|
+
(err?: Error | null) => {
|
|
561
|
+
/* istanbul ignore next */
|
|
562
|
+
if (!(Object.keys(this._channels).length === 0)) {
|
|
563
|
+
errorLog(" Bad !");
|
|
564
|
+
}
|
|
565
|
+
assert(Object.keys(this._channels).length === 0, "channel must have unregistered themselves");
|
|
566
|
+
callback(err || undefined);
|
|
567
|
+
}
|
|
568
|
+
);
|
|
569
|
+
});
|
|
570
|
+
} else {
|
|
571
|
+
callback();
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* @method start
|
|
577
|
+
* @async
|
|
578
|
+
* @param callback
|
|
579
|
+
*/
|
|
580
|
+
public start(callback: (err?: Error) => void): void {
|
|
581
|
+
assert(typeof callback === "function");
|
|
582
|
+
this.listen(callback);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
public get bytesWritten(): number {
|
|
586
|
+
const channels = Object.values(this._channels);
|
|
587
|
+
return (
|
|
588
|
+
this.bytesWrittenInOldChannels +
|
|
589
|
+
channels.reduce((accumulated: number, channel: ServerSecureChannelLayer) => {
|
|
590
|
+
return accumulated + channel.bytesWritten;
|
|
591
|
+
}, 0)
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
public get bytesRead(): number {
|
|
596
|
+
const channels = Object.values(this._channels);
|
|
597
|
+
return (
|
|
598
|
+
this.bytesReadInOldChannels +
|
|
599
|
+
channels.reduce((accumulated: number, channel: ServerSecureChannelLayer) => {
|
|
600
|
+
return accumulated + channel.bytesRead;
|
|
601
|
+
}, 0)
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
public get transactionsCount(): number {
|
|
606
|
+
const channels = Object.values(this._channels);
|
|
607
|
+
return (
|
|
608
|
+
this.transactionsCountOldChannels +
|
|
609
|
+
channels.reduce((accumulated: number, channel: ServerSecureChannelLayer) => {
|
|
610
|
+
return accumulated + channel.transactionsCount;
|
|
611
|
+
}, 0)
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
public get securityTokenCount(): number {
|
|
616
|
+
const channels = Object.values(this._channels);
|
|
617
|
+
return (
|
|
618
|
+
this.securityTokenCountOldChannels +
|
|
619
|
+
channels.reduce((accumulated: number, channel: ServerSecureChannelLayer) => {
|
|
620
|
+
return accumulated + channel.securityTokenCount;
|
|
621
|
+
}, 0)
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
public get activeChannelCount(): number {
|
|
626
|
+
return Object.keys(this._channels).length;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
private _dump_statistics() {
|
|
630
|
+
const self = this;
|
|
631
|
+
|
|
632
|
+
self._server!.getConnections((err: Error | null, count: number) => {
|
|
633
|
+
debugLog(chalk.cyan("CONCURRENT CONNECTION = "), count);
|
|
634
|
+
});
|
|
635
|
+
debugLog(chalk.cyan("MAX CONNECTIONS = "), self._server!.maxConnections);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
private _setup_server() {
|
|
639
|
+
assert(!this._server);
|
|
640
|
+
this._server = net.createServer({ pauseOnConnect: true }, this._on_client_connection.bind(this));
|
|
641
|
+
|
|
642
|
+
// xx console.log(" Server with max connections ", self.maxConnections);
|
|
643
|
+
this._server.maxConnections = this.maxConnections + 1; // plus one extra
|
|
644
|
+
|
|
645
|
+
this._listen_callback = null;
|
|
646
|
+
this._server
|
|
647
|
+
.on("connection", (socket: NodeJS.Socket) => {
|
|
648
|
+
// istanbul ignore next
|
|
649
|
+
if (doDebug) {
|
|
650
|
+
this._dump_statistics();
|
|
651
|
+
debugLog("server connected with : " + (socket as any).remoteAddress + ":" + (socket as any).remotePort);
|
|
652
|
+
}
|
|
653
|
+
})
|
|
654
|
+
.on("close", () => {
|
|
655
|
+
debugLog("server closed : all connections have ended");
|
|
656
|
+
})
|
|
657
|
+
.on("error", (err: Error) => {
|
|
658
|
+
// this could be because the port is already in use
|
|
659
|
+
debugLog(chalk.red.bold("server error: "), err.message);
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
private _on_client_connection(socket: Socket) {
|
|
664
|
+
// a client is attempting a connection on the socket
|
|
665
|
+
(socket as any).setNoDelay(true);
|
|
666
|
+
|
|
667
|
+
debugLog("OPCUAServerEndPoint#_on_client_connection", this._started);
|
|
668
|
+
if (!this._started) {
|
|
669
|
+
debugLog(
|
|
670
|
+
chalk.bgWhite.cyan(
|
|
671
|
+
"OPCUAServerEndPoint#_on_client_connection " +
|
|
672
|
+
"SERVER END POINT IS PROBABLY SHUTTING DOWN !!! - Connection is refused"
|
|
673
|
+
)
|
|
674
|
+
);
|
|
675
|
+
socket.end();
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const establish_connection = () => {
|
|
680
|
+
const nbConnections = Object.keys(this._channels).length;
|
|
681
|
+
debugLog(
|
|
682
|
+
" nbConnections ",
|
|
683
|
+
nbConnections,
|
|
684
|
+
" self._server.maxConnections",
|
|
685
|
+
this._server!.maxConnections,
|
|
686
|
+
this.maxConnections
|
|
687
|
+
);
|
|
688
|
+
if (nbConnections >= this.maxConnections) {
|
|
689
|
+
console.log(
|
|
690
|
+
chalk.bgWhite.cyan(
|
|
691
|
+
"OPCUAServerEndPoint#_on_client_connection " +
|
|
692
|
+
"The maximum number of connection has been reached - Connection is refused"
|
|
693
|
+
)
|
|
694
|
+
);
|
|
695
|
+
const reason = "maxConnections reached (" + this.maxConnections + ")";
|
|
696
|
+
const socketData = extractSocketData(socket, reason);
|
|
697
|
+
this.emit("connectionRefused", socketData);
|
|
698
|
+
|
|
699
|
+
socket.end();
|
|
700
|
+
(socket as any).destroy();
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
debugLog("OPCUAServerEndPoint._on_client_connection successful => New Channel");
|
|
705
|
+
|
|
706
|
+
const channel = new ServerSecureChannelLayer({
|
|
707
|
+
defaultSecureTokenLifetime: this.defaultSecureTokenLifetime,
|
|
708
|
+
// objectFactory: this.objectFactory,
|
|
709
|
+
parent: this,
|
|
710
|
+
timeout: this.timeout
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
socket.resume();
|
|
714
|
+
|
|
715
|
+
this._preregisterChannel(channel);
|
|
716
|
+
|
|
717
|
+
channel.init(socket, (err?: Error) => {
|
|
718
|
+
this._un_pre_registerChannel(channel);
|
|
719
|
+
debugLog(chalk.yellow.bold("Channel#init done"), err);
|
|
720
|
+
if (err) {
|
|
721
|
+
const reason = "openSecureChannel has Failed " + err.message;
|
|
722
|
+
const socketData = extractSocketData(socket, reason);
|
|
723
|
+
const channelData = extractChannelData(channel);
|
|
724
|
+
this.emit("openSecureChannelFailure", socketData, channelData);
|
|
725
|
+
|
|
726
|
+
socket.end();
|
|
727
|
+
(socket as any).destroy();
|
|
728
|
+
} else {
|
|
729
|
+
debugLog("server receiving a client connection");
|
|
730
|
+
this._registerChannel(channel);
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
channel.on("message", (message: any) => {
|
|
735
|
+
// forward
|
|
736
|
+
this.emit("message", message, channel, this);
|
|
737
|
+
});
|
|
738
|
+
};
|
|
739
|
+
// Each SecureChannel exists until it is explicitly closed or until the last token has expired and the overlap
|
|
740
|
+
// period has elapsed. A Server application should limit the number of SecureChannels.
|
|
741
|
+
// To protect against misbehaving Clients and denial of service attacks, the Server shall close the oldest
|
|
742
|
+
// SecureChannel that has no Session assigned before reaching the maximum number of supported SecureChannels.
|
|
743
|
+
this._prevent_DDOS_Attack(establish_connection);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
private _preregisterChannel(channel: ServerSecureChannelLayer) {
|
|
747
|
+
// _preregisterChannel is used to keep track of channel for which
|
|
748
|
+
// that are in early stage of the hand shaking process.
|
|
749
|
+
// e.g HEL/ACK and OpenSecureChannel may not have been received yet
|
|
750
|
+
// as they will need to be interrupted when OPCUAServerEndPoint is closed
|
|
751
|
+
assert(this._started, "OPCUAServerEndPoint must be started");
|
|
752
|
+
|
|
753
|
+
assert(!this._channels.hasOwnProperty(channel.hashKey), " channel already preregistered!");
|
|
754
|
+
|
|
755
|
+
this._channels[channel.hashKey] = channel;
|
|
756
|
+
|
|
757
|
+
(channel as any)._unpreregisterChannelEvent = () => {
|
|
758
|
+
debugLog("Channel received an abort event during the preregistration phase");
|
|
759
|
+
this._un_pre_registerChannel(channel);
|
|
760
|
+
channel.dispose();
|
|
761
|
+
};
|
|
762
|
+
channel.on("abort", (channel as any)._unpreregisterChannelEvent);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private _un_pre_registerChannel(channel: ServerSecureChannelLayer) {
|
|
766
|
+
if (!this._channels[channel.hashKey]) {
|
|
767
|
+
debugLog("Already un preregistered ?", channel.hashKey);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
delete this._channels[channel.hashKey];
|
|
772
|
+
assert(typeof (channel as any)._unpreregisterChannelEvent === "function");
|
|
773
|
+
channel.removeListener("abort", (channel as any)._unpreregisterChannelEvent);
|
|
774
|
+
(channel as any)._unpreregisterChannelEvent = null;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* @method _registerChannel
|
|
779
|
+
* @param channel
|
|
780
|
+
* @private
|
|
781
|
+
*/
|
|
782
|
+
private _registerChannel(channel: ServerSecureChannelLayer) {
|
|
783
|
+
if (this._started) {
|
|
784
|
+
debugLog(chalk.red("_registerChannel = "), "channel.hashKey = ", channel.hashKey);
|
|
785
|
+
|
|
786
|
+
assert(!this._channels[channel.hashKey]);
|
|
787
|
+
this._channels[channel.hashKey] = channel;
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* @event newChannel
|
|
791
|
+
* @param channel
|
|
792
|
+
*/
|
|
793
|
+
this.emit("newChannel", channel);
|
|
794
|
+
|
|
795
|
+
channel.on("abort", () => {
|
|
796
|
+
this._unregisterChannel(channel);
|
|
797
|
+
});
|
|
798
|
+
} else {
|
|
799
|
+
debugLog("OPCUAServerEndPoint#_registerChannel called when end point is shutdown !");
|
|
800
|
+
debugLog(" -> channel will be forcefully terminated");
|
|
801
|
+
channel.close();
|
|
802
|
+
channel.dispose();
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* @method _unregisterChannel
|
|
808
|
+
* @param channel
|
|
809
|
+
* @private
|
|
810
|
+
*/
|
|
811
|
+
private _unregisterChannel(channel: ServerSecureChannelLayer): void {
|
|
812
|
+
debugLog("_un-registerChannel channel.hashKey", channel.hashKey);
|
|
813
|
+
if (!this._channels.hasOwnProperty(channel.hashKey)) {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
assert(this._channels.hasOwnProperty(channel.hashKey), "channel is not registered");
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* @event closeChannel
|
|
821
|
+
* @param channel
|
|
822
|
+
*/
|
|
823
|
+
this.emit("closeChannel", channel);
|
|
824
|
+
|
|
825
|
+
// keep trace of statistics data from old channel for our own accumulated stats.
|
|
826
|
+
this.bytesWrittenInOldChannels += channel.bytesWritten;
|
|
827
|
+
this.bytesReadInOldChannels += channel.bytesRead;
|
|
828
|
+
this.transactionsCountOldChannels += channel.transactionsCount;
|
|
829
|
+
delete this._channels[channel.hashKey];
|
|
830
|
+
|
|
831
|
+
// istanbul ignore next
|
|
832
|
+
if (doDebug) {
|
|
833
|
+
this._dump_statistics();
|
|
834
|
+
debugLog("un-registering channel - Count = ", this.currentChannelCount);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/// channel.dispose();
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
private _end_listen(err?: Error) {
|
|
841
|
+
assert(typeof this._listen_callback === "function");
|
|
842
|
+
this._listen_callback(err);
|
|
843
|
+
this._listen_callback = null;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* shutdown_channel
|
|
848
|
+
* @param channel
|
|
849
|
+
* @param inner_callback
|
|
850
|
+
*/
|
|
851
|
+
private shutdown_channel(channel: ServerSecureChannelLayer, inner_callback: (err?: Error) => void) {
|
|
852
|
+
assert(typeof inner_callback === "function");
|
|
853
|
+
channel.once("close", () => {
|
|
854
|
+
// xx console.log(" ON CLOSED !!!!");
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
channel.close(() => {
|
|
858
|
+
this._unregisterChannel(channel);
|
|
859
|
+
setImmediate(inner_callback);
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* @private
|
|
865
|
+
*/
|
|
866
|
+
private _prevent_DDOS_Attack(establish_connection: () => void) {
|
|
867
|
+
const nbConnections = this.activeChannelCount;
|
|
868
|
+
|
|
869
|
+
if (nbConnections >= this.maxConnections) {
|
|
870
|
+
// istanbul ignore next
|
|
871
|
+
console.log(chalk.bgRed.white("PREVENTING DDOS ATTACK => Closing unused channels"));
|
|
872
|
+
|
|
873
|
+
const unused_channels: ServerSecureChannelLayer[] = this.getChannels().filter((channel1: ServerSecureChannelLayer) => {
|
|
874
|
+
return !channel1.isOpened && !channel1.hasSession;
|
|
875
|
+
});
|
|
876
|
+
if (unused_channels.length === 0) {
|
|
877
|
+
// all channels are in used , we cannot get any
|
|
878
|
+
|
|
879
|
+
// istanbul ignore next
|
|
880
|
+
console.log(" - all channel are used !!!!");
|
|
881
|
+
dumpChannelInfo(this.getChannels());
|
|
882
|
+
setImmediate(establish_connection);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
// istanbul ignore next
|
|
886
|
+
if (doDebug) {
|
|
887
|
+
console.log(
|
|
888
|
+
" - Unused channels that can be clobbered",
|
|
889
|
+
unused_channels.map((channel1: ServerSecureChannelLayer) => channel1.hashKey).join(" ")
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
const channel = unused_channels[0];
|
|
893
|
+
|
|
894
|
+
channel.close(() => {
|
|
895
|
+
// istanbul ignore next
|
|
896
|
+
if (doDebug) {
|
|
897
|
+
console.log(" _ Unused channel has been closed ", channel.hashKey);
|
|
898
|
+
}
|
|
899
|
+
this._unregisterChannel(channel);
|
|
900
|
+
establish_connection();
|
|
901
|
+
});
|
|
902
|
+
} else {
|
|
903
|
+
setImmediate(establish_connection);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
interface MakeEndpointDescriptionOptions {
|
|
909
|
+
/**
|
|
910
|
+
* port number s
|
|
911
|
+
*/
|
|
912
|
+
port: number;
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* @default default hostname (default value will be full qualified domain name)
|
|
916
|
+
*/
|
|
917
|
+
hostname: string;
|
|
918
|
+
/**
|
|
919
|
+
*
|
|
920
|
+
*/
|
|
921
|
+
endpointUrl: string;
|
|
922
|
+
|
|
923
|
+
serverCertificateChain: Certificate;
|
|
924
|
+
/**
|
|
925
|
+
*
|
|
926
|
+
*/
|
|
927
|
+
securityMode: MessageSecurityMode;
|
|
928
|
+
/**
|
|
929
|
+
*
|
|
930
|
+
*/
|
|
931
|
+
securityPolicy: SecurityPolicy;
|
|
932
|
+
|
|
933
|
+
securityLevel?: number;
|
|
934
|
+
|
|
935
|
+
server: ApplicationDescription;
|
|
936
|
+
/*
|
|
937
|
+
{
|
|
938
|
+
applicationUri: string;
|
|
939
|
+
applicationName: LocalizedTextOptions;
|
|
940
|
+
applicationType: ApplicationType;
|
|
941
|
+
gatewayServerUri: string;
|
|
942
|
+
discoveryProfileUri: string;
|
|
943
|
+
discoveryUrls: string[];
|
|
944
|
+
};
|
|
945
|
+
*/
|
|
946
|
+
resourcePath?: string;
|
|
947
|
+
allowAnonymous?: boolean; // default true
|
|
948
|
+
|
|
949
|
+
// allow un-encrypted password in userNameIdentity
|
|
950
|
+
allowUnsecurePassword?: boolean; // default false
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* onlyCertificateLessConnection
|
|
954
|
+
*/
|
|
955
|
+
onlyCertificateLessConnection?: boolean;
|
|
956
|
+
|
|
957
|
+
restricted: boolean;
|
|
958
|
+
|
|
959
|
+
collection: { [key: string]: number };
|
|
960
|
+
|
|
961
|
+
securityPolicies: SecurityPolicy[];
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
interface EndpointDescriptionEx extends EndpointDescription {
|
|
965
|
+
restricted: boolean;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function estimateSecurityLevel(securityMode: MessageSecurityMode, securityPolicy: SecurityPolicy): number {
|
|
969
|
+
if (securityMode === MessageSecurityMode.None) {
|
|
970
|
+
return 1;
|
|
971
|
+
}
|
|
972
|
+
let offset = 100;
|
|
973
|
+
if (securityMode === MessageSecurityMode.SignAndEncrypt) {
|
|
974
|
+
offset = 200;
|
|
975
|
+
}
|
|
976
|
+
switch (securityPolicy) {
|
|
977
|
+
case SecurityPolicy.Basic128:
|
|
978
|
+
case SecurityPolicy.Basic128Rsa15:
|
|
979
|
+
case SecurityPolicy.Basic192:
|
|
980
|
+
return 2; // deprecated => low
|
|
981
|
+
case SecurityPolicy.Basic192Rsa15:
|
|
982
|
+
return 3; // deprecated => low
|
|
983
|
+
case SecurityPolicy.Basic256:
|
|
984
|
+
return 4; // deprecated => low
|
|
985
|
+
case SecurityPolicy.Basic256Rsa15:
|
|
986
|
+
return 4 + offset;
|
|
987
|
+
case SecurityPolicy.Basic256Sha256:
|
|
988
|
+
return 6 + offset;
|
|
989
|
+
case SecurityPolicy.Aes128_Sha256_RsaOaep:
|
|
990
|
+
return 1;
|
|
991
|
+
|
|
992
|
+
default:
|
|
993
|
+
case SecurityPolicy.None:
|
|
994
|
+
return 1;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* @private
|
|
999
|
+
*/
|
|
1000
|
+
function _makeEndpointDescription(options: MakeEndpointDescriptionOptions): EndpointDescriptionEx {
|
|
1001
|
+
assert(isFinite(options.port), "expecting a valid port number");
|
|
1002
|
+
assert(Object.prototype.hasOwnProperty.call(options,"serverCertificateChain"));
|
|
1003
|
+
assert(!Object.prototype.hasOwnProperty.call(options,"serverCertificate"));
|
|
1004
|
+
assert(!!options.securityMode); // s.MessageSecurityMode
|
|
1005
|
+
assert(!!options.securityPolicy);
|
|
1006
|
+
assert(options.server !== null && typeof options.server === "object");
|
|
1007
|
+
assert(!!options.hostname && typeof options.hostname === "string");
|
|
1008
|
+
assert(typeof options.restricted === "boolean");
|
|
1009
|
+
|
|
1010
|
+
const u = (n: string) => getUniqueName(n, options.collection);
|
|
1011
|
+
options.securityLevel =
|
|
1012
|
+
options.securityLevel === undefined
|
|
1013
|
+
? estimateSecurityLevel(options.securityMode, options.securityPolicy)
|
|
1014
|
+
: options.securityLevel;
|
|
1015
|
+
assert(isFinite(options.securityLevel), "expecting a valid securityLevel");
|
|
1016
|
+
|
|
1017
|
+
const securityPolicyUri = toURI(options.securityPolicy);
|
|
1018
|
+
|
|
1019
|
+
const userIdentityTokens = [];
|
|
1020
|
+
|
|
1021
|
+
if (options.securityPolicy === SecurityPolicy.None) {
|
|
1022
|
+
if (options.allowUnsecurePassword) {
|
|
1023
|
+
userIdentityTokens.push({
|
|
1024
|
+
policyId: u("username_unsecure"),
|
|
1025
|
+
tokenType: UserTokenType.UserName,
|
|
1026
|
+
|
|
1027
|
+
issuedTokenType: null,
|
|
1028
|
+
issuerEndpointUrl: null,
|
|
1029
|
+
securityPolicyUri: null
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const onlyCertificateLessConnection =
|
|
1034
|
+
options.onlyCertificateLessConnection === undefined ? false : options.onlyCertificateLessConnection;
|
|
1035
|
+
|
|
1036
|
+
if (!onlyCertificateLessConnection) {
|
|
1037
|
+
if (options.securityPolicies.indexOf(SecurityPolicy.Basic256) >= 0) {
|
|
1038
|
+
userIdentityTokens.push({
|
|
1039
|
+
policyId: u("username_basic256"),
|
|
1040
|
+
tokenType: UserTokenType.UserName,
|
|
1041
|
+
|
|
1042
|
+
issuedTokenType: null,
|
|
1043
|
+
issuerEndpointUrl: null,
|
|
1044
|
+
securityPolicyUri: SecurityPolicy.Basic256
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
if (options.securityPolicies.indexOf(SecurityPolicy.Basic128Rsa15) >= 0) {
|
|
1049
|
+
userIdentityTokens.push({
|
|
1050
|
+
policyId: u("username_basic128Rsa15"),
|
|
1051
|
+
tokenType: UserTokenType.UserName,
|
|
1052
|
+
|
|
1053
|
+
issuedTokenType: null,
|
|
1054
|
+
issuerEndpointUrl: null,
|
|
1055
|
+
securityPolicyUri: SecurityPolicy.Basic128Rsa15
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (options.securityPolicies.indexOf(SecurityPolicy.Basic256Sha256) >= 0) {
|
|
1060
|
+
userIdentityTokens.push({
|
|
1061
|
+
policyId: u("username_basic256Sha256"),
|
|
1062
|
+
tokenType: UserTokenType.UserName,
|
|
1063
|
+
|
|
1064
|
+
issuedTokenType: null,
|
|
1065
|
+
issuerEndpointUrl: null,
|
|
1066
|
+
securityPolicyUri: SecurityPolicy.Basic256Sha256
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// X509
|
|
1071
|
+
if (options.securityPolicies.indexOf(SecurityPolicy.Basic256) >= 0) {
|
|
1072
|
+
userIdentityTokens.push({
|
|
1073
|
+
policyId: u("certificate_basic256"),
|
|
1074
|
+
tokenType: UserTokenType.UserName,
|
|
1075
|
+
|
|
1076
|
+
issuedTokenType: null,
|
|
1077
|
+
issuerEndpointUrl: null,
|
|
1078
|
+
securityPolicyUri: SecurityPolicy.Basic256
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
// Certificate
|
|
1082
|
+
if (options.securityPolicies.indexOf(SecurityPolicy.Basic256Sha256) >= 0) {
|
|
1083
|
+
userIdentityTokens.push({
|
|
1084
|
+
policyId: u("certificate_basic256Sha256"),
|
|
1085
|
+
tokenType: UserTokenType.Certificate,
|
|
1086
|
+
|
|
1087
|
+
issuedTokenType: null,
|
|
1088
|
+
issuerEndpointUrl: null,
|
|
1089
|
+
securityPolicyUri: SecurityPolicy.Basic256Sha256
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
} else {
|
|
1094
|
+
// note:
|
|
1095
|
+
// when channel session security is not "None",
|
|
1096
|
+
// userIdentityTokens can be left to null.
|
|
1097
|
+
// in this case this mean that secure policy will be the same as connection security policy
|
|
1098
|
+
userIdentityTokens.push({
|
|
1099
|
+
policyId: u("usernamePassword"),
|
|
1100
|
+
tokenType: UserTokenType.UserName,
|
|
1101
|
+
|
|
1102
|
+
issuedTokenType: null,
|
|
1103
|
+
issuerEndpointUrl: null,
|
|
1104
|
+
securityPolicyUri: null
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
userIdentityTokens.push({
|
|
1108
|
+
policyId: u("certificateX509"),
|
|
1109
|
+
tokenType: UserTokenType.Certificate,
|
|
1110
|
+
|
|
1111
|
+
issuedTokenType: null,
|
|
1112
|
+
issuerEndpointUrl: null,
|
|
1113
|
+
securityPolicyUri: null
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
if (options.allowAnonymous) {
|
|
1118
|
+
userIdentityTokens.push({
|
|
1119
|
+
policyId: u("anonymous"),
|
|
1120
|
+
tokenType: UserTokenType.Anonymous,
|
|
1121
|
+
|
|
1122
|
+
issuedTokenType: null,
|
|
1123
|
+
issuerEndpointUrl: null,
|
|
1124
|
+
securityPolicyUri: null
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// return the endpoint object
|
|
1129
|
+
const endpoint = new EndpointDescription({
|
|
1130
|
+
endpointUrl: options.endpointUrl,
|
|
1131
|
+
|
|
1132
|
+
server: undefined, // options.server,
|
|
1133
|
+
serverCertificate: options.serverCertificateChain,
|
|
1134
|
+
|
|
1135
|
+
securityMode: options.securityMode,
|
|
1136
|
+
securityPolicyUri,
|
|
1137
|
+
userIdentityTokens,
|
|
1138
|
+
|
|
1139
|
+
securityLevel: options.securityLevel,
|
|
1140
|
+
transportProfileUri: default_transportProfileUri
|
|
1141
|
+
}) as EndpointDescriptionEx;
|
|
1142
|
+
|
|
1143
|
+
(endpoint as any).__defineGetter__("endpointUrl", () => {
|
|
1144
|
+
return resolveFullyQualifiedDomainName(options.endpointUrl);
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
endpoint.server = options.server;
|
|
1148
|
+
|
|
1149
|
+
endpoint.restricted = options.restricted;
|
|
1150
|
+
|
|
1151
|
+
return endpoint;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* return true if the end point matches security mode and policy
|
|
1156
|
+
* @param endpoint
|
|
1157
|
+
* @param securityMode
|
|
1158
|
+
* @param securityPolicy
|
|
1159
|
+
* @internal
|
|
1160
|
+
*
|
|
1161
|
+
*/
|
|
1162
|
+
function matching_endpoint(
|
|
1163
|
+
securityMode: MessageSecurityMode,
|
|
1164
|
+
securityPolicy: SecurityPolicy,
|
|
1165
|
+
endpointUrl: string | null,
|
|
1166
|
+
endpoint: EndpointDescription
|
|
1167
|
+
): boolean {
|
|
1168
|
+
assert(endpoint instanceof EndpointDescription);
|
|
1169
|
+
const endpoint_securityPolicy = fromURI(endpoint.securityPolicyUri);
|
|
1170
|
+
if (endpointUrl && endpoint.endpointUrl! !== endpointUrl) {
|
|
1171
|
+
return false;
|
|
1172
|
+
}
|
|
1173
|
+
return endpoint.securityMode === securityMode && endpoint_securityPolicy === securityPolicy;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
const defaultSecurityModes = [MessageSecurityMode.None, MessageSecurityMode.Sign, MessageSecurityMode.SignAndEncrypt];
|
|
1177
|
+
const defaultSecurityPolicies = [
|
|
1178
|
+
SecurityPolicy.Basic128Rsa15,
|
|
1179
|
+
SecurityPolicy.Basic256,
|
|
1180
|
+
// xx UNUSED!! SecurityPolicy.Basic256Rsa15,
|
|
1181
|
+
SecurityPolicy.Basic256Sha256
|
|
1182
|
+
];
|