Thursday, January 18, 2018

OpenSIPS Geo-Distributed Clustering & Data-Exchange

OpenSIPS Clusterer Module


Summary:

OpenSIPS 2.4 now allows users to exchange user data between clustered proxy nodes via proto_bin module. As a result the receiving Proxy can perform an action.

In this blog post I'll try to show how we can exchange that data from the opensips script.

USE CASE: Multi-Proxy cluster sharing User Locations and Querying Cluster to Find Users.




The Slideshow above shows a simple setup where multiple proxies can "Talk" to each other without using any other application as the communication platform.

Previously we could share data as a one-way pipe to SQL, or NoSQL applications. By one-way pipe I mean that OpenSIPS server can deposit data but won't do anything as a recipient. since there is no way to tell each opensips box that you are the recipient and need to do something.

OpenSIPS introduced the Event_route and EBR (Event Based Routing) some time ago but the custom events can not trigger on their own w/o developing some MI script. Obviously, the concept of adding more scripts and tools to make a fully functional distributed OpenSIPS environment is complicated to develop and maintain.

Clusterer module in older versions of OpenSIPS (pre-devel/2.4) was only capable to exchange data from certain module internally i.e dialog, usrloc - user has no control over what data to be sent and how to perform an action when such a data is received.


WHATS NEW

1 - OpenSIPS cluster can broadcast script based Queries and wait for replies, perform action based on reply!
  • Cluster can detect and share IP of a hacker with other nodes to perform some action
  • Update cluster about Location of a user, inquire about location of a user and send call to relevant proxy
  • Cluster can update about an ongoing conference call, other boxes can route calls to the hosting proxy
  • A Central OpenSIPS talking to Database on behalf of rest of the OpenSIPS cluster. SQL statements only reside there rest of the cluster only sends small text to expect a small reply. Useful incase of depositing CDRs.

     If you've multiple FreeSWITCH boxes behind an OpenSIPS and multiple such deployments needing integration then this is going to be perfect to collect an ESL event and Broadcast it from local OpenSIPS to rest of the regions. This could be a game changer for multi-region deployments.


TIME FOR ACTION

Lets see how this all can be done from inside the opensips.cfg
A complete sample config file is available here:
https://github.com/goharahmed/opensips-clusterer

Loading Modules




The "db_mode" param is set to 0 so we don't depend on DB to find other Cluster nodes, its my personal preference you can load all cluster nodes from MySQL DB and run mi-fifo command to reload the new node data.
What I've done is added two core nodes in the config so any new node just needs to point to any one of the core servers and everyone will be aware of the whole cluster topology.

Event_routing module are critical for this to work, they invoke config file script when a certain event is triggered.


Sharing Data

First we need to identify what data needs to be shared across the cluster. In the code snippet below I figure I need to tell other boxes if some hacker is trying to make Bruteforce Registration to users which are not defined in my DB. Also, I share the successfully registered user info.




Here are the exact routes responsible for Broadcasting the message string to the cluster.



Infact these Lines are the real deal here:

        $var(cl_id) = 1;
        $var(msg_str) = "I_LOVE_OPENSIPS";
        cluster_broadcast_req("$var(cl_id)", "$var(msg_str)", "$avp(tag)");

Since we are sharing Data so we need to carefully fill the details as to which cluster this info needs to be routed to and what message to be delivered. The Tag is filled in automatically and is very important for retrieval of any replies.

Remote Proxy Data Retrieval

Once this is executed all Online nodes of the cluster will receive this message in an Event Route "E_CLUSTERER_REQ_RECEIVED"



Remote Proxy Replying Back

The AVP $avp(rcv_msg) will contain the sent string and we can script what to do further with that. Similarly the $avp(source_id) variable will contain the NodeID of the sender proxy so if we need to reply back we can send a reply using the same TAG. It is NOT mandatory for the cluster nodes to reply to each and every broadcasted message btw. They can remain silent if there is nothing to be replied with.



Receiving Reply

Once remote proxy replies with the same tag (OpenSIPS internaly differentiates between a Request and a Reply) an event is raised "E_CLUSTERER_RPL_RECEIVED" -

In this Event Route we don't need to do much, this is just collection of replied data. The real work is done elsewhere.



 Finding Location of a User in Cluster

This is approximately how the code will look like for a call where user was not found registered and query was dispatched to whole cluster. The Querying OpenSIPS waits for an event to occur in async mode (wait w/o blocking resources for as long as needed)


That is pretty much all that is required to create a Talking-Cluster of OpenSIPS servers.

WARNING: This is a work in progress from OpenSIPS developers and should be tested heavily before deploying in production. Version 2.4 is expected to be released as stable production release by May-2018 or around. 

Saturday, June 25, 2016

OpenSIPS mi_xmlrpc_ng Interfacing

OpenSIPS 2.2: Creating WebAPI 

OpenSIPS has multiple modules to interact with it in realtime. Sometimes we need to pull information out of the proxy and sometimes we need to insert, or check status of other modules. 

Here is a list of module which can be used to achieve the objective:

MI_DATAGRAMDATAGRAM (unix and network) support for Management Interfacestable
MI_FIFOFIFO support for Management Interfacestable
MI_HTTPHTTP support for Management Interfacestable
MI_JSONJSON support via HTTP GET for Management Interfacebeta
MI_XMLRPC_NGXMLRPC support for Management Interfacebeta

In my last blog on this topic I showed how to use module MI_XMLRPC to monitor the status of OpenSIPS: Read it here. That mi_xmlrpc module is now obsolete and a newer module mi_xmlrpc_ng has taken its place. 

Today we will use the new module with OpenSIPS and see how we can query our server from the internet to do something. 

First we need to know what we require to get started.

1 - HTTPD module, and MI_XMLRPC_NG module installed with OpenSIPS 2.2
2 - Apache2/ httpd running on the Server.

OpenSIPS' HTTPD module is a requirement since this module provides the web related backend to its other modules.

Lets load up this module into our opensips.cfg

loadmodule "httpd.so"
modparam("httpd", "ip", "127.0.0.1")
modparam("httpd", "port", 8887)
modparam("httpd", "buf_size", 524288)
modparam("httpd", "post_buf_size", 4096)

Next we need to load mi_xmlrpc_ng module.

loadmodule "mi_xmlrpc_ng.so"
modparam("mi_xmlrpc_ng", "http_root", "mybox")


The root directory 'mybox' can be anything and it does not represent any folder in the operating system. That infact tells us that when we start up our OpenSIPS the port for http will be 8887, and the web root directory allocated for mi_xmlrpc_ng module would be 'mybox' .

Web API script

Next we need to create the script that will be responsible to collect Requests from internet and then pass them to OpenSIPS, return data if any. 

This script can be written in any language, Pythin, PHP, Perl, GoLang, Ruby, whatever your language of choice for creating a web-api is use is. I'd kept it simple and use PHP.

Complete script link: https://github.com/goharahmed/scripts/blob/master/webapi.php

Copy the script into your Apache2 webroot directory , say file 'webapi.php'

<?php

$opensip_ip = '127.0.0.1';
//Should use the same HTTPD PORT as declared in opensips.cfg
$opensip_xmlrpc_port = '8887';
//Should use the same webroot as declared in mi_xmlrpc_ng module parameters
$xmlrpc_root = 'mybox';

$count = 0;
$method;
$param;
$web_req = 0;
if(isset($_GET) && isset($_GET['method'])){
 foreach($_GET as $key => $value)
 {
  if($key == 'method') { 
   $method = $value;
  }
  if(preg_match("/^param\d$/",$key)) {
   $param[] = $value;

  }
 }
 $web_req = 1;
} else {
 foreach ($argv as $arg) {
  if($count==1)
   $method =  $arg;
  else if($count>1) {  
   $param[] = $arg;
  }
  $count++; 
 }
}
$request = xmlrpc_encode_request($method,$param);
$context = stream_context_create(array('http' => array(
    'method' => "POST",
    'header' => "Content-Type: text/xml",
    'content' => $request
    )));
    
// Dispatchs Request to Local OpenSIPS instance
$server = 'http://'.$opensip_ip.':'.$opensip_xmlrpc_port.'/'.$xmlrpc_root;

//Collect Result from opensips module
$file = file_get_contents($server, false, $context);

//Decode the XML into Array
$response = xmlrpc_decode($file);
if (is_array($response)) {
 
 /* We can filter and sort Output here to do whatever you want to do */
 /* if($method == 'ul_show_contact') {
  Then filter only the required fields to be sent back
 } else if ($method == 'ds_list') {
  Then filter the output to show only the active servers
 }
 */
 if($web_req == 0) {
  RecursiveWrite($response);
 }else if($web_req == 1) {
  print_r(json_encode($response));
  
  
 }
}

function RecursiveWrite($array) {
    foreach ($array as $key => $vals) {
 if(is_array($vals)){
         RecursiveWrite($vals);
 } else {
  print "$key $vals\n";
 }
    }
}

?>
Now, make sure your opensips is restarted to include the new module, make sure your server is able to listen to the web requests. Time to send requests to our OpenSIPS.


Open your browser and paste the following URL to find if a particular suer is Online or Not.

http://YourServerIPHere/webapi.php?method=ul_show_contact&param1=location&param2=gohar@saevolgo.ca


Little Explanation:

method = MI command to query the user location status
param1 = The database table where the User Registrations are stored.
param2 = The AoR to find if Online

You can add as many param as you want , param3, param4...paramN depending if the MI command needs or not.

If a command requires no parameter then don't add any for example:
http://YourServerIPHere/webapi.php?method=lb_list

To get Statistics:
http://YourServerIPHere/webapi.php?method=get_statistics&param1=tm:

OpenSIPS Modules: How to read what command has how many parameters ?

Take example of Load-Balancer module:
The function lb_relaod has no parameters:
 :lb_reload:_reply_fifo_file_
  _empty_line_
So my URL would be:
http://YourServerIPHere/webapi.php?method=lb_reload


But the function lb_resize takes 3 parameteres:
:lb_resize:_reply_fifo_file_
  11   /*dstination id*/
  voicemail  /*resource name*/
  56   /* new resource capacity*/
  _empty_line_
For above the URL would be:
http://YourServerIPHere/webapi.php?method=lb_resize&param1=11&param2=voicemail&param3=56


Thats pretty much all, thanks for reading. 





Monday, March 28, 2016

FreeSWITCH mod_xml_curl With mod_callcenter

FreeSWITCH Loading Queues from Database

This has been a long due post waiting in my drafts for above two years now. This was done right after when I finished loading SIP users from Database in this blog post.

I always found myself at trouble when FreeSWITCH's MOD_XML_CURL was named together with loading configurations but as soon as I gave it a shot it became very easy and interesting. 

Here is how to get started with loading queue from Database in just few minutes. 

Here is what we need to get started:

1 - Database table containing Queue's parameters.
2 - A WebServer containing our DB-to-XML converter code
3 - FreeSWITCH with mod_xml_curl installed and configured.


Creating Table for Queue Parameters 

So, first things first lets create a simple Database table:

root@DBSERVER:# su - postgres
postgres@DBSERVER:~$ psql
psql (9.4.6)
Type "help" for help.

postgres=# \c freeswitch_configs
You are now connected to database "freeswitch_configs" as user "postgres".
freeswitch_configs=#

CREATE TABLE queues (
queue_id  serial primary key,
name VARCHAR(255) not null,
strategy VARCHAR(100) default 'longest-idle-agent',
moh_sound VARCHAR(255) default 'local_stream://moh',
announce_sound VARCHAR(255) default NULL,
announce_frequency VARCHAR(5) default NULL,
time_base_score VARCHAR(255) DEFAULT 'queue' ,
tier_rules_apply VARCHAR(25) DEFAULT 'false',
tier_rule_wait_second VARCHAR(4) DEFAULT '300',
tier_rule_wait_multiply_level VARCHAR(5) DEFAULT 'true',
tier_rule_no_agent_no_wait VARCHAR(255) DEFAULT 'false',
discard_abandoned_after VARCHAR(5) DEFAULT '60',
abandoned_resume_allowed VARCHAR(5) DEFAULT 'false',
max_wait_time VARCHAR(25) DEFAULT '0',
max_wait_time_with_no_agent VARCHAR(10) DEFAULT '0',
max_wait_time_with_no_agent_time_reached VARCHAR(10) DEFAULT '0',
record_template VARCHAR(255) DEFAULT NULL
);


Once Table is created you can go ahead and create some queues in it via INSERT statements.


Setting up WebServer


Alright, we've some queues defined into our database table. Now, is the time to create a web service which will query the Database and pull required configurations. Once configurations are found for a queue we will put them nicely into the required XML format and reply back.

Take the PHP sample code from my github repo:
https://github.com/goharahmed/saevolgo/blob/master/getconfig.php

Configure the Database IP, Port, User, and Password as required and make sure there is no error in it.
Once this is ready and running we can query to this web service and see that it returns an Error XML if no queue name is passed to it.

Test it:
http://ip.of.webserver:port/freeswitch/getconfig.php?CC-Queue=MyQueueName

If everything is configured properly and Queue: MyQueueName exists then it should print a fine XML - if that doesn't happen then check your PHP code, PHP PDO extension for PGSQL and verify that port is accessible and webserver is running. 


Configuring MOD_XML_CURL


Assuming mod_xml_curl is installed on your FreeSwitch server now its time to configure its module xml_curl to pull configurations from webserver instead of local file.

root@FREESWITCH1:# cd /usr/local/freeswitch/conf
root@FREESWITCH1:conf# cd autoload_configs/
root@FREESWITCH1:autoload_configs# vim xml_curl.conf.xml


<configuration name="xml_curl.conf" description="cURL XML Gateway">
  <bindings>
    <binding name="configuration">         
      <param name="gateway-url" value="http://WEBSERVER/freeswitch/getconfig.php" bindings="configuration"/>
    </binding>
  </bindings>
</configuration>

Ensure that there are no other configuration binding to conflict here.

Save and exit. Go inside freeswitch console and reload mod_xml_curl

root@FREESWITCH1:# fs_cli
fs_cli>reload mod_xml_curl 
+OK Reloading XML
+OK module unloaded
+OK module loaded

All set, now we are ready to load Queues from Database directly. To test things out issue this command

freeswitch@internal> callcenter_config queue load MynewQueue
+OK
freeswitch@internal> callcenter_config queue list
name|strategy|moh_sound|time_base_score|tier_rules_apply|tier_rule_wait_second|tier_rule_wait_multiply_level|tier_rule_no_agent_no_wait|discard_abandoned_after|abandoned_resume_allowed|max_wait_time|max_wait_time_with_no_agent|max_wait_time_with_no_agent_time_reached|record_template
support@default|longest-idle-agent|local_stream://moh|system|false|300|true|false|60|false|0|0|5|
MynewQueue|longest-idle-agent||queue|false|0|false|true|60|false|0|0|5|
+OK

To debug XML Curl

freeswitch@internal> xml_curl debug_on
OK

freeswitch@internal> callcenter_config queue load NOTMYQUEUE
-ERR Invalid Queue not found!

2016-03-28 19:32:21.128783 [CONSOLE] mod_xml_curl.c:323 XML response is in /tmp/3337e053-077c-4f39-9c3f-0805c4896851.tmp.xml
freeswitch@internal

root@FREESWITCH1:# cat /tmp/3337e053-077c-4f39-9c3f-0805c4896851.tmp.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="freeswitch/xml">
<section name="result"><result status="not found" />
</document>


Conclusion


So, now we see how simple it is to pull configurations from Database - for a practice try this all with Conferences