/*******************************************************************************
 * Copyright (c) 2019 Contributors to the Eclipse Foundation
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0, or the W3C Software Notice and
 * Document License (2015-05-13) which is available at
 * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document.
 *
 * SPDX-License-Identifier: EPL-2.0 OR W3C-20150513
 ********************************************************************************/ /**
 * Opcua protocol binding
 */ import { ProtocolClient , Content , ProtocolHelpers } from "@node-wot/core" ; import * as TD from "@node-wot/td-tools" ; import Url from "url-parse" ; import { OpcuaForm , OpcuaConfig } from "./opcua" ; import { OPCUAClient , MessageSecurityMode , SecurityPolicy , AttributeIds , TimestampsToReturn , MonitoringParametersOptions , ReadValueIdOptions , ReadValueId , DataValue , UserTokenType , ClientSession , OPCUAClientOptions , StatusCodes , ClientMonitoredItem , ClientSubscription , VariantLike , WriteValueOptions , DataType , UserIdentityInfo , } from "node-opcua-client" ; import * as cryptoUtils from "node-opcua-crypto" ; import { Subscription } from "rxjs/Subscription" ; import { Readable } from "stream" ; interface ThingOPCUACredentials { username ? : string ; password ? : string ; clientCertificate ? : string ; clientPrivateKey ? : string ; serverCertificate ? : string ; privateKey ? : string ; } async function decodeContent ( content ) { if ( content ) { const body = await ProtocolHelpers . readStreamFully ( content . body ) ; const payload = JSON . parse ( body . toString ( "ascii" ) ) ; return payload ; } else { return { } ; } } export default class OpcuaClient implements ProtocolClient { private client ? : OPCUAClient ; private credentials : ThingOPCUACredentials | null ; private session ? : ClientSession ; private subscription ? : ClientSubscription ; private clientOptions : OPCUAClientOptions ; private config : OpcuaConfig ; constructor ( _config = null ) { this . credentials = null ; this . clientOptions = { applicationName : "Client" , keepSessionAlive : true , securityMode : MessageSecurityMode . None , securityPolicy : SecurityPolicy . None , connectionStrategy : { initialDelay : 0 , maxRetry : 1 , } , requestedSessionTimeout : 10000 , endpointMustExist : false , } ; if ( _config ) { this . config = _config ; } } public toString ( ) { return "[OpcuaClient]" ; } private async connect ( endpointUrl ) { let userIdentity ; if ( this . credentials ) { if ( this . credentials . password ) { userIdentity = { userName : this . credentials . username , password : this . credentials . password , type : UserTokenType . UserName , } ; } else if ( this . credentials . clientCertificate ) { const clientCertificate = cryptoUtils . readCertificate ( this . credentials . clientCertificate ) ; const privateKey = cryptoUtils . readPrivateKeyPEM ( this . credentials . clientPrivateKey ) ; this . clientOptions . securityMode = MessageSecurityMode . SignAndEncrypt ; this . clientOptions . securityPolicy = SecurityPolicy . Basic256Sha256 ; this . clientOptions . certificateFile = this . credentials . clientCertificate ; this . clientOptions . privateKeyFile = this . credentials . clientPrivateKey ; this . clientOptions . serverCertificate = cryptoUtils . readCertificate ( this . credentials . serverCertificate ) ; userIdentity = { certificateData : clientCertificate , privateKey , type : UserTokenType . Certificate , } ; } } else { userIdentity = null ; } this . client = OPCUAClient . create ( this . clientOptions ) ; await this . client . connect ( endpointUrl ) ; this . session = await this . client . createSession ( userIdentity ) ; } public async readResource ( form ) { const url = new Url ( form . href ) ; const endpointUrl = `${ url . protocol }//${ url . host }` ; const contentType = "application/x.opcua-binary" ; await this . checkConnection ( endpointUrl ) ; const params = this . extractParams ( url . pathname . toString ( ) . substr ( 1 ) ) ; const nodeId = params . ns + ";" + params . idtype ; const nodeToRead = { nodeId , attributeId : AttributeIds . Value , } ; let result ; try { result = await this . session . read ( nodeToRead ) ; } catch ( err ) { console . debug ( "[binding-opcua]" , err ) ; throw err ; } if ( result . statusCode === StatusCodes . BadNodeIdUnknown ) { throw new Error ( "Invalid nodeId" ) ; } const body = JSON . stringify ( result . toJSON ( ) , ( k , v ) => { if ( ArrayBuffer . isView ( v as ArrayLike < unknown > ) ) { return Array . from ( v ) ; } else { return v ; } } ) ; return { type : contentType , body : Readable . from ( Buffer . from ( body ) ) } ; } public async writeResource ( form , content ) { const body = await ProtocolHelpers . readStreamFully ( content . body ) ; const payload = content ? JSON . parse ( body . toString ( ) ) : { } ; const url = new Url ( form . href ) ; const endpointUrl = `${ url . protocol }//${ url . host }` ; const dataType = payload . dataType as DataType | string ; await this . checkConnection ( endpointUrl ) ; const params = this . extractParams ( url . pathname . toString ( ) . substr ( 1 ) ) ; const nodeId = params . ns + ";" + params . idtype ; try { const nodeToWrite = { nodeId : nodeId , attributeId : AttributeIds . Value , value : { value : { dataType , value : payload . payload , } , } , } ; const statusCode = await this . session . write ( nodeToWrite ) ; if ( statusCode === StatusCodes . Good ) { return ; } else { const err = new Error ( statusCode . toString ( ) ) ; throw err ; } } catch ( err ) { console . debug ( "[binding-opcua]" , err ) ; throw err ; } } public async invokeResource ( form , content ) { const url = new Url ( form . href ) ; const endpointUrl = `${ url . protocol }//${ url . host }` ; const method = form [ "opc:method" ] ? form [ "opc:method" ] : "CALL_METHOD" ; const contentType = "application/x.opcua-binary" ; await this . checkConnection ( endpointUrl ) ; const params = this . extractParams ( url . pathname . toString ( ) . substr ( 1 ) ) ; const objectId = params . ns + ";" + params . idtype ; const nodeId = params . mns + ";" + params . midtype ; if ( method === "CALL_METHOD" ) { const payload = ( await decodeContent ( content ) ) as { inputArguments : VariantLike [ ] ; } ; const methodToCall = { methodId : nodeId , objectId : objectId , inputArguments : payload . inputArguments , } ; const result = await this . session . call ( methodToCall ) ; console . log ( "[binding-opcua]" , "[invoke]" , result . toString ( ) ) ; const statusCode = result . statusCode ; if ( statusCode !== StatusCodes . Good ) { console . debug ( "[binding-opcua]" , statusCode ) ; throw new Error ( statusCode . toString ( ) ) ; } const outputArgs = result . outputArguments . map ( ( outputArg ) => outputArg . toJSON ( ) ) ; const body = JSON . stringify ( outputArgs ) ; return { type : contentType , body : Readable . from ( Buffer . from ( body , "ascii" ) ) } ; } else { throw new Error ( "Invalid method : " + method ) ; } } public unlinkResource ( form ) { return new Promise < void > ( ( resolve , reject ) => { reject ( new Error ( `OpcuaClient does not implement unlink` ) ) ; } ) ; } private async checkConnection ( endpointUrl ) { if ( ! this . session ) { try { await this . connect ( endpointUrl ) ; } catch ( err ) { console . debug ( "[binding-opcua]" , err ) ; throw err ; } } } private async getOrCreateSubscription ( ) { if ( this . subscription ) { return this . subscription ; } const defaultSubscriptionOptions = { requestedPublishingInterval : 1000 , requestedLifetimeCount : 100 , requestedMaxKeepAliveCount : 10 , maxNotificationsPerPublish : 100 , publishingEnabled : true , priority : 10 , } ; const subscriptionParameters = this . config && this . config . subscriptionOptions ? this . config . subscriptionOptions : defaultSubscriptionOptions ; this . subscription = await this . session . createSubscription2 ( subscriptionParameters ) ; return this . subscription ; } public async subscribeResource ( form , next , error ? , complete ? ) { const url = new Url ( form . href ) ; const endpointUrl = `${ url . protocol }//${ url . host }` ; await this . checkConnection ( endpointUrl ) ; const subscription = await this . getOrCreateSubscription ( ) ; const params = this . extractParams ( url . pathname . toString ( ) . substr ( 1 ) ) ; const nodeId = params . ns + ";" + params . idtype ; const itemToMonitor = { nodeId : nodeId , attributeId : AttributeIds . Value , } ; const parameters = { samplingInterval : 100 , discardOldest : true , queueSize : 10 , } ; const monitoredItem = await new Promise < ClientMonitoredItem > ( ( resolve , reject ) => { const monitoredItem = ClientMonitoredItem . create ( subscription , itemToMonitor , parameters , TimestampsToReturn . Both ) ; monitoredItem . once ( "err" , ( ) => { const err = new Error ( "Error while subscribing property: " + monitoredItem . statusCode . toString ( ) ) ; reject ( err ) ; } ) ; monitoredItem . once ( "initialized" , async ( ) => { resolve ( monitoredItem ) ; } ) ; } ) ; monitoredItem . on ( "changed" , ( dataValue ) => { const body = JSON . stringify ( dataValue . toJSON ( ) ) ; const contentType = "application/x.opcua-binary" ; next ( { type : contentType , body : Readable . from ( Buffer . from ( body ) ) } ) ; } ) ; return new Subscription ( async ( ) => { await monitoredItem . terminate ( ) ; } ) ; } public async start ( ) { } public async stop ( ) { const { subscription , session , client } = this ; this . subscription = undefined ; this . session = undefined ; this . client = undefined ; if ( subscription ) { await subscription . terminate ( ) ; } if ( session ) { await session . close ( ) ; } if ( client ) { await client . disconnect ( ) ; } } public setSecurity ( metadata , credentials ? ) { if ( metadata === undefined || ! Array . isArray ( metadata ) || metadata . length === 0 ) { console . warn ( "[binding-opcua]" , `OpcuaClient without security` ) ; return false ; } if ( ! credentials || ( ! credentials . password && ! credentials . privateKey ) ) { console . warn ( "[binding-opcua]" , `Both password and certificate missing inside credentials` ) ; } this . credentials = credentials ; return true ; } private extractParams ( url ) { try { url = decodeURI ( url ) ; } catch ( err ) { console . error ( err ) ; } const res = { ns : null , idtype : null , mns : null , midtype : null , } ; for ( let i = 0 ; i < url . split ( ";" ) . length ; i ++ ) { const value = url . split ( ";" ) [ i ] ; if ( value . includes ( "mns=" ) ) { res . mns = value . replace ( "mns" , "ns" ) ; } else if ( value . includes ( "ns=" ) ) { res . ns = value ; } else if ( value . includes ( "mb=" ) || value . includes ( "ms=" ) || value . includes ( "mg=" ) || value . includes ( "mi=" ) ) { let midtype = value . split ( "=" ) [ 0 ] ; midtype = midtype . substr ( 1 ) ; res . midtype = midtype + "=" + value . split ( "=" ) [ 1 ] ; } else if ( value . includes ( "b=" ) || value . includes ( "s=" ) || value . includes ( "g=" ) || value . includes ( "i=" ) ) { res . idtype = value ; } } return res ; } }