Asterisk - fi, it's bad manners
Hello dear readers of this wonderful resource. By tradition, I am a longtime habr reader, but only now I decided to make a post. What, in fact, prompted you to write? Honestly, I don't know myself. Either the drawn articles about the performance of FreeSWITCH / Yate / 3CX / etc in comparison with Asterisk, or the real, real problems of the architecture of the latter, or, perhaps, the desire to do something unique.
And surprisingly, in the first case, as a rule, they compare soft and warm, so to speak, FreeSWITCH / Yate / etc and FreePBX. Yes, FreePBX. This is not a typo. Moreover, it is interesting that in all comparisons there is often one Asterisk in the default configuration. Well, you know, this configuration is loaded with all the available modules, the dialplan curve (FreePBX kind of contributes) and a bunch of other biases. As for the generic sores of Asterisk - yes, objectively, their carriage and small cart.
What to do with all this? Break stereotypes and repair birth trauma. This is what we will do.
We cross a hedgehog with a snake
Many of the newbies feel discomfort looking at the syntax for describing the dialplan in Asterisk, and some seriously justify the choice of another telephony server precisely by the need to write the dialplan in the form in which it is by default. Like, shoveling multi-line XML is the height of comfort. Yes, it is possible to use LUA / AEL, which is good. But personally, I would classify this as a disadvantage, and in particular with regard to pbx_lua.
As already mentioned, being able to describe a dialplan with a full-fledged programming language is good. The problem is that the lifetime of the script and its environment is equal to the lifetime of the channel. For each new channel, its own script instance is launched, therefore, goodbye, variables shared between channels, a single download of third-party modules, one shared connection to the database, etc., etc. Strictly speaking, this is not necessary from an embedded script description language, but I really want to. And if you want, then you have to.
So, we will take the principles of pbx_lua from the classic Asterisk, we will take the routing model from Yate, and we will not take anything from FreeSWITCH, because "overhead" is not needed. OK, we have decided on what we need to give birth to. What will we use for genetic experiments:
- Asterisk, . ARI , , 12- . , - 1.8/1.6, 1.4, .
- Lua β , . , , .
- Lunapark β github', voip-.
Lunapark . , AMI- FastAGI, . , ARI AGI AMI .
: ? Asterisk REST Interface, ! . , ARI : , , , "" , WebSockets , , , XML/JSON β . , , , . β . β - , .
? FastAGI-, , pbx_lua . Asteriskβ , FastAGI- AMI-. , FastAGI-, , , NewChannel. ARI, , stasis' ARI .
Lunapark , . "shared data". , . β , , - .
?
β ? , , . , . .
:
[test]
exten => _XXX/102,1,Hangup()
exten => _XXX,1,Dial(SIP/${EXTEN})
, 102. , , extended CallerID . , , CallerIDName , , regexp, . , , , :
[test]
exten => _XXX/102,1,Hangup()
; CallerIDName
exten => _XXX,1,ExecIf($[ "${CALLERID(name)}" == "Vasya" ]?Hangup())
;
exten => _XXX,n,ExecIf($[ "${CHANNEL(state)}" != "Ring" ]?Hangup())
;
exten => _XXX,n,ExecIf($[ "${CUT(CUT(CHANNEL,-,1),/,2)}" == "333" ]?Hangup())
exten => _XXX,n,Dial(SIP/${EXTEN})
, , Hangup', extensions.conf Goto, GoSub, Macro , , Local.
β .
:
${Exten}:match('%d%d%d')
and
(
${CallerIDNum}:match('201') or
${CallerIDName}:match('Vasya') or
${State}:lower() ~= 'ring' or
${Channel}:match('^[^/]+/([^%-]+)') == '333'
) => Hangup();
${Exten}:match('%d%d%d') => Dial {callee = ('SIP/%s'):format(${Exten})};
, , . , regexp' , , , .
, .
Lunapark pbx_lua. . ${...}
, ('...')
. .
, :
-- Exten = 123
-- Sate = Ring
-- CallerIDNum = 100
-- CallerIDName = Test
-- Channel = SIP/100-00000012c
if ('123'):match('%d%d%d') and
(
('100'):match('201') or
('Test'):match('Vasya') or
('Ring'):lower() ~= 'ring' or
('SIP/100-00000012c'):match('^[^/]+/([^%-]+)') == '333'
) then
Hangup()
end
if ('123'):match('%d%d%d') then
Dial {callee = ('SIP/%s'):format(('123'))}
end
fmt syntax :
local fmt = function(str, tab)
return (str:gsub('(%${[^}{]+})', function(w)
local mark = w:sub(3, -2)
return (mark:gsub('(.+)',function(v)
local out = tab[v] or v
return ("('%s')"):format(out)
end))
end))
end
local syntax = function(str)
return (str:gsub('([^;]+)=>([^;]+)',function(p,r)
return ([[
if %s then
%s
end
]]):format(p,r)
end))
end
, . β , . routes.
local routes = function(...)
local conf, content = ...
local f, err = io.open(conf, "r")
if io.type(f) ~= 'file' then
log.warn(err) -- LOG Lunapark'
return ""
else
content = f:read('*all')
end
f:close() return content
end
: Lunapark . β Lunapark handler'. , FastAGI- AMI .
, AMI β , AMI-, AMI . , extensions.conf.
[default]
exten => _[hit],1,NoOp()
exten => _.,n,Wait(5)
exten => _.,1,AGI(agi://127.0.0.1/${EXTEN}${IF($[ "X${PRMS}" != "X" ]?"?${PRMS}")})
Wait(5) FastAGI-, , Redirect default ${EXTEN}.
, Lunapark', FastAGI-.
-- rules
local rules = routes('routes.conf')
-- ,
-- HUP/QUIT
ami.removeEvents('*')
--
ami.addEvents {
['newchannel'] = function(e)
-- , users
if (e['Context'] and e['Context']:match('users')) and e['Exten'] then
-- , , FastAGI
local step
-- FatsAGI
local count = 0
--
local code, err = loadstring(syntax(fmt(rules,e)))
-- ,
if type(code) == 'function' then
-- FastAGI
setfenv(code,setmetatable({indexes = {}},{__index = function(t,k)
--
return coroutine.wrap(
function(...)
local prms = {} -- FastAGI
local owner = t --
local event = e -- event
local thread = coroutine.running() -- ID
-- URI
for p,v in pairs({...}) do
if type(v) == 'table' then
for key, val in pairs(v) do
table.insert(prms,("%s=%s"):format(key,val))
end
else
table.insert(prms,("%s=%s"):format(p,v))
end
end
-- FastAGI
if step then
--
local last = ("%s"):format(step)
-- UserEvent .
-- indexes( )
--
table.insert(owner['indexes'],ami.addEvent('UserEvent',function(evt)
-- AGIStatus
-- ,
if (evt['Channel'] and evt['Channel'] == event['Channel'])
and
(evt['UserEvent'] and evt['UserEvent']:match('AGIStatus'))
and
(evt['Script'] and evt['Script'] == last)
then
--
--
--
if owner['indexes'][count] == thread then
if coroutine.status(thread) ~= 'dead' then
coroutine.resume(thread)
end
end
end
end,thread))
-- FastAGI
step = k
--
coroutine.yield()
else -- FastAGI
local index -- Hangup
-- FastAGI
step = k
-- Hangup
--
index = ami.addEvent('Hangup',function(evt)
if evt['Channel'] and evt['Channel'] == event['Channel'] then
-- Hangup
ami.removeEvent('Hangup',index)
--
for _,v in pairs(owner['indexes']) do
ami.removeEvent('UserEvent',v)
end
--
owner = nil
end
end,thread)
end
-- AMI
ami.setvar{
Value = table.concat(prms,'&'),
Channel = event['Channel'],
Variable = 'PRMS'
}
-- AGI- default
ami.redirect{
Exten = k,
Priority = 1,
Channel = event['Channel'],
Context = 'default'
}
--
count = count + 1
end)
end}))()
else
-- -
log.warn(err)
end
end
end
}
, , , . , . , . , , , ..
, . β , redirect . , , FastAGI-. Lunapark UserEvent FastAGI- β . default , , PRMS.
, redirect' handler, AGI . Hangup() Dial(). .
function Hangup(...)
local app, channel = ... -- pbx_lua
app.verbose(('The Channel %s does not match by routing rules'):format(channel.get('CHANNEL')))
app.hangup()
end
function Dial(...)
local app, channel = ...
local leg = app.agi.params['callee'] or ''
app.verbose(('Trying to make a call from %s to %s'):format(
channel.get('CALLERID(num)'),
leg:match('^[^/]+/([^%-]+)'))
)
app.dial(leg)
end
, β
, . ?
- , ;
- VoIP-. Queue, , asterisk';
- , VoIP-, asterisk' Mediahub, VoIP- ;
- the ability to use a fairly simple, extensible and very flexible scripting language to create VoIP applications;
- expanded the possibilities of integration with external systems from VoIP applications.
As anyone, but I still like everything.
local fmt = function(str, tab)
return (str:gsub('(%${[^}{]+})', function(w)
local mark = w:sub(3, -2)
return (mark:gsub('(.+)',function(v)
local out = tab[v] or v
return ("('%s')"):format(out)
end))
end))
end
local syntax = function(str)
return (str:gsub('([^;]+)=>([^;]+)',function(p,r)
return ([[
if %s then
%s
end
]]):format(p,r)
end))
end
local routes = function(...)
local conf, content = ...
local f, err = io.open(conf, "r")
if io.type(f) ~= 'file' then
log.warn(err) -- LOG Lunapark'
return ""
else
content = f:read('*all')
end
f:close() return content
end
-- rules
local rules = routes('routes.conf')
-- ,
-- HUP/QUIT
ami.removeEvents('*')
--
ami.addEvents {
['newchannel'] = function(e)
-- , users
if (e['Context'] and e['Context']:match('users')) and e['Exten'] then
local step -- , , FastAGI
local count = 0 -- FatsAGI
--
local code, err = loadstring(syntax(fmt(rules,e)))
-- ,
if type(code) == 'function' then
-- FastAGI
setfenv(code,setmetatable({indexes = {}},{__index = function(t,k)
--
return coroutine.wrap(
function(...)
local prms = {} -- FastAGI
local owner = t --
local event = e -- event
local thread = coroutine.running() -- ID
-- URI
for p,v in pairs({...}) do
if type(v) == 'table' then
for key, val in pairs(v) do
table.insert(prms,("%s=%s"):format(key,val))
end
else
table.insert(prms,("%s=%s"):format(p,v))
end
end
-- FastAGI
if step then
--
local last = ("%s"):format(step)
-- UserEvent .
-- indexes( )
--
table.insert(owner['indexes'],ami.addEvent('UserEvent',function(evt)
-- AGIStatus
-- ,
if (evt['Channel'] and evt['Channel'] == event['Channel'])
and
(evt['UserEvent'] and evt['UserEvent']:match('AGIStatus'))
and
(evt['Script'] and evt['Script'] == last)
then
--
--
--
if owner['indexes'][count] == thread then
if coroutine.status(thread) ~= 'dead' then
coroutine.resume(thread)
end
end
end
end,thread))
-- FastAGI
step = k
--
coroutine.yield()
else -- FastAGI
local index -- Hangup
-- FastAGI
step = k
-- Hangup
--
index = ami.addEvent('Hangup',function(evt)
if evt['Channel'] and evt['Channel'] == event['Channel'] then
-- Hangup
ami.removeEvent('Hangup',index)
--
for _,v in pairs(owner['indexes']) do
ami.removeEvent('UserEvent',v)
end
--
owner = nil
end
end,thread)
end
-- AMI
ami.setvar{
Value = table.concat(prms,'&'),
Channel = event['Channel'],
Variable = 'PRMS'
}
-- AGI- default
ami.redirect{
Exten = k,
Priority = 1,
Channel = event['Channel'],
Context = 'default'
}
--
count = count + 1
end)
end}))()
else
-- -
log.warn(err)
end
end
end
}
function Hangup(...)
local app, channel = ... -- pbx_lua
app.verbose(('The Channel %s does not match by routing rules'):format(channel.get('CHANNEL')))
app.hangup()
end
function Dial(...)
local app, channel = ...
local leg = app.agi.params['callee'] or ''
app.verbose(('Trying to make a call from %s to %s'):format(
channel.get('CALLERID(num)'),
leg:match('^[^/]+/([^%-]+)'))
)
app.dial(leg)
end