@@ -17,8 +17,18 @@ import { Subscription } from "rxjs/Subscription";
1717import { promisify } from "util" ;
1818import { Readable } from "stream" ;
1919import { URL } from "url" ;
20-
21- import { ProtocolClient , Content , ContentSerdes , Form , SecurityScheme , createLoggers } from "@node-wot/core" ;
20+ import path from "path" ;
21+ import envPath from "env-paths" ;
22+ import {
23+ ProtocolClient ,
24+ Content ,
25+ ContentSerdes ,
26+ Form ,
27+ SecurityScheme ,
28+ createLoggers ,
29+ OPCUACAuthenticationScheme ,
30+ OPCUAChannelSecurityScheme ,
31+ } from "@node-wot/core" ;
2232
2333import {
2434 ClientSession ,
@@ -36,21 +46,32 @@ import {
3646 VariantArrayType ,
3747 Variant ,
3848 VariantOptions ,
49+ SecurityPolicy ,
3950} from "node-opcua-client" ;
40- import { ArgumentDefinition , getBuiltInDataType , readNamespaceArray } from "node-opcua-pseudo-session" ;
41-
51+ import {
52+ AnonymousIdentity ,
53+ ArgumentDefinition ,
54+ getBuiltInDataType ,
55+ readNamespaceArray ,
56+ UserIdentityInfo ,
57+ UserIdentityInfoUserName ,
58+ UserIdentityInfoX509 ,
59+ } from "node-opcua-pseudo-session" ;
4260import { makeNodeId , NodeId , NodeIdLike , NodeIdType , resolveNodeId } from "node-opcua-nodeid" ;
4361import { AttributeIds , BrowseDirection , makeResultMask } from "node-opcua-data-model" ;
4462import { makeBrowsePath } from "node-opcua-service-translate-browse-path" ;
4563import { StatusCodes } from "node-opcua-status-code" ;
64+ import { coercePrivateKeyPem , convertPEMtoDER , readPrivateKey } from "node-opcua-crypto" ;
4665
4766import { schemaDataValue } from "./codec" ;
4867import { opcuaJsonEncodeVariant } from "node-opcua-json" ;
49- import { Argument , BrowseDescription , BrowseResult } from "node-opcua-types" ;
50- import { isGoodish2 , ReferenceTypeIds } from "node-opcua" ;
68+ import { Argument , BrowseDescription , BrowseResult , MessageSecurityMode , UserTokenType } from "node-opcua-types" ;
69+ import { isGoodish2 , OPCUACertificateManager , ReferenceTypeIds } from "node-opcua" ;
5170
5271const { debug } = createLoggers ( "binding-opcua" , "opcua-protocol-client" ) ;
5372
73+ const env = envPath ( "binding-opcua" , { suffix : "node-wot" } ) ;
74+
5475export type Command = "Read" | "Write" | "Subscribe" ;
5576
5677export interface NodeByBrowsePath {
@@ -141,6 +162,36 @@ function _variantToJSON(variant: Variant, contentType: string) {
141162export class OPCUAProtocolClient implements ProtocolClient {
142163 private _connections : Map < string , OPCUAConnectionEx > = new Map < string , OPCUAConnectionEx > ( ) ;
143164
165+ private _securityMode : MessageSecurityMode = MessageSecurityMode . None ;
166+ private _securityPolicy : SecurityPolicy = SecurityPolicy . None ;
167+ private _userIdentity : UserIdentityInfo = < AnonymousIdentity > { type : UserTokenType . Anonymous } ;
168+
169+ private static _certificateManager : OPCUACertificateManager | null = null ;
170+
171+ public static async getCertificateManager ( ) : Promise < OPCUACertificateManager > {
172+ if ( OPCUAProtocolClient . _certificateManager ) {
173+ return OPCUAProtocolClient . _certificateManager ;
174+ }
175+ const rootFolder = path . join ( env . config , "PKI" ) ;
176+ debug ( "OPCUA PKI folder" , rootFolder ) ;
177+ const certificateManager = new OPCUACertificateManager ( {
178+ rootFolder,
179+ } ) ;
180+ await certificateManager . initialize ( ) ;
181+ certificateManager . referenceCounter ++ ;
182+ OPCUAProtocolClient . _certificateManager = certificateManager ;
183+ return certificateManager ;
184+ }
185+
186+ public static releaseCertificateManager ( ) : void {
187+ if ( OPCUAProtocolClient . _certificateManager ) {
188+ OPCUAProtocolClient . _certificateManager . referenceCounter -- ;
189+ // dispose is degined to free resources if referenceCounter==0;
190+ OPCUAProtocolClient . _certificateManager . dispose ( ) ;
191+ OPCUAProtocolClient . _certificateManager = null ;
192+ }
193+ }
194+
144195 private async _withConnection < T > ( form : OPCUAForm , next : ( connection : OPCUAConnection ) => Promise < T > ) : Promise < T > {
145196 const endpoint = form . href ;
146197 const matchesScheme : boolean = endpoint ?. match ( / ^ o p c .t c p : \/ \/ / ) != null ;
@@ -150,11 +201,15 @@ export class OPCUAProtocolClient implements ProtocolClient {
150201 }
151202 let c : OPCUAConnectionEx | undefined = this . _connections . get ( endpoint ) ;
152203 if ( ! c ) {
204+ const clientCertificateManager = await OPCUAProtocolClient . getCertificateManager ( ) ;
153205 const client = OPCUAClient . create ( {
154206 endpointMustExist : false ,
155207 connectionStrategy : {
156208 maxRetry : 1 ,
157209 } ,
210+ securityMode : this . _securityMode ,
211+ securityPolicy : this . _securityPolicy ,
212+ clientCertificateManager,
158213 } ) ;
159214 client . on ( "backoff" , ( ) => {
160215 debug ( `connection:backoff: cannot connection to ${ endpoint } ` ) ;
@@ -168,7 +223,19 @@ export class OPCUAProtocolClient implements ProtocolClient {
168223 this . _connections . set ( endpoint , c ) ;
169224 try {
170225 await client . connect ( endpoint ) ;
171- const session = await client . createSession ( ) ;
226+ } catch ( err ) {
227+ const errMessage = "Cannot connected to endpoint " + endpoint + "\nmsg = " + ( < Error > err ) . message ;
228+ debug ( errMessage ) ;
229+ throw new Error ( errMessage ) ;
230+ }
231+ try {
232+ // adjust with private key
233+ if ( this . _userIdentity . type === UserTokenType . Certificate && ! this . _userIdentity . privateKey ) {
234+ const internalKey = readPrivateKey ( client . clientCertificateManager . privateKey ) ;
235+ const privateKeyPem = coercePrivateKeyPem ( internalKey ) ;
236+ this . _userIdentity . privateKey = privateKeyPem ;
237+ }
238+ const session = await client . createSession ( this . _userIdentity ) ;
172239 c . session = session ;
173240
174241 const subscription = await session . createSubscription2 ( {
@@ -187,7 +254,10 @@ export class OPCUAProtocolClient implements ProtocolClient {
187254
188255 this . _connections . set ( endpoint , c ) ;
189256 } catch ( err ) {
190- throw new Error ( "Cannot connected to endpoint " + endpoint + "\nmsg = " + ( < Error > err ) . message ) ;
257+ await client . disconnect ( ) ;
258+ const errMessage = "Cannot handle session on " + endpoint + "\nmsg = " + ( < Error > err ) . message ;
259+ debug ( errMessage ) ;
260+ throw new Error ( errMessage ) ;
191261 }
192262 }
193263 if ( c . pending ) {
@@ -464,16 +534,82 @@ export class OPCUAProtocolClient implements ProtocolClient {
464534
465535 async stop ( ) : Promise < void > {
466536 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 ( ) ;
537+ for ( const connection of this . _connections . values ( ) ) {
538+ await connection . subscription . terminate ( ) ;
539+ await connection . session . close ( ) ;
540+ await connection . client . disconnect ( ) ;
471541 }
542+ await OPCUAProtocolClient . _certificateManager ?. dispose ( ) ;
472543 }
473544
474- setSecurity ( metadata : SecurityScheme [ ] , credentials ?: unknown ) : boolean {
545+ private setChannelSecurity ( security : OPCUAChannelSecurityScheme ) : boolean {
546+ const foundSecurity = SecurityPolicy [ security . policy as keyof typeof SecurityPolicy ] ;
547+
548+ if ( foundSecurity === undefined ) {
549+ return false ;
550+ }
551+
552+ this . _securityPolicy = foundSecurity ;
553+
554+ switch ( security . messageMode ) {
555+ case "sign" :
556+ this . _securityMode = MessageSecurityMode . Sign ;
557+ break ;
558+ case "sign_encrypt" :
559+ this . _securityMode = MessageSecurityMode . SignAndEncrypt ;
560+ break ;
561+ default :
562+ this . _securityMode = MessageSecurityMode . None ;
563+ break ;
564+ }
565+
566+ return true ;
567+ }
568+
569+ private setAuthentication ( security : OPCUACAuthenticationScheme ) : boolean {
570+ switch ( security . tokenType ) {
571+ case "username" :
572+ this . _userIdentity = < UserIdentityInfoUserName > {
573+ type : UserTokenType . UserName ,
574+ password : security . password ,
575+ userName : security . userName ,
576+ } ;
577+ break ;
578+ case "certificate" :
579+ this . _userIdentity = < UserIdentityInfoX509 > {
580+ type : UserTokenType . Certificate ,
581+ certificateData : convertPEMtoDER ( security . certificate ) ,
582+ privateKey : security . privateKey ,
583+ } ;
584+ break ;
585+ case "anonymous" :
586+ this . _userIdentity = < UserIdentityInfo > {
587+ type : UserTokenType . Anonymous ,
588+ } ;
589+ break ;
590+ default :
591+ return false ;
592+ }
593+ return true ;
594+ }
595+
596+ setSecurity ( securitySchemes : SecurityScheme [ ] , credentials ?: unknown ) : boolean {
597+ for ( const securityScheme of securitySchemes ) {
598+ let success = true ;
599+ switch ( securityScheme . scheme ) {
600+ case "opcua-channel-security" :
601+ success = this . setChannelSecurity ( securityScheme as OPCUAChannelSecurityScheme ) ;
602+ break ;
603+ case "opcua-authentication" :
604+ success = this . setAuthentication ( securityScheme as OPCUACAuthenticationScheme ) ;
605+ break ;
606+ default :
607+ // not for us , ignored
608+ break ;
609+ }
610+ if ( ! success ) return false ;
611+ }
475612 return true ;
476- // throw new Error("Method not implemented.");
477613 }
478614
479615 private _monitoredItems : Map <
0 commit comments