2020-10-17 22:23:24 -04:00
/ *
Copyright 2018 - 2020 Intel Corporation
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
/ * *
* @ description APF / CIRA Client for Duktape
* @ author Joko Sastriawan & Ylian Saint - Hilaire
* @ copyright Intel Corporation 2020
* @ license Apache - 2.0
* @ version v0 . 0.2
* /
function CreateAPFClient ( parent , args ) {
if ( ( args . clientuuid == null ) || ( args . clientuuid . length != 36 ) ) return null ; // Require a UUID if this exact length
var obj = { } ;
obj . parent = parent ;
obj . args = args ;
obj . http = require ( 'http' ) ;
obj . net = require ( 'net' ) ;
obj . forwardClient = null ;
obj . downlinks = { } ;
obj . pfwd _idx = 0 ;
obj . timer = null ; // Keep alive timer
// obj.onChannelClosed
// obj.onJsonControl
// Function copied from common.js
function ReadInt ( v , p ) { return ( v . charCodeAt ( p ) * 0x1000000 ) + ( v . charCodeAt ( p + 1 ) << 16 ) + ( v . charCodeAt ( p + 2 ) << 8 ) + v . charCodeAt ( p + 3 ) ; } ; // We use "*0x1000000" instead of "<<24" because the shift converts the number to signed int32.
function IntToStr ( v ) { return String . fromCharCode ( ( v >> 24 ) & 0xFF , ( v >> 16 ) & 0xFF , ( v >> 8 ) & 0xFF , v & 0xFF ) ; } ;
function hex2rstr ( d ) { var r = '' , m = ( '' + d ) . match ( /../g ) , t ; while ( t = m . shift ( ) ) { r += String . fromCharCode ( '0x' + t ) ; } return r ; } ;
function char2hex ( i ) { return ( i + 0x100 ) . toString ( 16 ) . substr ( - 2 ) . toUpperCase ( ) ; } ; // Convert decimal to hex
function rstr2hex ( input ) { var r = '' , i ; for ( i = 0 ; i < input . length ; i ++ ) { r += char2hex ( input . charCodeAt ( i ) ) ; } return r ; } ; // Convert a raw string to a hex string
function d2h ( d ) { return ( d / 256 + 1 / 512 ) . toString ( 16 ) . substring ( 2 , 4 ) ; }
function buf2hex ( input ) { var r = '' , i ; for ( i = 0 ; i < input . length ; i ++ ) { r += d2h ( input [ i ] ) ; } return r ; } ;
function Debug ( str ) { if ( obj . parent . debug ) { console . log ( str ) ; } }
function guidToStr ( g ) { return g . substring ( 6 , 8 ) + g . substring ( 4 , 6 ) + g . substring ( 2 , 4 ) + g . substring ( 0 , 2 ) + "-" + g . substring ( 10 , 12 ) + g . substring ( 8 , 10 ) + "-" + g . substring ( 14 , 16 ) + g . substring ( 12 , 14 ) + "-" + g . substring ( 16 , 20 ) + "-" + g . substring ( 20 ) ; }
function strToGuid ( s ) { s = s . replace ( /-/g , '' ) ; var ret = s . substring ( 6 , 8 ) + s . substring ( 4 , 6 ) + s . substring ( 2 , 4 ) + s . substring ( 0 , 2 ) + s . substring ( 10 , 12 ) + s . substring ( 8 , 10 ) + s . substring ( 14 , 16 ) + s . substring ( 12 , 14 ) + s . substring ( 16 , 20 ) + s . substring ( 20 ) ; return ret ; }
function binzerostring ( len ) { var res = '' ; for ( var l = 0 ; l < len ; l ++ ) { res += String . fromCharCode ( 0 & 0xFF ) ; } return res ; }
// CIRA state
var CIRASTATE = {
INITIAL : 0 ,
PROTOCOL _VERSION _SENT : 1 ,
AUTH _SERVICE _REQUEST _SENT : 2 ,
AUTH _REQUEST _SENT : 3 ,
PFWD _SERVICE _REQUEST _SENT : 4 ,
GLOBAL _REQUEST _SENT : 5 ,
FAILED : - 1
}
obj . cirastate = CIRASTATE . INITIAL ;
// REDIR state
var REDIR _TYPE = {
REDIR _UNKNOWN : 0 ,
REDIR _SOL : 1 ,
REDIR _KVM : 2 ,
REDIR _IDER : 3
}
// redirection start command
obj . RedirectStartSol = String . fromCharCode ( 0x10 , 0x00 , 0x00 , 0x00 , 0x53 , 0x4F , 0x4C , 0x20 ) ;
obj . RedirectStartKvm = String . fromCharCode ( 0x10 , 0x01 , 0x00 , 0x00 , 0x4b , 0x56 , 0x4d , 0x52 ) ;
obj . RedirectStartIder = String . fromCharCode ( 0x10 , 0x00 , 0x00 , 0x00 , 0x49 , 0x44 , 0x45 , 0x52 ) ;
// Intel AMT forwarded port list for non-TLS mode
//var pfwd_ports = [16992, 623, 16994, 5900];
var pfwd _ports = [ 16992 , 16993 ] ;
// protocol definitions
var APFProtocol = {
UNKNOWN : 0 ,
DISCONNECT : 1 ,
SERVICE _REQUEST : 5 ,
SERVICE _ACCEPT : 6 ,
USERAUTH _REQUEST : 50 ,
USERAUTH _FAILURE : 51 ,
USERAUTH _SUCCESS : 52 ,
GLOBAL _REQUEST : 80 ,
REQUEST _SUCCESS : 81 ,
REQUEST _FAILURE : 82 ,
CHANNEL _OPEN : 90 ,
CHANNEL _OPEN _CONFIRMATION : 91 ,
CHANNEL _OPEN _FAILURE : 92 ,
CHANNEL _WINDOW _ADJUST : 93 ,
CHANNEL _DATA : 94 ,
CHANNEL _CLOSE : 97 ,
PROTOCOLVERSION : 192 ,
KEEPALIVE _REQUEST : 208 ,
KEEPALIVE _REPLY : 209 ,
KEEPALIVE _OPTIONS _REQUEST : 210 ,
KEEPALIVE _OPTIONS _REPLY : 211 ,
JSON _CONTROL : 250 // This is a Mesh specific command that sends JSON to and from the MPS server.
}
var APFDisconnectCode = {
HOST _NOT _ALLOWED _TO _CONNECT : 1 ,
PROTOCOL _ERROR : 2 ,
KEY _EXCHANGE _FAILED : 3 ,
RESERVED : 4 ,
MAC _ERROR : 5 ,
COMPRESSION _ERROR : 6 ,
SERVICE _NOT _AVAILABLE : 7 ,
PROTOCOL _VERSION _NOT _SUPPORTED : 8 ,
HOST _KEY _NOT _VERIFIABLE : 9 ,
CONNECTION _LOST : 10 ,
BY _APPLICATION : 11 ,
TOO _MANY _CONNECTIONS : 12 ,
AUTH _CANCELLED _BY _USER : 13 ,
NO _MORE _AUTH _METHODS _AVAILABLE : 14 ,
INVALID _CREDENTIALS : 15 ,
CONNECTION _TIMED _OUT : 16 ,
BY _POLICY : 17 ,
TEMPORARILY _UNAVAILABLE : 18
}
var APFChannelOpenFailCodes = {
ADMINISTRATIVELY _PROHIBITED : 1 ,
CONNECT _FAILED : 2 ,
UNKNOWN _CHANNEL _TYPE : 3 ,
RESOURCE _SHORTAGE : 4 ,
}
var APFChannelOpenFailureReasonCode = {
AdministrativelyProhibited : 1 ,
ConnectFailed : 2 ,
UnknownChannelType : 3 ,
ResourceShortage : 4 ,
}
obj . onSecureConnect = function onSecureConnect ( resp , ws , head ) {
Debug ( "APF Secure WebSocket connected." ) ;
//console.log(JSON.stringify(resp));
obj . forwardClient . tag = { accumulator : [ ] } ;
obj . forwardClient . ws = ws ;
obj . forwardClient . ws . on ( 'end' , function ( ) {
Debug ( "APF: Connection is closing." ) ;
if ( obj . timer != null ) { clearInterval ( obj . timer ) ; obj . timer = null ; }
if ( obj . onChannelClosed ) { obj . onChannelClosed ( obj ) ; }
} ) ;
obj . forwardClient . ws . on ( 'data' , function ( data ) {
obj . forwardClient . tag . accumulator += hex2rstr ( buf2hex ( data ) ) ;
try {
var len = 0 ;
do {
len = ProcessData ( obj . forwardClient ) ;
if ( len > 0 ) { obj . forwardClient . tag . accumulator = obj . forwardClient . tag . accumulator . slice ( len ) ; }
if ( obj . cirastate == CIRASTATE . FAILED ) {
Debug ( "APF: in a failed state, destroying socket." ) ;
obj . forwardClient . ws . end ( ) ;
}
} while ( len > 0 ) ;
} catch ( ex ) { Debug ( ex ) ; }
} ) ;
obj . forwardClient . ws . on ( 'error' , function ( e ) {
Debug ( "APF: Connection error, ending connecting." ) ;
if ( obj . timer != null ) { clearInterval ( obj . timer ) ; obj . timer = null ; }
} ) ;
obj . state = CIRASTATE . INITIAL ;
2020-10-20 01:23:46 -04:00
if ( ( typeof obj . args . conntype == 'number' ) && ( obj . args . conntype != 0 ) ) {
SendJsonControl ( obj . forwardClient . ws , { action : 'connType' , value : obj . args . conntype } ) ;
if ( obj . args . meiState != null ) { SendJsonControl ( obj . forwardClient . ws , { action : 'meiState' , value : obj . args . meiState } ) ; }
}
2020-10-17 22:23:24 -04:00
SendProtocolVersion ( obj . forwardClient . ws , obj . args . clientuuid ) ;
SendServiceRequest ( obj . forwardClient . ws , 'auth@amt.intel.com' ) ;
}
2020-10-20 01:23:46 -04:00
obj . updateMeiState = function ( state ) { SendJsonControl ( obj . forwardClient . ws , { action : 'meiState' , value : state } ) ; }
2020-10-21 15:19:00 -04:00
obj . sendMeiDeactivationState = function ( state ) { SendJsonControl ( obj . forwardClient . ws , { action : 'deactivate' , value : state } ) ; }
2020-10-20 01:23:46 -04:00
2020-10-17 22:23:24 -04:00
function SendJsonControl ( socket , o ) {
var data = JSON . stringify ( o )
socket . write ( String . fromCharCode ( APFProtocol . JSON _CONTROL ) + IntToStr ( data . length ) + data ) ;
Debug ( "APF: Send JSON control: " + data ) ;
}
function SendProtocolVersion ( socket , uuid ) {
var data = String . fromCharCode ( APFProtocol . PROTOCOLVERSION ) + IntToStr ( 1 ) + IntToStr ( 0 ) + IntToStr ( 0 ) + hex2rstr ( strToGuid ( uuid ) ) + binzerostring ( 64 ) ;
socket . write ( data ) ;
Debug ( "APF: Send protocol version 1 0 " + uuid ) ;
obj . cirastate = CIRASTATE . PROTOCOL _VERSION _SENT ;
}
function SendServiceRequest ( socket , service ) {
var data = String . fromCharCode ( APFProtocol . SERVICE _REQUEST ) + IntToStr ( service . length ) + service ;
socket . write ( data ) ;
Debug ( "APF: Send service request " + service ) ;
if ( service == 'auth@amt.intel.com' ) {
obj . cirastate = CIRASTATE . AUTH _SERVICE _REQUEST _SENT ;
} else if ( service == 'pfwd@amt.intel.com' ) {
obj . cirastate = CIRASTATE . PFWD _SERVICE _REQUEST _SENT ;
}
}
function SendUserAuthRequest ( socket , user , pass ) {
var service = "pfwd@amt.intel.com" ;
var data = String . fromCharCode ( APFProtocol . USERAUTH _REQUEST ) + IntToStr ( user . length ) + user + IntToStr ( service . length ) + service ;
//password auth
data += IntToStr ( 8 ) + 'password' ;
data += binzerostring ( 1 ) + IntToStr ( pass . length ) + pass ;
socket . write ( data ) ;
Debug ( "APF: Send username password authentication to MPS" ) ;
obj . cirastate = CIRASTATE . AUTH _REQUEST _SENT ;
}
function SendGlobalRequestPfwd ( socket , amthostname , amtport ) {
var tcpipfwd = 'tcpip-forward' ;
var data = String . fromCharCode ( APFProtocol . GLOBAL _REQUEST ) + IntToStr ( tcpipfwd . length ) + tcpipfwd + binzerostring ( 1 , 1 ) ;
data += IntToStr ( amthostname . length ) + amthostname + IntToStr ( amtport ) ;
socket . write ( data ) ;
Debug ( "APF: Send tcpip-forward " + amthostname + ":" + amtport ) ;
obj . cirastate = CIRASTATE . GLOBAL _REQUEST _SENT ;
}
function SendKeepAliveRequest ( socket ) {
socket . write ( String . fromCharCode ( APFProtocol . KEEPALIVE _REQUEST ) + IntToStr ( 255 ) ) ;
Debug ( "APF: Send keepalive request" ) ;
}
function SendKeepAliveReply ( socket , cookie ) {
socket . write ( String . fromCharCode ( APFProtocol . KEEPALIVE _REPLY ) + IntToStr ( cookie ) ) ;
Debug ( "APF: Send keepalive reply" ) ;
}
function ProcessData ( socket ) {
var cmd = socket . tag . accumulator . charCodeAt ( 0 ) ;
var len = socket . tag . accumulator . length ;
var data = socket . tag . accumulator ;
if ( len == 0 ) { return 0 ; }
// Respond to MPS according to obj.cirastate
switch ( cmd ) {
case APFProtocol . SERVICE _ACCEPT : {
var slen = ReadInt ( data , 1 ) , service = data . substring ( 5 , 6 + slen ) ;
Debug ( "APF: Service request to " + service + " accepted." ) ;
if ( service == 'auth@amt.intel.com' ) {
if ( obj . cirastate >= CIRASTATE . AUTH _SERVICE _REQUEST _SENT ) {
SendUserAuthRequest ( socket . ws , obj . args . mpsuser , obj . args . mpspass ) ;
}
} else if ( service == 'pfwd@amt.intel.com' ) {
if ( obj . cirastate >= CIRASTATE . PFWD _SERVICE _REQUEST _SENT ) {
SendGlobalRequestPfwd ( socket . ws , obj . args . clientname , pfwd _ports [ obj . pfwd _idx ++ ] ) ;
}
}
return 5 + slen ;
}
case APFProtocol . REQUEST _SUCCESS : {
if ( len >= 5 ) {
var port = ReadInt ( data , 1 ) ;
Debug ( "APF: Request to port forward " + port + " successful." ) ;
// iterate to pending port forward request
if ( obj . pfwd _idx < pfwd _ports . length ) {
SendGlobalRequestPfwd ( socket . ws , obj . args . clientname , pfwd _ports [ obj . pfwd _idx ++ ] ) ;
} else {
// no more port forward, now setup timer to send keep alive
Debug ( "APF: Start keep alive for every " + obj . args . mpskeepalive + " ms." ) ;
obj . timer = setInterval ( function ( ) {
SendKeepAliveRequest ( obj . forwardClient . ws ) ;
} , obj . args . mpskeepalive ) ; //
}
return 5 ;
}
Debug ( "APF: Request successful." ) ;
return 1 ;
}
case APFProtocol . USERAUTH _SUCCESS : {
Debug ( "APF: User Authentication successful" ) ;
// Send Pfwd service request
SendServiceRequest ( socket . ws , 'pfwd@amt.intel.com' ) ;
return 1 ;
}
case APFProtocol . USERAUTH _FAILURE : {
Debug ( "APF: User Authentication failed" ) ;
obj . cirastate = CIRASTATE . FAILED ;
return 14 ;
}
case APFProtocol . KEEPALIVE _REQUEST : {
Debug ( "APF: Keep Alive Request with cookie: " + ReadInt ( data , 1 ) ) ;
SendKeepAliveReply ( socket . ws , ReadInt ( data , 1 ) ) ;
return 5 ;
}
case APFProtocol . KEEPALIVE _REPLY : {
Debug ( "APF: Keep Alive Reply with cookie: " + ReadInt ( data , 1 ) ) ;
return 5 ;
}
// Channel management
case APFProtocol . CHANNEL _OPEN : {
// Parse CHANNEL OPEN request
var p _res = parseChannelOpen ( data ) ;
Debug ( "APF: CHANNEL_OPEN request: " + JSON . stringify ( p _res ) ) ;
// Check if target port is in pfwd_ports
if ( pfwd _ports . indexOf ( p _res . target _port ) >= 0 ) {
// Connect socket to that port
var chan = obj . net . createConnection ( { host : obj . args . clientaddress , port : p _res . target _port } , function ( ) {
//require('MeshAgent').SendCommand({ action: 'msg', type: 'console', value: "CHANNEL_OPEN-open" });
// obj.downlinks[p_res.sender_chan].setEncoding('binary');//assume everything is binary, not interpreting
SendChannelOpenConfirm ( socket . ws , p _res ) ;
} ) ;
// Setup flow control
chan . maxInWindow = p _res . window _size ; // Oddly, we are using the same window size as the other side.
chan . curInWindow = 0 ;
chan . on ( 'data' , function ( ddata ) {
// Relay data to fordwardclient
// TODO: Implement flow control
SendChannelData ( socket . ws , p _res . sender _chan , ddata ) ;
} ) ;
chan . on ( 'error' , function ( e ) {
2020-10-23 00:46:32 -04:00
//Debug("Downlink connection error: " + e);
SendChannelOpenFailure ( socket . ws , p _res ) ;
2020-10-17 22:23:24 -04:00
} ) ;
chan . on ( 'end' , function ( ) {
var chan = obj . downlinks [ p _res . sender _chan ] ;
if ( chan != null ) {
Debug ( "Socket ends." ) ;
try { SendChannelClose ( socket . ws , p _res . sender _chan ) ; } catch ( ex ) { }
delete obj . downlinks [ p _res . sender _chan ] ;
}
} ) ;
obj . downlinks [ p _res . sender _chan ] = chan ;
} else {
// Not a supported port, fail the connection
SendChannelOpenFailure ( socket . ws , p _res ) ;
}
return p _res . len ;
}
case APFProtocol . CHANNEL _OPEN _CONFIRMATION : {
Debug ( "APF: CHANNEL_OPEN_CONFIRMATION" ) ;
return 17 ;
}
case APFProtocol . CHANNEL _CLOSE : {
var rcpt _chan = ReadInt ( data , 1 ) ;
Debug ( "APF: CHANNEL_CLOSE: " + rcpt _chan ) ;
try { obj . downlinks [ rcpt _chan ] . end ( ) ; } catch ( ex ) { }
return 5 ;
}
case APFProtocol . CHANNEL _DATA : {
Debug ( "APF: CHANNEL_DATA: " + JSON . stringify ( rstr2hex ( data ) ) ) ;
var rcpt _chan = ReadInt ( data , 1 ) ;
var chan _data _len = ReadInt ( data , 5 ) ;
var chan _data = data . substring ( 9 , 9 + chan _data _len ) ;
var chan = obj . downlinks [ rcpt _chan ] ;
if ( chan != null ) {
chan . curInWindow += chan _data _len ;
try {
chan . write ( Buffer . from ( chan _data , 'binary' ) , function ( ) {
Debug ( "Write completed." ) ;
// If the incoming window is over half used, send an adjust.
if ( this . curInWindow > ( this . maxInWindow / 2 ) ) { SendChannelWindowAdjust ( socket . ws , rcpt _chan , this . curInWindow ) ; this . curInWindow = 0 ; }
} ) ;
} catch ( ex ) { Debug ( "Cannot forward data to downlink socket." ) ; }
}
return 9 + chan _data _len ;
}
case APFProtocol . CHANNEL _WINDOW _ADJUST : {
Debug ( "APF: CHANNEL_WINDOW_ADJUST" ) ;
return 9 ;
}
case APFProtocol . JSON _CONTROL : {
Debug ( "APF: JSON_CONTROL" ) ;
var len = ReadInt ( data , 1 ) ;
if ( obj . onJsonControl ) { var o = null ; try { o = JSON . parse ( data . substring ( 5 , 5 + len ) ) ; } catch ( ex ) { } if ( o != null ) { obj . onJsonControl ( o ) ; } }
return 5 + len ;
}
default : {
Debug ( "CMD: " + cmd + " is not implemented." ) ;
obj . cirastate = CIRASTATE . FAILED ;
return 0 ;
}
}
}
function parseChannelOpen ( data ) {
var result = { cmd : APFProtocol . CHANNEL _OPEN } ;
var chan _type _slen = ReadInt ( data , 1 ) ;
result . chan _type = data . substring ( 5 , 5 + chan _type _slen ) ;
result . sender _chan = ReadInt ( data , 5 + chan _type _slen ) ;
result . window _size = ReadInt ( data , 9 + chan _type _slen ) ;
var c _len = ReadInt ( data , 17 + chan _type _slen ) ;
result . target _address = data . substring ( 21 + chan _type _slen , 21 + chan _type _slen + c _len ) ;
result . target _port = ReadInt ( data , 21 + chan _type _slen + c _len ) ;
var o _len = ReadInt ( data , 25 + chan _type _slen + c _len ) ;
result . origin _address = data . substring ( 29 + chan _type _slen + c _len , 29 + chan _type _slen + c _len + o _len ) ;
result . origin _port = ReadInt ( data , 29 + chan _type _slen + c _len + o _len ) ;
result . len = 33 + chan _type _slen + c _len + o _len ;
return result ;
}
function SendChannelOpenFailure ( socket , chan _data ) {
socket . write ( String . fromCharCode ( APFProtocol . CHANNEL _OPEN _FAILURE ) + IntToStr ( chan _data . sender _chan ) + IntToStr ( 2 ) + IntToStr ( 0 ) + IntToStr ( 0 ) ) ;
Debug ( "APF: Send ChannelOpenFailure" ) ;
}
function SendChannelOpenConfirm ( socket , chan _data ) {
socket . write ( String . fromCharCode ( APFProtocol . CHANNEL _OPEN _CONFIRMATION ) + IntToStr ( chan _data . sender _chan ) + IntToStr ( chan _data . sender _chan ) + IntToStr ( chan _data . window _size ) + IntToStr ( 0xFFFFFFFF ) ) ;
Debug ( "APF: Send ChannelOpenConfirmation" ) ;
}
function SendChannelWindowAdjust ( socket , chan , size ) {
socket . write ( String . fromCharCode ( APFProtocol . CHANNEL _WINDOW _ADJUST ) + IntToStr ( chan ) + IntToStr ( size ) ) ;
Debug ( "APF: Send ChannelWindowAdjust, channel: " + chan + ", size: " + size ) ;
}
function SendChannelData ( socket , chan , data ) {
socket . write ( Buffer . concat ( [ Buffer . from ( String . fromCharCode ( APFProtocol . CHANNEL _DATA ) + IntToStr ( chan ) + IntToStr ( data . length ) , 'binary' ) , data ] ) ) ;
Debug ( "APF: Send ChannelData: " + data . toString ( 'hex' ) ) ;
}
function SendChannelClose ( socket , chan ) {
socket . write ( String . fromCharCode ( APFProtocol . CHANNEL _CLOSE ) + IntToStr ( chan ) ) ;
Debug ( "APF: Send ChannelClose " ) ;
}
obj . connect = function ( ) {
if ( obj . forwardClient != null ) {
try { obj . forwardClient . ws . end ( ) ; } catch ( ex ) { Debug ( ex ) ; }
//obj.forwardClient = null;
}
obj . cirastate = CIRASTATE . INITIAL ;
obj . pfwd _idx = 0 ;
//obj.forwardClient = new obj.ws(obj.args.mpsurl, obj.tlsoptions);
//obj.forwardClient.on("open", obj.onSecureConnect);
var wsoptions = obj . http . parseUri ( obj . args . mpsurl ) ;
wsoptions . rejectUnauthorized = 0 ;
obj . forwardClient = obj . http . request ( wsoptions ) ;
obj . forwardClient . upgrade = obj . onSecureConnect ;
obj . forwardClient . end ( ) ; // end request, trigger completion of HTTP request
}
obj . disconnect = function ( ) { try { obj . forwardClient . ws . end ( ) ; } catch ( ex ) { Debug ( ex ) ; } }
return obj ;
}
module . exports = CreateAPFClient ;