Skip to content

Instantly share code, notes, and snippets.

@lgaetz
Last active July 28, 2022 16:25
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lgaetz/8633920 to your computer and use it in GitHub Desktop.
Save lgaetz/8633920 to your computer and use it in GitHub Desktop.
Asterisk AGI file for a FreePBX system that examines outbound dialed digits against inbound DIDs specified in inbound routes. With Asterisk dial plan, it can be used to redirect outbound calls back in for local DIDs.
#!/usr/bin/php -q
<?php
/*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
*
* Latest Version: https://gist.github.com/lgaetz/8633920
*
* Description:
*
* Script lgaetz-didloopback.php is used on a FreePBX server, with custom dialplan that calls
* this AGI on oubound calls. If the outbound dialed digits match an existing CID on an
* inbound route, the call is redirected back to the inbound route. Works similarly to
* a loopback route/trunk, but with this method there are no dial patterns to maintain.
*
* Save a copy in the Asterisk agi-bin folder, probably /var/lib/asterisk/agi-bin and ensure
* it's owned by the asterisk user and make exectuable.
*
* Usage:
*
* [macro-dialout-trunk-predial-hook]
* exten => s,1,Noop(Entering user defined context macro-dialout-trunk-predial-hook in extensions_custom.conf)
* exten => s,n,agi(lgaetz-didloopback.php,${DIAL_NUMBER},verbose)
* exten => s,n,GotoIf($["${didloop}" != ""]?redirect)
* exten => s,n,MacroExit
* exten => s,n(redirect),goto(from-trunk,${DIAL_NUMBER},1)
*
* License:
* Released as GNU GPL/2
*
* Version Info:
* 2014-01-26 A bit rough but working
* 2014-01-29 Verbose output option added for debugging
* 2014-01-30 Added suppport for Asterisk dial pattern matching
* 2020-03-29 Update notes
*
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***/
// *** *** *** *** USER CONFIG OPTIONS *** *** *** *** *** *** *** *** *** *** *** ***
/* The $did_patterns variable can be used to appy Asterisk style dial patterns to
* system DIDs. These rules are optional, and if no matching dial pattern is
* provided, the script proceeds with unmodified number(s). Separate multiple dial patterns with
* commas.
* example: Rules to convert 7 and 11 digit numbers to standard NAPA 10 digit format might look like:
* $did_patterns = "902+NXXXXXX,1|NXXNXXXXXX";
*/
$did_patterns = "";
/* The outbound patterns variable works identically to did patterns, except the rules will
* be applied to the digits dialled
*/
$outbound_patterns = "";
//*** *** *** *** END USER CONFIG OPTIONS *** *** *** *** *** *** *** *** *** *** ***
// Set up environment, assumes files phpagi.php and sql.php are in the same folder
// use /var/lib/asterisk/agi-bin
require('phpagi.php');
require('sql.php');
// don't know if this is necessary
error_reporting(0);
// Initialize classes for FreePBX
$AGI = new AGI();
$db = new AGIDB($AGI);
// script only works if outbound number is received as dial plan argument #1
if (!isset($argv[1])) {
$AGI->verbose('AGI missing phone number argument',3);
exit(1);
}
else {
// get arguments passed to agi
$outnum = $argv[1]; // outbound dialled digits
if (strcasecmp($argv[2],'verbose') == 0 ) {
$verbose = true;
}
if ($verbose) $AGI->verbose("Executing didloopback.php script in verbose mode", 3);
if ($verbose) $AGI->verbose("Oubound digits received: ".$outnum, 3);
// break up user dial patterns into arrays
if ($did_patterns) {
if ($verbose) $AGI->verbose("User defined DID dial patterns: ".$did_patterns, 3);
if ($verbose) $AGI->verbose("Converting DID dial patterns to array", 3);
$did_patttern_arr = explode(",",$did_patterns);
}
if ($outbound_patterns) {
if ($verbose) $AGI->verbose("User defined Outbound dial patterns: ".$outbound_patterns, 3);
if ($verbose) $AGI->verbose("Converting outbound dial patterns to array", 3);
$outbound_patttern_arr = explode(",",$outbound_patterns);
}
$foo = match_pattern_all($outbound_patttern_arr,$outnum);
if ($foo['number']) {
if ($verbose) $AGI->verbose("Matched outbound pattern: ".$foo['pattern'], 3);
if ($verbose) $AGI->verbose("Oubound number changed to: ".$foo['number'], 3);
$outnum = $foo['number'];
}
// Get defined DIDs for all inbound routes
// the WHERE ...LIKE '%%' is added to suppress error message in asterisk log
$query = "SELECT `extension` FROM `incoming` WHERE `extension` LIKE '%%'";
if ($verbose) $AGI->verbose("MySQL query: ".$query, 3);
$results = $db->sql($query, 'BOTH');
// really should catch an error exception here
// step thru results returned from query and add DIDs (if defined) to $dids array
// $dids may also contain asterisk dial patterns which should not be a problem, they won't match outbound dialling
// Possible issues where inbound routes have DIDs specified with special characters like '+' but
// should not be an issue for NAPA numbers
foreach ($results as $result) {
if (trim($result['extension']) != '') {
$bar = match_pattern_all($did_patttern_arr,trim($result['extension']));
if ($bar['number']) {
$dids[] = $bar['number'];
if ($verbose) $AGI->verbose("Found DID: ".trim($result['extension'])." matching pattern ".$bar['pattern']." Converted to ".$bar['number'], 3);
}
else {
$dids[] = trim($result['extension']);
if ($verbose) $AGI->verbose("Found DID: ".trim($result['extension']), 3);
}
}
}
// check $dids array to see if outnum is present
if (in_array($outnum,$dids)){
// now we know outnum has a corresponding inbound route DID
// set channel variable 'didloop' is set here, dialplan can check this variable to reroute call
$AGI->verbose('Outbound number has a corresponding inbound DID defined. Redirecting...',3);
$AGI->set_variable("didloop","redirect");
}
else {
if ($verbose) $AGI->verbose("No matching DIDs found, continuing ...", 3);
}
}
/**
Match a phone number against an array of patterns
return array containing
'pattern' = the pattern that matched
'number' = the number that matched, after applying rules
'status' = true if a valid array was supplied, false if not
*/
function match_pattern_all($array, $number) {
// If we did not get an array, it's probably a list. Convert it to an array.
if (!is_array($array)) {
$array = explode("\n", trim($array));
}
$match = false;
$pattern = false;
// Search for a match
foreach ($array as $pattern) {
// Strip off any leading underscore
$pattern = (substr($pattern, 0, 1) == "_") ? trim(substr($pattern, 1)) : trim($pattern);
if ($match = match_pattern($pattern, $number)) {
break;
} elseif ($pattern == $number) {
$match = $number;
break;
}
}
// Return an array with our results
return array(
'pattern' => $pattern,
'number' => $match,
'status' => (isset($array[0]) && (strlen($array[0]) > 0))
);
}
/**
Parses Asterisk dial patterns and produces a resulting number if the match is successful or false if there is no match.
*/
function match_pattern($pattern, $number) {
$pattern = trim($pattern);
$p_array = str_split($pattern);
$tmp = "";
$expression = "";
$new_number = false;
$remove = NULL;
$insert = "";
$error = false;
$wildcard = false;
$match = $pattern ? true : false;
$regx_num = "/^\[[0-9]+(\-*[0-9])[0-9]*\]/i";
$regx_alp = "/^\[[a-z]+(\-*[a-z])[a-z]*\]/i";
// Try to build a Regular Expression from the dial pattern
$i = 0;
while (($i < strlen($pattern)) && (!$error) && ($pattern)) {
switch (strtolower($p_array[$i])) {
case 'x':
// Match any number between 0 and 9
$expression .= $tmp . "[0-9]";
$tmp = "";
break;
case 'z':
// Match any number between 1 and 9
$expression .= $tmp . "[1-9]";
$tmp = "";
break;
case 'n':
// Match any number between 2 and 9
$expression .= $tmp . "[2-9]";
$tmp = "";
break;
case '[':
// Find out if what's between the brackets is a valid expression.
// If so, add it to the regular expression.
if (preg_match($regx_num, substr($pattern, $i), $matches)
|| preg_match($regx_alp, substr(strtolower($pattern), $i), $matches)) {
$expression .= $tmp . "" . $matches[0];
$i = $i + (strlen($matches[0]) - 1);
$tmp = "";
} else {
$error = "Invalid character class";
}
break;
case '.':
// Match one or more occurrences of any number
if(!$wildcard){
$wildcard = true;
$expression .= $tmp."[0-9]+";
$tmp = "";
}else{
$error = "Cannot have more than one wildcard";
}
break;
case '!':
// zero or more occurrences of any number
if (!$wildcard) {
$wildcard = true;
$expression .= $tmp . "[0-9]*";
$tmp = "";
} else {
$error = "Cannot have more than one wildcard";
}
break;
case '+':
// Prepend any numbers before the '+' to the final match
// Store the numbers that will be prepended for later use
if (!$wildcard) {
if ($insert) {
$error = "Cannot have more than one '+'";
} elseif ($expression) {
$error = "Cannot use '+' after X,Z,N or []'s";
} else {
$insert = $tmp;
$tmp = "";
}
} else {
$error = "Cannot have '+' after wildcard";
}
break;
case '|':
// Any numbers/expression before the '|' will be stripped
if (!$wildcard) {
if ($remove) {
$error = "Cannot have more than one '|'";
} else {
// Move any existing expression to the "remove" expression
$remove = $tmp . "" . $expression;
$tmp = "";
$expression = "";
}
} else {
$error = "Cannot have '|' after wildcard";
}
break;
default:
// If it's not any of the above, is it a number betwen 0 and 9?
// If so, store in a temp buffer. Depending on what comes next
// we may use in in an expression, or a prefix, or a removal expression
if (preg_match("/[0-9]/i", strtoupper($p_array[$i]))) {
$tmp .= strtoupper($p_array[$i]);
} else {
$error = "Invalid character '" . $p_array[$i] . "' in pattern";
}
}
$i++;
}
$expression .= $tmp;
$tmp = "";
if ($error) {
// If we had any error, report them
$match = false;
} else {
// Else try out the regular expressions we built
if (isset($remove)) {
// If we had a removal expression, se if it works
if (preg_match("/^" . $remove . "/i", $number, $matches)) {
$number = substr($number, strlen($matches[0]));
} else {
$match = false;
}
}
// Check the expression for the rest of the number
if (preg_match("/^" . $expression . "$/i", $number, $matches)) {
$new_number = $matches[0];
} else {
$match = false;
}
// If there was a prefix defined, add it.
$new_number = $insert . "" . $new_number;
}
if (!$match) {
// If our match failed, return false
$new_number = false;
}
return $new_number;
}
@Kamik
Copy link

Kamik commented Sep 2, 2020

Hello lgaetz. First of all thanks for the script. But it doesn't work for me. For the first time I get the message of "Intrusion detection" and then the call lands in noservice. What could be the problem?

    -- lgaetz-didloopback.php,00XXXXXXXXXX,verbose: Outbound number has a corresponding inbound DID defined. Redirecting...
    -- <SIP/201-00000480>AGI Script lgaetz-didloopback.php completed, returning 0
    -- Executing [s@macro-dialout-trunk-predial-hook:3] GotoIf("SIP/201-00000480", "1?redirect") in new stack
    -- Goto (macro-dialout-trunk-predial-hook,s,5)
    -- Executing [s@macro-dialout-trunk-predial-hook:5] Goto("SIP/201-00000480", "from-trunk,00XXXXXXXXXX,1") in new stack
    -- Goto (from-trunk,00XXXXXXXXXX,1)
  == Channel 'SIP/201-00000480' jumping out of macro 'dialout-trunk-predial-hook'
  == Channel 'SIP/201-00000480' jumping out of macro 'dialout-trunk'
    -- Executing [00XXXXXXXXXX@from-trunk:1] NoOp("SIP/201-00000480", "Attempting change CALLERID 00ZZZZZZZZZZ  to german format") in new stack
    -- Executing [00XXXXXXXXXX@from-trunk:2] NoOp("SIP/201-00000480", "Received an unknown call with DID set to 00XXXXXXXXXX") in new stack
    -- Executing [00XXXXXXXXXX@from-trunk:3] Goto("SIP/201-00000480", "s,a2") in new stack
    -- Goto (from-trunk,s,2)
    -- Executing [s@from-trunk:2] Answer("SIP/201-00000480", "") in new stack
       > 0x7ff4c82b6100 -- Strict RTP switching to RTP target address 172.16.20.60:12766 as source
    -- Executing [s@from-trunk:3] Log("SIP/201-00000480", "WARNING,Friendly Scanner from 172.16.XX.XX") in new stack
[2020-09-02 13:07:43] WARNING[17332][C-0000043f]: Ext. s:3 @ from-trunk: Friendly Scanner from 172.16.XX.XX
Executing [s@from-trunk:4] Wait("SIP/201-00000480", "2") in new stack
Executing [s-INVALIDNMBR@macro-dialout-trunk:4] Busy("SIP/502-0000047f", "20") in new stack
[2020-09-02 13:07:44] WARNING[17284][C-0000043e]: channel.c:4963 ast_prod: Prodding channel 'SIP/502-0000047f' failed
  == Spawn extension (macro-dialout-trunk, s-INVALIDNMBR, 4) exited non-zero on 'SIP/502-0000047f' in macro 'dialout-trunk'
  == Spawn extension (restrictedroute-ed4f978c111a9712b2140eafa3b45aac, 01764397046, 12) exited non-zero on 'SIP/502-0000047f'
    -- Executing [h@restrictedroute-ed4f978c111a9712b2140eafa3b45aac:1] Hangup("SIP/502-0000047f", "") in new stack
  == Spawn extension (restrictedroute-ed4f978c111a9712b2140eafa3b45aac, h, 1) exited non-zero on 'SIP/502-0000047f'
Executing [s@from-trunk:5] Playback("SIP/201-00000480", "ss-noservice") in new stack

@Kamik
Copy link

Kamik commented Sep 2, 2020

ok. i have no inbound route with DID 00XXXXXXXXXX, but XXXXXXXXXX.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment