cancel
Showing results for 
Search instead for 
Did you mean: 
cancel
Announcements

UCCX: HTTP Application as an IP Phone Directory Service

4942
Views
20
Helpful
9
Comments

Problem: a company has an LDAP-compliant directory services infrastructure, for instance, Active Directory, where all/most of the user profiles have the Telephone attribute filled in. The company would like this directory be searchable by Cisco IP Phones, but they don't want to spend extra money for another product, which is based on the Cisco IP Phone SDK. The company has IP IVR (or UCCX Premium) installed, though.

Analysis/Solution: UCCX Premium (also IP IVR) is perfectly capable of running an application, which would do the lookups in the LDAP directory, and present the result on the Cisco IP Phone display, using HTTP triggers and some custom Java code.

Details:

*** DISCLAIMER: just the usual blah-blah. This is a proof of concept. The author (me) is not responsible for any damage this solution may cause. Use at your own risk. Yes, I know it's not elegant at all, and I know it could be enhanced in a lot of different ways. Created and tested with UCCX 8.5 and CUCM 8.0 ***

*** NOTICE: I don't post the actual script for a reason. This is an expert forum and you can gain valuable knowledge writing your own script by following the instructions written in the document. However, if you are ultra-mega-super lazy, I can send you the script, if you send me a bottle of wine and a picture postcard from your country. ***

1. We will start off by creating a script in CCX Editor. Save it as ldapsearch.aef.

2. Insert the following variables:

Variable name
Variable typeInitial valueExplanation
bufferStringBuffernullJust a string buffer used to build the response. Remember, StringBuffer and StringBuilder are faster than using Strings, if there's String concatenation involved repeatedly.
myHttpUrlString""This variable holds the full request URL.
quoteString'\"'For some reason, the Expression Editor chokes on the escaped double quote \" if it appears within a String. So I use this variable to insert double quotes where needed. For example: "<?xml version="1.0" ?>" - see the double quotes around 1.0 within the string enclosed by the quotes.
scriptNameString""This variable holds the SCRIPT_NAME CGI variable (used to construct myHttpUrl).
searchBaseString"CN=Users,DC=icm80,DC=in"The LDAP Search Base - this means, the lookup will take place only at this partition of the directory. In this example, it is the Users container of the icm80.in domain.
searchStringString""We will pass this as the expression to be searched (and for other purposes).
securityCredentialsString"somePassword"The password of the user whose identity will be used to log into the LDAP server and do the search. Also see securityPrincipal.
securityPrincipalString

"cn=ldap u. user,cn=Users,dc=icm80,dc=in"

The Common Name (CN) of the LDAP user whose identity will be used to log into LDAP. In this example, this is the user named "ldap u. user" in the Users container, in the icm80.in domain.
serverNameString""This variable holds the SERVER_NAME CGI variable (used to construct myHttpUrl).
serverPortint0This variable holds the SERVER_PORT CGI variable (used to construct myHttpUrl).
urlString"ldap://192.168.80.10:389/The LDAP server URL. In this example, it's an Active Directory domain controller at 192.168.80.10. Windows 2003 Standard Edition if I am not mistaken, but any Windows will do. Also, OpenLDAP, but I did not have the time to test it.

3. Our first step would be Get Http Contact Info. On the Parameters tab, map "searchString" to variable searchString. On the CGI Variables tab, map SERVER_NAME to serverName, SERVER_PORT to serverPort, SCRIPT_NAME to scriptName.

4. The next Set step is used to construct myHttpUrl - it will be needed later. This allows us not to hardcode any IP address of the UCCX server and application name into the script - let's keep things dynamic.

5. An If step follows, with the following condition: (searchString==null || searchString.length()==0)


If it evaluates to False, it will just fall through the next step (which is another If node, see 6.). But if it evaluates to True, the following two steps will be executed:

5.1. Set, variable buffer, and the following code block:

{

StringBuffer buffer = new StringBuffer(300);

buffer.append("<?xml version=" + quote + "1.0" + quote + " ?>");

buffer.append("<CiscoIPPhoneMenu>");

buffer.append("<MenuItem><Name>Simple Phone Directory</Name><URL>");

buffer.append(myHttpUrl + "?searchString=presentForm");

buffer.append("</URL></MenuItem>");

buffer.append("<Prompt>Choose a menu option</Prompt>");

buffer.append("</CiscoIPPhoneMenu>");

return buffer;

}

Basically, we are constructing - in a poor man's way, by concatenating Strings - an XML object, in an expected format, which is to be presented on the phone display.

5.2. This next step - Goto HTTP_RESPONSE - sends the script execution to the end of the script - for details, see 8.

Screenshot:

ldapsearch-if-1.png

So what happened so far? Although we did not get to that point, we can assume we created everything necessary in UCCX: saved the script, created an application, mapped the application to an HTTP trigger, we set the Directories URL on the phone to the URL of this application - without any parameters. So pretend, someone presses the Directories button on the phone, which would trigger the application, still without any parameters. So the condition at 5 will evaluate to True, since we did not set the HTTP GET parameter searchString (actually, we did not send any parameters). So the script will create an XML document, and then send it to the IP Phone, which will result in a row added to the list, titled "Simple Phone Directory" (for a screenshot, take a look at 12).

6. If the script execution hits this step, it means that the condition at 5 (which is an If node) evaluated to False. Based on what I have written in the previous paragraph, we can say that there IS a HTTP GET parameter named searchString, its length is greater than zero. So this step, which is actually an If node, too, we will check the following condition:

(searchString=="presentForm")

If it evaluates to False, meaning the parameter's value is not "presentForm", just fall through to 7. Otherwise, continue to

6.1. this is a Set step, similar to 5.1., with the following code block - the variable we use here is the same as 5.1., buffer.

{

StringBuffer buffer = new StringBuffer(300);

buffer.append("<?xml version=" + quote + "1.0" + quote + " ?>");

buffer.append("<CiscoIPPhoneInput>");

buffer.append("<Prompt>Enter the name or a part of it</Prompt>");

buffer.append("<URL>");

buffer.append(myHttpUrl);

buffer.append("</URL>");

buffer.append("<InputItem><DisplayName>Name</DisplayName><QueryStringParam>searchString</QueryStringParam>");

buffer.append("<DefaultValue></DefaultValue><InputFlags>A</InputFlags>");

buffer.append("</InputItem>");

buffer.append("</CiscoIPPhoneInput>");

return buffer;

}

6.2. A Goto step, to label HTTP_RESPONSE - identical to 5.1. Again, for details, see 8.

ldapsearch-if-2.png

Again, let's stop for a moment. So if our application was triggered, this time with the HTTP GET parameter searchString of which value was presentForm, we create an XML document and send it to the phone. This XML is used as the search form. You might ask: where did we set the value of searchString? Take a look at 5.1., row 6.

7. Yet another Set step, the variable is buffer, again, with this code block. Don't panic, I will try to explain it. First of all, there's no If's. So this means, we already know that there is a HTTP GET parameter with the name of searchString, and its value is different from "presentForm". So this means we got the actual search string.

{

java.util.Hashtable env = new java.util.Hashtable();

String icf = "com.sun.jndi.ldap.LdapCtxFactory";

env.put(javax.naming.Context.INITIAL_CONTEXT_FACTORY, icf);

env.put(javax.naming.Context.PROVIDER_URL, url);

env.put(javax.naming.Context.SECURITY_AUTHENTICATION, "simple");

env.put(javax.naming.Context.SECURITY_PRINCIPAL, securityPrincipal);

env.put(javax.naming.Context.SECURITY_CREDENTIALS, securityCredentials);

javax.naming.directory.DirContext dirContext = new javax.naming.directory.InitialDirContext(env);

javax.naming.directory.SearchControls searchControls = new javax.naming.directory.SearchControls();

String[] returningAttributes = new String[] {"cn" , "telephoneNumber" };

searchControls.setReturningAttributes(returningAttributes);

searchControls.setSearchScope(javax.naming.directory.SearchControls.SUBTREE_SCOPE);

String filter = "(name=*" + searchString +  "*)";

javax.naming.NamingEnumeration result = dirContext.search(searchBase,filter,searchControls);

StringBuffer buffer = new StringBuffer(400);

buffer.append("<?xml version=" + quote + "1.0" + quote + " ?>");

buffer.append("<CiscoIPPhoneDirectory>");

buffer.append("<Title>Look what I found</Title>");

buffer.append("<Prompt>REPLACE_ME</Prompt>");

int numResults = 0;

while (result.hasMore()) {

    javax.naming.directory.SearchResult searchResult = (javax.naming.directory.SearchResult) result.next();

    javax.naming.directory.Attributes attributes = searchResult.getAttributes();

    javax.naming.directory.Attribute attribute = attributes.get("cn");

    String cn = attribute.get();

    attribute = attributes.get("telephoneNumber");

    if (attribute!=null) {

        numResults = numResults + 1;

        String telephoneNumber = attribute.get();

        buffer.append("<DirectoryEntry>");

        buffer.append("<Name>").append(cn).append("</Name>");

        buffer.append("<Telephone>").append(telephoneNumber).append("</Telephone>");

        buffer.append("</DirectoryEntry>");

    }

}

dirContext.close();

if (numResults >0) {

    buffer.replace(buffer.indexOf("REPLACE_ME"),buffer.indexOf("REPLACE_ME")+10,"Found " + numResults + " entries");

} else {

    buffer.replace(buffer.indexOf("REPLACE_ME"),buffer.indexOf("REPLACE_ME")+10,"Sorry, nothing relevant");

}

buffer.append("</CiscoIPPhoneDirectory>");

return buffer;

}

What it does is quite simple: it establishes a session to the LDAP server, sends the search string, parses the response and constructs the XML.

Notice the following rows:

  • String[] returningAttributes = new String[] {"cn" , "telephoneNumber" };
    searchControls.setReturningAttributes(returningAttributes);

    This basically says we are only interested in the CN and the telephoneNumber attributes from the user profile, so don't bother sending us all attributes.
  • String filter = "(name=*" + searchString +  "*)"; - this means we search the name attribute.

You may also notice we only create a DirectoryEntry node if the telephoneNumber attribute is set (not null).

8. This is a step labeled HTTP_RESPONSE, so the Goto steps at 5.1. and 6.1. will send the script execution to here, and we will also hit this step after 7. A Set Http Contact Info, which is used to set the correct content type - the IP Phone expects text/xml, but the default value is text/html. So on the Headers tab, we set the Name to "Content-Type" and the Value to "text/xml".

9. And the last relevant step is a Send Http Response, where the Document is set to (Document) buffer.toString() - this instructs the script to cast the value of the buffer variable into the type of Document, which is expected by this step.

The last step, is of course, End.

10. Save the script, and then using the UCCX admin web page, create an application named "ldapsearch". Map the ldapsearch.aef script to it. Also, create a Trigger of type HTTP, with the following URL: ldapsearch.

11. In CallManager, aka CUCM, Phone-Device page, set the Directory and Secure Directory URL to the following URL:

http://192.168.80.81:9080/ldapsearch - assuming the UCCX is at IP 192.168.80.81, then reset the phone.

*** HINT: if the setting is overwritten, but the phone just won't display a new row titled Simple Phone Directory, go and check the following Enterprise Parameter in CUCM: Services Provisioning. By default, it's set to Internal, which prevents the phone from accessing external service URLs, like ours. So set this Enterprise Parameter to either Both or External. ***

12. Try it out. This is what you should be able to see:

- after pressing the Directories button (in my case, the Services Provisioning EntParam is Both) - again, the XML object constructed at 5.1. is used to generate this last row titled "Simple Phone Directory":

list.jpg

- the search form, after I chose Simple Phone Directory on the previous screen - the XML object at 6.1. creates this form:

form.jpg

- and the results (generated by the Set step at 7):

results.jpg

If there are no results, an empty screen appears with the following prompt: Sorry, nothing relevant.

Tips for enhancement: you can enhance this script to communicate with two different LDAP servers. Or, combine the results of an SQL lookup with LDAP lookup. Or, use more LDAP attributes in your search. You could also implement some intelligent regex search/replace to prefix the phone numbers for out dialling as no one can assume the LDAP telephoneNumber attribute will be prefixed with 0 or 9 used to get an outside line.

Final thoughts: the other day, a company approached me with the following request: they have UCCX Premium, a well maintained LDAP based on Active Directory, but they did not want to integrate it with CUCM, so they asked whether there were any other ways to have IP Phone Directory lookup. I created this script in one day, including some basic documentation, so I did not have the guts to ask like thousands of EUR's for this little piece of app. Interestingly, the company chose product X which required them to buy a server, buy a non-free operating system for it, and now they have a solution for a couple of thousands Euros. Why? They told me I was too cheap. This means: if it's cheap, it can't be serious. Actually, they were kind of right.

But anyway, the script is done and so if you want it, use it. Again, it's either Do-It-Yourself, or BottleOfWinePostCardWare.

Enjoy.

G.

Edit:

20130724: due to popular demand, more screenshots added.

Comments
Enthusiast

Gergely,

I've been doing UCCX based IP Phone Services for a while now and have seen to a large extent the same as you mention. In my experience (with US companies) the reason a company won't want to go with something like this is because of legal ramifications, they want someone who they can contact for support (or sue if things go wrong).

It's unfortunate, but it is what it is. Glad to see you're exploring all the fun stuff UCCX is capable of

Tanner

Advocate

Yeah, but in this case I was approached as a company not an individual. I guess if I had a fancy office in the city center with a dozen well dressed people, then my script could have been accepted.

Here's yet another similar argument, used by one of my clients, after I tried to convince him to save some money by using Linux: "Who should I kick if something breaks, Linus Torvalds?".

G.

Hi Gergely

Why is it that every time I have an idea, you've already executed it?

Aaron

Advocate

Aaron, apologies. Next time, I will try to contain myself

Beginner

Gergely - thank you again for posting this. I have been working off you example; and i have run into a snag that i wanted to run past you.

The CiscoIPPhoneDirectory XML object only allows for a maximum of 32 DirectoryEntry objects. I will need to limit the number of search results then to 32, what would be the best way to tackle that?

I have tried implmenting  "

searchControls.setCountLimit( 32L ) ;

and even

searchControls.setCountLimit( maxResults ) ;    // max Results is a "long" variable with a value of 32L

but in either case i am getting exceptions; like the UCCX doesn't allow for that class - however if i run it with just

searchControls.setCountLimit( 0 ) ;

all is well

here is a look at my exception

CLIENT1.jpeg

any advice you can offer would be greatly appreciated; if you want to see the script let me know and ill be happy to send it over

Hi G,

I was wondering if you could possibly assist me here;

I need to query via LDAP (AD in our case) and return email address against the employee ID.

Would appreciate if you could point me of  above coding what I would need.

Thanks

 

Got the same exception.

Advocate

Hi Henrique, could you please post that Java code from the Set step that does the LDAP search?

Thanks.

G.

sure...i just changed the quote string...got the user AD search base info from CUCM, so it can't be wrong.

{
java.util.Hashtable env = new java.util.Hashtable();
String icf = "com.sun.jndi.ldap.LdapCtxFactory";
env.put(javax.naming.Context.INITIAL_CONTEXT_FACTORY, icf);
env.put(javax.naming.Context.PROVIDER_URL, url);
env.put(javax.naming.Context.SECURITY_AUTHENTICATION, "simple");
env.put(javax.naming.Context.SECURITY_PRINCIPAL, securityPrincipal);
env.put(javax.naming.Context.SECURITY_CREDENTIALS, securityCredentials);
javax.naming.directory.DirContext dirContext = new javax.naming.directory.InitialDirContext(env);
javax.naming.directory.SearchControls searchControls = new javax.naming.directory.SearchControls();
String[] returningAttributes = new String[] {"cn" , "telephoneNumber" };
searchControls.setReturningAttributes(returningAttributes);
searchControls.setSearchScope(javax.naming.directory.SearchControls.SUBTREE_SCOPE);
String filter = "(name=*" + searchString + "*)";
javax.naming.NamingEnumeration result = dirContext.search(searchBase,filter,searchControls);
StringBuffer buffer = new StringBuffer(400);
buffer.append("<?xml version="+'\"' + "1.0" + '\"' + " ?>");
buffer.append("<CiscoIPPhoneDirectory>");
buffer.append("<Title>Look what I found</Title>");
buffer.append("<Prompt>REPLACE_ME</Prompt>");
int numResults = 0;
while (result.hasMore()) {
javax.naming.directory.SearchResult searchResult = (javax.naming.directory.SearchResult) result.next();
javax.naming.directory.Attributes attributes = searchResult.getAttributes();
javax.naming.directory.Attribute attribute = attributes.get("cn");
String cn = attribute.get();
attribute = attributes.get("telephoneNumber");
if (attribute!=null) {
numResults = numResults + 1;
String telephoneNumber = attribute.get();
buffer.append("<DirectoryEntry>");
buffer.append("<Name>").append(cn).append("</Name>");
buffer.append("<Telephone>").append(telephoneNumber).append("</Telephone>");
buffer.append("</DirectoryEntry>");
}
}
dirContext.close();
if (numResults >0) {
buffer.replace(buffer.indexOf("REPLACE_ME"),buffer.indexOf("REPLACE_ME")+10,"Found " + numResults + " entries");
} else {
buffer.replace(buffer.indexOf("REPLACE_ME"),buffer.indexOf("REPLACE_ME")+10,"Sorry, nothing relevant");
}
buffer.append("</CiscoIPPhoneDirectory>");
return buffer;
}

CreatePlease to create content
Content for Community-Ad
August's Community Spotlight Awards