@@ -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,35 @@ 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+ public static releaseCertificateManager ( ) : void {
186+ if ( OPCUAProtocolClient . _certificateManager ) {
187+ OPCUAProtocolClient . _certificateManager . referenceCounter -- ;
188+ // dispose is degined to free resources if referenceCounter==0;
189+ OPCUAProtocolClient . _certificateManager . dispose ( ) ;
190+ OPCUAProtocolClient . _certificateManager = null ;
191+ }
192+ }
193+
144194 private async _withConnection < T > ( form : OPCUAForm , next : ( connection : OPCUAConnection ) => Promise < T > ) : Promise < T > {
145195 const endpoint = form . href ;
146196 const matchesScheme : boolean = endpoint ?. match ( / ^ o p c .t c p : \/ \/ / ) != null ;
@@ -150,11 +200,15 @@ export class OPCUAProtocolClient implements ProtocolClient {
150200 }
151201 let c : OPCUAConnectionEx | undefined = this . _connections . get ( endpoint ) ;
152202 if ( ! c ) {
203+ const clientCertificateManager = await OPCUAProtocolClient . getCertificateManager ( ) ;
153204 const client = OPCUAClient . create ( {
154205 endpointMustExist : false ,
155206 connectionStrategy : {
156207 maxRetry : 1 ,
157208 } ,
209+ securityMode : this . _securityMode ,
210+ securityPolicy : this . _securityPolicy ,
211+ clientCertificateManager,
158212 } ) ;
159213 client . on ( "backoff" , ( ) => {
160214 debug ( `connection:backoff: cannot connection to ${ endpoint } ` ) ;
@@ -168,7 +222,19 @@ export class OPCUAProtocolClient implements ProtocolClient {
168222 this . _connections . set ( endpoint , c ) ;
169223 try {
170224 await client . connect ( endpoint ) ;
171- const session = await client . createSession ( ) ;
225+ } catch ( err ) {
226+ const errMessage = "Cannot connected to endpoint " + endpoint + "\nmsg = " + ( < Error > err ) . message ;
227+ debug ( errMessage ) ;
228+ throw new Error ( errMessage ) ;
229+ }
230+ try {
231+ // adjust with private key
232+ if ( this . _userIdentity . type === UserTokenType . Certificate && ! this . _userIdentity . privateKey ) {
233+ const internalKey = readPrivateKey ( client . clientCertificateManager . privateKey ) ;
234+ const privateKeyPem = coercePrivateKeyPem ( internalKey ) ;
235+ this . _userIdentity . privateKey = privateKeyPem ;
236+ }
237+ const session = await client . createSession ( this . _userIdentity ) ;
172238 c . session = session ;
173239
174240 const subscription = await session . createSubscription2 ( {
@@ -187,7 +253,10 @@ export class OPCUAProtocolClient implements ProtocolClient {
187253
188254 this . _connections . set ( endpoint , c ) ;
189255 } catch ( err ) {
190- throw new Error ( "Cannot connected to endpoint " + endpoint + "\nmsg = " + ( < Error > err ) . message ) ;
256+ await client . disconnect ( ) ;
257+ const errMessage = "Cannot handle session on " + endpoint + "\nmsg = " + ( < Error > err ) . message ;
258+ debug ( errMessage ) ;
259+ throw new Error ( errMessage ) ;
191260 }
192261 }
193262 if ( c . pending ) {
@@ -464,16 +533,72 @@ export class OPCUAProtocolClient implements ProtocolClient {
464533
465534 async stop ( ) : Promise < void > {
466535 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 ( ) ;
536+ for ( const connection of this . _connections . values ( ) ) {
537+ await connection . subscription . terminate ( ) ;
538+ await connection . session . close ( ) ;
539+ await connection . client . disconnect ( ) ;
471540 }
541+ await OPCUAProtocolClient . _certificateManager ?. dispose ( ) ;
472542 }
473543
474- setSecurity ( metadata : SecurityScheme [ ] , credentials ?: unknown ) : boolean {
544+ private setChannelSecurity ( security : OPCUAChannelSecurityScheme ) : boolean {
545+ const foundSecurity = SecurityPolicy [ security . policy as keyof typeof SecurityPolicy ] ;
546+ if ( ! foundSecurity ) return false ;
547+ this . _securityPolicy = foundSecurity ;
548+ switch ( security . messageMode ) {
549+ case "sign" :
550+ this . _securityMode = MessageSecurityMode . Sign ;
551+ break ;
552+ case "sign_encrypt" :
553+ this . _securityMode = MessageSecurityMode . SignAndEncrypt ;
554+ break ;
555+ default :
556+ this . _securityMode = MessageSecurityMode . None ;
557+ break ;
558+ }
559+ return true ;
560+ }
561+ private setAuthentication ( security : OPCUACAuthenticationScheme ) {
562+ switch ( security . tokenType ) {
563+ case "username" :
564+ this . _userIdentity = < UserIdentityInfoUserName > {
565+ type : UserTokenType . UserName ,
566+ password : security . password ,
567+ userName : security . userName ,
568+ } ;
569+ break ;
570+ case "certificate" :
571+ {
572+ this . _userIdentity = < UserIdentityInfoX509 > {
573+ type : UserTokenType . Certificate ,
574+ certificateData : convertPEMtoDER ( security . certificate ) ,
575+ privateKey : security . privateKey ,
576+ } ;
577+ }
578+ break ;
579+ default :
580+ this . _userIdentity = < UserIdentityInfo > {
581+ type : UserTokenType . Anonymous ,
582+ } ;
583+ }
584+ }
585+ setSecurity ( securitySchemes : SecurityScheme [ ] , credentials ?: unknown ) : boolean {
586+ for ( const securityScheme of securitySchemes ) {
587+ let success = true ;
588+ switch ( securityScheme . scheme ) {
589+ case "opcua-channel-security" :
590+ success = this . setChannelSecurity ( securityScheme as OPCUAChannelSecurityScheme ) ;
591+ break ;
592+ case "opcua-authentication" :
593+ this . setAuthentication ( securityScheme as OPCUACAuthenticationScheme ) ;
594+ break ;
595+ default :
596+ // not for us , ignored
597+ break ;
598+ }
599+ if ( ! success ) return false ;
600+ }
475601 return true ;
476- // throw new Error("Method not implemented.");
477602 }
478603
479604 private _monitoredItems : Map <
0 commit comments