Hi,
If it’s could be usefull for someone, here is a little AGI script I wrote in order to grant extension’s outbound dialing, without aut.
I used the “account” field, so it would be improved as billing capabilities are disabled.
Be careful as this is not a standard @ pattern matching. We only watch the begenning of the dialed number.
All explanations and user guide inside the code.
[code:1]
//
// RESTRICTION D’ACCES SUR ROUTES SORTANTES PROGRAMMABLE PAR EXTENSION
//
// Written By: Bruno MOLTO (makko at wanadoo.fr)
//
//
//
//
// ContrÃÆôle d’accÃÆès aux routes sortantes pour chaque extension.
// Permet de contrÃÆôler poste per poste l’accÃÆès aux routes sortantes, sans avoir ÃÆàsaisir le mot de passe systÃÆématiquement.
// Si l’accÃÆès est interdit, le fonctionnement standard est conservÃÆé, et un mot de passe est demandÃÆé ÃÆàl’utilisateur.
// Ceci permet ÃÆàun administrateur de pouvoir tout de mÃÆême utiliser un poste restreint.
// Utilise le champ account code :
// - Le billing est donc inutilisable.
//
//
//
// Version 1.0 - le 05/05/2006
// todo:
// Impl̮̩menter correctement dans un champ sp̮̩cifique de la base asterisk
// ImplÃÆémenter les couples route/pw route quand on aura de la place…
// GÃÆérer les connexions d’agent afin de bloquer/dÃÆébloquer provisoirement le poste
//
// Arguments d’appel :
// 1er argument : mot de passe affectÃÆé ÃÆàla route
// 2e argument : le numÃÆéro que l’utilisateur a composÃÆé
// 3e argument : le numÃÆéro de prioritÃÆé oÃÆù on jumpe si appel OK
// 4e argument : le numÃÆéro de prioritÃÆé oÃÆù on jumpe si appel interdit
//
//
//
// Fonctionnement :
//
// On utilise le mot de passe param̮̩trable sur les routes sortantes.
// et le code account code param̮̩trable sur chaque extension, et normalement utilis̮̩ par le billing.
//
// Pour protÃÆéger l’accÃÆès d’une route, il faut commencer par lui affecter un mot de passe.
//
// Pour le rÃÆéglage de l’accÃÆès de chaque poste (extension), paramÃÆétrer le account code comme suit:
//
// p=xxxx,y,ab,cd,ef,p=tttt,y,gh,ij
// xxxx et tttt sont les passwords correspondants aux routes trait̮̩es
// y est la strat̮̩gie appliqu̮̩e :
// - Si y=a, (stratÃÆégie “accept”) on accepte par dÃÆéfaut touts les numÃÆéros, sauf ceux qui commencent par les exceptions qui suivent
// - Si y=d, (stratÃÆégie “deny”) on rejette par dÃÆéfaut touts les numÃÆéros, sauf ceux qui commencent par les exceptions qui suivent
// ab,cd et ef dÃÆécrivent la liste d’exceptions pour le premier mot de passe (donc la premiÃÆère route)
// gh et ij dÃÆécrivent la liste d’exceptions pour le second mot de passe (donc la deuxiÃÆème route)
//
// Il n’y a pas de limitation concernant le nbre de routes ou de prÃÆéfixes d’exception traitÃÆés.
// La seule limitation est la longueur totale de la chaine qui ne peut excÃÆéder 150 caractÃÆères (taille du champ “data” ds MySQL)
//
// Exemple :
// p=123,a,06,08,p=5678,d,0466,0467,0468
// La route qui a le pw 123 va accepter tous les num̮̩ros, sauf ceux qui commencent par 06 ou 08
// La route qui a le pw 5678 va refuser tous les num̮̩ros, sauf ceux qui commencent par 0466,0467 ou 0468.
//
// Note : la recheche d’exception est effectuÃÆée de gauche ÃÆàdroite dans la liste.
// La premi̮̬re exception qui correspond est valid̮̩e.
//
// Attention : afin de simplifier la chaine d’accÃÆès, on ne tient compte que du mot de passe de la route,
// pas du couple (route/mot de passe). Ceci signifie qu’il faut impÃÆérativement attribuer un mot de passe diffÃÆérent pour
// chaque route pour que la gestion soit correcte.
//
//
// Mise en service :
// Il faut simplement modifier la macro [macro-dialout-trunk] de extensions.conf comme suit :
// (insersion appel agi et dÃÆécalage des lignes suivantes (et des destinations de saut bien sÃÆûr)
//
//
/******MODIFICATION MACRO DIALOUT-TRUNK (Freepbx 2.0.1)
;BEFORE
; dialout using a trunk, using pattern matching (don’t strip any prefix)
; arg1 = trunk number, arg2 = number, arg3 = route password
;[macro-dialout-trunk]
;exten => s,1,GotoIf($["${ARG3}" = “”]?3:2) ; arg3 is pattern password
;exten => s,2,Authenticate(${ARG3})
;exten => s,3,Macro(user-callerid)
;exten => s,4,Macro(record-enable,${CALLERID(number)},OUT)
;exten => s,5,Macro(outbound-callerid,${ARG1})
;exten => s,6,Set(GROUP()=OUT_${ARG1})
;exten => s,7,GotoIf($[ ${GROUP_COUNT()} > ${OUTMAXCHANS_${ARG1}} ]?108)
; if we’ve used up the max channels, continue at (n+101)
;exten => s,8,Set(DIAL_NUMBER=${ARG2})
;exten => s,9,Set(DIAL_TRUNK=${ARG1})
;exten => s,10,AGI(fixlocalprefix) ; this sets DIAL_NUMBER to the proper dial string for this trunk
;exten => s,11,Set(OUTNUM=${OUTPREFIX_${ARG1}}${DIAL_NUMBER}) ; OUTNUM is the final dial number
;exten => s,12,Set(custom=${CUT(OUT_${ARG1},:,1)}) ; Custom trunks are prefixed with “AMP:”
;exten => s,13,GotoIf($["${custom}" = “AMP”]?16)
;exten => s,14,Dial(${OUT_${ARG1}}/${OUTNUM}|120|WT) ; Regular Trunk Dial, allow recording.Modifi̮̩ BM ajout̮̩ T
;exten => s,15,Goto(s-${DIALSTATUS},1)
; This is a custom trunk. Substitute $OUTNUM$ with the actual number and rebuild the dialstring
; example trunks: “AMP:CAPI/XXXXXXXX:b$OUTNUM$,30,r”, “AMP:OH323/[email protected]:XXXX”
;exten => s,16,Set(pre_num=${CUT(OUT_${ARG1},$,1)})
;exten => s,17,Set(the_num=${CUT(OUT_${ARG1},$,2)}) ; this is where we expect to find string OUTNUM
;exten => s,18,Set(post_num=${CUT(OUT_${ARG1},$,3)})
;exten => s,19,GotoIf($["${the_num}" = “OUTNUM”]?20:21) ; if we didn’t find “OUTNUM”, then skip to Dial
;exten => s,20,Set(the_num=${OUTNUM}) ; replace “OUTNUM” with the actual number to dial
;exten => s,21,Dial(${pre_num:4}${the_num}${post_num})
;exten => s,22,Goto(s-${DIALSTATUS},1)
;exten => s,108,Noop(max channels used up)
;exten => s-BUSY,1,NoOp(Trunk is reporting BUSY)
;exten => s-BUSY,2,Busy()
;exten => s-BUSY,3,Wait(60)
;exten => s-BUSY,4,NoOp()
;exten => _s-.,1,NoOp(Dial failed due to ${DIALSTATUS})
;AFTER
; dialout using a trunk, using pattern matching (don’t strip any prefix)
; arg1 = trunk number, arg2 = number, arg3 = route password
[macro-dialout-trunk]
exten => s,1,GotoIf($["${ARG3}" = “”]?4:2) ; arg3 is pattern password
exten => s,2,AGI(checktrunkrights,${ARG3}|${ARG2}|4|3);Verification autorisation acc̮̬s du poste sur la route
exten => s,3,Authenticate(${ARG3})
exten => s,4,Macro(user-callerid)
exten => s,5,Macro(record-enable,${CALLERID(number)},OUT)
exten => s,6,Macro(outbound-callerid,${ARG1})
exten => s,7,Set(GROUP()=OUT_${ARG1})
exten => s,8,GotoIf($[ ${GROUP_COUNT()} > ${OUTMAXCHANS_${ARG1}} ]?109)
; if we’ve used up the max channels, continue at (n+101)
exten => s,9,Set(DIAL_NUMBER=${ARG2})
exten => s,10,Set(DIAL_TRUNK=${ARG1})
exten => s,11,AGI(fixlocalprefix) ; this sets DIAL_NUMBER to the proper dial string for this trunk
exten => s,12,Set(OUTNUM=${OUTPREFIX_${ARG1}}${DIAL_NUMBER}) ; OUTNUM is the final dial number
exten => s,13,Set(custom=${CUT(OUT_${ARG1},:,1)}) ; Custom trunks are prefixed with “AMP:“
exten => s,14,GotoIf($[”${custom}” = “AMP”]?17)
exten => s,15,Dial(${OUT_${ARG1}}/${OUTNUM}|120|WT) ; Regular Trunk Dial, allow recording.Modifi̮̩ BM ajout̮̩ T
exten => s,16,Goto(s-${DIALSTATUS},1)
; This is a custom trunk. Substitute $OUTNUM$ with the actual number and rebuild the dialstring
; example trunks: “AMP:CAPI/XXXXXXXX:b$OUTNUM$,30,r”, “AMP:OH323/[email protected]:XXXX"
exten => s,17,Set(pre_num=${CUT(OUT_${ARG1},$,1)})
exten => s,18,Set(the_num=${CUT(OUT_${ARG1},$,2)}) ; this is where we expect to find string OUTNUM
exten => s,19,Set(post_num=${CUT(OUT_${ARG1},$,3)})
exten => s,20,GotoIf($[”${the_num}" = “OUTNUM”]?21:22) ; if we didn’t find “OUTNUM”, then skip to Dial
exten => s,21,Set(the_num=${OUTNUM}) ; replace “OUTNUM” with the actual number to dial
exten => s,22,Dial(${pre_num:4}${the_num}${post_num})
exten => s,23,Goto(s-${DIALSTATUS},1)
exten => s,109,Noop(max channels used up)
exten => s-BUSY,1,NoOp(Trunk is reporting BUSY)
exten => s-BUSY,2,Busy()
exten => s-BUSY,3,Wait(60)
exten => s-BUSY,4,NoOp()
exten => _s-.,1,NoOp(Dial failed due to ${DIALSTATUS})
******MODIFICATION MACRO DIALOUT-TRUNK/
#include <stdio.h>
#include <ctype.h>
#include <stdarg.h>
#include <mysql/mysql.h>
//Prototypes bibliotheque cagi
#include “cagi.h”
//ParamÃÆètres de connexion ÃÆàla base
#define MYSQL_HOST “localhost”
#define MYSQL_DB “asterisk”
#define MYSQL_LOGIN “asteriskuser”
#define MYSQL_PASSWD “amp109”
int main(int argc, char *argv[])
{
//argc contient le nombre d’arguments passÃÆés lors de l’appel
//argv est un tableau de chaines contenant les arguments
//ATTENTION : le premier argument est le nom de l’executable
AGI_TOOLS agi;//Objet agi
AGI_CMD_RESULT agires;
char texte [1024];//pour messages debug
int i,j;
char routepw[100];//Mot de passe de la route pass̮̩ en argument au script agi
char dialnumber[100];//Num̮̩ro compos̮̩ pass̮̩ en argument au script agi
char account[1024];//Chaine account code d̮̩finie dans les extensions et pass̮̩e dans les variables agi
int priok,prinok;//Priorit̮̩s cibles extraites le la ligne de commande
//Variables temporaires pour extraction des chaines account
char temppassword[100];
char strat;//Strat̮̩gie d'autorisation a=accepte tout par d̮̩faut/d=refuse tout par d̮̩faut
int ok;//sort de la recherche. ok=0 =>appel interdit. ok=1 =>appel ok
int debutex;//flag extraction exception
char except[100];//Chaine d'extraction des exceptions
int outsearch;//Fin de scrutation des exceptions
//Variables pour connexion mySQL
MYSQL mysql;
MYSQL_RES *result;
MYSQL_ROW row;
int baseconnectee=1;
char query[512];
char table[10];//La table varie en fct de la technologie du channel
char extension[10];//Le num̮̩ro d'"extension" (la cl̮̩ dans la table)
//Init agitools
AGITool_Init(&agi);
agi_verbose("Beginning check route rigths",4);
//lecture de la chaine channel dans les variables agi
strcpy(table,AGITool_ListGetVal(agi.agi_vars,"agi_type"));
//On passe en minuscules car c'est ainsi que sont nomm̮̩es les tables ds la base
for(i=0;i<strlen(table);i++)
{
table[i]=tolower(table[i]);
}
//lecture de la chaine channel dans les variables agi
strcpy(texte,AGITool_ListGetVal(agi.agi_vars,"agi_channel"));
//extraction de l'extension
for(i=0;i<strlen(texte);i++)
{
if(texte[i]=='-')
{
texte[i]=0;
break;
}
}
strcpy(extension,&texte[4]);
//Message debug
sprintf(texte,"Type=%s Extension=%s",table,extension);
agi_verbose(texte,4);
//lecture de la chaine account code dans les variables agi
strcpy(account,AGITool_ListGetVal(agi.agi_vars,"agi_accountcode"));
//Message debug
//sprintf(texte,"Account code (AGI):%s",account);
//agi_verbose(texte, 4);
//REMARQUE IMPORTANTE :
//FreePBX stocke les valeus accountcode dans les champs "data" des tables SIP ou ZAP suivant la techno...
//Bien que FreePBX ait fixÃÆé ÃÆà150 la largeur maxi du champ "data", asterisk limite ÃÆà20 caractÃÆères la largeur
//de accountcode. Ceci signifie que lorsqu'on r̮̩cup̮̬re cette valeur via les variables agi, elle se trouve
//tronqu̮̩e.
//On utilise donc une requ̮̻te directe vers la table concern̮̩e pour r̮̩cup̮̩rer la totalit̮̩ de la chaine param̮̩tr̮̩e
//J'ai laiss̮̩ la lecture par la variable agi ci-dessus pour info. En+, si pas de connexion SQL, ̮̤a marche au moins
//sur les 20 premiers caract̮̬res
mysql_init(&mysql);
if (!mysql_real_connect(&mysql,MYSQL_HOST,MYSQL_LOGIN,MYSQL_PASSWD,MYSQL_DB,0,NULL,0))
{
baseconnectee=0;
agi_verbose("Unable to connect to MySQL server",4);
}
else agi_verbose("Connected to MySQL server",4);
if(baseconnectee==1)
{
//On cherche la valeur dans la bonne table et pour la bonne extension...
sprintf(query,"SELECT data FROM %s WHERE id='%s' AND keyword='accountcode'",table,extension);
//Message debug
//sprintf(texte,"Query:%s",query);
//agi_verbose(texte,4);
mysql_real_query(&mysql,query,strlen(query));
result = mysql_use_result(&mysql);
while((row = mysql_fetch_row(result)))
{
strcpy(account,row[0]);
//Message debug
//sprintf(texte,"Account code (MySQL):%s",row[0]);
//agi_verbose(texte,4);
}
}
mysql_close(&mysql);
//Message debug
agi_verbose("Checking rights for the route",4);
//lecture du mot de passe de la route (1er argument d'appel)
strcpy(routepw,argv[1]);
//Message debug
//sprintf(texte,"Route password :%s",routepw);
//agi_verbose(texte, 4);
//lecture du num̮̩ro compos̮̩ (2e argument d'appel)
strcpy(dialnumber,argv[2]);
//Message debug
sprintf(texte,"Dialed number :%s",dialnumber);
agi_verbose(texte, 4);
//lecture du num̮̩ro de priorit̮̩ cible si appel ok (3e argument d'appel)
priok=atoi(argv[3]);
//lecture du num̮̩ro de priorit̮̩ cible si appel interdit (4e argument d'appel)
prinok=atoi(argv[4]);
//Par dÃÆéfaut, appel interdit et fin de scrutation ÃÆà0
ok=0;
outsearch=0;
//Recherche d'un password valide dans le chaine account
for(i=0;i<strlen(account);i++)
{
if(outsearch==1) break;//Fin de scrutation
if(account[i]=='p')//D̮̩but du la cl̮̩ "p=" ?
{
if(account[i+1]=='=')//On est bien sur un d̮̩but de chaine
{
i++;i++;//i pointe maintenant sur le d̮̩but du password
j=0;//Pour remplissage temppassword
while((account[i]!=',')&&(i<strlen(account)))//On extrait le password
{
temppassword[j]=account[i];
i++;j++;
}
temppassword[j]=0;//On ins̮̬re la fin de chaine au password extrait
//Message debug
//sprintf(texte,"Password found:%s",temppassword);
//agi_verbose(texte, 4);
if(strcmp(routepw,temppassword)!=0)//Mot de passe diff̮̩rent ?
{
agi_verbose("password do not match the route password. Search for forward pw...",4);
continue;//A la recherche d'un autre mot de passe valide
}
//Si on est ici, c'est qu'un mot de passe valide a ̮̩t̮̩ trouv̮̩.
agi_verbose("password match the route password. check strategy & exceptions...",4);
//On pointe actuellement sur la virgule, on va donc regarder le caract̮̬re suivant
i++;
if((account[i]!='a')&&(account[i]!='d'))//Code strat̮̩gie invalide ?
{
sprintf(texte,"Invalid strategy code:%c",account[i]);
agi_verbose(texte, 4);
continue;
}
//M̮̩mo code strat̮̩gie
strat=account[i];
if(strat=='a') strcpy(texte,"Strategy : accept");
else strcpy(texte,"Strategy : deny");
agi_verbose(texte, 4);
//On positionne le ok en fct de la trat̮̩gie
if(strat=='a') ok=1; else ok=0;
if(i==strlen(account)-1)//Il n'y a pas d'exceptions apr̮̬s le code strat̮̩gie
{
agi_verbose("No exceptions. End of search", 4);
break;
}
//Gestion des exceptions
debutex=0;
while(i<strlen(account)-1)
{
i++;
//sprintf(texte,"excep. suiv. i=%d account[i]=%c debutex=%d strlen=%d",i,account[i],debutex,strlen(account));
//agi_verbose(texte, 4);
if((account[i]==',')&&(debutex==0))//D̮̩tection d̮̩but exception
{
debutex=1;
j=0;
continue;
}
if((account[i]!=',')&&(debutex==1))//Constitution chaine exception
{
except[j]=account[i];
j++;
if(i<strlen(account)-1)continue;//On continue si on n'est pas en fin de chaine
}
//D̮̩tection fin exception par virgule trouv̮̩e ou fin de chaine atteinte
if(((account[i]==',')&&(debutex==1)) || ((i==strlen(account)-1)&&(debutex==1)))
{
debutex=0;//On baisse le flag exception
except[j]=0;//On finalise la chaine d'exception
sprintf(texte,"Found exception:%s",except);
agi_verbose(texte, 4);
//Le numÃÆéro composÃÆé correspond-il ÃÆàl'exception trouvÃÆée ?
if(strncmp(except,dialnumber,strlen(except))==0)
{
//Le num̮̩ro correspond
agi_verbose("Exception match the dialed number", 4);
//Si on est dans une strat̮̩gie deny, on est ok pour l'appel
if(strat=='d')
{
ok=1;
agi_verbose("Ok for dial", 4);
outsearch=1;//Pour sortir de la boucle de recherche du d̮̩but de chaine password
break;//Sortie de la boucle de scrutation des exceptions
}
else //on est dans une strat̮̩gie accept, on n'est donc pas ok
{
ok=0;
agi_verbose("Dial forbidden", 4);
outsearch=1;//Pour sortir de la boucle de recherche du d̮̩but de chaine password
break;//Sortie de la boucle de scrutation des exceptions
}
}
else agi_verbose("Exception do not match the dialed number", 4);
//On recule sur la virgule pour que l'̮̩ventuelle exception suivante soit d̮̩tect̮̩e
i--;
}
}
}
}
}
//Recherche termin̮̩e, on saute aux priorit̮̩s correspondantes
//Appel autoris̮̩
if(ok==1)
{
sprintf(texte,"End - Call ok. Jump to priority %d",priok);
agi_verbose(texte,4);
AGITool_set_priority(&agi,&agires,priok);
}
//Appel interdit
else
{
sprintf(texte,"End - Call forbidden. Jump to priority %d",prinok);
agi_verbose(texte,4);
AGITool_set_priority(&agi,&agires,prinok);
}
//Destruction objet AGITool et fin
AGITool_Destroy(&agi);
return 0;
}
[/code:1]