Sunday, September 28, 2014

CTI with Node + Redis

Summary

In this article I'll be discussing a way to implement call data passing between disparate telephony systems, i.e., Computer Telephony Integration (CTI).  The techniques I employ in this article (DNIS pooling, database storage of call data, etc) are nothing groundbreaking.  In fact, these techniques are as old as the hills (well, maybe not that old).  What will be new here is the use of current software architectures such as REST with highly-efficient run-time environments such as Node.js and Redis to realize an open data integration between 3rd party systems.

This article is the culmination of a series I've written on this topic.  In Part 1, REST calls from Genesys Routing Strategies, I discussed how to make REST web service calls from one particular vendor's CTI system (Genesys).  In Part 2, VXML and REST Web Services, I discussed REST web service execution from VXML applications.

I'll be leveraging information in this article that I've presented in the following previous articles:

Node/Basic Auth - HTTP Basic Auth on Node.js

Implementation

Figure 1 below depicts the overall architecture.  The 'RAM Cache' component is the focus of this article.  The overall use-case of that cache is to provide 3rd Party telephony systems, with proprietary-only integrations, an open standard method for passing call data between them.  Example:  call that is transferred between two disparate systems.  It would be preferable to retain any data that was gathered by the transferer and pass it to the transferee.

Figure 1

Figure 2 depicts the details of the transfer scenario mentioned above.  A caller calls Telephony System X, during the call System X gathers various pieces of information relevant to the caller, then System X transfers the call to System Y.  The RAM Cache provides the mechanism for call and data matching on that transfer.  The RAM Cache provides a target DNIS, from a pool of numbers assigned to System Y, for System X to transfer the call.  Additionally, the RAM Cache stores any caller data under a DB key that is the combination of the target DNIS and caller's ANI.  The combination of the two provides some additional protection from call/data collisions vs. systems based on either DNIS  or ANI alone.  System Y can subsequently match the call and data upon receipt of the transfer.

Figure 2

The RAM Cache architecture itself is depicted in Figure 3.  The Node runtime with various modules are utilized to implement: a.  HTTP server that provides the REST interface and b.  a client to the Redis database.  A static configuration file is utilized for all server information.  Redis provides a key/value store for the DNIS:ANI-keyed values.  Additionally, by configuring key expirations in Redis we can implement automatic expulsion of stale key/values.

Figure 3
Figure 4 depicts the REST interface I implemented here.  POST's insert data into the store (Create).  GET's fetch data from the store (Retrieve).  DELETE's both fetch the data and cause it to be deleted from the store (Delete).

Figure 4

Example Use

Below are some cURL examples on use of the REST interface.

POST


curl --user "username:password" -v -i -H "Content-Type:application/json" -X POST -d '{"value" : "12345678", "ani" : "1234567890"}' https://yourDomain.com/cticache/DNIS-ANI



HTTP/1.1 201 Created
< X-Powered-By: Express
X-Powered-By: Express
< Location: /cticache/DNIS-ANI/5551234568:1234567890
Location: /cticache/DNIS-ANI/5551234568:1234567890
< Content-Type: application/json; charset=utf-8
Content-Type: application/json; charset=utf-8
< Content-Length: 59
Content-Length: 59
< Date: Mon, 29 Sep 2014 00:19:53 GMT
Date: Mon, 29 Sep 2014 00:19:53 GMT
< Connection: keep-alive
Connection: keep-alive

* Connection #0 to host yourDomain.com left intact
{"dnis":"5551234568","ani":"1234567890","value":"12345678"}


GET


curl --user "username:password" -v -i https://yourDomain.com/cticache/DNIS-ANI/5551234568:1234567890



HTTP/1.1 200 OK
< X-Powered-By: Express
X-Powered-By: Express
< Content-Type: application/json; charset=utf-8
Content-Type: application/json; charset=utf-8
< Content-Length: 20
Content-Length: 20
< ETag: W/"14-2814665325"
ETag: W/"14-2814665325"
< Date: Mon, 29 Sep 2014 00:24:14 GMT
Date: Mon, 29 Sep 2014 00:24:14 GMT
< Connection: keep-alive
Connection: keep-alive

* Connection #0 to host yourDomain.com left intact
{"value":"12345678"}



DELETE


curl --user "username:password" -v -i -X DELETE https://yourDomain.com/cticache/DNIS-ANI/5551234568:1234567890



HTTP/1.1 200 OK
< X-Powered-By: Express
X-Powered-By: Express
< Content-Type: application/json; charset=utf-8
Content-Type: application/json; charset=utf-8
< Content-Length: 20
Content-Length: 20
< Date: Mon, 29 Sep 2014 00:26:35 GMT
Date: Mon, 29 Sep 2014 00:26:35 GMT
< Connection: keep-alive
Connection: keep-alive

* Connection #0 to host yourDomain.com left intact
{"value":"12345678"}
Copyright ©1993-2024 Joey E Whelan, All rights reserved.

Sunday, September 21, 2014

VXML and REST Web Services

Summary

I'll be describing how to make REST web service calls from VXML scripts in this post.  In a nutshell, the VXML <data> element can be utilized.  Though per spec, this element only provides support for XML input and output - many VXML browsers today provide support for more efficient encoding schemes, such as JSON.

REST POST then Transfer 

Main script

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?xml version="1.0" encoding="UTF-8"?>
<!-- 
 Author: Joey Whelan
 Simple VXML script to illustrate REST calls.  Script receives an inbound call then makes a
 subdialog call.  The subdialog handles the execution of the REST (HTTP POST) call.  
 The returned value from that POST is used as input to a blind transfer.
 -->
 
<vxml version="2.1">
 <form>
  <var name="value" expr="87654321" />
  
  <subdialog name="results" src="cticacheSubD.vxml">
   <param name="srcURI" expr="'https://username:password@yourDomain.com/cticache/DNIS-ANI/'" />
   <param name="methodType" expr="'post'" />
   <param name="val" expr="value"/>
   
   <filled>
    <if cond="results.respObj.error != undefined" >
     <prompt>
      <value expr="results.respObj.error"/>
     </prompt>
     <exit/>
    <else/>
     <assign name="target" expr="results.respObj.dnis"/>
     <goto next="#transfer"/> 
    </if>
   </filled>
  </subdialog> 
 </form>

 <form id="transfer">
  <transfer type="blind" destexpr="'tel:' +target">
   <prompt>
    Transferring this call to 
    <say-as interpret-as="digits">
     <value expr="target"/>
    </say-as>
   </prompt>
  </transfer>
 </form>

</vxml>

Lines 13-16  

A call is made to a subdialog which handles the REST transaction.  The input parameters to that subdialog are the URI of the REST service, the HTTP method to be used, and an input value to the service.

Regarding the URI parameter - HTTP Basic Authentication can be utilized with the <data> element by passing the username + password as part of the URI ('client:password' below).  Combining basic auth with TLS (as below) provides a modicum of security for the REST service.  The authentication credentials will be encrypted under TLS.

1
   <param name="srcURI" expr="'https://username:password@yourDomain.com/cticache/DNIS-ANI/'" />

Lines 18-28

Results of the subdialog are passed back as a JSON object.  I check to see if an error occurred as a result the REST call.  If so, I simply play out the error as TTS.  If the call was successful, I set a variable ('target') to the value of the results and then goto a form that will perform a call transfer.

Lines 32-41

This form takes the REST call result (a dialable number) and performs a blind transfer to that number.

REST Subdialog

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
<?xml version="1.0" encoding="UTF-8"?>
<!-- 
 Author: Joey Whelan
 Subdialog for making REST web service calls.  It is parameterized to provide flexibility on URI and 
 method types.
 -->
 
<vxml version="2.1">
 
 <catch event="error.badfetch.https.400 error.badfetch.http.400">
  <log label="Error" expr="'HTTP 400 - Bad Request'"/>
  <prompt>
   This request could not be understood
   <break time="1000"/>
  </prompt>
  <var name="respObj" />
  <script>
    respObj = {'error':'HTTP 400'};
  </script>
  <return namelist="respObj" />
 </catch>
 
 <catch event="error.badfetch.https.401 error.badfetch.http.401">
  <log label="Error" expr="'HTTP 401 - Unauthorized'"/>
  <prompt>
   This request did not have the proper authentication
   <break time="1000"/>
  </prompt>
  <var name="respObj" />
  <script>
    respObj = {'error':'HTTP 401'};
  </script>
  <return namelist="respObj" />
 </catch>
 
 <catch event="error.badfetch.https.403 error.badfetch.http.403">
  <log label="Error" expr="'HTTP 403 - Forbidden'"/>
  <prompt>
   This request can not be fulfilled on this server
   <break time="1000"/>
  </prompt>
  <var name="respObj" />
  <script>
    respObj = {'error':'HTTP 403'};
  </script>
  <return namelist="respObj" />
 </catch>
 
 <catch event="error.badfetch.https.404 error.badfetch.http.404">
  <log expr="'HTTP 404 - Not Found'"/>
  <prompt>
   This request does not match anything on this server
   <break time="1000"/>
  </prompt>
  <var name="respObj" />
  <script>
    respObj = {'error':'HTTP 404'};
  </script>
  <return namelist="respObj" />
 </catch>
 
 
 <catch event="error.badfetch.https.500 error.badfetch.http.500">
  <log label="Error" expr="'HTTP 500 - Internal Server Error'"/>
  <prompt>
   The server has experienced an internal error
   <break time="1000"/>
  </prompt>
  <var name="respObj" />
  <script>
    respObj = {'error':'HTTP 500'};
  </script>
  <return namelist="respObj" />
 </catch>
 
 <form>
  <var name="srcURI" />
  <var name="methodType" />
  <var name="val" />
  
  <block>
   <script>
    methodType = methodType.toLowerCase();
   </script>
   <var name="key" expr="session.calledid + ':' + session.callerid" />
   
   <!-- the ecmaxlmtype="e4x" and <assign name="respObj" ... JSON.parse(...)/> statements are hacks for the VXML
   browser I'm using.  It has 'challenges' with application/json input parameters.  If your browser supports JSON 
   input natively these statements are unnecessary
    -->
   <if cond="methodType == 'delete'">
    <data name="response" srcexpr="srcURI + key" ecmaxmltype="e4x" method="delete"/>
    <assign name="respObj" expr="JSON.parse(response.toString())"/>
    <return namelist="respObj" />  
   </if>

   <if cond="methodType == 'get'">
    <data name="response" srcexpr="srcURI + key" ecmaxmltype="e4x" method="get"/>
    <assign name="respObj" expr="JSON.parse(response.toString())"/>
    <return namelist="respObj" />  
   </if>
   
   <!--  Similar browser 'challenge' with JSON here, except now with output (enctype).
   If your browser supports JSON natively - change enctype to "application/json"
    -->
   <if cond="methodType == 'post'">
    <var name="ani" expr="session.callerid" />
    <var name="value" expr="val"/>
    <data name="response" srcexpr="srcURI" ecmaxmltype="e4x" enctype="application/x-www-form-urlencoded" method="post" namelist="ani value"/>
    <assign name="respObj" expr="JSON.parse(response.toString())"/>
    <return namelist="respObj" />  
   </if> 
  </block>
 </form>
</vxml>

Lines 10-74

This is a series of <catch> blocks for the various HTTP error codes this particular REST service can throw.  In all cases here I'm setting the result to an object, playing an error prompt, then returning from the subdialog with that error object.

Lines 77-79

Input parameters for this subdialog.

Line 85

Here I'm setting a variable to the dialed number and caller ID.  This stored as a string delimited with a colon.

Lines 91-112

This is the main code of the subdialog.  <if> blocks determine which HTTP method is used.  The URI of the REST service is appended with the DNIS:ANI value from Line 85.  The REST result is returned from the subdialog as a JSON object.

REST DELETE (Fetch)

The VXML code below implements the analog of the previous script (which POSTed data).  The code below fetches that POSTed data and deletes it.

Main Script

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="UTF-8"?>
<!-- 
 Author: Joey Whelan
 Simple VXML script to illustrate REST calls.  Script makes a fetch (implemented as a HTTP DELETE) to
 a REST service.
 -->
<vxml version="2.1">
 <form>
   <subdialog name="results" src="cticacheSubD.vxml">
    <param name="srcURI" expr="'https://username:password@yourDomain.com/cticache/DNIS-ANI/'" />
    <param name="methodType" expr="'delete'" />
   
    
   <filled>
    <if cond="results.respObj.error != undefined" >
     <prompt>
      <value expr="results.respObj.error"/>
     </prompt>
    <else/>
     <prompt>
      The account number is
      <say-as interpret-as="digits">
       <value expr="results.respObj.value"/>
      </say-as>
     </prompt>
    </if>
   </filled>
  </subdialog> 
 </form>
</vxml>

Lines 9-11

Similar to the previous <form>, the REST-handling subdialog is called with the appropriate input parameters.  The method type is set to 'delete'.

Line 14-27

The results of the subdialog are inspected.  If an error occurred, its message is played back as TTS.  If the call was successful, the result value is played back as TTS.


Copyright ©1993-2024 Joey E Whelan, All rights reserved.

REST calls from Genesys Routing Strategies

Summary

This article is a continuation of my previous discussion regarding web service access from Genesys strategies.  In that article, I described how to access traditional/SOAP-based web services from Genesys Interaction Routing Designer(IRD)-created routing strategies.  For this article, I show how to access REST-ful web service implementations with IRD strategies.

HTTP Post Implementation

Figure 1 depicts a simplistic/non-production ready strategy that executes a HTTP POST call with a JSON parameter.  The web service returns a phone number that the strategy subsequently uses as a transfer target.

Figure 1

In Figure 2 I'm using a Multi-assign object to build up a JSON-formatted string that will be used as an input parameter to the POST call.  The resultant string looks like this:

{ "value" : '12345678', "ani" : '<the actual ANI of the inbound call to this strategy>' }

That string is assigned to a Genesys variable named 'json'.
Figure 2

Figure 3 shows the General Tab of the IRD web service object.  Here I'm setting up a HTTP POST call to a SSL-based REST service.  I explained how to configure Universal Routing Server (URS) for SSL access in this post.  The input parameter of this HTTP call will be sent as an application/json content type in the request body.

Figure 3

The Security tab of the web service object is depicted in Figure 4.  This tab sets up HTTP Basic Authentication for this POST call.  In this type of authentication credentials are passed in clear text, hence, by itself, basic authentication is inherently insecure.   However, combining basic auth with HTTPS does provide a fairly secure interface.  The authentication credentials are encrypted in TLS.

Figure 4

Finally, Figure 5 shows the Result tab of this web service object.  As was discussed in my previous post, Genesys URS will return the results of a web service call into an IRD List object.

Figure 5

In Figure 6, I pull the first item off this List object and assign it to an IRD variable called 'transferTarget'.  This web service call returns a JSON object with 3 properties.  The value of the first property of this JSON object is a phone number.  That phone number (now in the 'transferTarget' variable) is passed to a TRoute function which initiates a transfer.

Figure 6
As an aside - This strategy is loaded against a Genesys SIP Server (SIPS) route point.  You'll notice I have a "Silence" treatment object as the first object in this strategy.  That object is there to cause an 'answer' of the inbound call.  By answering the call (with either Genesys Media Server or Stream Manager), this subsequent transfer is implemented by SIPS as a SIP REFER.  If the call is never answered in the strategy (meaning no voice treatment of any sort), the transfer from the strategy is implemented as a re-INVITE, which keeps SIPS in the signalling path for the duration of the call - or - a 302 Moved, which is rejected by my SIP trunk service provider.  A REFER transfer takes SIPS out of the signalling path.  Whether a Re-INVITE or REFER/302 is initiated on the transfer is determined by how the oosp-transfer-enabled and refer-enable options are set on the Genesys trunk object used for the outbound transfer.

HTTP Delete Implementation

Figure 7 depicts a simple IRD strategy for illustrating a REST Delete operation.  This strategy accepts a call transferred from 3rd Party IVR, CTI framework, etc.  Based on ANI and DNIS, the strategy performs a fetch of the data associated with the call that was stored by the 3rd Party system.  The fetch is implemented as an HTTP Delete.  The Delete returns the data associated with the call and clears that data item from storage.

Figure 7
The first IRD object in this strategy (Multi-Assign) is depicted in Figure 8.  Here I'm building up a URL to the REST interface using string concats of the DNIS and ANI values of the inbound call.

Figure 8

The IRD Web Service object is shown in Figure 9.  The 'Web Service URL' parameter is set to the URL I built up in the previous step.  The 'Method' is set to DELETE.  Otherwise, this object is configured identically to the previous (Figure 3).

Figure 9

The remainder of this strategy is straight-forward.  The value fetched from the REST call is attached and then the call is routed to a skill.  The agent receives a screen-pop with that attached data item. Copyright ©1993-2024 Joey E Whelan, All rights reserved.

Sunday, September 14, 2014

CA-signed SSL Certificates with Node

Summary

I just recently went through the exercise of bringing up a Node HTTPS server with 'real' SSL credentials (meaning, not a self-signed certificate).  Although there's a lot of bits and pieces of info out there on the Interweb on how to obtain a CA-signed certificate, configure Node HTTPS, etc - I couldn't find a concise guide in any one place on how to do a real-world SSL implementation with Node.  Below is an attempt to provide clear guidance and hopefully save you some time if you ever have to configure this.

Step 1:  Generate a CSR
Below is the openSSL command to create a 2048-bit private key and certificate signing request (CSR - that will contain the corresponding public key).  That command will generate a series of questions for info required in a CSR. At a minimum, you'll need to provide a legitimate Common Name (a domain name you control) and email address.
openssl req -nodes -newkey rsa:2048 -keyout yourPrivate.key -out yourServer.csr


Step 2:  Submit the CSR to CA
I went with Comodo as my CA for this exercise.  They currently provide a 90-day free SSL certificate.  Additionally, I thought their cert request process was well-documented and easy to use. That request process amounted to a copy/paste of the text of the CSR to their web form and then proof that I actually controlled the domain in question.  That can be accomplished in multiple ways (email to the domain, CNAME mods, etc).  After Comodo has that proof, they'll send an email to you with a zip archive containing the following files:

  1. Certificate file.  Example:  yourDomain_com.crt
  2. Certificate chain file.  Example:  yourDomain_com.ca-bundle
Step 3:  Combine SSL Credentials into a PKCS #12 file
This is the part that took some time and digging to figure out.  Simply putting the credentials you've created/received into the HTTPS options on the createServer call doesn't cut it.  The problem is with that chain file.  It needs to be passed as an array for the 'ca' option.  That doesn't happen with just a fs.read.  You wind up with SSL errors when a client attempts to connect to a server with this config.


//WILL NOT WORK
var privateKey = fs.readFileSync('/path/to/yourPrivate.key');
var certificate = fs.readFileSync('/path/to/yourDomain_com.crt');
var certAuth = fs.readFileSync('/path/to/yourDomain_com.ca-bundle');
var credentials = {key: privateKey, cert: certificate, ca: certAuth};

var appHttps = express();
var httpsServer = https.createServer(credentials, appHttps).listen(yourHttpsPort);



Folks have written some examples on how to parse out that chain file into an array that Node can digest, but I ran across a post with a the better idea of just combining all three files into one PKCS file with the openSSL command below:

openssl pkcs12 -export -out yourDomain.com.pfx -inkey yourPrivate.key -in yourDomain_com.crt -certfile yourDomain_com.ca-bundle



Now, you can do the following:


//THIS WORKS
var pfxFile = fsys.readFileSync('/path/to/yourDomain.com.pfx');
var credentials = {pfx: pfxFile};
var appHttps = express();
var httpsServer = https.createServer(credentials, appHttps).listen(yourHttpsPort);
Copyright ©1993-2024 Joey E Whelan, All rights reserved.

Saturday, September 13, 2014

Enabling SSL for Genesys URS/HTTP Bridge

Summary

In this post I'm going to show how to get the Genesys Universal Routing Server (URS) HTTP Bridge capable of handling SSL connections.  The bridge is used to enable traditional Genesys routing strategies to access external SOAP or REST web services.  SSL integration is not well-documented in the product docs, so I'll save you some of my pain here (Linux deployment).


Environmentals

Firstly, you need to install the Genesys "Security Pack."  That software bundle isn't included with URS, but has the SSL libraries necessary for URS to access HTTPS-based web services.  After it is installed, you need to ensure the URS process has its LD_LIBRARY_PATH variable set to the directory where you put the Security Pack.  I made a shell script for starting up URS and added this command to it:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/genesys/secpack

Now that URS can find its SSL libraries, now you need to tell it where to find its Certificate Authority (CA).  Interestingly enough, the product will actually trust a self-generated CA.  In any case, you have to set a URS configuration option that tells it where to go for that CA file.  Below is a screen-shot of the config option that needs to be set (def_trusted_ca).

Figure 1

And that's it.  Assuming you have a valid CA file, you should be able access HTTPS web services from a Genesys routing strategy with the above mods.
Copyright ©1993-2024 Joey E Whelan, All rights reserved.