Cómo realizar una llamada automática con Cisco Webdialer sin exponer credenciales

Cisco WebDialer

El WebDialer consiste en una componente provista por Cisco para realizar llamadas telefónicas de forma programática, desde cualquier teléfono registrado a un Communications Manager. Existen dos modos de funcionamiento de WebDialer:

  • HTML: En esta modalidad, es posible de forma sencilla incorporar la capacidad para Click to dial dentro de cualquier página web. Basta con agregar un poco de Javascript a la página para poder conseguir esta funcionalidad, tal y como se aprecia en este ejemplo en DevNet. Una desventaja de esta modalidad es que se necesita la intervención del usuario para que ingrese sus credenciales y así completar la llamada. Por esta razón no es posible realizar un Click to Dial sin que el usuario deba ingresar sus credenciales.
  • SOAP: En este caso la petición se realiza desde un servidor externo y no desde el navegador. La petición SOAP consiste en enviar un documento XML con un formato muy específico al CUCM, en donde se especifican las credenciales y el número al que se desea llamar. Si bien es técnicamente posible realizar una petición SOAP desde un navegador web, esto implicaría exponer las credenciales del usuario, ya que cualquiera podría ver el código fuente de la página por medio de la opción View Source del navegador.

Solución: un servidor intermediario

Una posible solución a este problema consiste en utilizar un servidor intermediario entre el navegador y el CUCM, que reciba una petición no autenticada tipo REST desde el navegador y a su vez envíe una petición SOAP autenticada al CUCM, eliminando la necesidad de especificar las credenciales en el código fuente de la página web.

Para crear el servicio web que recibe la petición REST desde el navegador vamos a usar la popular librería de Python Flask. Flask es un microframework para la fácil creación de servicios web en Python. Por medio de tan solo unas cuantas líneas de código es posible crear un servidor web que procese peticiones REST, tal y como se ilustra a continuación:

from flask import Flask, request
app = Flask(__name__)

@app.route('/hello')
def hello():
    return "Hello World"

if __name__ == '__main__':
    app.run(host='0.0.0.0')


Vamos a guardar este script con el nombre hello.py y luego correrlo:

$ python hello.py
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)


Utilizando una herramienta como curl podemos fácilmente probar el servicio web que recién creamos:

$ curl localhost:5000/hello
Hello World

o bien, utilizando Postman:

Postman

Pasando parámetros en el URL

Para que nuestro servicio web intermediario sea verdaderamente útil, necesitamos que la extensión a la que deseamos llamar se pueda enviar como un parámetro. Para esto vamos a utilizar un URL de la siguiente forma:

http://localhost:5000/dial?extension=1234


La componente ?extension=1234 es lo que se conoce como el querystring de la petición.

En Flask, el querystring se accede por medio de

request.args.get('extension')


Incorporando esto a nuestro script:

from flask import Flask, request
app = Flask(__name__)

@app.route('/dial')
def dial():
    extension = request.args.get('extension')
    callExtension(extension)
    return "Success"

if __name__ == '__main__':
    app.run(host='0.0.0.0')

Aquí callExtension() es la función que ejecuta la petición SOAP hacia el CUCM, y es el tema que vamos a abordar a continuación:

Enviando la petición SOAP al CUCM

La librería para Python más popular para ejecutar peticiones HTTP es, sin duda alguna, Requests. Realizar una petición es sumamente sencillo:

import requests

r = requests.get("https://api.github.com/users/racampos")
print(r.text)


Si corremos este script obtendremos el siguiente resultado:

{
  "login": "racampos",
  "id": 4667189,
  "avatar_url": "https://avatars1.githubusercontent.com/u/4667189?v=4",
  "gravatar_id": "",
  "url": "https://api.github.com/users/racampos",
  "html_url": "https://github.com/racampos",
  "followers_url": "https://api.github.com/users/racampos/followers",
  
  ...

  "type": "User",
  "site_admin": false,
  "name": "Rafael Campos",
  "public_repos": 15,
  "public_gists": 0,
  "followers": 1,
  "following": 2,
  "created_at": "2013-06-11T04:38:10Z",
  "updated_at": "2017-07-08T19:17:25Z"
}


Armando la petición SOAP

Ahora vamos a aprender cómo utilizar Requests para enviar una petición SOAP hacia el CUCM, usando el número de extensión que recibimos por medio de nuestro servicio web en Flask. Pero antes es necesario investigar cómo es el formato de la petición SOAP que el CUCM espera recibir.

La especificación WSDL del servicio WebDialer modalidad SOAP, se puede obtener directamente del CUCM mediante una petición HTTP GET, según se explica en este artículo de DevNet.

En nuestro caso, para realizar una llamada necesitamos usar la función makeCallSoap definida en la siguiente plantilla XML:

<?xml version="1.0" encoding="utf-8" ?> 
 <soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:WD70">
    <soapenv:Header/>
    <soapenv:Body>
       <urn:makeCallSoap soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
          <in0 xsi:type="urn:Credential">
             <userID xsi:type="xsd:string">{username}</userID>
             <password xsi:type="xsd:string">{password}</password>
          </in0>
          <in1 xsi:type="soapenc:string" 
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">{extension_to_call}</in1>
          <in2 xsi:type="urn:UserProfile">
             <user xsi:type="xsd:string">{username}</user>
             <deviceName xsi:type="xsd:string">{device_name}</deviceName>
             <lineNumber xsi:type="xsd:string">{user_extension}</lineNumber>
          </in2>
       </urn:makeCallSoap>
    </soapenv:Body>
 </soapenv:Envelope>


donde {username}, {password}, {extension_to_call}, {device_name} y {user_extension} son los parámetros que es necesario especificar para que la petición SOAP sea exitosa.


La petición SOAP debe enviarse al siguiente URL, según especificación de Cisco:

    https://{cucm_ip}:8443/webdialer/services/WebdialerSoapService70

donde {cucm_ip} es la dirección IP (o nombre de dominio) del servidor de CUCM.

Los encabezados de la petición SOAP

Debido a que SOAP utiliza XML como lenguaje de modelado, es neceario especificar en el encabezado HTTP de la petición el content-type como text/xml. Asimismo, la especificación del WebDialer exige enviar un encabezado HTTP adicional llamado SOAPAction. En nuestro caso, vamos a enviar la versión de CUCM que estamos utilizando: UCM:DB ver=10.0.

Para efectos de Requests, los encabezados se deben especificar como un diccionario:

headers = {'content-type': 'text/xml', 'SOAPAction': 'CUCM:DB ver=10.0'}



Juntándolo todo

Finalmente estamos en capacidad de escribir la función callExtension() mencionada anteriormente, utilizando la librería requests:

def callExtension(extension_to_call):

    makeCallSoapXML = """<?xml version="1.0" encoding="utf-8" ?> 
 <soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:WD70">
    <soapenv:Header/>
    <soapenv:Body>
       <urn:makeCallSoap soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
          <in0 xsi:type="urn:Credential">
             <userID xsi:type="xsd:string">{username}</userID>
             <password xsi:type="xsd:string">{password}</password>
          </in0>
          <in1 xsi:type="soapenc:string" 
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">{extension_to_call}</in1>
          <in2 xsi:type="urn:UserProfile">
             <user xsi:type="xsd:string">{username}</user>
             <deviceName xsi:type="xsd:string">{device_name}</deviceName>
             <lineNumber xsi:type="xsd:string">{user_extension}</lineNumber>
          </in2>
       </urn:makeCallSoap>
    </soapenv:Body>
 </soapenv:Envelope>"""

    username = "pedroperez"
    password = "12345"
    device_name = "SEPC0626BD25259"
    user_extension = "1891"
    cucm_ip = "192.168.200.1"

    url = "https://{cucm_ip}:8443/webdialer/services/WebdialerSoapService70".format(cucm_ip=cucm_ip)

    headers = {'content-type': 'text/xml', 'SOAPAction': 'CUCM:DB ver=10.0'}
    body = makeCallSoapXML.format(
        username=username,
        password=password,
        device_name=device_name,
        extension_to_call=extension_to_call,
        user_extension=user_extension
    )

    r = requests.post(url, headers=headers, data=body, verify=False)

El parámetro verify=False indica a Requests que realice la petición sin verificar el certificado SSL del servidor, ya que de lo contrario nuestro script no funcionaría. No se recomienda hacer esto con código de producción, por razones de seguridad.

Realizando la petición desde una página web

El último paso que nos queda es crear una página web sencilla que contenga un enlace que al hacer click en él se realice una llamada telefónica automáticamente y sin necesidad de especificar credenciales.

<html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

<body>
    <h2>Demostración de Cisco Webdialer con servidor intermediario</h2>
    <a href="http://localhost:5000/dial?extension=1234">Llamar a extensión 1234</a>
</body>

</html>

Postman