685 lines
17 KiB
Lua
685 lines
17 KiB
Lua
local log = require "resources.functions.log".route_to_bridge
|
|
require "resources.functions.split"
|
|
|
|
local allows_functions = {
|
|
['user_data'] = true,
|
|
}
|
|
|
|
local pcre_match
|
|
|
|
if freeswitch then
|
|
api = api or freeswitch.API()
|
|
|
|
local function escape_regex(s)
|
|
s = string.gsub(s, '\\', '\\\\')
|
|
s = string.gsub(s, '|', '\\|')
|
|
return s
|
|
end
|
|
|
|
--! @todo find better way to extract captures
|
|
local unpack = unpack or table.unpack
|
|
function pcre_match(str, pat)
|
|
local a = escape_regex(str) .. "|/" .. escape_regex(pat) .."/"
|
|
if api:execute("regex", a) == 'false' then return end
|
|
local t = {}
|
|
for i = 1, 5 do
|
|
t[i] = api:execute("regex", a .. '|$' .. i)
|
|
end
|
|
return unpack(t)
|
|
end
|
|
else
|
|
local pcre = require "rex_pcre"
|
|
function pcre_match(str, pat)
|
|
return pcre.match(str, pat)
|
|
end
|
|
end
|
|
|
|
local function pcre_self_test()
|
|
io.write('Test regex ')
|
|
local a, b, c
|
|
a,b,c = pcre_match('abcd', '(\\d{3})(\\d{3})')
|
|
assert(a == nil)
|
|
|
|
a,b,c = pcre_match('123456', '(\\d{3})(\\d{3})')
|
|
assert(a == '123', a)
|
|
assert(b == '456', b)
|
|
|
|
a,b,c = pcre_match('999', '(888|999)')
|
|
assert(a == '999', a)
|
|
|
|
a,b,c = pcre_match('888|999', '(888\\|999)')
|
|
assert(a == '888|999', a)
|
|
|
|
io.write(' - ok\n')
|
|
end
|
|
|
|
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)
|
|
t[#t + 1] = v
|
|
return t
|
|
end
|
|
|
|
local function order_keys(t)
|
|
local o = {}
|
|
for k in pairs(t) do append(o, k) end
|
|
table.sort(o)
|
|
local i = 0
|
|
return function(o)
|
|
i = i + 1
|
|
return o[i], t[o[i]]
|
|
end, o
|
|
end
|
|
|
|
local function check_conditions(group, fields)
|
|
local matches, pass, last, break_on
|
|
|
|
for n, condition in ipairs(group.conditions) do
|
|
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 unknown fields
|
|
log.errf('Unsupported condition: %s', condition.type)
|
|
matches, pass = {}, false
|
|
else
|
|
if condition.type == '' then
|
|
matches, pass = {}, true
|
|
else
|
|
matches = {pcre_match(value, condition.data)}
|
|
pass = #matches > 0
|
|
end
|
|
log.debugf('%s condition %s(%s) to `%s`', pass and 'PASS' or 'FAIL', condition.type, condition.data, value or '<NONE>')
|
|
end
|
|
|
|
break_on = condition.break_on
|
|
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 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 should break
|
|
return act, not not break_on, matches
|
|
end
|
|
|
|
local function apply_match(s, match)
|
|
return string.gsub(s, "%$(%d)", function(i)
|
|
return match[tonumber(i)] or ''
|
|
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_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
|
|
|
|
if action.type == 'bridge' then
|
|
value = apply_match(value, matches)
|
|
value = apply_var(value, fields)
|
|
actions.bridge = apply_match(value, matches)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
return do_break
|
|
end
|
|
|
|
local function extension_to_bridge(extension, actions, fields)
|
|
for _, group in order_keys(extension) do
|
|
local do_break = group_to_bridge(actions, group, fields)
|
|
if do_break then break end
|
|
end
|
|
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 = {
|
|
{
|
|
{conditions={
|
|
{type='destination_number', data='100', break_on=''};
|
|
}};
|
|
{destination_number = 100};
|
|
{'action', false};
|
|
};
|
|
|
|
{
|
|
{conditions={
|
|
{type='destination_number', data='100', break_on='on-true'};
|
|
}};
|
|
{destination_number = 100};
|
|
{'action', true};
|
|
};
|
|
|
|
{
|
|
{conditions={
|
|
{type='destination_number', data='101', break_on=''};
|
|
}};
|
|
{destination_number = 100};
|
|
{'anti-action', true};
|
|
};
|
|
|
|
{
|
|
{conditions={
|
|
{type='destination_number', data='100', break_on=''};
|
|
{type='destination_number', data='101', break_on=''};
|
|
{type='destination_number', data='102', break_on=''};
|
|
}};
|
|
{destination_number = 100};
|
|
{nil, true};
|
|
};
|
|
|
|
{
|
|
{conditions={
|
|
{type='destination_number', data='100', break_on='never'};
|
|
{type='destination_number', data='101', break_on='never'};
|
|
{type='destination_number', data='102', break_on='never'};
|
|
}};
|
|
{destination_number = 102};
|
|
{'action', false};
|
|
};
|
|
|
|
{
|
|
{conditions={
|
|
{type='destination_number', data='100', break_on='never'};
|
|
{type='destination_number', data='101', break_on='never'};
|
|
{type='destination_number', data='102', break_on='never'};
|
|
}};
|
|
{destination_number = 103};
|
|
{'anti-action', false};
|
|
};
|
|
|
|
{
|
|
{conditions={
|
|
{type='destination_number', data='100', break_on=''};
|
|
{type='destination_number', data='101', break_on=''};
|
|
{type='destination_number', data='102', break_on=''};
|
|
}};
|
|
{destination_number = 102};
|
|
{nil, true};
|
|
};
|
|
|
|
{
|
|
{conditions={
|
|
{type='', data='', break_on=''};
|
|
}};
|
|
{};
|
|
{'action', false};
|
|
};
|
|
|
|
{
|
|
{conditions={
|
|
{type='caller_id_number', data='123456', break_on=''};
|
|
}};
|
|
{};
|
|
{'anti-action', true};
|
|
};
|
|
|
|
}
|
|
|
|
for i, test in ipairs(test_conditions) do
|
|
io.write('Test conditions #' .. i)
|
|
local group, fields, result = test[1], test[2], test[3]
|
|
local action, do_break, matches = check_conditions(group, fields)
|
|
assert(action == result[1], tostring(action))
|
|
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
|
|
|
|
-- 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 hostname = fields.hostname
|
|
if not hostname then
|
|
require "resources.functions.trim";
|
|
hostname = trim(api:execute("switchname", ""))
|
|
end
|
|
|
|
-- try filter by context
|
|
local context = fields.context
|
|
if context == '' then context = nil end
|
|
|
|
--connect to the database
|
|
if (dbh == nil) then
|
|
local Database = require "resources.functions.database";
|
|
dbh = Database.new('system');
|
|
end
|
|
|
|
local current_dialplan_uuid, extension
|
|
dbh:query(select_outbound_dialplan_sql, {domain_uuid=domain_uuid, hostname=hostname}, function(route)
|
|
if (route.dialplan_context ~= '${domain_name}') and (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)
|
|
return {query = function(self, sql, params, callback)
|
|
local i = 0
|
|
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 (route.dialplan_context ~= '${domain_name}') and (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})
|