Outbound call restriction

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]