No secret, that FusionPBX has it's own AddressBook named Contacts. But I actually found it too heavy for a simple app for provisioning phones. And lack of organizing.
Quick googling found this app by DigiDaz. I've been using it a while, but found way it's organized with groups a bit strange.
I redesigned it (I'd say redo around 70-80% of code) and comes up with my version of it.
So, new version of FusionPBX Phonebook app.
1. Supports Yealink, Cisco (via XML_Directory_Service), Snom. Adding new formats is quite simple.
2. More focus on security, as other apps requite auth and normally does it with API key mechanism build into FusionPBX. If you want to stay without auth, you can set variable Phonebook - Auth (text) to False in Default Settings.
For Cisco XML directory need to change default nginx profile, as Cisco does not support HTTPS unless it's Cisco singed certificates.
/etc/nginx/sites-enabled/fusionpbx
...
server {
listen 80;
server_name fusionpbx;
if ($uri !~* ^.*(provision|phonebook\/directory).*$) {
rewrite ^(.*) https://$host$1 permanent;
break;
}
...
Example of how to put this in template for Cisco can be found here.
For Yealink - same as described in video in original post.
For Snom - add this lines at your FusionPBX template
<!-- Phonebook Support via app -->
{if isset($snom_phonebook_url)}
{fetch file=''|cat:$snom_phonebook_url|cat:'&vendor=snom'}
{/if}
For every template use corresponding variables in Device (or Domain/Global) settings.
App itself could be found for both 4.4 and 4.2 versions of FusionPBX.
Monday, July 2, 2018
Wednesday, April 11, 2018
FreeSwitch - call through registered extension
Sometimes with SIP PBX you have a trunk, that registers on PBX. Most common example - various gateways.
Asterisk has a simple solution for this (as in Asterisk difference between trunk and extension is more cosmetic, than functional), but FreeSWITCH uses concept of gateways.
But it's a very powerful system. So we will make bridge statement by ourselves.
Adoption for FusionPBX.
condition - destination_number - ^(\d{10,20})$
action - set - reged_ext=username_of_extension
action - set - num_to_dial=$1
action - bridge - {absolute_codec_string='${outbound_codec_prefs}'}${regex(${sofia_contact(${reged_ext}@${domain_name})}|(^\w+/\w+)/|%1)}/sip:${num_to_dial}@${regex(${sofia_contact(${reged_ext}@${domain_name})}|(\d+.\d+.\d+.\d+:\d+.*)|%1)}
Original source (rus)
Asterisk has a simple solution for this (as in Asterisk difference between trunk and extension is more cosmetic, than functional), but FreeSWITCH uses concept of gateways.
But it's a very powerful system. So we will make bridge statement by ourselves.
Adoption for FusionPBX.
condition - destination_number - ^(\d{10,20})$
action - set - reged_ext=username_of_extension
action - set - num_to_dial=$1
action - bridge - {absolute_codec_string='${outbound_codec_prefs}'}${regex(${sofia_contact(${reged_ext}@${domain_name})}|(^\w+/\w+)/|%1)}/sip:${num_to_dial}@${regex(${sofia_contact(${reged_ext}@${domain_name})}|(\d+.\d+.\d+.\d+:\d+.*)|%1)}
Original source (rus)
Thursday, October 12, 2017
FreeSwitch variable length
Googling does not helps, so, made own bicycle.
Quick inliner to get variable length in FreeSwitch.
In $1 assuming our variable.
dest_len=${lua(~stream:write(tostring("$1"):len()))}
Quick inliner to get variable length in FreeSwitch.
In $1 assuming our variable.
dest_len=${lua(~stream:write(tostring("$1"):len()))}
Thursday, September 28, 2017
OpenSIPS proxy with auth on external trunk
A new task has arrived.
Make OpenSIPS (actually, here can be Kamailio as well) handle registration to external service, but also provide capability to dial out from other trunks, that are not aware of any credentials of other trunk and know about only OpenSIPS as a trunk. So, in some sort it's b2b.
Very simplified scheme here. But the main issue here - on INVITE with auth headers you have to increase CSeq number by 1. Actually creating new dialog with side B. But you still have to hold old dialog with side A. And you have dynamically change on requests CSeq number.
On question, "is there any OpenSIPS-wise way of making that", got an answer
Make OpenSIPS (actually, here can be Kamailio as well) handle registration to external service, but also provide capability to dial out from other trunks, that are not aware of any credentials of other trunk and know about only OpenSIPS as a trunk. So, in some sort it's b2b.
1. A -> INVITE -> OpenSIPS B
2. A OpenSIPS -> INVITE -> B
3. A OpenSIPS <- 401(7) <- B
4. A OpenSIPS -> INVITE (auth) -> B
5. A OpenSIPS <- 200 <- B
6. A <- 200 <- OpenSIPS
Very simplified scheme here. But the main issue here - on INVITE with auth headers you have to increase CSeq number by 1. Actually creating new dialog with side B. But you still have to hold old dialog with side A. And you have dynamically change on requests CSeq number.
On question, "is there any OpenSIPS-wise way of making that", got an answer
"Right now OpenSIPs does not support increasing the cseq during UAC authentication. At the end this is a limitation of the a proxy versus a B2B :)"
So, nothing is left, only to change it by hand.
So, parts of the script would looks like
...
#------------ uac related parts
loadmodule "uac_auth.so"
modparam("uac_auth","auth_realm_avp","$avp(uac_realm)")
modparam("uac_auth","auth_username_avp","$avp(uac_username)")
modparam("uac_auth","auth_password_avp","$avp(uac_password)")
loadmodule "uac.so"
loadmodule "uac_registrant.so"
modparam("uac_registrant", "db_url", "mysql://astercc:astercc@localhost/opensips")
modparam("uac_registrant", "timer_interval", 120)
modparam("uac_registrant", "hash_size", 2)
# In DB we're storing info on external services where to register
modparam("uac_registrant", "db_url", DBURL)
...
route {
...
# Handle sequential requests part
if (has_totag()) {
if (loose_route()) {
...
} else {
...
if (is_method("ACK") && isflagset(AUTH_DONE)) {
# Process ACK's
if ($cs == $avp(original_cseq)) {
route(INCREASE_CSEQ);
}
}
...
}
}
...
# CANCEL processing
if (is_method("CANCEL")) {
# Addition of process CSeq in a case of auth call
if (isflagset(AUTH_DONE)) {
# Process CANCEL's to both sides
if ($cs == $avp(original_cseq)) {
route(INCREASE_CSEQ);
} else {
route(RESTORE_CSEQ);
}
}
...
}
...
# INVITE processing
if (is_method("INVITE")) {
...
# Here to find out call to external auth trunk
if (...) {
$avp(original_cseq) = $cs;
setflag(IS_OUTBOUND_CALL);
# Also point, here, if we already know that trunk with username and pass, provider may require specific Form and To fields
#if (...) {
# uac_replace_to("sip:$tU@$rd");
# uac_replace_from("sip:$avp(uac_username)@$rd");
#}
}
...
}
onreply_route[1] {
...
# On reply just restore original CSeq.
route(RESTORE_CSEQ);
...
}
failure_route[1] {
...
# Authentication reply on outbound call received?
if (t_check_status("40[17]") && isflagset(IS_OUTBOUND_CALL)) {
# Have we already tried to authenticate?
if (isflagset(AUTH_DONE)) {
t_reply("503","Authentication failed");
exit;
}
# Set flag of auth preformed
setflag(AUTH_DONE);
# Getting realm from responce
# Get Proxy-Auth header
if ($(<reply>hdr(Proxy-Authenticate))) {
$var(raw_auth) = $(<reply>hdr(Proxy-Authenticate));
}
# Prefer WWW-Authenticate to Proxy-Authenticate
if ($(<reply>hdr(WWW-Authenticate))) {
$var(raw_auth) = $(<reply>hdr(WWW-Authenticate));
}
$var(reg_start) = "/(.*?)realm=\"//g";
$var(reg_end) = "/\"(.*)//g";
$var(raw_auth) = $(var(raw_auth){re.subst,$var(reg_start)});
$avp(uac_realm) = $(var(raw_auth){re.subst,$var(reg_end)});
# --- Here we assume, that avp(uac_username) and avp(uac_password) are set elsewhere
if (uac_auth()) {
route(INCREASE_CSEQ);
} else {
exit;
}
route(RELAY);
}
...
}
route[RESTORE_CSEQ] {
if (isflagset(AUTH_DONE) && is_avp_set("$avp(original_cseq)")) {
remove_hf("CSeq:");
append_hf("CSeq: $avp(original_cseq) $rm\r\n", "Call-ID");
}
}
route[INCREASE_CSEQ] {
if (isflagset(AUTH_DONE) && is_avp_set("$avp(original_cseq)")) {
$var(inc_cseq) = $(avp(original_cseq){s.int}) + 1;
remove_hf("CSeq:");
append_hf("CSeq: $var(inc_cseq) $rm\r\n", "Call-ID");
}
}
Wednesday, August 9, 2017
Freeswitch HA mode based on Keepalived.
Idea to have 2 instances of Freeswitch in Master-Slave mode, if one node dies, second will automatically takeover calls with 2-3 seconds lag. Yep, media would be restored.
Main scheme is looks like this
We need 3 IP's. 2 for nodes and 1 for floating.
Solution based on this, but with a bit of additional config.
Used Debian 8x64 as a host OS.
Assume, that we have FreeSWITCH'es already installed.
Install keepalived on both nodes.
apt-get install keepalived
Create files on both nodes
/etc/keepalived/keepalived.conf
global_defs {
router_id FREESW
}
vrrp_script chk_fs {
script "/etc/keepalived/scripts/ka-status.pl"
interval 1
}
vrrp_instance VI_FREESW {
# replace with SLAVE on a slave node
state MASTER
# change to SLAVE on slave node
interface <YOUR_INTERFACE_HERE>
#interface eth0
virtual_router_id <YOUR_ROUTER_ID>
# virtual_router_id 15
# higher is preferred for master
# disable to have failover be sticky
priority 1
advert_int 1
unicast_src_ip <CURRENT_NODE_IP>
#unicast_src_ip 192.168.10.10
unicast_peer {
<SLAVE_NODE_IP>
#192.168.10.11
}
authentication {
auth_type PASS
auth_pass YourPassHere
}
notify "/etc/keepalived/scripts/ka-notify.pl"
virtual_ipaddress {
<FLOATING_IP/CIDR> dev <YOUR_INTERFACE_HERE>
# 192.168.0.15/24 dev eth0 - Example
}
track_script {
chk_fs
}
}
/etc/keepalived/scripts/ka-notify.pl
#!/usr/bin/perl
# INSTANCE|VI_FREESW|BACKUP|50
my ($what,$id,$state,$prio) = @ARGV;
open(STDOUT, "|/usr/bin/logger -t ka-notify");
print "what($what) id($id) state($state) prio($prio)\n";
if ( $state eq "MASTER" )
{
print "Instance went to master, issuing sofia recover.\n";
system("/usr/bin/fs_cli", "-x", "sofia recover");
}
/etc/keepalived/scripts/ka-status.pl
#!/usr/bin/perl
use Sys::Syslog;
openlog "ka-status", "ndelay,pid", "local0";
my @required = ("internal", "external");
my %saw = ();
open(my $in, "-|") || exec("/usr/bin/fs_cli", "-x", "sofia xmlstatus");
while ( defined(my $line = <$in>) )
{
if ( $line =~ m|<name>(.*)</name>|o )
{
$saw{$1} = 1;
}
}
close($in);
foreach my $profile ( @required )
{
if ( ! $saw{$profile} )
{
syslog(LOG_INFO, "sip profile $profile not found, marking failure");
exit(1);
}
}
exit(0);
chmod +x /etc/keepalived/scripts/*.pl
echo "net.ipv4.ip_nonlocal_bind = 1" >> /etc/sysctl.conf
Make sure, that FreeSWITCH'es uses same runtime database and uses same hostname in switch.conf Also profiles you control, need to listen on FLOATING_IP address.
Better reboot here.
service keepalived start
To check state, best is to use ip addr show. On which node FLOATING_IP address is, this is master :)
You can play with priority parameter in keepalived.conf file to make one node Master at all cases.
Point, there is a issue, when you restore calls on one node, you can't get same calls back with sofia recover.
Main scheme is looks like this
We need 3 IP's. 2 for nodes and 1 for floating.
Solution based on this, but with a bit of additional config.
Used Debian 8x64 as a host OS.
Assume, that we have FreeSWITCH'es already installed.
Install keepalived on both nodes.
apt-get install keepalived
Create files on both nodes
/etc/keepalived/keepalived.conf
global_defs {
router_id FREESW
}
vrrp_script chk_fs {
script "/etc/keepalived/scripts/ka-status.pl"
interval 1
}
vrrp_instance VI_FREESW {
# replace with SLAVE on a slave node
state MASTER
# change to SLAVE on slave node
interface <YOUR_INTERFACE_HERE>
#interface eth0
virtual_router_id <YOUR_ROUTER_ID>
# virtual_router_id 15
# higher is preferred for master
# disable to have failover be sticky
priority 1
advert_int 1
unicast_src_ip <CURRENT_NODE_IP>
#unicast_src_ip 192.168.10.10
unicast_peer {
<SLAVE_NODE_IP>
#192.168.10.11
}
authentication {
auth_type PASS
auth_pass YourPassHere
}
notify "/etc/keepalived/scripts/ka-notify.pl"
virtual_ipaddress {
<FLOATING_IP/CIDR> dev <YOUR_INTERFACE_HERE>
# 192.168.0.15/24 dev eth0 - Example
}
track_script {
chk_fs
}
}
/etc/keepalived/scripts/ka-notify.pl
#!/usr/bin/perl
# INSTANCE|VI_FREESW|BACKUP|50
my ($what,$id,$state,$prio) = @ARGV;
open(STDOUT, "|/usr/bin/logger -t ka-notify");
print "what($what) id($id) state($state) prio($prio)\n";
if ( $state eq "MASTER" )
{
print "Instance went to master, issuing sofia recover.\n";
system("/usr/bin/fs_cli", "-x", "sofia recover");
}
/etc/keepalived/scripts/ka-status.pl
#!/usr/bin/perl
use Sys::Syslog;
openlog "ka-status", "ndelay,pid", "local0";
my @required = ("internal", "external");
my %saw = ();
open(my $in, "-|") || exec("/usr/bin/fs_cli", "-x", "sofia xmlstatus");
while ( defined(my $line = <$in>) )
{
if ( $line =~ m|<name>(.*)</name>|o )
{
$saw{$1} = 1;
}
}
close($in);
foreach my $profile ( @required )
{
if ( ! $saw{$profile} )
{
syslog(LOG_INFO, "sip profile $profile not found, marking failure");
exit(1);
}
}
exit(0);
chmod +x /etc/keepalived/scripts/*.pl
echo "net.ipv4.ip_nonlocal_bind = 1" >> /etc/sysctl.conf
Make sure, that FreeSWITCH'es uses same runtime database and uses same hostname in switch.conf Also profiles you control, need to listen on FLOATING_IP address.
Better reboot here.
service keepalived start
To check state, best is to use ip addr show. On which node FLOATING_IP address is, this is master :)
You can play with priority parameter in keepalived.conf file to make one node Master at all cases.
Point, there is a issue, when you restore calls on one node, you can't get same calls back with sofia recover.
FusionPBX/FreeSWITCH save CLID on transfer
Idea is when call is received, transfer to next destination should come with callerID was received originally, not updated in moment of transfer. Sometimes it's needed for correct CRM integration or peoples just get used to it, cause it's default with blind transfer.
Actually, same ideas that described here, but for use in FusionPBX.
1. Create context save_transfer with order ~85
condition - <empty>
action - export - nolocal:execute_on_answer_1=lua number_save_on_transfer_store.lua
action - export - api_hangup_hook=lua number_save_on_transfer_db_cleanup.lua
2. Create context restore_transfer with order higher, than save_trasfer, ~80
condition - ${db(exists/number_transfer_store/${sip_from_user})} - ^true$
action - set - restored_number_on_transfer=${db(select/number_transfer_store/${sip_from_user})} - inline
action - set - effective_caller_id_number=${restored_number_on_transfer}
action - set - effective_caller_id_name=${restored_number_on_transfer}
action - db - delete/number_transfer_store/${sip_from_user}
condition - ${db(exists/number_transfer_store/${sip_from_user}_name)} - ^true$
action - set - restored_number_on_transfer_name=${db(select/number_transfer_store/${sip_from_user}_name)} - inline
action - set - effective_caller_id_name=${restored_number_on_transfer_name}
action - db - delete/number_transfer_store/${sip_from_user}_name
Lua files:
/usr/share/freeswitch/scripts/number_save_on_transfer_store.lua
-- Save number_answered / original caller_id to database
--api = freeswitch.API()
if (session:ready()) then
answered_extension = session:getVariable("dialed_user")
caller_id = session:getVariable("restored_number_on_transfer")
caller_name = session:getVariable("restored_number_on_transfer_name")
if (caller_id == nil) then
caller_id = session:getVariable("sip_from_user")
end
if (caller_name == nil) then
caller_name = session:getVariable("sip_from_display")
end
if (answered_extension ~= nil and caller_id ~= nil) then
freeswitch.consoleLog("INFO", "[NUMBER_ON_TRANSFER_SAVE] Got answered call from "..caller_id.." to "..answered_extension.."\n")
session:execute('db', 'insert/number_transfer_store/'..answered_extension..'/'..caller_id)
if (caller_name ~= nil) then
session:execute('db', 'insert/number_transfer_store/'..answered_extension..'_name/'..caller_name)
end
end
end
/usr/share/freeswitch/scripts/number_save_on_transfer_db_cleanup.lua
-- Cleanup database
api = freeswitch.API()
sip_to_user = env:getHeader("variable_last_sent_callee_id_number")
if (sip_to_user ~= nil) then
--serialized = env:serialize()
--freeswitch.consoleLog("INFO","[hangup]\n" .. serialized .. "\n")
freeswitch.consoleLog("INFO", "[DB_CLEANUP] Cleaning " .. sip_to_user .. "\n")
api:executeString('db delete/number_transfer_store/'..sip_to_user)
api:executeString('db delete/number_transfer_store/'..sip_to_user..'_name')
end
P.S.: At the end, db is can be easily replaced with hash. As we don't need persistence storage here.
Actually, same ideas that described here, but for use in FusionPBX.
1. Create context save_transfer with order ~85
condition - <empty>
action - export - nolocal:execute_on_answer_1=lua number_save_on_transfer_store.lua
action - export - api_hangup_hook=lua number_save_on_transfer_db_cleanup.lua
2. Create context restore_transfer with order higher, than save_trasfer, ~80
condition - ${db(exists/number_transfer_store/${sip_from_user})} - ^true$
action - set - restored_number_on_transfer=${db(select/number_transfer_store/${sip_from_user})} - inline
action - set - effective_caller_id_number=${restored_number_on_transfer}
action - set - effective_caller_id_name=${restored_number_on_transfer}
action - db - delete/number_transfer_store/${sip_from_user}
condition - ${db(exists/number_transfer_store/${sip_from_user}_name)} - ^true$
action - set - restored_number_on_transfer_name=${db(select/number_transfer_store/${sip_from_user}_name)} - inline
action - set - effective_caller_id_name=${restored_number_on_transfer_name}
action - db - delete/number_transfer_store/${sip_from_user}_name
Lua files:
/usr/share/freeswitch/scripts/number_save_on_transfer_store.lua
-- Save number_answered / original caller_id to database
--api = freeswitch.API()
if (session:ready()) then
answered_extension = session:getVariable("dialed_user")
caller_id = session:getVariable("restored_number_on_transfer")
caller_name = session:getVariable("restored_number_on_transfer_name")
if (caller_id == nil) then
caller_id = session:getVariable("sip_from_user")
end
if (caller_name == nil) then
caller_name = session:getVariable("sip_from_display")
end
if (answered_extension ~= nil and caller_id ~= nil) then
freeswitch.consoleLog("INFO", "[NUMBER_ON_TRANSFER_SAVE] Got answered call from "..caller_id.." to "..answered_extension.."\n")
session:execute('db', 'insert/number_transfer_store/'..answered_extension..'/'..caller_id)
if (caller_name ~= nil) then
session:execute('db', 'insert/number_transfer_store/'..answered_extension..'_name/'..caller_name)
end
end
end
/usr/share/freeswitch/scripts/number_save_on_transfer_db_cleanup.lua
-- Cleanup database
api = freeswitch.API()
sip_to_user = env:getHeader("variable_last_sent_callee_id_number")
if (sip_to_user ~= nil) then
--serialized = env:serialize()
--freeswitch.consoleLog("INFO","[hangup]\n" .. serialized .. "\n")
freeswitch.consoleLog("INFO", "[DB_CLEANUP] Cleaning " .. sip_to_user .. "\n")
api:executeString('db delete/number_transfer_store/'..sip_to_user)
api:executeString('db delete/number_transfer_store/'..sip_to_user..'_name')
end
P.S.: At the end, db is can be easily replaced with hash. As we don't need persistence storage here.
Tuesday, June 27, 2017
Freeswitch/Fusion stop/resume record on the fly
Most common solution to pause/resume recording on Freeswitch is use stop_record_session and after - record_session along with RECORD_APPEND=true.
What is bad with this solution.
1. Only WAV is supported
2. Seems it's broken for now (1.6.18)
Other option to use is
uuid_record mask/unmask
For FusionPBX it will looks like
action - bind_digit_action - local,*1,api:uuid_record,${uuid} mask ${recordings_dir}/${domain_name}/archive/${strftime(%Y)}/${strftime(%b)}/${strftime(%d)}/${uuid}.${record_ext},${bind_target}
action - bind_digit_action - local,*2,api:uuid_record,${uuid} unmask ${recordings_dir}/${domain_name}/archive/${strftime(%Y)}/${strftime(%b)}/${strftime(%d)}/${uuid}.${record_ext},${bind_target}
So, you may press *1 to stop recording and *2 to resume it. Bad in this solution - you will have pauses in conversation while record is masked.
What is bad with this solution.
1. Only WAV is supported
2. Seems it's broken for now (1.6.18)
Other option to use is
uuid_record mask/unmask
For FusionPBX it will looks like
action - bind_digit_action - local,*1,api:uuid_record,${uuid} mask ${recordings_dir}/${domain_name}/archive/${strftime(%Y)}/${strftime(%b)}/${strftime(%d)}/${uuid}.${record_ext},${bind_target}
action - bind_digit_action - local,*2,api:uuid_record,${uuid} unmask ${recordings_dir}/${domain_name}/archive/${strftime(%Y)}/${strftime(%b)}/${strftime(%d)}/${uuid}.${record_ext},${bind_target}
So, you may press *1 to stop recording and *2 to resume it. Bad in this solution - you will have pauses in conversation while record is masked.
Subscribe to:
Posts (Atom)
