Add. Use route_to_bridge module to build routes for ring groups. (#2907)

* Add. Use `route_to_bridge` module to build routes fro ring groups.

This commit has several improvements
1. Select only needed fields. (do not select quite big XML text strings)
2. Filter routes also by context name
3. Filter dialplans also by hostname
4. Handle conditions based not only `destination_number`
5. Handle `break` and `continue` attributes for extensions
6. Escape vars inside dial-string
7. Add log messages similar as FS dialplan do

* Add. `route_to_bridge` set inline vars so it possible use then in next conditions.
Add. `route_to_bridge` can execute basic api commands from allowed lists.
`route_to_bridge` expand all known vars. If var is unknown then it pass as is.
Fix. `export nolocal:` action.

* Fix. Short variable names

* Add. some comments

* Fix. Do not try execute empty string

This produce error messages `[ERR] switch_cpp.cpp:759 No application specified`

* Fix. Export nolocal values.
This commit is contained in:
Alexey Melnichuk
2017-11-17 17:15:47 +03:00
committed by FusionPBX
parent 0ccbad7d53
commit b0422af3e2
2 changed files with 515 additions and 163 deletions

View File

@@ -30,6 +30,9 @@
--include the log
local log = require "resources.functions.log".ring_group
-- include libs
local route_to_bridge = require "resources.functions.route_to_bridge"
--connect to the database
local Database = require "resources.functions.database";
dbh = Database.new('system');
@@ -118,6 +121,7 @@
uuid = session:getVariable("uuid");
context = session:getVariable("context");
call_direction = session:getVariable("call_direction");
accountcode = session:getVariable("accountcode");
end
--default to local if nil
@@ -170,6 +174,9 @@
-- error();
--end
--get current switchname
hostname = trim(api:execute("switchname", ""))
--get the ring group
ring_group_forward_enabled = "";
ring_group_forward_destination = "";
@@ -187,7 +194,7 @@
missed_call_app = row["ring_group_missed_call_app"];
missed_call_data = row["ring_group_missed_call_data"];
end);
--get the ring group user
sql = "SELECT r.*, u.user_uuid FROM v_ring_groups as r, v_ring_group_users as u ";
sql = sql .. "where r.ring_group_uuid = :ring_group_uuid ";
@@ -304,7 +311,7 @@
--process the ring group
if (ring_group_forward_enabled == "true" and string.len(ring_group_forward_destination) > 0) then
--forward the ring group
session:setVariable("toll_allow",ring_group_forward_toll_allow);
session:setVariable("toll_allow",ring_group_forward_toll_allow);
session:execute("transfer", ring_group_forward_destination.." XML "..context);
else
--get the strategy of the ring group, if random, we use random() to order the destinations
@@ -422,33 +429,9 @@
--get the dialplan data and save it to a table
if (external) then
sql = [[select * from v_dialplans as d, v_dialplan_details as s
where (d.domain_uuid = :domain_uuid or d.domain_uuid is null)
and d.app_uuid = '8c914ec3-9fc0-8ab5-4cda-6c9288bdc9a3'
and d.dialplan_enabled = 'true'
and d.dialplan_uuid = s.dialplan_uuid
order by
d.dialplan_order asc,
d.dialplan_name asc,
d.dialplan_uuid asc,
s.dialplan_detail_group asc,
CASE s.dialplan_detail_tag
WHEN 'condition' THEN 1
WHEN 'action' THEN 2
WHEN 'anti-action' THEN 3
ELSE 100 END,
s.dialplan_detail_order asc
]];
params = {domain_uuid = domain_uuid};
if debug["sql"] then
freeswitch.consoleLog("notice", "[ring group] SQL:" .. sql .. "; params:" .. json.encode(params) .. "\n");
end
dialplans = {};
x = 1;
assert(dbh:query(sql, params, function(row)
dialplans[x] = row;
x = x + 1;
end));
dialplans = route_to_bridge.preload_dialplan(
dbh, domain_uuid, {hostname = hostname, context = context}
)
end
--process the destinations
@@ -580,77 +563,65 @@
extension_uuid = trim(api:executeString(cmd));
--send to user
local dial_string_to_user = "[sip_invite_domain="..domain_name..",call_direction="..call_direction..","..group_confirm.."leg_timeout="..destination_timeout..","..delay_name.."="..destination_delay..",dialed_extension=" .. row.destination_number .. ",extension_uuid="..extension_uuid .. row.record_session .. "]user/" .. row.destination_number .. "@" .. domain_name;
dial_string = dial_string_to_user;
dial_string = dial_string_to_user;
elseif (tonumber(destination_number) == nil) then
--sip uri
dial_string = "[sip_invite_domain="..domain_name..",call_direction="..call_direction..","..group_confirm.."leg_timeout="..destination_timeout..","..delay_name.."="..destination_delay.."]" .. row.destination_number;
else
--external number
y = 0;
dial_string = '';
previous_dialplan_uuid = '';
regex_match = false;
for k, r in pairs(dialplans) do
if (y > 0) then
if (previous_dialplan_uuid ~= r.dialplan_uuid) then
regex_match = false;
bridge_match = false;
square = square .. "]";
y = 0;
dial_string = nil
local session_mt = {__index = function(_, k) return session:getVariable(k) end}
local params = setmetatable({
__api__ = api,
destination_number = destination_number,
user_exists = 'false',
call_direction = 'outbound',
domain_name = domain_name,
domain_uuid = domain_uuid,
destination_timeout = destination_timeout,
destination_delay = destination_delay,
}, session_mt)
local confirm = string.gsub(group_confirm, ',$', '') -- remove `,` from end of string
local route = route_to_bridge.apply_vars({ -- predefined actions
"domain_name=${domain_name}",
"domain_uuid=${domain_uuid}",
"sip_invite_domain=${domain_name}",
"leg_timeout=${destination_timeout}",
delay_name .. "=${destination_delay}",
"ignore_early_media=true",
confirm,
}, params)
route = route_to_bridge(dialplans, domain_uuid, params, route)
if route and route.bridge then
local remove_actions = {
["effective_caller_id_name="] = true;
["effective_caller_id_number="] = true;
['sip_h_X-accountcode='] = true;
}
-- cleanup variables
local i = 1 while i < #route do
-- remove vars from prev variant
if remove_actions[ route[i] ] then
table.remove(route, i)
i = i - 1
-- remove vars with unresolved vars
elseif string.find(route[i], '%${.+}') then
table.remove(route, i)
i = i - 1
-- remove vars with empty values
elseif string.find(route[i], '=$') then
table.remove(route, i)
i = i - 1
end
i = i + 1
end
if (r.dialplan_detail_tag == "condition") then
if (r.dialplan_detail_type == "destination_number") then
dial_string = "regex m:~"..destination_number.."~"..r.dialplan_detail_data
if (api:execute("regex", "m:~"..destination_number.."~"..r.dialplan_detail_data) == "true") then
--get the regex result
destination_result = trim(api:execute("regex", "m:~"..destination_number.."~"..r.dialplan_detail_data.."~$1"));
--set match equal to true
regex_match = true;
end
end
end
--regex_match = true;
--dial_string = r.dialplan_detail_data;
if (r.dialplan_detail_tag == "action") then
if (regex_match) then
--dial_string = 'match';
--replace $1
dialplan_detail_data = r.dialplan_detail_data:gsub("$1", destination_result);
--if the session is set then process the actions
if (y == 0) then
square = "[domain_name="..domain_name..",domain_uuid="..domain_uuid..",sip_invite_domain="..domain_name..",call_direction=outbound,"..group_confirm.."leg_timeout="..destination_timeout..","..delay_name.."="..destination_delay..",ignore_early_media=true,";
end
if (r.dialplan_detail_type == "set") then
--session:execute("eval", dialplan_detail_data);
if (dialplan_detail_data == "sip_h_X-accountcode=${accountcode}") then
if (session) then
accountcode = session:getVariable("accountcode");
if (accountcode) then
square = square .. "sip_h_X-accountcode="..accountcode..",";
end
end
elseif (dialplan_detail_data == "effective_caller_id_name=${outbound_caller_id_name}") then
elseif (dialplan_detail_data == "effective_caller_id_number=${outbound_caller_id_number}") then
else
square = square .. dialplan_detail_data..",";
end
elseif (r.dialplan_detail_type == "bridge") then
if (bridge_match) then
dial_string = dial_string .. delimiter .. square .."]"..dialplan_detail_data;
square = "[";
else
dial_string = square .."]"..dialplan_detail_data;
end
bridge_match = true;
break;
end
--increment the value
y = y + 1;
end
end
previous_dialplan_uuid = r.dialplan_uuid;
dial_string = '[' .. table.concat(route, ',') .. ']' .. route.bridge
end
end
@@ -776,12 +747,16 @@
or session:getVariable("originate_disposition") == "failure"
) then
--execute the time out action
session:execute(ring_group_timeout_app, ring_group_timeout_data);
if ring_group_timeout_app and #ring_group_timeout_app > 0 then
session:execute(ring_group_timeout_app, ring_group_timeout_data);
end
end
else
if (ring_group_timeout_app ~= nil) then
--execute the time out action
session:execute(ring_group_timeout_app, ring_group_timeout_data);
if ring_group_timeout_app and #ring_group_timeout_app > 0 then
session:execute(ring_group_timeout_app, ring_group_timeout_data);
end
else
local sql = "SELECT ring_group_timeout_app, ring_group_timeout_data FROM v_ring_groups ";
sql = sql .. "where ring_group_uuid = :ring_group_uuid";
@@ -791,7 +766,9 @@
end
dbh:query(sql, params, function(row)
--execute the time out action
session:execute(row.ring_group_timeout_app, row.ring_group_timeout_data);
if row.ring_group_timeout_app and #row.ring_group_timeout_app > 0 then
session:execute(row.ring_group_timeout_app, row.ring_group_timeout_data);
end
end);
end
end

View File

@@ -1,4 +1,9 @@
local log = require "resources.functions.log".route_to_bridge
require "resources.functions.split"
local allows_functions = {
['user_data'] = true,
}
local pcre_match
@@ -48,20 +53,35 @@ local function pcre_self_test()
io.write(' - ok\n')
end
local select_routes_sql = [[
select *
from v_dialplans
where (domain_uuid = :domain_uuid or domain_uuid is null)
and (hostname = :hostname or hostname is null)
and app_uuid = '8c914ec3-9fc0-8ab5-4cda-6c9288bdc9a3'
and dialplan_enabled = 'true'
order by dialplan_order asc
]]
local select_extensions_sql = [[
select * from v_dialplan_details
where dialplan_uuid = :dialplan_uuid
order by dialplan_detail_group asc, dialplan_detail_order asc
local select_outbound_dialplan_sql = [[
SELECT
d.dialplan_uuid,
d.dialplan_context,
d.dialplan_continue,
s.dialplan_detail_group,
s.dialplan_detail_break,
s.dialplan_detail_data,
s.dialplan_detail_inline,
s.dialplan_detail_tag,
s.dialplan_detail_type
FROM v_dialplans as d, v_dialplan_details as s
WHERE (d.domain_uuid = :domain_uuid OR d.domain_uuid IS NULL)
AND (d.hostname = :hostname OR d.hostname IS NULL)
AND d.app_uuid = '8c914ec3-9fc0-8ab5-4cda-6c9288bdc9a3'
AND d.dialplan_enabled = 'true'
AND d.dialplan_uuid = s.dialplan_uuid
ORDER BY
d.dialplan_order ASC,
d.dialplan_name ASC,
d.dialplan_uuid ASC,
s.dialplan_detail_group ASC,
CASE s.dialplan_detail_tag
WHEN 'condition' THEN 1
WHEN 'action' THEN 2
WHEN 'anti-action' THEN 3
ELSE 100
END,
s.dialplan_detail_order ASC
]]
local function append(t, v)
@@ -87,6 +107,11 @@ local function check_conditions(group, fields)
last = (n == #group.conditions)
local value = fields[condition.type]
if (not value) and (condition_type ~= '') then -- try var name
local condition_type = string.match(condition.type, '^%${(.*)}$')
if condition_type then value = fields[condition_type] end
end
if (not value) and (condition.type ~= '') then -- skip unkonw fields
log.errf('Unsupportded condition: %s', condition.type)
matches, pass = {}, false
@@ -101,20 +126,20 @@ local function check_conditions(group, fields)
end
break_on = condition.break_on
if break_on == 'always' then break end
if break_on ~= 'never' then
if pass and break_on == 'on-true' then break end
if not pass and (break_on == 'on-false' or break_on == '') then break end
if break_on == 'always' then break
elseif break_on ~= 'never' then
if pass then if break_on == 'on-true' then break end
elseif break_on == 'on-false' or break_on == '' then break end
end
break_on = nil
end
-- we shuld execute action/anti-action only if we check ALL conditions
-- we should execute action/anti-action only if we check ALL conditions
local act
if last then act = pass and 'action' or 'anti-action' end
-- we shuld break
-- we should break
return act, not not break_on, matches
end
@@ -124,23 +149,67 @@ local function apply_match(s, match)
end)
end
local function apply_var(s, fields)
local str = string.gsub(s, "%$?%${([^$%(%){}= ]-)}", function(var)
return fields[var]
end)
if fields.__api__ then
local api = fields.__api__
-- try call functions like ('set result=${user_data(args)}')
str = string.gsub(str, "%${([^$%(%){}= ]+)%s*%((.-)%)%s*}", function(fn, par)
if allows_functions[fn] then
return api:execute(fn, par) or ''
end
log.warningf('try call not allowed function %s', tostring(fn))
end)
-- try call functions like 'set result=${user_data args}'
str = string.gsub(str, "%${([^$%(%){}= ]+)%s+(%S.-)%s*}", function(fn, par)
if allows_functions[fn] then
return api:execute(fn, par) or ''
end
log.warningf('try call not allowed function %s', tostring(fn))
end)
end
if string.find(str, '%${.+}') then
log.warningf('can not resolve vars inside `%s`', tostring(str))
end
return str
end
local function group_to_bridge(actions, group, fields)
local action, do_break, matches = check_conditions(group, fields)
if action then
local t = (action == 'action') and group.actions or group.anti_actions
for _, element in ipairs(t) do
local value = element.data
if element.type == 'export' and string.sub(value, 1, 8) == 'nolocal:' then
value = string.sub(value, 9)
local action_type, do_break, matches = check_conditions(group, fields)
if action_type then
local t = (action_type == 'action') and group.actions or group.anti_actions
for _, action in ipairs(t) do
local value = action.data
-- we only support set/export actions
if action.type == 'export' or action.type == 'set' then
local key
key, value = split_first(value, '=', true)
if key then
local bleg_only = (action.type == 'export') and (string.sub(key, 1, 8) == 'nolocal:')
if bleg_only then key = string.sub(key, 9) end
value = apply_match(value, matches)
value = apply_var(value, fields)
if action.inline and not bleg_only then
fields[key] = value
end
--! @todo do value escape?
append(actions, key .. '=' .. value)
end
end
-- we only support action/export
if element.type == 'export' or element.type == 'set' then
if action.type == 'bridge' then
value = apply_match(value, matches)
append(actions, value)
end
if element.type == 'bridge' then
value = apply_var(value, fields)
actions.bridge = apply_match(value, matches)
break
end
@@ -158,6 +227,69 @@ local function extension_to_bridge(extension, actions, fields)
end
local function self_test()
local unpack = unpack or table.unpack
local function assert_equal(expected, actions)
for i = 1, math.max(#expected, #actions) do
local e, v, msg = expected[i], actions[i]
if not e then
msg = string.format("unexpected value #%d - `%s`", i, v)
elseif not v then
msg = string.format("expected value `%s` at position #%d, but got no value", e, i)
elseif e ~= v then
msg = string.format("expected value `%s` at position #%d but got: `%s`", e, i, v)
end
assert(not msg, msg)
end
for name, e in pairs(expected) do
local v, msg = actions[name]
if not v then
msg = string.format("%s expected as `%s`, but got no value", name, e)
elseif e ~= v then
msg = string.format("expected value for %s is `%s`, but got: `%s`", name, e, v)
end
assert(not msg, msg)
end
for name, v in pairs(actions) do
local e, msg = expected[name]
if not e then
msg = string.format("expected value %s = `%s`", name, v)
end
assert(not msg, msg)
end
end
local function test_grout_to_bridge(group, params, ret, expected)
local actions = {}
local result = group_to_bridge(actions, group, params)
if result ~= ret then
local msg = string.format('expected `%s` but got `%s`', tostring(ret), tostring(result))
assert(false, msg)
end
assert_equal(expected, actions)
end
-- mock for API
local function API(t)
local api = {
execute = function(self, cmd, args)
cmd = assert(t[cmd])
return cmd[args]
end;
}
return api
end
local old_log = log
log = {
errf = function() end;
warningf = function() end;
debugf = function() end;
}
pcre_self_test()
local test_conditions = {
@@ -251,53 +383,296 @@ local function self_test()
assert(do_break == result[2])
io.write(' - ok\n')
end
local test_actions = {
{ -- should not touch unknown vars
{actions={
{type='set', data='a=${b}'}
};
conditions={{type='', data='', break_on='on-true'}};
},
{ -- parameters
},
{ -- result
'a=${b}'
}
},
{ -- should call execute command with braces
{actions={
{type='set', data='a=${user_data(a b c)}'}
};
conditions={{type='', data='', break_on='on-true'}};
},
{ -- parameters
__api__ = API{user_data={['a b c'] = 'value'}}
},
{ -- result
'a=value'
}
},
{ -- should call execute command with spaces
{actions={
{type='set', data='a=${user_data a b c }'}
};
conditions={{type='', data='', break_on='on-true'}};
},
{ -- parameters
__api__ = API{user_data={['a b c'] = 'value'}}
},
{ -- result
'a=value'
}
},
{ -- should not call not allowed function
{actions={
{type='set', data='a=${user_exists( a b c )}'}
};
conditions={{type='', data='', break_on='on-true'}};
},
{ -- parameters
__api__ = API{user_data={['a b c'] = 'value'}}
},
{ -- result
'a=${user_exists( a b c )}'
}
},
{ -- should set inline vars
{actions={
{type='set', data='a=hello', inline=true},
{type='set', data='b=${a}'},
};
conditions={{type='', data='', break_on='on-true'}};
},
{ -- parameters
__api__ = API{user_data={['a b c'] = 'value'}}
},
{ -- result
'a=hello',
'b=hello',
}
},
{ -- should not set not inline vars
{actions={
{type='set', data='a=hello'},
{type='set', data='b=${a}'},
};
conditions={{type='', data='', break_on='on-true'}};
},
{ -- parameters
__api__ = API{user_data={['a b c'] = 'value'}}
},
{ -- result
'a=hello',
'b=${a}',
}
},
{ -- should expand vars inside call
{actions={
{type='set', data='a=${user_data(${a}${b})}'},
};
conditions={{type='', data='', break_on='on-true'}};
},
{ -- parameters
__api__ = API{user_data={['helloworld'] = 'value'}},
a = 'hello',
b = 'world',
},
{ -- result
'a=value',
}
},
{ -- should export nolocal
{actions={
{type='export', data='a=nolocal:value', inline=true},
{type='export', data='b=${a}'},
};
conditions={{type='', data='', break_on='on-true'}};
},
{ -- parameters
},
{ -- result
'a=value',
'b=${a}',
}
},
{ -- should handle bridge as last action
{actions={
{type='bridge', data='sofia/gateway/${a}'},
{type='set', data='a=123', inline=true},
};
conditions={{type='', data='', break_on='on-true'}};
},
{ -- parameters
a='gw'
},
{ -- result
bridge = 'sofia/gateway/gw'
}
},
{ -- should ingnore `nolocal` for set
{actions={
{type='set', data='a=nolocal:123', inline=true},
{type='export', data='b=${a}'},
};
conditions={{type='', data='', break_on='on-true'}};
},
{ -- parameters
},
{ -- result
'a=nolocal:123';
'b=nolocal:123';
}
},
{ -- should ingnore unsupportded actions
{actions={
{type='ring_ready', data=''},
{type='answer', data=''},
};
conditions={{type='', data='', break_on='on-true'}};
},
{ -- parameters
},
{ -- result
}
},
}
for i, test_case in ipairs(test_actions) do
local group, params, expected = unpack(test_case)
io.write('Test execute #' .. i)
test_grout_to_bridge(group, params, true, expected)
io.write(' - ok\n')
end
log = old_log
end
local function outbound_route_to_bridge(dbh, domain_uuid, fields)
local actions, dial_string = {}
require "resources.functions.trim";
local hostname = trim(api:execute("switchname", ""));
-- Returns array of set/export actions and bridge command.
--
-- This function does not set any var to session.
--
-- @param dbh database connection
-- @param domain_uuid
-- @param fields list of avaliable channel variables.
-- if `context` provided then dialplan will be filtered by this var
-- `__api__` key can be used to pass freeswitch.API object for execute
-- some functions in actions (e.g. `s=${user_data ...}`)
-- @param actions optional list of predefined actions
-- @return array part of table will contain list of actions.
-- `bridge` key will contain bridge statement
local function outbound_route_to_bridge(dbh, domain_uuid, fields, actions)
actions = actions or {}
local params = {}
dbh:query(select_routes_sql, {domain_uuid=domain_uuid,hostname=hostname}, function(route)
local extension = {}
params.dialplan_uuid = route.dialplan_uuid
dbh:query(select_extensions_sql, params, function(ext)
local group_no = tonumber(ext.dialplan_detail_group)
local tag = ext.dialplan_detail_tag
local element = {
type = ext.dialplan_detail_type;
data = ext.dialplan_detail_data;
break_on = ext.dialplan_detail_break;
inline = ext.dialplan_detail_inline;
}
local hostname = fields.hostname
if not hostname then
require "resources.functions.trim";
hostname = trim(api:execute("switchname", ""))
end
local group = extension[ group_no ] or {
conditions = {};
actions = {};
anti_actions = {};
}
extension[ group_no ] = group
-- try filter by context
local context = fields.context
if context == '' then context = nil end
if tag == 'condition' then append(group.conditions, element) end
if tag == 'action' then append(group.actions, element) end
if tag == 'anti-action' then append(group.anti_actions, element) end
end)
local n = #actions
extension_to_bridge(extension, actions, fields)
if actions.bridge or (n > #actions and route.dialplan_continue == 'false') then
return 1
local current_dialplan_uuid, extension
dbh:query(select_outbound_dialplan_sql, {domain_uuid=domain_uuid, hostname=hostname}, function(route)
if context and context ~= route.dialplan_context then
-- skip dialplan for wrong contexts
return
end
if current_dialplan_uuid ~= route.dialplan_uuid then
if extension then
local n = #actions
extension_to_bridge(extension, actions, fields)
-- if we found bridge or add any action and there no continue flag
if actions.bridge or (n > #actions and route.dialplan_continue == 'false') then
extension = nil
return 1
end
end
extension = {}
current_dialplan_uuid = route.dialplan_uuid
end
local group_no = tonumber(route.dialplan_detail_group)
local tag = route.dialplan_detail_tag
local element = {
type = route.dialplan_detail_type;
data = route.dialplan_detail_data;
break_on = route.dialplan_detail_break;
inline = route.dialplan_detail_inline;
}
local group = extension[ group_no ] or {
conditions = {};
actions = {};
anti_actions = {};
}
extension[ group_no ] = group
if tag == 'condition' then append(group.conditions, element) end
if tag == 'action' then append(group.actions, element) end
if tag == 'anti-action' then append(group.anti_actions, element) end
end)
if extension and next(extension) then
extension_to_bridge(extension, actions, fields)
end
if actions.bridge then return actions end
end
local function apply_vars(actions, fields)
for i, action in ipairs(actions) do
actions[i] = apply_var(action, fields)
end
return actions
end
local function wrap_dbh(t)
local i = 0
return {query = function(self, sql, params, callback)
while true do
i = i + 1
local row = t[i]
if not row then break end
local result = callback(row)
if result == 1 then break end
end
end}
end
-- Load all extension for outbound routes and
-- returns object which can be used instead real DBH object to build
-- dialplan for specific destination_number
local function preload_dialplan(dbh, domain_uuid, fields)
local hostname = fields and fields.hostname
if not hostname then
require "resources.functions.trim";
hostname = trim(api:execute("switchname", ""))
end
-- try filter by context
local context = fields and fields.context
if context == '' then context = nil end
local dialplan = {}
dbh:query(select_outbound_dialplan_sql, {domain_uuid=domain_uuid, hostname=hostname}, function(route)
if context and context ~= route.dialplan_context then
-- skip dialplan for wrong contexts
return
end
dialplan[#dialplan + 1] = route
end)
return wrap_dbh(dialplan), dialplan
end
return setmetatable({
__self_test = self_test;
apply_vars = apply_vars;
preload_dialplan = preload_dialplan;
}, {__call = function(_, ...)
return outbound_route_to_bridge(...)
end})