@@ -17,8 +17,16 @@ import { Subscription } from "rxjs/Subscription";
17
17
import { promisify } from "util" ;
18
18
import { Readable } from "stream" ;
19
19
import { URL } from "url" ;
20
-
21
- import { ProtocolClient , Content , ContentSerdes , Form , SecurityScheme , createLoggers } from "@node-wot/core" ;
20
+ import {
21
+ ProtocolClient ,
22
+ Content ,
23
+ ContentSerdes ,
24
+ Form ,
25
+ SecurityScheme ,
26
+ createLoggers ,
27
+ AllOfSecurityScheme ,
28
+ OneOfSecurityScheme ,
29
+ } from "@node-wot/core" ;
22
30
23
31
import {
24
32
ClientSession ,
@@ -36,19 +44,29 @@ import {
36
44
VariantArrayType ,
37
45
Variant ,
38
46
VariantOptions ,
47
+ SecurityPolicy ,
39
48
} from "node-opcua-client" ;
40
- import { ArgumentDefinition , getBuiltInDataType , readNamespaceArray } from "node-opcua-pseudo-session" ;
41
-
49
+ import {
50
+ AnonymousIdentity ,
51
+ ArgumentDefinition ,
52
+ getBuiltInDataType ,
53
+ readNamespaceArray ,
54
+ UserIdentityInfo ,
55
+ } from "node-opcua-pseudo-session" ;
42
56
import { makeNodeId , NodeId , NodeIdLike , NodeIdType , resolveNodeId } from "node-opcua-nodeid" ;
43
57
import { AttributeIds , BrowseDirection , makeResultMask } from "node-opcua-data-model" ;
44
58
import { makeBrowsePath } from "node-opcua-service-translate-browse-path" ;
45
59
import { StatusCodes } from "node-opcua-status-code" ;
46
-
47
- import { schemaDataValue } from "./codec" ;
60
+ import { coercePrivateKeyPem , readPrivateKey } from "node-opcua-crypto" ;
48
61
import { opcuaJsonEncodeVariant } from "node-opcua-json" ;
49
- import { Argument , BrowseDescription , BrowseResult } from "node-opcua-types" ;
62
+ import { Argument , BrowseDescription , BrowseResult , MessageSecurityMode , UserTokenType } from "node-opcua-types" ;
50
63
import { isGoodish2 , ReferenceTypeIds } from "node-opcua" ;
51
64
65
+ import { schemaDataValue } from "./codec" ;
66
+ import { OPCUACAuthenticationScheme , OPCUAChannelSecurityScheme } from "./security_scheme" ;
67
+ import { CertificateManagerSingleton } from "./certificate-manager-singleton" ;
68
+ import { resolveChannelSecurity , resolvedUserIdentity } from "./opcua-security-resolver" ;
69
+
52
70
const { debug } = createLoggers ( "binding-opcua" , "opcua-protocol-client" ) ;
53
71
54
72
export type Command = "Read" | "Write" | "Subscribe" ;
@@ -141,6 +159,10 @@ function _variantToJSON(variant: Variant, contentType: string) {
141
159
export class OPCUAProtocolClient implements ProtocolClient {
142
160
private _connections : Map < string , OPCUAConnectionEx > = new Map < string , OPCUAConnectionEx > ( ) ;
143
161
162
+ private _securityMode : MessageSecurityMode = MessageSecurityMode . None ;
163
+ private _securityPolicy : SecurityPolicy = SecurityPolicy . None ;
164
+ private _userIdentity : UserIdentityInfo = < AnonymousIdentity > { type : UserTokenType . Anonymous } ;
165
+
144
166
private async _withConnection < T > ( form : OPCUAForm , next : ( connection : OPCUAConnection ) => Promise < T > ) : Promise < T > {
145
167
const endpoint = form . href ;
146
168
const matchesScheme : boolean = endpoint ?. match ( / ^ o p c .t c p : \/ \/ / ) != null ;
@@ -150,11 +172,15 @@ export class OPCUAProtocolClient implements ProtocolClient {
150
172
}
151
173
let c : OPCUAConnectionEx | undefined = this . _connections . get ( endpoint ) ;
152
174
if ( ! c ) {
175
+ const clientCertificateManager = await CertificateManagerSingleton . getCertificateManager ( ) ;
153
176
const client = OPCUAClient . create ( {
154
177
endpointMustExist : false ,
155
178
connectionStrategy : {
156
179
maxRetry : 1 ,
157
180
} ,
181
+ securityMode : this . _securityMode ,
182
+ securityPolicy : this . _securityPolicy ,
183
+ clientCertificateManager,
158
184
} ) ;
159
185
client . on ( "backoff" , ( ) => {
160
186
debug ( `connection:backoff: cannot connection to ${ endpoint } ` ) ;
@@ -168,7 +194,19 @@ export class OPCUAProtocolClient implements ProtocolClient {
168
194
this . _connections . set ( endpoint , c ) ;
169
195
try {
170
196
await client . connect ( endpoint ) ;
171
- const session = await client . createSession ( ) ;
197
+ } catch ( err ) {
198
+ const errMessage = "Cannot connected to endpoint " + endpoint + "\nmsg = " + ( < Error > err ) . message ;
199
+ debug ( errMessage ) ;
200
+ throw new Error ( errMessage ) ;
201
+ }
202
+ try {
203
+ // adjust with private key
204
+ if ( this . _userIdentity . type === UserTokenType . Certificate && ! this . _userIdentity . privateKey ) {
205
+ const internalKey = readPrivateKey ( client . clientCertificateManager . privateKey ) ;
206
+ const privateKeyPem = coercePrivateKeyPem ( internalKey ) ;
207
+ this . _userIdentity . privateKey = privateKeyPem ;
208
+ }
209
+ const session = await client . createSession ( this . _userIdentity ) ;
172
210
c . session = session ;
173
211
174
212
const subscription = await session . createSubscription2 ( {
@@ -187,7 +225,10 @@ export class OPCUAProtocolClient implements ProtocolClient {
187
225
188
226
this . _connections . set ( endpoint , c ) ;
189
227
} catch ( err ) {
190
- throw new Error ( "Cannot connected to endpoint " + endpoint + "\nmsg = " + ( < Error > err ) . message ) ;
228
+ await client . disconnect ( ) ;
229
+ const errMessage = "Cannot handle session on " + endpoint + "\nmsg = " + ( < Error > err ) . message ;
230
+ debug ( errMessage ) ;
231
+ throw new Error ( errMessage ) ;
191
232
}
192
233
}
193
234
if ( c . pending ) {
@@ -464,16 +505,56 @@ export class OPCUAProtocolClient implements ProtocolClient {
464
505
465
506
async stop ( ) : Promise < void > {
466
507
debug ( "stop" ) ;
467
- for ( const c of this . _connections . values ( ) ) {
468
- await c . subscription . terminate ( ) ;
469
- await c . session . close ( ) ;
470
- await c . client . disconnect ( ) ;
508
+ for ( const connection of this . _connections . values ( ) ) {
509
+ await connection . subscription . terminate ( ) ;
510
+ await connection . session . close ( ) ;
511
+ await connection . client . disconnect ( ) ;
471
512
}
513
+ CertificateManagerSingleton . releaseCertificateManager ( ) ;
514
+ }
515
+
516
+ #setChannelSecurity( security : OPCUAChannelSecurityScheme ) : boolean {
517
+ const { messageSecurityMode, securityPolicy } = resolveChannelSecurity ( security ) ;
518
+ this . _securityMode = messageSecurityMode ;
519
+ this . _securityPolicy = securityPolicy ;
520
+ return true ;
472
521
}
473
522
474
- setSecurity ( metadata : SecurityScheme [ ] , credentials ?: unknown ) : boolean {
523
+ #setAuthentication( security : OPCUACAuthenticationScheme ) : boolean {
524
+ this . _userIdentity = resolvedUserIdentity ( security ) ;
525
+ return true ;
526
+ }
527
+
528
+ setSecurity ( securitySchemes : SecurityScheme [ ] , credentials ?: unknown ) : boolean {
529
+ for ( const securityScheme of securitySchemes ) {
530
+ let success = true ;
531
+ switch ( securityScheme . scheme ) {
532
+ case "uav:channel-security" :
533
+ success = this . #setChannelSecurity( securityScheme as OPCUAChannelSecurityScheme ) ;
534
+ break ;
535
+ case "uav:authentication" :
536
+ success = this . #setAuthentication( securityScheme as OPCUACAuthenticationScheme ) ;
537
+ break ;
538
+ case "combo" : {
539
+ const combo = securityScheme as AllOfSecurityScheme | OneOfSecurityScheme ;
540
+ if ( combo . allOf !== undefined ) {
541
+ success = this . setSecurity ( combo . allOf , credentials ) ;
542
+ } else if ( combo . oneOf !== undefined ) {
543
+ // pick the first one for now
544
+ // later we might use credentials to select the most appropriate one
545
+ success = this . setSecurity ( [ combo . oneOf [ 0 ] ] , credentials ) ;
546
+ } else {
547
+ success = false ;
548
+ }
549
+ break ;
550
+ }
551
+ default :
552
+ // not for us , ignored
553
+ break ;
554
+ }
555
+ if ( ! success ) return false ;
556
+ }
475
557
return true ;
476
- // throw new Error("Method not implemented.");
477
558
}
478
559
479
560
private _monitoredItems : Map <
0 commit comments