Monday, March 19, 2012

CallerID AGI for Asterisk: trifecta 2012.0 includes opencnam, anywho and fonefinder


I have taken the Nerd Vittles calleridname.agi script and updated it to work with currently available free online caller ID services. My changes are in public domain, what's left of original code obviously retains original copyright and whatever license terms went with it.

I have never checked if the asteridex/npanxx part of the code has any chance of working, so tread carefully.

I have implemented lookups for OpenCnam, AnyWho (YellowPages took over them) and FoneFinder. The original code was useless for the latter two, as the pages got redesigned.

This was a quick hack, I need to redo it in Python -- properly this time. I obviously have no clue about Perl and don't care enough about this code to make it any prettier, sorry.


#!/usr/bin/perl -w 

use Asterisk::AGI;
use LWP::UserAgent;
use DBI;

$AGI = new Asterisk::AGI;

$Timeout       = 2;
$OpenCnam      = '1';
$AnyWho        = '0';
$FoneFinder    = '1';
$Asteridex     = '0';
$TermOnSuccess = 1;

my %input = $AGI->ReadParse();

my $callerid     = $input{'calleridnum'};
my $calleridfull = $input{'callerid'};

if ( $callerid eq '' ) {
   $callerid = $input{'callerid'};
}

if ( substr( $callerid, 0, 1 ) eq '1' ) {
   $callerid = substr( $callerid, 1 );
}

if ( substr( $callerid, 0, 2 ) eq '+1' ) {
   $callerid = substr( $callerid, 2 );
}

$calleridfull =~ s/[\,\"\']+/ /g;

$AGI->verbose( "CALLERID IS: $calleridfull\n", 1 );

if ( $callerid =~ /^(\d{3})(\d{3})(\d{4})$/ ) {
   $npa     = $1;
   $nxx     = $2;
   $station = $3;
   $AGI->verbose( "Checking $npa $nxx $station...\n", 1 );
}
elsif ( $callerid =~ /\<(\d{3})(\d{3})(\d{4})\>/ ) {
   $npa     = $1;
   $nxx     = $2;
   $station = $3;
   $AGI->verbose( "Checking $npa $nxx $station...\n", 1 );
}
else {
   $AGI->verbose(
"Unable to parse phone number for NPA/NXX/station. Phone number is: $callerid\n",
      1
   );
   exit(0);
}

# handle timeouts
alarm($Timeout);
$SIG{ALRM} = sub {
   $AGI->set_callerid("\"... <$npa$nxx$station>\"");
   $AGI->verbose( "Lookup timed out\n", 1 );
   exit(0);
};

#$npa='641';
#$nxx='892';
#$station='8019';

&lookup( \&opencnam_lookup,   $npa, $nxx, $station, "OpenCnam",   $OpenCnam );
&lookup( \&anywho_lookup,     $npa, $nxx, $station, "AnyWho",     $AnyWho );
&lookup( \&fonefinder_lookup, $npa, $nxx, $station, "FoneFinder", $FoneFinder );
&lookup( \&asteridex_lookup,  $npa, $nxx, $station, "AsteriDex",  $AsteriDex );
exit(0);

sub lookup {
   my ( $fun, $npa, $nxx, $station, $description, $enabled ) = @_;
   if ( !$enabled ) {
      $AGI->verbose( "$description lookup disabled.\n", 2 );
      return "";
   }
   $AGI->verbose( "Ready for $description lookup...\n", 2 );
   if ( $name = $fun->( $npa, $nxx, $station ) ) {
      $newcallerid = "\"$name <$npa$nxx$station>\"";
      $AGI->set_callerid($newcallerid);
      $AGI->verbose( "$description match. New CallerIDName = $name\n", 1 );
      if ( $TermOnSuccess > 0 ) { exit(0); }
   }
   else {
      $AGI->verbose( "Unable to find an $description match.\n", 1 );
   }
}

sub opencnam_lookup {
   my ( $npa, $nxx, $station ) = @_;
   my $ua = LWP::UserAgent->new( timeout => 45 );
   my $URL =
       'https://api.opencnam.com/v1/phone/'
     . $npa
     . $nxx
     . $station
     . '?format=text';
   my $req = new HTTP::Request GET => $URL;
   my $res = $ua->request($req);
   if ( $res->is_success() ) {
      my $cid = $res->content;
      if ( length $cid > 0 ) {

         #print $cid . "\n";
         return $cid;
      }
   }
   return "";
}

sub fonefinder_lookup {
   my ( $npa, $nxx ) = @_;
   my $ua = LWP::UserAgent->new( timeout => 45 );
   my $URL =
       'http://www.fonefinder.net/findome.php?npa='
     . $npa . '&nxx='
     . $nxx
     . '&thoublock=&usaquerytype=Search+by+Number&cityname=';
   $ua->agent(
'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)'
   );
   my $req = new HTTP::Request GET => $URL;
   my $res = $ua->request($req);
   if ( $res->is_success() ) {
      if ( $res->content =~ /areacode=(.+)H[1-9]>/ ) {
         my $listing = $1;

         #print "$listing\n\n";
         if ( $listing =~ /<A HREF='findcity.php\?cityname=([^&]+)&.+>/ ) {
            my $city = $1;

            #print "$city\n";
            if ( $listing =~
               /npamap[^>]+>([^<]+)<.+<A HREF=[^>]+>([^<]+)<\/A><TD>([^<]+)<TD>/
              )
            {
               my $state    = $1;
               my $provider = $2;
               my $type     = $3;

               #print "$state // $provider // $type\n";
               return $city . ' ' . $state . ' ' . $provider;
            }
         }
      }
   }
   return "";
}

sub anywho_lookup {
   my ( $npa, $nxx, $station ) = @_;
   my $ua = LWP::UserAgent->new( timeout => 45 );

   # lookup a person
   my $URL =
     'http://anywhoyp.yellowpages.com/reversephonelookup?fap_terms%5Bphone%5D=';
   $URL .= $npa . $nxx . $station;
   $ua->agent('AsteriskAGIQuery/1');
   my $req = new HTTP::Request GET => $URL;
   my $res = $ua->request($req);
   if ( $res->is_success() ) {
      if ( $res->content =~ />([^<]+)<\/a>[^<]+<span/ ) {
         my $name = $1;

         #print $name . "\n";
         return $name;

      }
   }

   # lookup a business
   $URL = 'http://anywhoyp.yellowpages.com/phone/?phone_search_terms=';
   $URL .= $npa . $nxx . $station;
   $req = new HTTP::Request GET => $URL;
   $res = $ua->request($req);
   if ( $res->is_success() ) {
      if ( $res->content =~
         /<h3 class="business-name[^<]+<a href=[^>]+>([^<]+)</ )
      {
         my $name = $1;

         #print $name . "\n";
         return $name;
      }
   }

   return "";
}

sub asteridex_lookup {
   my ( $npa, $nxx, $station ) = @_;
   my $dbh = DBI->connect( "dbi:mysql:asteridex", "root", "passw0rd" )
     or die("Connect failed");
   my $sth = $dbh->prepare("select * from user1 where out = '$npa$nxx$station'")
     or die("Prepare failed.");
   $sth->execute;
   if ( $sth->rows == 0 ) {
      return "";
   }
   else {
      my $resptr   = $sth->fetchrow_hashref();
      my $clidname = $resptr->{"name"};
      return $clidname;
   }

   $dbh->disconnect;
   return "";
}

sub npanxx_lookup {
   my ( $npa, $nxx ) = @_;
   my $dbh = DBI->connect( "dbi:mysql:phone", "root", "passw0rd" )
     or die("Connect failed");
   my $lookup = $npa . "-" . $nxx;

   #print $lookup ;
   my $sth = $dbh->prepare("select * from npanxx where npanxx = '$lookup'")
     or die("Prepare failed.");
   $sth->execute;

   #print $sth->rows ;
   if ( $sth->rows == 0 ) {
      return "";
   }
   else {
      my $resptr     = $sth->fetchrow_hashref();
      my $ratecenter = $resptr->{"ratecenter"};
      my $state      = $resptr->{"state"};
      my $company    = $resptr->{"company"};
      $company = substr( $company, 0, -1 );
      $company = substr( $company, 1, 9 );
      my $clidname =
        substr( $ratecenter, 0, 3 ) . "-" . $state . "-" . $company;
      return $clidname;
   }

   $dbh->disconnect;
   return "";
}

Asterisk 1.8 with SELinux on RHEL 6 / CentOS 6

Asterisk 1.8 is the current long term support (LTS) version of Asterisk. You can find it in the atrpms repository. Using atrpms requires a bit of ingenuity, since you must enable yum priorities. Here's how I've set up my yum priorities and excludes to play well with RHEL (lower priority is higher):

# /etc/yum/pluginconf.d/rhnplugin.conf
priority = 9

# files in /etc/yum/repos.d - current samba and subversion override those of RHEL
sernet-samba - 5
wandisco - 6
rhnplugin - 9
centos-base - 20, includepkgs=xfs* fftw-* glpk-*
dell-firmware-repository - 30
dell-omsa-repository - 30
rpmforge-repo - 50, exclude=hdf5*
epel - 60, exclude=dahdi*
atrpms - 70

I'm using a bunch of non-redhat packages, including Asterisk, recent subversion, octave, xfs tools, and Dell server management tools.

For Asterisk proper, I'm running an AGI caller id script, and a fax receiving script. The fax script uses cups to print. Those scripts require exceptions to the targeted SELinux policy. Note that the policy_module macro pulls in a big bunch of requires; had we used the plain module header the require section would be many times longer.

# file asterisk-local.te
policy_module(asterisk-local, 1.10);


require {
        type asterisk_t;
        type asterisk_var_lib_t;
        type usr_t;
        type http_port_t;
}


# adds lpr_t to the system role, prevents this SELINUX_ERR:
# invalid context unconfined_u:system_r:lpr_t:s0
# for scontext=unconfined_u:system_r:asterisk_t:s0 tcontext=system_u:object_r:lpr_exec_t:s0 tclass=process
role system_r types lpr_t;


# allow execution of agi scripts
allow asterisk_t asterisk_var_lib_t:file execute;
allow asterisk_t asterisk_var_lib_t:file execute_no_trans;
allow asterisk_t usr_t:file execute;
allow asterisk_t usr_t:file execute_no_trans;


# allow phone directory lookup via http
allow asterisk_t http_port_t:tcp_socket name_connect;


# allow printing
lpd_domtrans_lpr(asterisk_t)


This policy is a bit lax about executing stuff, but it's still way better than letting asterisk run in totally permissive mode. It's a work in progress. Over time as I learn selinux I'll set it up better.

To build and install it, this script will do:

#! /bin/bash
module=asterisk-local
[ "$1" ] && module="$1"
make -f /usr/share/selinux/devel/Makefile && \
        semodule -i $module.pp

RedHat Network rhnplugin Yum Priority Support Patch

Below is the patch one had to apply to /usr/share/yum-plugins/rhnplugin.py to support yum priorities. The most recent yum-rhn-plugin-0.9.1-36.el6.noarch doesn't need this patch anymore, thus it's of historical interest only. I didn't bother checking exactly at what plugin version did the patch become obsolete.

--- rhnplugin.py.org    2011-10-03 16:30:04.722883767 -0400
+++ rhnplugin.py        2011-10-03 16:34:28.712885617 -0400
@@ -149,6 +149,7 @@
     cachedir = conduit.getConf().cachedir
     default_gpgcheck = conduit.getConf().gpgcheck
     gpgcheck = conduit.confBool('main', 'gpgcheck', default_gpgcheck)
+    priority = conduit.confInt('main', 'priority', 1)
     sslcacert = get_ssl_ca_cert(up2date_cfg)
     enablegroups = conduit.getConf().enablegroups
     metadata_expire = conduit.getConf().metadata_expire
@@ -173,6 +174,7 @@
                     repos.delete(repo.id)
             repo.basecachedir = cachedir
             repo.gpgcheck = gpgcheck
+            repo.priority = priority
             repo.proxy = proxy_url
             repo.sslcacert = sslcacert
             repo.enablegroups = enablegroups