FreeSWITCH通过XML_CURL 和脚本实现一切: 拨号方案,目录

        我们之前所见过的XML拨号方案和用户目录(实际上包括所有FreeSWITCH配置),都可以通过服务动态构建,实时修改,不需要从文件系统中读取。

          此外,这不是一种非此即彼的选择:你可以两者并用,仅动态配置其中的部分内容,比如用户目录或拨号方案,甚至是具体的模块配置。当动态配置没有涉及到某个具体内容时(比如动态查询返回404),你可以回退回来,从文件系统中读取。

        可以通过web服务器向mod_xml_curl模块提供这类(动态)XML信息,也可能通过支持的脚本语言(Lua、Perl等)提供。脚本可以做任何事情来构建返回给FreeSWITCH的XML片段(查询数据库、使用模板,等等)。

        每当FreeSWITCH需要了解某些信息时(一个用户配置,或一个拨号方案extension),它将用脚本(或mod_xml_curl)来检索相关的信息的XML片段。

        通过这种方式,你可以构建一个FreeSWITCH集群,让它们共用一份动态实时的配置。此外,你还可以用这类配置决定FreeSWITCH的行为,更改影响每次呼叫的逻辑行为。

我们将研究:

  • mod_xml_curl背后的逻辑
  • 如何配置mod_xml_curl
  • 如何设置mod_xml_curl集成的基于php和脚本的web服务器
  • 如何使用脚本动态检索XML片段

mod_xml_curl基本原理

        mod_xml_curl使用curl开发库从web服务器上获取XML片段,这个库和curl可执行工具背后的库是相同的(https://curl.haxx.se/ 上面有详尽的文档)。FreeSWITCH向web服务器发请求,配置信息在应答消息中携带。

        在它的基础配置中,FreeSWITCH启动时从文件系统读取XML配置。FreeSWITCH XML配置文件由许多小的XML文件组成,按层级“包含”导入。XML文件的包含机制和固有结构,最终在FreeSWITCH内存中构建也一棵庞大的XML树。

        每当FreeSWITCH需要了解其自身配置的某些内容时(模块参数、拨号方案、用户目录,等等),FreeSWITCH使用其内部的XML工具向内存中的XML大树查询。在一台繁忙的服务器上,这类查询每秒可达上千次,每次查询,FreeSWITCH会收到一个包含相关信息的XML片段应答。查询/响应是即时的,轻量级的,就像RAM中的二叉树。

        可以配置mod_xml_curl来执完成或部分这类XML查询,它会与web服务器交(本地或远程的)互并返回XML片段。此外,如果第一个web服务查询失败,没有返回需要的信息,可以配置mod_xml_curl查询第二、第三服务器,或让FreeSWITCH回退到本地文件系统读取XML(和mod_xml_curl不存在一样)。在mod_xml_curl中找不到配置?没关系,继续从文件系统中读取就是了。


 

mod_xml_curl配置

        mod_xml_curl的配置文件是/usr/local/freeswitch/conf/autoload_configs/xml_curl.conf.xml。它的极简配置是这样的:

<configuration name="xml_curl.conf" description="cURL XML Gateway">

<bindings>

<binding name="localhostweb">

<param name="gateway-url" value="http://127.0.0.1/xml_handler.php" bindings="directory"/>

</binding>

</bindings>

</configuration>

        现在,让我们解释一下这些标记的含义:

  • configuration它是文档容器,指定这个XML配置片段的名称--- "xml_curl.conf"( mod_xml_curl模块加载时会查找这个名字)。
    • bindings包含所有“绑定”的标记
      • binding每个binding标记包含有关mod_xml_curl在何处以及如何查找检索XML设置类的规范
        • gateway-url这是binding标记内唯一强制要求的参数。它的值是一个完整的URL(协议,服务,端口,资源),mod_xml_curl将用它检索XML片段。它有一个属性叫bindings,它说明这个网关提供的服务能力,即这个网关能够查询哪类型的XML片段。

        在每个binding标记内,只能有一个gateway-url。每个gateway-url可以有一个或多个bindings属性(以管道符"|"分隔),bindings的属性值说明XML片段的类型:

  • directory用户目录,注册用户认证信息和参数
  • dialplancontext与extension
  • phrases"say" API用到的语言和XML IVR结构
  • configuration上面三个没有覆盖的所有内容

        这可能有点令人困惑,因为相同的术语总是以单数和复数形式出现,如标记名和属性值。坚强点!你可以做到!

        在每个binding标记内部,你可以有许多选项标记,它们进一步告诉curl库如何检索XML片段(比如:GET/POST、SSL相关设置、密码、超时限定,等等)。

        如果你有多个binding,每个有唯一的gateway-url,它们配置同一类型的XML标记,那么,如果第一个查询失败之后会尝试第二个,以此类推。这个机制可用于构建“客户端的高可靠性”(显然,你可以在web服务侧实现HA,使用经典的负载均衡技术)。

        如果能够处理某类XML的所有binding全部处理失败,比如服务器宕机,超时,返回非法结果,那么 将回到文件系统中的“传统”XML配置里检索。

        如果一个binding处理失败,会在控制台和日志文件里报告错误信息,以便你标查觉并修正问题。比如,failover工作,但还是报告错误。

        如果你不希望报告错误信息,只想静静地fail over到另一个binding或文件系统(因为这是预料中的事情,我们不指望一个binding能处理所有可能的XML片段),那么web服务必须返回"not found"(与“空”、“无效”不同),请看下一节“mod_xml_curl的php/mysql设置”中的示例。

        让我们来看几个更复杂的mod-xml-curl配置,这些配置是从Confluence文档中修改而来的:

<configuration name="xml_curl.conf" description="cURL XML Gateway">

<bindings>

<!-- Ask example1.com for a dialplan config, if we get a valid response then don't continue onto example2.com. However if we don't get a valid response, then continue onto example2.com -->

<binding name="dialplan">
<param name="gateway-url" value="http://example1.com:80/fsapi" bindings="dialplan"/>
</binding>


<!-- This will only get called if example1.com returns an invalid or not found response. If this gateway also fails to return a valid configuration, then FreeSWITCH will look for the static configuration file on disk -->

<binding name="dialplan backup">
<param name="gateway-url" value="http://example2.com:80/fsapi" bindings="dialplan"/>
</binding>


<!-- Ask example1.com for directory and phrases config, if not found in case of directory then revert to disk, in case of phrases then go to example2 -->

<binding name="example1">
<param name="gateway-url" value="http://example1:80/fsapi" bindings="directory|phrases"/>
</binding>


<!-- Ask example1.com for a configuration config, if not found then revert to disk -->
<binding name="configuration">
<param name="gateway-url" value="http://example1:80/fsapi" bindings="configuration"/>
</binding>


<!-- Ask example2.com for a phrases config, if not found then revert to disk -->
<binding name="example2">
<param name="gateway-url" value="http://example2:80/fsapi" bindings="phrases"/>
</binding>


</bindings>
</configuration>

        此外,你需要检查演示配置实例中的注释内容:

<binding name="example">

<!-- Allow to bind on a particular IP for requests sent -->
<!--<param name="bind-local" value="$${local_ip_v4}" />-->
<!-- The url to a gateway cgi that can generate xml similar to what's in this file only on-the-fly (leave it commented if you dont need it) -->


<!-- one or more |-delim of configuration|directory|dialplan -->

<!-- <param name="gateway-url" value="http://www.freeswitch.org/gateway.xml" bindings="dialplan"/> -->


<!-- set this to provide authentication credentials to the server -->

<!--<param name="gateway-credentials" value="muser:mypass"/>-->

<!--<param name="auth-scheme" value="basic"/>-->



<!-- optional: this will enable the CA root certificate check by libcurl to verify that the certificate was issued by a major Certificate Authority. note: default value is disabled. only enable if you want this! -->

<!--<param name="enable-cacert-check" value="true"/>-->

<!-- optional: verify that the server is actually the one listed in the cert -->
<!-- <param name="enable-ssl-verifyhost" value="true"/> -->


<!-- optional: these options can be used to specify custom SSL certificates to use for HTTPS communications. Either use both options or neither.

Specify your public key with 'ssl-cert-path' and the private key with 'ssl-key-path'. If your private key has a password, specify it with 'ssl-key-password'. -->

<!-- <param name="ssl-cert-path" value="$${certs_dir}/public_key.pem"/> -->

<!-- <param name="ssl-key-path" value="$${certs_dir}/private_key.pem"/> -->

<!-- <param name="ssl-key-password" value="MyPrivateKeyPassword"/> -->

<!-- optional timeout -->

<!-- <param name="timeout" value="10"/> -->



<!-- optional: use a custom CA certificate in PEM format to verify the peer with. This is useful if you are acting as your own certificate authority. note: only makes sense if used in combination with "enable-cacert-check." -->


<!-- <param name="ssl-cacert-file" value="$${certs_dir}/cacert.pem"/> -->

<!-- optional: specify the SSL version to force HTTPS to use. Valid options are

"SSLv3" and "TLSv1". Otherwise libcurl will auto-negotiate the version. -->

<!-- <param name="ssl-version" value="TLSv1"/> -->

<!-- optional: enables cookies and stores them in the specified file. -->

<!-- <param name="cookie-file" value="$${temp_dir}/cookie- mod_xml_curl.txt"/> -->

<!-- one or more of these imply you want to pick the exact variables that are transmitted -->

<!--<param name="enable-post-var" value="Unique-ID"/>-->

</binding>

mod_xml_curl: 警告和陷阱

        现在,你会对mod_xml_curl提供的无限可能感到兴奋,并希望自己实现它。很好!

        但是,你还需要考虑一些事情:

  • FreeSWITCH的XML查询量非常大。真的,非常大!快速,体量巨大!
  • 你希望你的web服务能够快速返回XML片段、失败信息或超时,所有事情都需要快速响应,因为FreeSWITCH正等着它
  • 你希望对每个binding设定超时时间。在web服务没响应或响应慢时,FreeSWITCH过时不候
  • 特别是对于目录处理,在前一个注册完成或失败前,FreeSWITCH不会启动新的电话注册
  • 你可能需要FreeSWITCH缓存XML片段。这方面请搜索confluence上的mod_xml_curl相关文档
  • 你可能需要使用PHP缓存,memcached或类似系统
  • 你希望解决所有web安全相关问题,包括DOS,服务器脚本攻击,数据库的开放性,等等
  • 你不希望把FreeSWITCH,数据库/web服务部署在同一台物理机器上。(把所有服务部署在同一台设备)这是一种禁忌。同样,在同一台硬件设备上部署两台虚拟机,一台跑FreeSWITCH,另一台跑数据库/web服务也是一种禁忌。如果你需要大量并发,不要这样部署。

mod_xml_curl的php/mysql设置

        下面的所有示例是我改编的,包括数据库和PHP代码,它源自《CostruireCentraliniTelefonici Con Freeswitch》一书,由意大利FreeSWITCH专家Christian Bergamaschi所著。可以在亚马逊或其他地方查找!(在亚马逊,你甚至可以把它当作纸质书)。

        如果仅仅是测试目的,你可以在同一台机器上安装FreeSWITCH、web服务、PHP和mysql。如果是生产环境,最好是在千兆局域网内的不同硬件上独立部署它们。

        在 Debian 8 Jessie 上执行:

apt-get install mysql-server nginx php5-fpm php5-mysql

        它会要求你设置mysql的root账户密码,记住它,后面你会用到。

 

创建并填充数据库

        在Linux命令行上,以root用户执行:

mysql -A -p

进入mysql控制台,执行:

create database freeswitch; use freeswitch;

CREATE TABLE `extensions` ( `userid` varchar(5) NOT NULL DEFAULT '', `password` varchar(30) NOT NULL DEFAULT '', `displayname` varchar(14) NOT NULL DEFAULT '', `vmpasswd` varchar(10) DEFAULT NULL, `accountcode` varchar(10) DEFAULT NULL, `outbound_caller_id_name` varchar(14) DEFAULT NULL, `outbound_caller_id_number` varchar(14) DEFAULT NULL );


insert into extensions (userid,password,displayname,vmpasswd,accountcode,outbound_caller_id_name,o utbound_caller_id_number) values ("1000","12345","Giovanni Maruzzelli","12345","1000","OpenTelecom.IT","+393472665618"), ("1001","12345","Sara Sissi","12345","1001","OpenAdassi.IR","+12125551212"),


("1002","12345","Francesca Francesca","12345","1002","OpenSantuzza","+4433344422"), ("1003","12345","Tommaso Stella","12345","1003","OpenBoat.LUV","+9188877755");


select * from extensions;

 

设置NGINX 和 xmlhandler.php

        编辑/etc/nginx/sites-enabled/default,取消激活php和php5所需的行的注释,然后在命令行中执行"service nginx restart"命令。

        创建Create the file /var/www/html/xml_handler.php并添加以下内容:

<?php

function not_found()

{

Header("Content-type: text/xml");

$xmlw = new XMLWriter();

$xmlw -> openMemory();

$xmlw -> setIndent(true);

$xmlw -> setIndentString('');

$xmlw -> startDocument('1.0', 'UTF-8', 'no');

$xmlw -> startElement('document');

$xmlw -> writeAttribute('type', 'freeswitch/xml');

$xmlw -> startElement('section');

$xmlw -> writeAttribute('name', 'result');

$xmlw -> startElement('result');

$xmlw -> writeAttribute('status', 'not found');

$xmlw -> endElement(); //end result

$xmlw -> endElement(); //end section

$xmlw -> endDocument(); //end document echo $xmlw -> outputMemory();

return TRUE;

}



function directory()

{

global $_SERVER; global $_POST;

# connect to MySQL

$connect = mysql_connect("localhost", "root", "mysql_root_password"); mysql_select_db("freeswitch", $connect) or die(mysql_error());



# Query
$query = "SELECT userid,password,displayname,vmpasswd,accountcode,outbound_caller_id_name,ou tbound_caller_id_number from extensions where userid='" . $_POST['user'] . "'";


# perform the query
$result = mysql_query($query, $connect) or die(mysql_error());

$num_rows = mysql_num_rows($result);


if($num_rows==0){

# if no database row, fallback to filesystem, no FS error

not_found(); return TRUE;

}



Header("Content-type: text/xml");

$xmlw = new XMLWriter();

$xmlw -> openMemory();

$xmlw -> setIndent(true);

$xmlw -> setIndentString('');

$xmlw -> startDocument('1.0', 'UTF-8', 'no');

$xmlw -> startElement('document');

$xmlw -> writeAttribute('type', 'freeswitch/xml');

$xmlw -> startElement('section');

$xmlw -> writeAttribute('name', 'directory');

$xmlw -> startElement('domain');

$xmlw -> writeAttribute('name','$${domain}');

$xmlw -> startElement('params');

$xmlw -> startElement('param');

$xmlw -> writeAttribute('name', 'dial-string');

$xmlw -> writeAttribute('value', '{^^:sip_invite_domain=$

{dialed_domain}:presence_id=${dialed_user}@${dialed_domain}}$

{sofia_contact(*/${dialed_user}@${dialed_domain})}');

$xmlw -> endElement(); //end param

$xmlw -> endElement(); //end param

while( $row = mysql_fetch_array($result, MYSQL_ASSOC) ) {

$xmlw -> startElement('user');

$xmlw -> writeAttribute('id', $row['userid']);



// Params

$xmlw -> startElement('params');

$xmlw -> startElement('param');

$xmlw -> writeAttribute('name', 'password');

$xmlw -> writeAttribute('value', $row['password']);

$xmlw -> endElement();

$xmlw -> startElement('param');

$xmlw -> writeAttribute('name', 'vm-password');

$xmlw -> writeAttribute('value', $row['vmpasswd']);

$xmlw -> endElement();

$xmlw -> endElement(); //end params


// Variables

$xmlw -> startElement('variables');

$xmlw -> startElement('variable');

$xmlw -> writeAttribute('name', 'accountcode');

$xmlw -> writeAttribute('value', $row['accountcode']);

$xmlw -> endElement();

$xmlw -> startElement('variable');

$xmlw -> writeAttribute('name', 'user_context');

$xmlw -> writeAttribute('value', 'default');

$xmlw -> endElement();

$xmlw -> startElement('variable');

$xmlw -> writeAttribute('name', 'effective_caller_id_name');

$xmlw -> writeAttribute('value', $row['displayname']);

$xmlw -> endElement();

$xmlw -> startElement('variable');

$xmlw -> writeAttribute('name', 'effective_caller_id_number');

$xmlw -> writeAttribute('value', $row['userid']);

$xmlw -> endElement();

$xmlw -> startElement('variable');

$xmlw -> writeAttribute('name', 'outbound_caller_id_name');

$xmlw -> writeAttribute('value', $row['outbound_caller_id_name']);

$xmlw -> endElement();

$xmlw -> startElement('variable');

$xmlw -> writeAttribute('name', 'outbound_caller_id_number');

$xmlw -> writeAttribute('value', $row['outbound_caller_id_number']);

$xmlw -> endElement();

$xmlw -> endElement(); //end variables

$xmlw -> endElement(); //end user

} // end while

$xmlw -> endElement(); //end domain

$xmlw -> endElement(); //end section

$xmlw -> endDocument(); //end document echo $xmlw -> outputMemory();

return TRUE;
}


if (isset($_POST['section'])) { if($_POST['section']=='directory') { 
directory();

} else {

# if section is not directory, fallback to filesystem, no FS error not_found();
}

}

?>

        在PHP脚本的最后,我们看到代码中检查POST请求是否携带"section"字段。如果请求中"section"字段的值为"directory",那么执行directory()函数,否则调用not_found()函数。这样,我们就可以用这个脚本来处理mod_xml_curl的请求,根据XML类别给出适当应答,如果不是查询"directory",那么返回"not found",FreeSWITCH将愉快地接受应答并回退到文件系统,不会在日志中抱怨。

        如果请求是"directory"类别,那么我们执行数据库查询,要求提供所需用户的XML片段,以"userid"(对应SIP用户)为标识。

        如果数据库里查到相关记录,我们恰当地把数据格式化为XML片段并返回。

        如果数据库中没有找到相关userid的记录(数据库查询返回空集),我们还是返回"not found",同样,FreeSWITCH将愉快地接受应答并回退到文件系统,不会在日志中抱怨。

mod_xml_curl的配置

        编辑文件/usr/local/freeswitch/conf/autoload_configs/xml_curl.conf.xml,内容为:

<configuration name="xml_curl.conf" description="cURL XML Gateway">

<bindings>

<binding name="localhostweb">

<param name="gateway-url" value="http://127.0.0.1:8888/xml_handler.php" bindings="directory"/>

<binding name="localhostweb2">

<param name="gateway-url" value="http://127.0.0.1:80/xml_handler.php" bindings="directory"/>

</binding>

</bindings>

</configuration>

        这个配置里,将首先尝试从FreeSWITCH本机的8888端口连接web服务以获取XML片段,这个web服务是不存在的。它会立刻返回(因为这个端口没有进程监听),记录错误日志,并尝试连接第二个binding。第二个binding尝试连接本机的80端口。

测试mod_xml_curl 的php/mysql设置

        在FreeSWITCH控制台执行"load mod_xml_curl"(或者是"reload")命令加载模块。

        每当FreeSWITCH需要检查用户信息时(可能是认证SIP或Verto的注册信息;或授权注册话机的出局呼叫;或为SIP/Verto用户提供来电服务的详细信息,等等),mod_xml_curl将触发一系列操作。

        它首先尝试连接8888端口上的web服务,失败后记录日志。然后连接80端口,请求XML片段,如果请求用户不在数据库里,FreeSWITCH会悄悄地(没有错误日志)回退到文件系统。


        我们以用户1011尝试呼叫,数据库里没有他的记录(我们在数据库里插入1000到1003这几个用户)。我们首先看到连接8888端口失败的错误提示。然后尝试连接80端口,得到"not found"的应答,最最后回退到文件系统并找到1011用户,最终授权通过。这就像油电混合动力车的双能工作模式。

Lua XML 处理器

        和mod_xml_curl模块完全一样,FreeSWITCH可以通过执行一个脚本来检索它的XML配置片段,脚本语言可以在它支持的语言中任意选择。

        你必须首先配置脚本语言模块,本节以mod_lua为例,处理XML查询请求,获取XML片段。

        你可以写一个脚本,设置一个名为XML_STRING的通道变量,在变量里存储XML片段。因为lua已经嵌入到FreeSWITCH中(python和perl也一样),所以不需要其它额外的操作。

         在脚本内,你可以连接数据库、web服务器,完成自己的数据封装等等。只要求脚本返回一个包含XML片段的字符串,为了实现这个目的,可以采取任何必要的手段。

         这个示例Lua脚本配置为xml-handler-script(脚本模块配置文件中的配置选项),它以FreeSWITCH 所查询的XML文档设置(填充)XML_STRING变量:

 

lua xml 处理脚本

xml_snippet = [[

<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<document type="freeswitch/xml">

<section name="directory">

<domain name="lab.opentelecomsolutions.com">

<params>


<param name="dial-string" value="{^^:sip_invite_domain=${dialed_domain}:presence_id=${dialed_user}@${ dialed_domain}}${sofia_contact(*/${dialed_user}@${dialed_domain})}"/>


</params>

<user id="1000">

<params>

<param name="password" value="56789"/>

<param name="vm-password" value="56789"/>

</params>

<variables>


<variable name="accountcode" value="1000"/>

<variable name="user_context" value="default"/>

<variable name="effective_caller_id_name" value="Giovanni Maruz"/>

<variable name="effective_caller_id_number" value="1000"/>

<variable name="outbound_caller_id_name" value="OpenTelecom.IT"/>

<variable name="outbound_caller_id_number" value="+393472665618"/>

</variables>


</user>

<user id="1001">

<params>

<param name="password" value="56789"/>

<param name="vm-password" value="56789"/>

</params>

<variables>

<variable name="accountcode" value="1001"/>

<variable name="user_context" value="default"/>

<variable name="effective_caller_id_name" value="Sara Sissi"/>

<variable name="effective_caller_id_number" value="1001"/>

<variable name="outbound_caller_id_name" value="OpenAdassi.IR"/>

<variable name="outbound_caller_id_number" value="+12125551212"/>

</variables>

</user>

</domain>

</section>

</document>

]]


XML_STRING = xml_snippet freeswitch.consoleLog("notice", XML_STRING .. "\n")

 

lua.conf.xml

        你必须在/usr/local/freeswitch/conf/autoload_configs/lua.conf.xml里添加以下两行内容:

 

<param name="xml-handler-script" value="xml_handler.lua"/>


<param name="xml-handler-bindings" value="directory"/>

测试xml_handler.lua

        你必须先重启FreeSWITCH(因为不能unload或reload  mod_lua模块)。

        然后,以用户1000或1001尝试呼叫。注意,脚本里,它们的密码被设置为"56789"。因此FreeSWITCH会首先拒绝呼叫或注册,然后再次询问你的密码,最终进行授权。

 

总结

        这一章,我们研究如何动态获取XML片段,如何在动态获取失败后回退到文件系统本身。

        我们通过两种机制实现同一个目标:获取同一份XML片段。

        我们可以用mod_xml_curl查询远端或本地数据库。

        我们还可用一种支持的脚本语言来检索XML片段。

        我们还描述了可能的注意事项:FreeSWITCH需要非常快速地执行这类查询,需要设置超时时间以防止后端应用响应缓慢拖累整个平台。

       下一章,我们将研究如何在外部控制FreeSWITCH----通过ESL,微观管理FreeSWITCH最强有力的方式。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值