Showing posts with label fusionpbx. Show all posts
Showing posts with label fusionpbx. Show all posts

Monday, July 6, 2020

FusionPBX - VTiger 7 Integration

Every telecom engineer should build a PBX, CallCenter, and CRM Integration.

So, my time for CRM integration came. The idea is quite simple.
  1. Calls are registering in CRM with call recording
  2. Calls are being notified with popup while using CRM
  3. Calls from CRM directly. Click 2 Call actually
Integration is done for popular opensource CRM VTiger 7 and FusionPBX. Have to say, not plain, reworked, my fork actually.

This project consists of 3 parts.
  1. PBXManager  - module for VTiger CRM, built on top of the original PBXManager module made for Asterisk.
  2. VFusion Daemon - Docker Image that is running with FusionPBX (FreeSwitch) and listening to FreeSwitch events and making requests to VTiger CRM. Made on NodeJS
  3. FusionPBX - Actually my fork of it. Version 4.4

I actually tried to avoid daemons or so, resides on FusionPBX server, but really can't avoid it totally. So, architecture is not perfect. Some of the requests are made from FreeSwitch via mod_curl, some - in v_xml_cdr app by Fusion, some - in VFusion daemon. Not clean, but as of now it's working.
So, how it looks like?

1. Call from unknown contact


2. Call from a known contact

 
3. Call details records

yes, you can listen to the recordings directly from CRM.


Click 2 Call is made quite simple. Just click on a phone number inside CRM and you will have a callback to your extension. After picking up, a call to that number would be established.

As it's intended to be a commercial product, code is open. But documentation is still not. Maybe in the future documentation also would be open.

If you have any interest in deploying this product - pls contact me at support <at> consertis.at. Or here.

Friday, March 13, 2020

Periodic signals with FusionPBX (School Bells)

A small app designed actually for schools. When you have PBX connected to your announcement system, it would be logical if PBX can handle periodic signals as well. So, the name is also obvious in this case - School Bells

Now it's a part of my FusionPBX fork and fusionpbx-apps repo as well.

The idea is quite simple - to manage signals from GUI in cron-like style. Not super friendly, but most of us know what cron is.


Limitation - it will allow you to choose only from Recordings, not like tone_stream or so.

Installation:


# cd /usr/src/
# git clone https://github.com/fusionpbx/fusionpbx-apps
# cd fusionpbx-apps/
# cp -R school_bells /var/www/fusionpbx/app/


Add

* * * * * php /var/www/fusionpbx/app/school_bells/school_bells_cron.php 

to your crontab

Go to GUI

Upgrades -> Schema; App Defaults; Menu Defaults; Permission Defaults

Log out and back in

Apps -> School Bells

Enjoy!

Saturday, February 29, 2020

SIP Monitoring based on SIP3 and Heplify

Few words upfronts.

 - What is SIP3?

Follow the link :) If you don't know why you need it (or similar system) - you don't.

 - Why SIP3 and not Homer? 
Homer 3 was really good. It actually covers all my needs in SIP traffic monitoring. Than Homer 5 with new GUI was introduced. And things slowly start getting worse. Mostly cause of really strange changes in UI, which can stop working at any moment. No, it will respond, show something, but sometimes not the real picture. The usual answer - log out and back in. With Homer 7 things went even worse. Recommendation from developers is using the latest alpha version. If set up the system with a recommended setup (set of docker containers), it's not a "set and forget" solution. No. It will stop processing traffic at some time without any warning. For monitoring system which I need not every day (actually I need mostly post-mortem traces), it always ends up with log into the system and finds out it stops working like a week ago. Not the best option.
But it's really my experience. Maybe I'm not that lucky at all.

 - Why heplify and not captain (which is a part of SIP3)?
The answer is quite simple. Heplify - single binary, captain (at the moment) - a full zoo of tools, like docker, ansible, python, etc. Yes, I'm aware, that captain also can provide RTCP stats, but overhead is too big.

So, getting to the good part.

Installation and setting up

Central server

I'm gonna use CentOS 8 (minimal). Not like I'm a big fan of it, but developers like it. So, most tested one

1. Install docker

# yum install -y yum-utils   device-mapper-persistent-data  lvm2
# yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# containerdSource=https://download.docker.com/linux/centos/7/x86_64/stable/Packages/containerd.io-1.2.6-3.3.el7.x86_64.rpm
# dnf install $containerdSource -y
# dnf install docker-ce docker-ce-cli -y
# systemctl disable firewalld
# systemctl start docker
# systemctl enable --now docker

 

2. Install ansible, pip, git


# yum install -y epel-release
# yum install -y ansible git python3-pip
# pip3 install docker-py


3. Install SIP3 backend


# cd /usr/src/
# git clone https://github.com/sip3io/sip3-ansible.git


Note on taking metrics from public network services.

/usr/src/sip3-ansible/roles/sip3-salto/templates/application.yml.j2
#! Server
server:
  uri: udp://0.0.0.0:15060


#! Management socket
management:
  uri: udp://0.0.0.0:15090

#! Metrics
metrics:
  logging:
    step: 1000
  influxdb:
    uri: http://sip3-influxdb:8086
    db: sip3
    step: 1000

#! MongoDB
mongo:
  uri: mongodb://sip3-mongodb:27017
  db: sip3
  bulk-size: 1
  collections:
    - prefix: attributes
      indexes:
        ascending: [name]
      max-collections: 7
    - prefix: sip_register_index
      indexes:
        ascending: [created_at, terminated_at, src_addr, src_host, dst_addr, dst_host, caller, callee, state]
        hashed: [call_id, x_call_id]
      max-collections: 7
    - prefix: sip_register_raw
      indexes:
        ascending: [created_at]
        hashed: [call_id]
      max-collections: 7
    - prefix: sip_call_index
      indexes:
        ascending: [created_at, terminated_at, src_addr, src_host, dst_addr, dst_host, caller, callee, state, error_code, error_type, duration, setup_time, establish_time]
        hashed: [call_id, x_call_id]
      max-collections: 7
    - prefix: sip_call_raw
      indexes:
        ascending: [created_at]
        hashed: [call_id]
      max-collections: 7
    - prefix: sip_message_index
      indexes:
        ascending: [created_at, terminated_at, src_addr, src_host, dst_addr, dst_host, caller, callee, state]
        hashed: [call_id, x_call_id]
      max-collections: 7
    - prefix: sip_message_raw
      indexes:
        ascending: [created_at]
        hashed: [call_id]
      max-collections: 7
    - prefix: sip_options_index
      indexes:
        ascending: [created_at, terminated_at, src_addr, src_host, dst_addr, dst_host, caller, callee, state]
        hashed: [call_id, x_call_id]
      max-collections: 7
    - prefix: sip_options_raw
      indexes:
        ascending: [created_at]
        hashed: [call_id]
      max-collections: 7

#! Application
sip:
  message:
    x-correlation-header: X-Call-ID
    exclusions: [MESSAGE, SUBSCRIBE, NOTIFY, OPTIONS, REGISTER]
  calls:
    aggregation-timeout: 120000

attributes:
  record-ip-addresses: false
  record-call-users: false


It's not to index in the database all IP addresses and usernames are seen on the system, cause the production system usually resides on public networks, where are tons of spam/attack calls and packets. Also retention period for database is set to 7 days, which is ok for me.

# ansible-playbook /usr/src/sip3-ansible/playbooks/trial/sip3-backend.yml

Relax and wait for system installs.

4. Add autostart


 /etc/systemd/system/sip3-backend.service

[Unit]
Description=Start Ansible Playbook SIP3
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/ansible-playbook /usr/src/sip3-ansible/playbooks/trial/sip3-backend.yml
TimeoutStartSec=10

[Install]
WantedBy=default.target




# systemctl daemon-reload
# systemctl enable sip3-backend


5. (Optional )Add hosts aliases to db

To get more friendly trances to read. Point, no spaces in names are allowed.


# docker exec -it sip3-mongodb mongo
# use sip3
# db.hosts.insert({"name" : "my_softswitch", "sip": ["X.X.X.X", "10.10.0.1"]})

Installing heplify (capture agent)

Log in to the server you want to collect SIP traffic from

# cd /usr/local/bin/
# wget https://github.com/sipcapture/heplify/releases/download/1.56/heplify
# chmod +x heplify


/etc/systemd/system/heplify.service
[Unit]
Description=Captures packets from wire and sends them to SIP3
After=network.target

[Service]
WorkingDirectory=/usr/local/bin
ExecStart=/usr/local/bin/heplify -i any -m SIP -hs SIP3_SERVER_ADDRESS:15060
ExecStop=/bin/kill ${MAINPID}
Restart=on-failure
RestartSec=10s
Type=simple

[Install]
WantedBy=multi-user.target




# systemctl daemon-reload
# systemctl enable heplify
# systemctl start heplify


System using

Log into your newly installed system on
http://SIP3_SEVER_ADDRESS with admin:admin credentials.

Sometimes, just after login, you will need to refresh your page. Actually, make sure Go! button is active
It's your search string. Simple, but yet powerful. It really looks like Wireshark one.
You can add search critera there based on sip.
or ip.
criteria
ip.src_host and ip.dst_host are hosts we added at step 5 installing Central Server paragraph.

Changing password is quite tricky, cause SIP3 relies on Grafana auth (Grafana is shipped as well) and to change password, head to

 http://SIP3_SEVER_ADDRESS/grafana

and use it interface to change password. As a bonus, you have Grafana dashboard with some useful metrics. And for sure you can add one you like. How - read Grafana manual :)

Adding batteries

One is a key feature of SIP3 is the ability to glue call legs in a 1 call flow. Usual algorithm is based on a strict match From and To numbers, but it's really rare case (at least, in my scenarios). There is a possibility to write user-defined functions, but I'll leave it to official manual. Another way to match call is to use custom SIP headers to match call legs. The default SIP3 header is X-Call-ID. So, if one leg holds Call-ID standart header matching other call X-Call-ID header, they would consider as one call and glue together.

In my case, I'm using FusionPBX, the solution is really simple. Add dialplan entry:
It will add X-Call-ID header to call equal to current Call-ID, if it's not present. Also, FreeSwitch preserves X-Headers over call legs. which is perfect for us.

So, we will have a picture like this

Upgrading

# cd /usr/src
# git pull
# ansible-playbook /usr/src/sip3-ansible/playbooks/trial/sip3-backend.yml


Happy using!

Thursday, March 14, 2019

Simple Call ACL App for FusionPBX

Made a simple Call Access Control List application for FusionPBX

Idea is to have a set of rules, that will be match on Caller and Callee number to allow or reject this call.
As an example - simple limit certain extension to call only certain numbers. Or block some callers to call exact this extension.

All is done on regex-like simple patterns.

As an example in screen below, extension 902 (or any number contains 902) can't dial any number, unless it contains 901 (extension 901 as well)


Rules are applied in order.

This app now is a part of my fork of FusionPBX, but fully compatible with vanilla Fusion version 4.4

To install it

# cd /usr/src
# git clone -b 4.4 https://github.com/samael33/fusionpbx.git fusionpbx-samael
# cp -r fusionpbx-samael/app/call_acl /var/www/fusionpbx/app/
# mkdir -p /var/www/fusionpbx/resources/install/scripts/app/custom
# cp -r fusionpbx-samael/resources/install/scripts/app/custom/call_acl /var/www/fusionpbx/resources/install/scripts/app/custom
# cp -r fusionpbx-samael/resources/install/scripts/app/app_custom.lua /var/www/fusionpbx/resources/install/scripts/app/
# cp -r fusionpbx-samael/app/dialplans/resources/switch/conf/dialplan/041_call_acl.xml /var/www/fusionpbx/app/dialplans/resources/switch/conf/dialplan

(optional)
# chown -R www-data. /var/www/fusionpbx

FusionPBX Menu -> Advanced -> Upgrade -> Schema + App Defaults + Menu Defaults + Permission Defaults

Note, by default in Dialplan call_acl by default is disabled. Done this is mainly cause you don't want to enable it  on all domains. So, enable it per domain.

For cons - it's really heavy under high load and with big number of rules, cause heavily using regular expressions which are not super fast.

Friday, August 3, 2018

Bulk Import Extensions/Devices to FusionPBX

FusionPBX is a really great software. So, community is constantly expanding it possibilities and adding new features.
On of these expands - app for bulk import extensions/devices/voicemails from CSV file.


Idea is quite simple - you upload CSV file, it's being processed and shows you a preview where you can select which field corresponding to which row (or column) of the file.

Installation is quite simple. I've made it in my fork of FusionPBX, but it's fully compatible with original one.

On your server

cd /usr/src

# If you're using 4.2 of FusionPBX - replace 4.4 with 4.2 below, 
#  but point, only 4.4 version is supported now
git clone -b 4.4 https://github.com/samael33/fusionpbx fusionpbx-samael 

mv fusionpbx-samael/app/bulk_import_extensions /var/www/fusionpbx/app
chown -R www-data:www-data /var/www/fusionpbx/app/bulk_import_extensions
rm -r fusionpbx-samael

::
Log into the FusionPBX webpage
Advanced -> Upgrade
Menu Defaults and Permission Defaults.
Log out and back in.

Monday, July 2, 2018

Phonebook app for FusionPBX

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.

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)

Wednesday, August 9, 2017

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.

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.

Tuesday, January 17, 2017

FusionPBX parking based on mod_fifo

Got an interesting task recently. To make parking slots on FusionPBX like asterisk parking. With BLF support, but without work "park+<slot>", like mod_valet_parking wants to.
Actually, googling shows this solution.

I've just modified it a bit to fit Fusion logic + make BLF more stable.
Here I use *771 is single slot parking extension.
<extension name="ParkExtension771">
   <condition field="destination_number" expression="^\*771$" break="on-false">
    <action inline="true" application="set" data="presence_id=${destination_number}@${domain_name}"/>
     <action inline="true" application="set" data="slot_count=${fifo(count ${destination_number}@${domain_name})}"/>
     <action inline="true" application="set" data="slot_count=${slot_count:-9:2}"/>
   </condition>
   <condition field="${slot_count}" expression="^\:0|no$" break="always">
     <action application="unset" data="fifo_chime_list"/>
     <action application="set" data="fifo_chime_freq=0"/>
     <action application="fifo" data="${destination_number}@${domain_name} in undef ${hold_music}"/>
     <anti-action application="fifo" data="${destination_number}@${domain_name} out nowait"/>
   </condition>
 </extension>

Actually, almost same with Confluence solution, but need add presence_id variable set and make in inline (without inline BLF is not working on transfer calls, only on direct calls)
Also add additional condition to slot_count. It's for initialize fifo slot on first time.
So, just set your BLF key to *771 (in this case) and this is your parking slot number. When it's red - you have line on it.
"Your wife is on line 2", like in old US police TV-series :)

Monday, January 25, 2016

FusionPBX(Freeswitch) HA sync gateways across servers

There was a little task to do.
Assume, we have several FusionPBX in HA (shared database incl. runtime data is on). On "master managing" server we add a gateway to external profile. To add them on other servers we have to go to each server and push rescan on external profile. Eh... To lazy for this.
We will use script, that will subscribe to CUSTOM sofia::gateway_add and gateway_delete event and run sofia profile external rescan over other servers.
We will do this over xmlrpc, so no need to add ssh keys or so.

"Slave" servers:

  • Go to Advanced -> Modules and enable XML RPC module
  • Go to Advanced -> Settings on each "slave" server and put common data for XMLRPC connection, like username, pass and port
  • Restart mod_xml_rpc on each "slave" server
"Master" server
  • Create files /usr/local/freeswitch/scripts/app/gateway_states/resources/gateway_states_add.lua and /usr/local/freeswitch/scripts/app/gateway_states/resources/gateway_states_delete.lua
(I can't get add and delete event work from a single file)
  • gateway_states_add.lua
--description
--monitor custom sofia:gateway_* event and run sofia profile external rescan over other servers
--protect xmlrpc using a firewall on the server to limit access by ip address

--iptables
-- /sbin/iptables -I INPUT -j ACCEPT -p tcp --dport 8080 -s x.x.x.x/32

--define the servers running freeswitch do not include local
servers = {}
x = 0;
servers[x] = {}
servers[x]['username'] = "freeswitch";
servers[x]['password'] = "work";
servers[x]['hostname'] = "10.0.0.2";
servers[x]['port'] = "8080";
x = x + 1;
servers[x] = {}
servers[x]['username'] = "freeswitch";
servers[x]['password'] = "work";
servers[x]['hostname'] = "10.0.0.3";
servers[x]['port'] = "8080";
--x = x + 1;
--servers[x] = {}
--servers[x]['username'] = "ccc";
--servers[x]['password'] = "xxxxx";
--servers[x]['hostname'] = "127.0.0.3";
--servers[x]['port'] = "8080";

--subscribe to the events
--events = freeswitch.EventConsumer("all");
events = freeswitch.EventConsumer("CUSTOM", "sofia::gateway_add");
        -- events = freeswitch.EventConsumer("CUSTOM", "sofia::gateway_delete");
        -- Modify line above to get gateway_states_delete.lua
--prepare the api object
api = freeswitch.API();

--get the events
for event in (function() return events:pop(1) end) do
api_command_argument = "profile%20external%20rescan";
  -- event_gateway = event:getHeader("Gateway");
  -- api_command_argument = "profile%20external%20killgw%20"..event_gateway;
  -- Modify line above to get gateway_states_delete.lua
for key,row in pairs(servers) do
url = [[http://]]..row.username..[[:]]..row.password..[[@]]..row.hostname..[[:]]..row.port..[[/webapi/sofia?]]..api_command_argument;
response = api:execute("curl", url);
freeswitch.consoleLog("INFO", "[notice] ".. url .. " "..response.."\n");
end
end



  • Modify /usr/local/freeswitch/conf/autoload_configs/lua.conf.xml

    <param name="startup-script" value="app/gateway_states/resources/gateway_states_add.lua"/>
    <param name="startup-script" value="app/gateway_states/resources/gateway_states_delete.lua"/>


  • Restart "master" server. At this point, every gateway add or delete will trigger an event, that will go across all our servers and command them pull new config from common database.

This script was modified from memcache.lua, that flushes memcaches over all Fusion servers.