#!/usr/bin/perl
# This file is part of PWHOIS.
# #
# # The PWHOIS software provided in this Distribution is
# # Copyright 2005 VOSTROM Holdings, Inc.
# #
# # The full text of our legal notices is contained in the file called
# # COPYING, included with this Distribution
use strict;
use DBI;
use Time::Format qw(time_format time_strftime time_manip);
use Getopt::Long;
use Log::Dispatch;
use Log::Dispatch::Screen;
use Log::Dispatch::File;
use Net::Patricia;
use Net::Socket::NonBlock;
use POSIX;

package Pwhoisd;

# globals
my $DEBUG = 'false';		# force debugging on: automatically turned on if verbose>=2
my $VERSION = '1.0.3.27';	# the version number to print out at various screens
my $PROGNAME = 'Prefix WhoIs';	# title of our program
my $COPYRIGHT = 'Copyright (c) 2005 VOSTROM Holdings, Inc.'; # copyright holder

# DO NOT MODIFY UNLESS YOU KNOW WHAT YOU ARE DOING: EDIT THE CONFIG FILE IF POSSIBLE
# See: /etc/pwhois/pwhoisd.conf or -c option to specify different config file

my $DEFAULT_CONFIG = '/etc/pwhois/pwhoisd.conf';
my $DEFAULT_WHOIS_PORT=43;
my $DEFAULT_MAX_QUERIES=1000;
my $DEFAULT_PIDFILE='/var/run/pwhoisd.pid';
my $DEFAULT_DATABASE_TYPE = 'postgres';
my $DEFAULT_DATABASE_SERVER = '';
my $DEFAULT_DATABASE_USER = 'pwhois';
my $DEFAULT_DATABASE_PASSWD = '';
my $DEFAULT_DATABASE_NAME = 'pwhois';
my $DEFAULT_UID=65334;
my $DEFAULT_GID=65334;
my $DEFAULT_ROUTER_ID=0;	# used to control which (view) of the data this pwhois server looks at

# DO NOT EDIT BELOW THIS LINE

my $dbh;
my $dsn = 'NA';		# do not edit: these are changed later from defaults or config
my $user = 'NA';	# do not edit: these are changed later from defaults or config
my $password = 'NA';    # do not edit: these are changed later from defaults or config

my $select_from_bgp_routes_sth;
my $select_from_bgp_routes_by_routerid_sth;
my $select_from_bgp_routes_by_prefix_sth;
my $select_from_bgp_routes_by_prefix_bestonly_sth;
my $select_from_bgp_routes_by_prefix_by_routerid_sth;
my $select_from_bgp_routes_by_prefix_by_routerid_bestonly_sth;
my $select_from_bgp_routes_by_prefix_nocidr_sth;
my $select_from_bgp_routes_by_prefix_nocidr_bestonly_sth;
my $select_from_bgp_routes_by_prefix_nocidr_by_routerid_sth;
my $select_from_bgp_routes_by_prefix_nocidr_by_routerid_bestonly_sth;
my $select_from_bgp_routes_by_source_as_sth;
my $select_from_bgp_routes_by_source_as_bestonly_sth;
my $select_from_bgp_routes_by_source_as_by_routerid_sth;
my $select_from_bgp_routes_by_source_as_by_routerid_bestonly_sth;
my $select_from_pwhois_acl_sth;
my $select_from_bgp_routes_next_hops_sth;
my $select_from_bgp_routes_next_hops_by_routerid_sth;

my $server_connections_paused=0;

my %routes = ();  # routing table (hash)
my %peers = ();   # peer routers
my %config = ();  # configuration options

my $pt = new Net::Patricia;	# RIB/FIB (fast search)
my %owner = ();
my %connections = ();		# connection tracking
my %requests = ();		# request tracking (sessions)
my $socket;
my $last_table_update;		# date of last RIB update
my $program_start_date;		# date program started
my %counters = ();		# keeps track of the number of different statistics
# initialize some of the counters -- does not include hosts that are in the ACL
$counters{'whois'} = 0;					# number of queries for whois requests
$counters{'routeview.prefix'} = 0;		# number of queries for routeview prefix requests
$counters{'routeview.source-as'} = 0;	# number of queries for routeview source-as requests
$counters{'peers'} = 0;					# number of queries for the peers
$counters{'hosts'} = 0;					# number of queries from unique hosts (regardless of query type)

my $INVALID_INPUT="Sorry, I don't like your input.  You may ask for 'help'";

# enviroment

$ENV{'TZ'} = 'GMT';

#log handle
my $log;
my %opt = ();

# install signal handlers
$SIG{HUP} = \&reload;
$SIG{INT} = \&shutdown;
$SIG{TERM} = \&shutdown;
$SIG{KILL} = \&shutdown;
$SIG{QUIT} = 'IGNORE';    # ignore SIGQUIT
$SIG{__WARN__} =\&handle_warn;
#$SIG{__DIE__} = \&handle_error;

sub new {
   my $self = {};
   my $class = shift;
   return bless _init($self), $class
}

sub _init {
   my $self = shift;

   processArgs();
  # connect to the database
   openConfigFile();
   openDatabase();
   return $self
}

sub processArgs
{
        # change our process name in the PS list
        $0 = "pwhoisd";

        Getopt::Long::Configure('no_ignore_case');
        Getopt::Long::GetOptions(\%opt, 'help|h', 'logfile|l=s', 
					'version|V', 'verbose|v+', 
					'configfile|c=s',
					'port|p=s', 'bind|b=s', 
					'uid|u=n', 'gid|g=n',
					'daemon|d', 'pidfile=s',
					'limit-max-queries|mq=n',
					'no-load',		# testing
					'router-id|r=s'		# controls which router-id to select data from in the database
                ) or die "Can't parse command-line options";
        usage() if $opt{help};


	# config file
	if(!defined($opt{'configfile'})) {
		$opt{'configfile'} = $DEFAULT_CONFIG;
	}

        # set version output to 0
        if(!defined($opt{verbose})) {
                $DEBUG = 'false';
		$opt{verbose} = 0;
        }
	else
	{
		if($opt{verbose} >= 2) {
                        $DEBUG = 'true';
                }
	}	

	if($opt{logfile}) {
	        #  open our log file ... nothing should be written to stdout or stderr
        	# from now on

		if(! -f $opt{logfile}) {
			system("touch ".$opt{logfile});
		}        	
		$log = Log::Dispatch::File->new( name      => 'file1',
                                       min_level => 'debug',
                                       filename  => "$opt{logfile}",
                                       mode      => 'append');
                                       
        }
        else
        {
                  $log = Log::Dispatch::Screen->new( name      => 'screen',
                                                      min_level => 'debug',
                                                      stderr    => 0 );
        }

	if(!defined($opt{'bind'})) {
		$opt{'bind'} = '';
	}

	# set the default port listenn on
	if(!defined($opt{'port'})) {
		$opt{'port'} = $DEFAULT_WHOIS_PORT;
	}

	if(!defined($opt{'uid'})) {
		$opt{'uid'} = $DEFAULT_GID;
	}

	if(!defined($opt{'gid'})) {
		$opt{'gid'} = $DEFAULT_UID;
	}

	# set the limits on maximum queries per host IP/per day
	if(!defined($opt{'limit-max-queries'})) {
		$opt{'limit-max-queries'} = $DEFAULT_MAX_QUERIES;
	}

	if($opt{version}) {
                print getVersion(1) . "\n";
                exit;
        }
	
	if($opt{daemon}) {
                 daemonize();
	}
	$program_start_date = time();
        $log->log( level => 'info', message => "$0 started at ". getDateTimeFormat(time()) ."\n") if $opt{verbose};
}


sub usage
{
        print "usage: $0 [*options*]\n\n",
              "  -h, --help         display this help and exit\n",
              "  -v, --verbose      be verbose about what you do (add more -v's to increase verbosity: above v=2 is considered debug)\n",
              "  -V, --version      output version information and exit\n",
              "  -l, --logfile f    write misc progress output to logfile instead of stdout\n",
	      "  -c, --configfile f   read startup settings from configuration file: default is $DEFAULT_CONFIG\n",
	      "  -d, --daemon       start in the background\n",
	      "  --pidfile	    use alternative PID file location: default is /var/run/pwhoisd.pid\n",
	      "  -p, --port <n>	    port number to listen on: defaults to 43\n",
	      "  --b|bind <ip>      the IP address to bind on: defaults to all interfaces (*)\n",
	      "  -u, --uid          the effective user to run as \n",
	      "  -g, --gid          the effective group to run as \n",
	      "  --limit-max-queries <n>  The maximum number of queries (per IP/per day) default is $DEFAULT_MAX_QUERIES\n",
	      "  -r, --router-id <id>  the router id to use for this server; useful if there is more that one set of data\n",
	      "     in the database and the server should only serve responses from one set of data.\n",
	      "  --no-load          Do not load data -- for testing purposes\n";
        exit;
}

sub daemonize()
{
	# create default options
	if(!defined($opt{pidfile})) {
		$opt{pidfile} = $DEFAULT_PIDFILE;
	}

	# fork and save the process ID
 	defined(my $pid = fork) or die "$0: can't fork: $!";
 	if($pid) {
		# parent
		open PIDFILE, ">".$opt{pidfile}
		or die "$0: can't write to ". $opt{pidfile} .": $!\n";
		print PIDFILE "$pid\n";
		close(PIDFILE);
		exit;
	}
	# child
	POSIX::setsid or die "$0: can't start a new session: $!";
}


# error or warning occurred, bail out
sub handle_warn($)
{
        my $msg = shift;
	if(defined($log)) {
	        $log->log(level=>'warning', message=>"Warning: $msg\n") if $opt{verbose} >= 1;
	}
	else
	{
	        print STDERR "Warning: $msg\n" if $opt{verbose} >= 1;

	}
}

sub handle_error($)
{
        my $msg = shift;
	if(defined($log)) {
	        $log->log(level=>'error', message=>"Error: $msg\n") if $opt{verbose} >= 1;
	}
	else
	{
	        print STDERR "Error: $msg\n" if $opt{verbose} >= 1;

	}
        &shutdown;
}

sub shutdown
{
	if(defined($log)) {
        	$log->log(level=>'info', message=>getCurrentSyslogDateTime() . " $0 shutdown requested.\n") if $opt{verbose};
	}
	# close our database connection
        closeDatabase();
	# close our listening socket
	foreach my $con (keys %connections) {
		$socket->Close($con);
	}
	$socket->Close() if defined($socket);
	undef $socket;
        exit(0);
}

sub reload()
{
	$log->log(level=>"info", message=>getCurrentSyslogDateTime() ." $0 refresh signal received.  Reloading database\n") if $opt{verbose} >= 1;

	# wait for any existing connections to complete?
	# pause communication

	$server_connections_paused=1;
	load_data();
	$server_connections_paused=0;
}


sub openConfigFile()
{
	if(defined($opt{'configfile'})) {
		if(-r $opt{'configfile'}) {
			open(CONFIG, $opt{'configfile'}) or die "Can't open config file: $!";
			while(<CONFIG>)
			{
				
				next if $_ =~ /^\s*#.*$/;  # skip comments
				next if $_ =~ /^\s+$/;	# skip blank lines
				
				my ($name,$value) = ($_ =~ /^\s*([A-z0-9_\-.]+) ?= ?"?([^"\r\n]*)"? ?$/); 
				$log->log(level=>"info", message=>"Found setting $name=$value\n") if $opt{verbose} >=1;
				
				$config{$name} = $value;				
			}
			close(CONFIG);
		}
	}
	else
	{
						
	}
	
	# configure based upon configuration settings
	
	my $database = $DEFAULT_DATABASE_TYPE;
	my $dbserver = $DEFAULT_DATABASE_SERVER;
	my $dbuser = $DEFAULT_DATABASE_USER;
	my $dbpasswd = $DEFAULT_DATABASE_PASSWD;
	my $dbname = $DEFAULT_DATABASE_NAME;
	
	if(defined($config{'db.type'})) {
		$database = $config{'db.type'};	
	}
	
	if(defined($config{'db.server'})) {
		$dbserver = $config{'db.server'};	
	}
	
	if(defined($config{'db.name'})) {
		$dbname = $config{'db.name'};	
	}
	
	if(defined($config{'db.user'})) {
		$dbuser = $config{'db.user'};
		$user = $dbuser;	
	}

	if(defined($config{'db.password'})) {
		$dbpasswd = $config{'db.password'};
		$password = $dbpasswd;
	}
	
	# configure the DSN
	
	if($database eq 'postgres' or 
	   $database eq 'pgsql' or 
	   $database eq 'postgresql')
	{
		$dsn = "dbi:Pg:dbname=$dbname";
	}
	elsif($database eq 'mysql')
	{
		$dsn = "dbi:mysql:dbname=$dbname:$dbserver";		
	}
	else
	{
		$log->log(level=>"error", message=>"Invalid database type specified '$database'\n");
		exit(-1);
	}
	
	
	# configure other settings (config-file options override command-line)
	if(defined($config{'pwhoisd.verbose'}))
	{
		if($config{'pwhoisd.verbose'} =~ /\d+/) {
			if($config{'pwhoisd.verbose'} > $opt{verbose}) {	
				$opt{'verbose'} = $config{'pwhoisd.verbose'};
			}
		}
		else
		{
			$log->log(level=>"error", message=>"Invalid configuration option value for 'pwhoisd.verbose'\n");
			exit(-1);
		}
	}
	
	
	if(defined($config{'pwhoisd.logfile'}))
	{
		if($config{'pwhoisd.logfile'} =~ /[A-z0-9\-_\/ \.]+/) {
			$opt{'logfile'} = $config{'pwhoisd.logfile'};

  			if($opt{logfile}) {
                	#  open our log file ... nothing should be written to stdout or stderr
                	# from now on

                		if(! -f $opt{logfile}) {
                        	system("touch ".$opt{logfile});
                		}
                		$log = Log::Dispatch::File->new(name      => 'file1',
								min_level => 'debug',
								filename  => $opt{logfile},
								mode      => 'append');
			}		
		}
		else
		{
			$log->log(level=>"error", message=>"Invalid configuration option value for 'pwhoisd.logfile'\n");
			exit(-1);
		}
	}
	
	if(defined($config{'pwhoisd.pidfile'}))
	{
		if($config{'pwhoisd.pidfile'} =~ /[A-z0-9\-_\/ \.]+/) {
			$opt{'pidfile'} = $config{'pwhoisd.pidfile'};
		}
		else
		{
			$log->log(level=>"error", message=>"Invalid configuration option value for 'pwhoisd.pidfile'\n");
			exit(-1);
		}
	}
	
	if(defined($config{'pwhoisd.bind'}))
	{
		if($config{'pwhoisd.bind'} =~ /\d{1,3}\.\d{1.3}\.\d{1,3}\.\d{1.3}/) {
			$opt{'bind'} = $config{'pwhoisd.bind'};
		}
		else
		{
			$log->log(level=>"error", message=>"Invalid configuration option value for 'pwhoisd.bind'\n");
			exit(-1);
		}
	}
	
	if(defined($config{'pwhoisd.port'}))
	{
		if($config{'pwhoisd.port'} =~ /\d+/) {
			$opt{'port'} = $config{'pwhoisd.port'};
		}
		else
		{
			$log->log(level=>"error", message=>"Invalid configuration option value for 'pwhoisd.port'\n");
			exit(-1);
		}
	}
	
	if(defined($config{'pwhoisd.uid'}))
	{
		if($config{'pwhoisd.uid'} =~ /\d+/) {
			$opt{'uid'} = $config{'pwhoisd.uid'};
		}
		else
		{
			$log->log(level=>"error", message=>"Invalid configuration option value for 'pwhoisd.uid'\n");
			exit(-1);
		}
	}
	
	if(defined($config{'pwhoisd.gid'}))
	{
		if($config{'pwhoisd.gid'} =~ /\d+/) {
			$opt{'gid'} = $config{'pwhoisd.gid'};
		}
		else
		{
			$log->log(level=>"error", message=>"Invalid configuration option value for 'pwhoisd.gid'\n");
			exit(-1);
		}
	}
	
	if(defined($config{'pwhoisd.default.queries.max'}))
	{
		if($config{'pwhoisd.default.queries.max'} =~ /\d+/) {
			$opt{'limit-max-queries'} = $config{'pwhoisd.default.queries.max'};
		}
		else
		{
			$log->log(level=>"error", message=>"Invalid configuration option value for 'pwhoisd.default.queries.max'\n");
			exit(-1);
		}
	}

	if(defined($config{'pwhoisd.router-id'}))
	{
		$opt{'router-id'} = $config{'pwhoisd.router-id'};
	}
}
# open the database and create prepared statements
sub openDatabase()
{	
	$dbh = DBI->connect($dsn, $user, $password,
                  { RaiseError => 1, AutoCommit => 0 }) 
		or die "Can't open database: $DBI::errstr";

	$log->log(level=>"debug", message=>"Database opened dsn=$dsn\n") if $DEBUG eq 'true'; 

	$select_from_bgp_routes_sth = $dbh->prepare(qq(
				SELECT id,router_id,network,cidr,next_hop,asn,asn_paths,createDate,modifyDate,status,best_route
				FROM bgp_routes WHERE best_route=1 AND status=1 ORDER BY network ASC
		))
		or die "Can't prepare statement: $DBI::errstr";
	
	$select_from_bgp_routes_by_routerid_sth = $dbh->prepare(qq(
				SELECT id,router_id,network,cidr,next_hop,asn,asn_paths,createDate,modifyDate,status,best_route
				FROM bgp_routes WHERE best_route=1 AND status=1 AND router_id=? ORDER BY network ASC
		))
		or die "Can't prepare statement: $DBI::errstr";

	$select_from_bgp_routes_by_prefix_sth = $dbh->prepare(qq(
				SELECT next_hop,asn,asn_paths,createDate,modifyDate,best_route FROM bgp_routes
				WHERE status=1 AND network=? AND cidr=? 
				ORDER BY best_route DESC, next_hop ASC, asn ASC
		))
		or die "Can't prepare statement: $DBI::errstr";

	# select all the routes by prefix which are best-routes only
	$select_from_bgp_routes_by_prefix_bestonly_sth = $dbh->prepare(qq(
				SELECT next_hop,asn,asn_paths,createDate,modifyDate,best_route FROM bgp_routes
				WHERE status=1 AND best_route=1 AND network=? AND cidr=? 
				ORDER BY best_route DESC, next_hop ASC, asn ASC
		))
		or die "Can't prepare statement: $DBI::errstr";


	# select all the routes by prefix only for the one router-id
	$select_from_bgp_routes_by_prefix_by_routerid_sth = $dbh->prepare(qq(
                SELECT next_hop,asn,asn_paths,createDate,modifyDate,best_route FROM bgp_routes
                WHERE status=1 AND network=? AND cidr=? AND router_id=? 
                ORDER BY best_route DESC, next_hop ASC, asn ASC
		))
		or die "Can't prepare statement: $DBI::errstr";

	# select all the routes by prefix only for the one router-id and are best-routes only
	$select_from_bgp_routes_by_prefix_by_routerid_bestonly_sth = $dbh->prepare(qq(
                SELECT next_hop,asn,asn_paths,createDate,modifyDate,best_route FROM bgp_routes
                WHERE status=1 AND best_route=1 AND network=? AND cidr=? AND router_id=? 
                ORDER BY best_route DESC, next_hop ASC, asn ASC
		))
		or die "Can't prepare statement: $DBI::errstr";

	# select all the routes by prefix (without CIDR)
   	$select_from_bgp_routes_by_prefix_nocidr_sth = $dbh->prepare(qq(
                SELECT next_hop,asn,asn_paths,createDate,modifyDate,best_route FROM bgp_routes
				WHERE status=1 AND network=?
				ORDER BY best_route DESC, next_hop ASC, asn ASC
		))
		or die "Can't prepare statement: $DBI::errstr";

	# select all the routes by prefix (without CIDR) and are best-routes only
   	$select_from_bgp_routes_by_prefix_nocidr_bestonly_sth = $dbh->prepare(qq(
                SELECT next_hop,asn,asn_paths,createDate,modifyDate,best_route FROM bgp_routes
				WHERE status=1 AND best_route=1 AND network=?
				ORDER BY best_route DESC, next_hop ASC, asn ASC
		))
		or die "Can't prepare statement: $DBI::errstr";

	# select all the routes by prefix (without CIDR) only for the one router-id
  	$select_from_bgp_routes_by_prefix_nocidr_by_routerid_sth = $dbh->prepare(qq(
                SELECT next_hop,asn,asn_paths,createDate,modifyDate,best_route FROM bgp_routes
                WHERE status=1 AND network=? AND router_id=?
                ORDER BY best_route DESC, next_hop ASC, asn ASC
		))
		or die "Can't prepare statement: $DBI::errstr";

	# select all the routes by prefix (without CIDR) only for the one router-id and are best-routes only
  	$select_from_bgp_routes_by_prefix_nocidr_by_routerid_bestonly_sth = $dbh->prepare(qq(
                SELECT next_hop,asn,asn_paths,createDate,modifyDate,best_route FROM bgp_routes
                WHERE status=1 AND best_route=1 AND network=? AND router_id=?
                ORDER BY best_route DESC, next_hop ASC, asn ASC
		))
		or die "Can't prepare statement: $DBI::errstr";

	# search for all routes by source-as
 	$select_from_bgp_routes_by_source_as_sth = $dbh->prepare(qq(
                SELECT network,cidr,next_hop,asn,asn_paths,createDate,modifyDate,best_route FROM bgp_routes
				WHERE status=1 AND asn=? 
				ORDER BY best_route DESC, next_hop ASC, asn_paths ASC
		))
		or die "Can't prepare statement: $DBI::errstr";

	# search for all routes by source-as that are only marked best-route
	$select_from_bgp_routes_by_source_as_bestonly_sth = $dbh->prepare(qq(
                SELECT network,cidr,next_hop,asn,asn_paths,createDate,modifyDate,best_route FROM bgp_routes
				WHERE status=1 AND best_route=1 AND asn=? 
				ORDER BY best_route DESC, next_hop ASC, asn_paths ASC
		))
		or die "Can't prepare statement: $DBI::errstr";

	# search for all routes by source-as only from the one router-id
	$select_from_bgp_routes_by_source_as_by_routerid_sth = $dbh->prepare(qq(
                SELECT network,cidr,next_hop,asn,asn_paths,createDate,modifyDate,best_route FROM bgp_routes
                WHERE status=1 AND asn=? AND router_id=?
                ORDER BY best_route DESC, next_hop ASC, asn_paths ASC
		))
		or die "Can't prepare statement: $DBI::errstr";

	# search for all routes by source-as only from the one router-id and only best routes
	$select_from_bgp_routes_by_source_as_by_routerid_bestonly_sth = $dbh->prepare(qq(
                SELECT network,cidr,next_hop,asn,asn_paths,createDate,modifyDate,best_route FROM bgp_routes
                WHERE status=1 AND best_route=1 AND asn=? AND router_id=?
                ORDER BY best_route DESC, next_hop ASC, asn_paths ASC
		))
		or die "Can't prepare statement: $DBI::errstr";

	$select_from_pwhois_acl_sth = $dbh->prepare(qq(
				SELECT ip,max_count,status FROM pwhois_acl WHERE status=1 or status=0
		)) or die "Can't prepare statement: $DBI::errstr";

	# receive a list of the routers we have received routes from
	# or are announcing "active" best route prefixes from the route view feed
	$select_from_bgp_routes_next_hops_sth = $dbh->prepare(qq(
				SELECT next_hop FROM bgp_routes WHERE best_route=1 AND status=1
				GROUP BY next_hop ORDER BY next_hop ASC
		)) or die "Can't prepare statement: $DBI::errstr"; 

  	# receive a list of the routers we have received routes from
	# or are announcing "active" best route prefixes from the route view feed (only from the one router-id)
	$select_from_bgp_routes_next_hops_by_routerid_sth = $dbh->prepare(qq(
				SELECT next_hop FROM bgp_routes WHERE best_route=1 AND status=1 AND router_id=?
				GROUP BY next_hop ORDER BY next_hop ASC
		)) or die "Can't prepare statement: $DBI::errstr";

}

# close down the database connection
sub closeDatabase()
{
	if(defined($log)) {
		$log->log(level=>"debug", message=>"Database closed dsn=$dsn\n") if $DEBUG eq 'true';
	}
	undef $select_from_bgp_routes_sth;
	undef $select_from_bgp_routes_by_routerid_sth;
	undef $select_from_bgp_routes_by_prefix_sth;
	undef $select_from_bgp_routes_by_prefix_bestonly_sth;
	undef $select_from_bgp_routes_by_prefix_by_routerid_sth;
	undef $select_from_bgp_routes_by_prefix_by_routerid_bestonly_sth;
	undef $select_from_bgp_routes_by_prefix_nocidr_sth;
	undef $select_from_bgp_routes_by_prefix_nocidr_bestonly_sth;
	undef $select_from_bgp_routes_by_prefix_nocidr_by_routerid_sth;
	undef $select_from_bgp_routes_by_prefix_nocidr_by_routerid_bestonly_sth;
	undef $select_from_bgp_routes_by_source_as_sth;
	undef $select_from_bgp_routes_by_source_as_bestonly_sth;
	undef $select_from_bgp_routes_by_source_as_by_routerid_sth;
	undef $select_from_bgp_routes_by_source_as_by_routerid_bestonly_sth;
	undef $select_from_pwhois_acl_sth;
	undef $select_from_bgp_routes_next_hops_sth;
	undef $select_from_bgp_routes_next_hops_by_routerid_sth;
	$dbh->disconnect if defined($dbh);
}

sub getHelp()
{
return "$PROGNAME ($VERSION) $COPYRIGHT\n".
qq{
Help is here: 

DESCRIPTION

       Prefix WhoIs displays the origin-as and other interesting information
related to the most specific prefix currently advertised within the
Internet's global routing table that corresponds to the IP address in
your query.

       The only mandatory parameter is an IP address (optionally            
in CIDR notation, though pwhois assumes a /32 prefix).  You may
provide IP addresses with port numbers, though the port numbers will be 
removed and not returned to you in the result.  You may optionally use 
the "type" operator to change the display format between the default pwhois 
format, RPSL (RFC 2622), and the format used by Cymru (see www.cymru.com).


STANDARD QUERY CONSTRUCTION

       [type=pwhois|cymru|rpsl] <ip_address[/bits]>

BULK QUERIES

Prefix WhoIs supports bulk queries using optional commands.  Only
native and Cymru display types support bulk output.  When submitting a bulk 
query, simply make the term "bulk" or "begin" the first item/line sent in 
your query.  Then, you may optionally set the "type" attribute and then 
enter one IP address per line.  To signify the end of your query, simply 
provide "quit" or "end" as the last line of your query.

EXAMPLES (single query)
				               
       "1.2.3.4"  or  "type=cymru 1.2.3.4"  or  "type=rpsl 1.2.3.4"

EXAMPLES (bulk query)
					               
       begin                   begin
       type=cymru              1.2.3.4:80
       1.2.3.4                 5.6.7.8 
       5.6.7.8/32              ...
       ...                     end
       end

The "netcat" may be used to easily submi bulk queries.  To do so, simply
write your query to a file and concatenate the file contents into
netcat like so:

       \$ netcat <any-pwhois-server> 43 < ./ip_list.txt

Our "WhoB" whois client and our lightweight whois library also support bulk 
queries. Both are available at http://www.pwhois.org/

HELP AND STATUS QUERIES

       The "help" command or "?" displays this help text.

       The "version" command displays the PWHOIS server version
and other details such as the date of the last routing table cache update and 
the number of prefixes in the global table.

        The "peers" command displays the (route server's) peer IP addresses 
from which the PWHOIS server is receiving data.

        These queries are simply:

        [help|version|peers]

SOFTWARE

	You may download this source code and run your own PWHOIS server.  See 
http://www.pwhois.org/ for more details.

DISCLAIMER

	The PWHOIS service is provided for informational purposes only.  We do 
not guarantee its accuracy. By submitting a query, you agree to abide by the 
following terms of use: the compilation, repackaging, dissemination or other 
use of this data is expressly prohibited without our prior written consent. 
You agree not to use electronic processes that are automated to access or 
query this database except as reasonably necessary.  We reserve the right to 
restrict your access to this database in our sole discretion.  We may 
restrict or terminate your access to this database for failure to abide by 
these terms of use.  We reserve the right to modify these terms at any time.

AUTHORS AND THANKS

	The PWHOIS service was created and is maintained by the following
individuals and organizations, and wouldn't be possible without their time, 
energy, and on-going support. Thanks!

   Zachary Kanner, Victor Oppleman, Robb Ballard, Rob Thomas and Team Cymru,
   Rodney Joffe, and Brett Watson.
   
QUESTIONS OR COMMENTS

	Please send questions or comments about this service to:

	pwhois-support\@pwhois.org -- someone will get back to you shortly.
};
}

sub getExtraHelp()
{
return qq{
EXTRA SPECIAL COMMANDS 

	Another interesting query is the "route view" query which displays 
all the active routes in the global routing table cache (at the time of the 
last routing table update) for the prefix specified.

	A query with this feature may look like:

        "routeview prefix=1.2.3.4[/8]" 

	Another form is to search by source-as, showing all the prefixes being
announced by this source.

        "routeview source-as=12345"
		
	Optionally, these commands may be augmented with two type qualifiers, 
best and all, which display only the best routes, or all the routes, respectively.

If the type qualifier is not specified, the default behavior is 'best'.
		
	[type=best|all]
		
	A query with this feature may look like:
		
	"type=all routeview source-as=12345"
		
		or
		
	"type=best routeview prefix=1.2.3.4/24"
	
	Another command for "debugging" and other monitoring, is the statistics command.

	A command of this form may look like this:
	
	"statistics"
};
}

sub getVersion
{	
	my $cmd_line = shift;
	my $ip = shift;
	my $table_update;
	my $start_date;
	
	if(defined($last_table_update)) {
		$table_update = getDateTimeFormat($last_table_update);
	}
	else
	{
		$table_update = '(pending)';
	}
	
	if(defined($program_start_date)) {
		$start_date = getDateTimeFormat($program_start_date);
	}
	else
	{
		$start_date = '(pending)';
	}
	
	
	if(!$cmd_line) {
		if($opt{'bind'} ne '') {
	                        $ip = $opt{'bind'};
		}
	}
	else
	{
		if($opt{'bind'} ne '') {		
			$ip = $opt{'bind'};
		}
	}
		
return "$PROGNAME ($VERSION) $COPYRIGHT\n\n".
       
       "Server running on $ip:". $opt{'port'} ."\n".
       "Global BGP routing table cache last updated: $table_update\n".
       "Cache contains ". scalar(keys %routes) ." global prefixes from ". scalar(keys %peers) ." peers\n".
       "Server responded to ". $counters{'whois'} . " requests from ". $counters{'hosts'} ." unique IPs since: ". $start_date;
       
}

# get the routeview data from the database searching by prefix 
# (after getting the most specific network from the FIB first)
# based upon the IP address specified
sub getRouteviewPrefixSearchByIP($$)
{
	my ($ip, $search_type) = @_;
	my $cidr;
              
	# find the most specific prefix from the FIB -- use this prefix found
	my ($result, $network, $asn, $asn_paths, $cache_date) = findInNetwork($ip);
	if($result) {
		($ip,$cidr) = ipv4_parse($network);
	}
	else
	{
		return "Route for $ip not found in global routing table";
	}
	return getRouteviewPrefixSearchByCIDR($ip, $cidr, $search_type);
}

sub getRouteviewPrefixSearchByCIDR($$$)
{
	my ($ip,$cidr,$search_type) = @_;
	my $response;
	if(ipv4_is_valid_quaddot($ip .'/'. $cidr))
	{
		# some of the database entries do not have CIDR length, as classful routing is in Global RIB/FIB
		# our FIB has the default prefix length tacked on, but we need to search without the CIDR or we won't
		# find the data in the database -- this shouldn't matter as it is an exact network match search.
		
		my $sth;		
			
		if(defined($opt{'router-id'}))
		{
			if($search_type eq 'best')
			{
				$log->log(level=>"debug", message=>"Searching for routes by prefix=". $ip ."/". $cidr ." router-id=". $opt{'router-id'} . " and only best-routes\n") if opt{'verbose'} >= 4;
				$sth = $select_from_bgp_routes_by_prefix_nocidr_by_routerid_bestonly_sth;
				$sth->execute(ipv4_quaddot_to_decimal($ip), ipv4_quaddot_to_decimal($opt{'router-id'}));
				#$sth = $select_from_bgp_routes_by_prefix_by_routerid_bestonly_sth;
				#$sth->execute(ipv4_quaddot_to_decimal($ip), $cidr, ipv4_quaddot_to_decimal($opt{'router-id'}));
			}
			else
			{
				$log->log(level=>"debug", message=>"Searching for routes by prefix=". $ip ."/". $cidr ." router-id=". $opt{'router-id'} . " and all routes\n") if opt{'verbose'} >= 4;
				$sth = $select_from_bgp_routes_by_prefix_nocidr_by_routerid_sth;
				$sth->execute(ipv4_quaddot_to_decimal($ip), ipv4_quaddot_to_decimal($opt{'router-id'}));
				#$sth = $select_from_bgp_routes_by_prefix_by_routerid_sth;
				#$sth->execute(ipv4_quaddot_to_decimal($ip), $cidr, ipv4_quaddot_to_decimal($opt{'router-id'}));
			}
		}
		else
		{
			if($search_type eq 'best')
			{
				$log->log(level=>"debug", message=>"Searching for routes by prefix=". $ip ."/". $cidr ." and only best-routes\n") if $opt{'verbose'} >= 4;
				$sth =  $select_from_bgp_routes_by_prefix_nocidr_bestonly_sth;
				$sth->execute(ipv4_quaddot_to_decimal($ip));
				#$sth =  $select_from_bgp_routes_by_prefix_bestonly_sth;
				#$sth->execute(ipv4_quaddot_to_decimal($ip), $cidr);
			}
			else
			{
				$log->log(level=>"debug", message=>"Searching for routes by prefix=". $ip ."/". $cidr ." and all routes\n") if $opt{'verbose'} >= 4;
				$sth =  $select_from_bgp_routes_by_prefix_nocidr_sth;
				$sth->execute(ipv4_quaddot_to_decimal($ip));
				#$sth =  $select_from_bgp_routes_by_prefix_sth;
				#$sth->execute(ipv4_quaddot_to_decimal($ip), $cidr);
			}
		}
								
		my $count=0;
		while(my($next_hop, $asn, $asn_paths, $createDate, $modifyDate, $best_route) = $sth->fetchrow_array())
		{
			my $line;
				
			if($count == 0)
			{
				$response = "\n".
						    "Origin-AS: $asn\n".
						    "   Prefix: $ip/$cidr\n\n".
					
						    "    Create-Date             | Modify-Date              | Next-Hop        | AS-Path\n";
			}
				
			if($best_route)
			{
				$line = sprintf("*> %+24s | %+24s | %+15s | %-s\n", 
							    getDateTimeFormat($createDate), 
							    getDateTimeFormat($modifyDate), 
							    ipv4_decimal_to_quaddot($next_hop) , $asn_paths);
			}
			else
			{
				$line = sprintf("*  %+24s | %+24s | %+15s | %-s\n", 
							    getDateTimeFormat($createDate), 
							    getDateTimeFormat($modifyDate), 
							    ipv4_decimal_to_quaddot($next_hop) , $asn_paths);
				
			}
			$response .= $line;
			$count++;
		}

		$sth->finish();

		if($response eq '')
		{
			$response = "No prefixes found in routeview database for prefix=$ip/$cidr";
		}
	}
	else
	{
		$log->log(level=>"debug", message=>"Invalid ip address specified: $ip\n") if $DEBUG eq 'true';
		$response=$INVALID_INPUT; 
	}
	return $response;
}

# get the routes being announced by this source-as
sub getRouteviewSourceASSearch($$)
{
	my ($source_as, $search_type) = @_;
	my $response;
	my $sth;
	
	if(defined($opt{'router-id'}))
	{
		if($search_type eq 'best')
		{
			$log->log(level=>"debug", message=>"Searching for routes by source-as=". $source_as ." router-id=". $opt{'router-id'} 
												." and only best-routes\n") if $opt{'verbose'} >= 4;
												
			$sth = $select_from_bgp_routes_by_source_as_by_routerid_bestonly_sth;
			$sth->execute($source_as, ipv4_quaddot_to_decimal($opt{'router-id'}));
		}
		else
		{
			$log->log(level=>"debug", message=>"Searching for routes by source-as=". $source_as ." router-id=". $opt{'router-id'} 
												." and all routes\n") if $opt{'verbose'} >= 4;
												
			$sth = $select_from_bgp_routes_by_source_as_by_routerid_sth;
			$sth->execute($source_as, ipv4_quaddot_to_decimal($opt{'router-id'}));
		}
	}
	else
	{
		if($search_type eq 'best')
		{
			$log->log(level=>"debug", message=>"Searching for routes by source-as=". $source_as ." and only best-routes\n") if $opt{'verbose'} >= 4;
			$sth = $select_from_bgp_routes_by_source_as_bestonly_sth;
			$sth->execute($source_as);
		}
		else
		{
			$log->log(level=>"debug", message=>"Searching for routes by source-as=". $source_as ." and all routes\n") if $opt{'verbose'} >= 4;
			$sth = $select_from_bgp_routes_by_source_as_sth;
			$sth->execute($source_as);
		}
	}

	my $count=0;
	while(my($prefix, $network_cidr, $next_hop, $asn, $asn_paths, $createDate, $modifyDate, $best_route) = $sth->fetchrow_array())
	{
		my $line;
		my $network;

		if(defined($network_cidr))
		{
			$network = ipv4_decimal_to_quaddot($prefix) .'/'. $network_cidr;
		}
		else
		{
			$network = ipv4_decimal_to_quaddot($prefix);
		}

		if($count == 0)
		{
			$response = "\n".
					    "Origin-AS: $asn\n\n".

					    "    Prefix            | Create-Date              | Modify-Date              | Next-Hop        | AS-Path\n";
		}

		if($best_route)
		{
			$line = sprintf("*> %+18s | %+24s | %+24s | %+15s | %-s\n",
							$network,	
							getDateTimeFormat($createDate),
							getDateTimeFormat($modifyDate),
							ipv4_decimal_to_quaddot($next_hop) , $asn_paths);
		}
		else
		{
			$line = sprintf("*  %+18s | %+24s | %+24s | %+15s | %-s\n",
							$network,
							getDateTimeFormat($createDate),
							getDateTimeFormat($modifyDate),
							ipv4_decimal_to_quaddot($next_hop) , $asn_paths);
		}
		$response .= $line;
		$count++;
	}
	$sth->finish();

	if($response eq '')
	{
		$response = "No prefixes found in routeview database for source-as=$source_as";
	}
	return $response;
}

# get the list of peer routers
sub getPeers()
{
	my $response = "Peers:\n";
	foreach my $peer (sort keys %peers) {
		$response .= "$peer\n";
	}
	return $response;
}

# get the list of peer routers
sub getStatistics($)
{
	my $type = shift;
	my $response;
	if($type eq 'summary' or $type eq 'all')
	{
	   $response = "Server Statistics:\n".
				   "                  Unique IPs: ". $counters{'hosts'} ."\n".
				   "               Whois Queries: ". $counters{'whois'} ."\n".
				   "    Routeview Prefix Queries: ". $counters{'routeview.prefix'} ."\n".
				   " Routeview Source-AS Queries: ". $counters{'routeview.source-as'} ."\n".
				   "               Peers Queries: ". $counters{'peers'} ."\n\n";
	}
	
	if($type eq 'all')
	{			 
	  $response .= "Cache/ACL Statistics:\n".
				   "     Unique IPs: ". scalar(keys %requests) ."\n\n".
				   "   IP              | Count      | Limit      | First                    | Last\n";
	
		foreach my $ip (sort keys %requests)
		{
			my $line;
			if($requests{$ip}{'acl'} == 1) {
				$line = sprintf("+> %+15s | %+10s | %+10s | %+24s | %+24s\n",
								   $ip,	
								   $requests{$ip}{'count'},
								   $requests{$ip}{'limit'},
								   getDateTimeFormat($requests{$ip}{'firstQuery'}),
								   getDateTimeFormat($requests{$ip}{'lastQuery'})
								   );
				$response .= $line;
			}
			else
			{
				$line = sprintf("-> %+15s | %+10s | %+10s | %+24s | %+24s\n",
								   $ip,	
								   $requests{$ip}{'count'},
								   $requests{$ip}{'limit'},
								   getDateTimeFormat($requests{$ip}{'firstQuery'}),
								   getDateTimeFormat($requests{$ip}{'lastQuery'})
								   );
				$response .= $line;
			}
		}
	}
				
	return $response;
}


# the general request handler loop to parse out and determine what type of request it is
sub do_query
{
	my $req = shift;
	my $client = shift;
	my $client_ip = $socket->PeerAddr($client);
	my $display_type = 'pwhois';
	my $search_type = 'best';
	my $host_ip;
	my $response;
	my $application='unknown';
	if($connections{$client}{'bulk'}) {
		$application = $connections{$client}{'application'};
	}

	my $requestDate = time();

	$log->log(level=>"debug", message=>"Request received from $client_ip with query='$req'\n") if $opt{'verbose'} >= 4;

	return "Error: server is reloading internal datasets or it is too busy for your request.  Please try again in a few minutes."
		if $server_connections_paused;


	# check the request to make sure it is well formed

	if($req =~ /^\s?help|\?\s?$/i) {
		return getHelp();
	}
	if($req =~ /^\s?extra-help\s?$/i) {
		return getHelp() . getExtraHelp();
	}
	if($req =~ /^\s?version\s?$/i) {
		return getVersion(0, $socket->LocalAddr($client));
	}
	if($req =~ /^\s?statistics\s?$/i) {
		return getStatistics('summary');
	}
	if($req =~ /^\s?type=all statistics\s?$/i) {
		return getStatistics('all');
	}
	
	if($req =~ /^\s?(begin)|(bulk)$/i) {
		$connections{$client}{'bulk'} = 1;
		$connections{$client}{'bulk_end'} = 0;
		$connections{$client}{'bulk_count'} = 0;
		$log->log(level=>"debug", message=>"bulk mode selected for client: $client_ip waiting for more data\n")		
			if $opt{'verbose'} >= 2;

		return '';
	}
	if($req =~ /^\s?(end)|(quit)$/i) {
		$connections{$client}{'bulk_end'} = 1;
		$log->log(level=>"debug", message=>"end of bulk mode found from client: $client_ip ... closing connection.\n")
			if $opt{'verbose'} >= 2;
		return;
	}
	
	# match app="blah blah" or app=blah
	if($req =~ /^\s?app=[A-z0-9\-.]+|\"[A-z0-9\-. ]+\"\s?/i)
	{
		my ($app,$rest) = ($req =~ /^\s?app=([A-z0-9\-.]+|\"[A-z0-9\-. ]+\")\s?(.*)\s?$/i);
		if($connections{$client}{'bulk'}) {
			$connections{$client}{'application'} = $app;
		}
		if($app ne '') {
			$app =~ s/\"//g;
			$application = $app;
		}
		$log->log(level=>"debug", message=>"setting the application to \'$application\' rest=\'$rest\' ... \n") if $opt{'verbose'} >= 2;
		$req = $rest;
	}
	
	# match type=XXX on separate line (in bulk mode only)
	if($req =~ /^\s?type=((pwhois)|(cymru)|(rpsl))\s?$/i)
	{
		my $type = $1;
		if($connections{$client}{'bulk'})
		{
		
			if(!defined($connections{$client}{'displayType'}))
			{
				$connections{$client}{'displayType'} = $type;
				$display_type = 'cymru';
				$log->log(level=>"debug", message=>"setting the display type to ". $connections{$client}{'displayType'} ." for bulk session\n") 
						if $opt{'verbose'} >= 2; 
			}
			return;
		}
		else
		{
			return $INVALID_INPUT; 
		}

	}
	# match type=XXX not on a separate line but on the same line as the query
	if($req =~ /^\s?type=(pwhois|cymru|rpsl)\s?(.*)/i)
	{	
		$display_type = $1;
		my $rest = $2;
		$log->log(level=>"debug", message=>"setting the display type to \'$display_type\' rest=\'$rest\' ... \n") if $opt{'verbose'} >= 2; 
		$req = $rest;
	}
	# match type=XXX not on a separate line but on the same line as the query (search-type)
	if($req =~ /^\s?type=(all|best)\s?(.*)/i)
	{	
		$search_type = $1;
		my $rest = $2;
		$log->log(level=>"debug", message=>"setting the search type to $search_type\n") if $opt{'verbose'} >= 2; 
		$req = $rest;
	}

	# perform queries, only after checking limits
	
	# check to make sure the client IP hasn't exceeded their request limit
	if(defined($requests{$client_ip}))
	{
		# check to see if the user has been restricted entirely
		if(defined($requests{$client_ip}{'acl'})) {
			if($requests{$client_ip}{'acl'} == 0) {
				$log->log(level=>"debug", message=>"Request not allowed from $client_ip - ACCESS DENIED\n") if $opt{'verbose'} >= 1;
				return "Error: Unable to access server at this time - limit exceeded or access is denied.";
			}
		}
		
		my $count = $requests{$client_ip}{'count'};
		my $limit = $requests{$client_ip}{'limit'};
		# if the source IP has exceeded their query limit in the past 24 hours, bail out and
		# don't let them continue.  If the limit is 0, then they can submit unlimited queries
		if($count >= $limit and $requests{$client_ip}{'lastQuery'} > $requestDate - 1*24*3600 and $limit > 0)
		{
			$log->log(level=>"info", message=>"Request not processed -- limit exceeded by $client_ip\n") if $opt{'verbose'} >= 1;
			return "Error: Unable to perform lookup; Query limit exceeded.";	
		}
		elsif($count >= $limit and $requests{$client_ip}{'lastQuery'}  <= $requestDate - 1*24*3600 and $limit > 0)
		{
			# update the count so the user can start over
			$requests{$client_ip}{'count'} == 0;	
			$requests{$client_ip}{'lastQuery'} = $requestDate;	        
		}
		else
		{
			if($count == 0) {
				$counters{'hosts'}++;
			}
			
			$requests{$client_ip}{'count'}++;
			$requests{$client_ip}{'lastQuery'} = $requestDate;
			if(!defined($requests{$client_ip}{'firstQuery'})){
				$requests{$client_ip}{'firstQuery'} = $requestDate;
			}
		}
	}
	else 
	{
		$counters{'hosts'}++;		# total count of unique client requests
		$requests{$client_ip}{'count'}=1;
		$requests{$client_ip}{'lastQuery'} = $requestDate; 
		$requests{$client_ip}{'firstQuery'} = $requestDate;
		$requests{$client_ip}{'limit'} = $opt{'limit-max-queries'};
	}

  	# search for all prefixes (as stored in our database snapshot) after first finding most specific from FIB
	# and return all the routers, Source-AS, Next-Hops, and AS-Paths (no CIDR specified, just the IP)
	if($req =~ /^\s?routeview prefix=\d{0,3}\.\d{0,3}\.\d{0,3}\.\d{0,3}\s?$/i)
	{
		my($ip)=($req =~ /^\s?routeview prefix=(\d{0,3}\.\d{0,3}\.\d{0,3}\.\d{0,3})\s?$/i);
		$log->log(level=>"info", message=> getCurrentSyslogDateTime() ." Performing routeview prefix query for $ip src=$client_ip app=\"$application\" count=".
										   $requests{$client_ip}{'count'} ." limit=". $requests{$client_ip}{'limit'} ."\n") if $opt{verbose} >= 1;

		$counters{'routeview.prefix'}++;
		
		# pass the ip only (the CIDR will be found from the FIB)
		return getRouteviewPrefixSearchByIP($ip, $search_type);
	}
			
	# search for all prefixes (as stored in our database snapshot) -- by a specific prefix
	# and return all the routers, Source-AS, Next-Hops, and AS-Paths
	if($req =~ /^\s?routeview prefix=\d{0,3}\.\d{0,3}\.\d{0,3}\.\d{0,3}\/\d{1,2}\s?$/i)
	{
		my($ip, $cidr)=($req =~ /^\s?routeview prefix=(\d{0,3}\.\d{0,3}\.\d{0,3}\.\d{0,3})\/(\d{1,2})\s?$/i);
		$log->log(level=>"info", message=> getCurrentSyslogDateTime() ." Performing routeview prefix db query for $ip/$cidr src=$client_ip app=\"$application\" count=".
										   $requests{$client_ip}{'count'} ." limit=". $requests{$client_ip}{'limit'} ."\n") if $opt{verbose} >= 1;
		
		$counters{'routeview.prefix'}++;
		
		# pass the ip and cidr prefix		
		return getRouteviewPrefixSearchByCIDR($ip, $cidr, $search_type);
	}
	
	# search for all prefixes (as stored in our database snapshot)
	# and return all the routers, Source-AS, Next-Hops, and AS-Paths
	if($req =~ /^\s?routeview source-as=\d{1,7}\s?$/i)
	{
		my($source_as)=($req =~ /^\s?routeview source-as=(\d{1,7})\s?$/i);
		$log->log(level=>"info", message=> getCurrentSyslogDateTime() ." Performing routeview source-as db query for $source_as src=$client_ip app=\"$application\" count=".
		                                   $requests{$client_ip}{'count'} ." limit=". $requests{$client_ip}{'limit'} ."\n") if $opt{verbose} >= 1;
		
		$counters{'routeview.source-as'}++;
		
		# pass the source-as 
		return getRouteviewSourceASSearch($source_as, $search_type);
	}
	# get a list of the peer routers
	if($req =~ /^\s?peers/i)
	{
		$log->log(level=>"info", message=> getCurrentSyslogDateTime() ." Performing peers request src=$client_ip app=\"$application\" count=".
		                                   $requests{$client_ip}{'count'} ." limit=". $requests{$client_ip}{'limit'} ."\n") if $opt{verbose} >= 1;

		$counters{'peers'}++;
		
		return getPeers();
	}

	# regular prefix whois queries (no extra fields or parameters)
	
	# IP notation	
	if($req =~ /^\s?\d{0,3}\.\d{0,3}\.\d{0,3}\.\d{0,3}\s?$/)
	{
		my($ip)=($req =~ /^\s?(\d{0,3}\.\d{0,3}\.\d{0,3}\.\d{0,3})\s?$/);
		if(ipv4_is_valid_quaddot($ip)) {
			$host_ip = $ip;
		}
		else
		{
		        $log->log(level=>"debug", message=>"Invalid ip address specified: $ip\n") if $DEBUG eq 'true';
			return $INVALID_INPUT; 
		}
		
	} # prefix notation
	elsif($req =~ /^\s?\d{0,3}\.\d{0,3}\.\d{0,3}\.\d{0,3}\/\d{1,2}\s?$/)
	{
		my($ip,$cidr)=($req =~ /^\s?(\d{0,3}\.\d{0,3}\.\d{0,3}\.\d{0,3})\/(\d{1,2})\s?$/);
			
		if(ipv4_is_valid_quaddot($ip)) {			
			$host_ip = $ip;
		}
		else
		{
			$log->log(level=>"debug", message=>"Invalid ip address specified: $ip\n") if $DEBUG eq 'true';
			return $INVALID_INPUT;
		}
	} # ip/port notation	
	elsif($req =~ /^\s?\d{0,3}\.\d{0,3}\.\d{0,3}\.\d{0,3}:\d{1,6}\s?$/i)
	{
		my($ip,$port)=($req =~ /^\s?(\d{0,3}\.\d{0,3}\.\d{0,3}\.\d{0,3}):(\d{1,6})\s?$/i);
			
		if(ipv4_is_valid_quaddot($ip)) {			
			$host_ip = $ip;
		}
		else
		{
			$log->log(level=>"debug", message=>"Invalid ip address specified: $ip\n") if $DEBUG eq 'true';
			return $INVALID_INPUT;
		}
	}
	else
	{
	        $log->log(level=>"debug", message=>"Invalid request specified: $req\n") if $DEBUG eq 'true';
		return $INVALID_INPUT; 
	}

	$log->log(level=>"info", message=> getCurrentSyslogDateTime() ." Performing whois query for $host_ip src=$client_ip app=\"$application\" count=". 
				$requests{$client_ip}{'count'} ." limit=". $requests{$client_ip}{'limit'} ."\n") if $opt{verbose} >= 1;

	# we are now going to process the query -- count this as a hit regardless of the response
	$counters{'whois'}++;
	
	# take the IP address provided and find the network address that matches
	# The most specific.  Then return the ASN for that netblock based upon
	# The BGP routing table (FIB) that is cached in memory.

	my ($result, $network, $asn, $asn_paths, $cache_date) = findInNetwork($host_ip);
	if($result) {
	
		if($display_type eq 'cymru' or 
		  ($connections{$client}{'bulk'} and $connections{$client}{'displayType'} eq 'cymru'))
		{
			if($connections{$client}{'bulk'} == 0 or 
				($connections{$client}{'bulk'} and 
				$connections{$client}{'bulk_count'} == 0)) 
			{
				$response = "Origin-AS | IP              | Prefix             | AS-Path\n"; 
			}
			my $line = sprintf("%+9s | %+15s | %+18s | %-s", 
					$asn, $host_ip, $network , $asn_paths);
			$response .= $line;

			if($connections{$client}{'bulk'}) {
				$connections{$client}{'bulk_count'}++;
			}
		}
		elsif($display_type eq 'rpsl' and !$connections{$client}{'bulk'})
		{
			# origin: AS<asn> AS12266
			# route: <cidr_prefix> 4.0.0.0/8
			# date: <date of update in rpsl>
			# source: PWHOIS Server x.x.x.x:port at <date of update>

			$response = "origin: AS$asn\n" .
				    "route: $network\n" .
				    "date: ". getRpslDateFormat($cache_date) ."\n" .
				    "source: PWHOIS Server ". $socket->LocalAddr($client) .":". $opt{port} ." at ".
				    	getRpslDateFormat($cache_date);					
		}
		elsif($display_type eq 'pwhois' or # pwhois format
		     ($connections{$client}{'bulk'} and $connections{$client}{'displayType'} eq 'pwhois'))
		{		
			$response = "IP: $host_ip\n" .
				    "Origin-AS: $asn\n" .
				    "Prefix: $network\n" .
				    "AS-Path: $asn_paths\n" .
				    "Cache-Date: $cache_date";
		
			if($connections{$client}{'bulk'} and $connections{$client}{'bulk_end'} == 0) {
				$response .= "\n";
			}
		}
		else
		{
			return $INVALID_INPUT; 
		}
	}
	else # not found
	{	
		if($connections{$client}{'bulk'})
		{
			if($display_type eq 'cymru' or 
		  	  ($connections{$client}{'bulk'} and $connections{$client}{'displayType'} eq 'cymru'))
			{
				if($connections{$client}{'bulk_count'} == 0) {
					$response = "Origin-AS | IP              | Prefix             | AS-Path\n"; 
				}
				my $line = sprintf("%+9s | %+15s | %+18s | %-s", 
						0, "NULL", "NULL", "NULL");
				$response .= $line;

				$connections{$client}{'bulk_count'}++;
	
			}
			elsif($display_type eq 'rpsl')
			{
				return "No matching data found";
			}
			elsif($display_type eq 'pwhois' or 
		  	  ($connections{$client}{'bulk'} and $connections{$client}{'displayType'} eq 'pwhois'))
			{
				$response = "IP: $host_ip\n" .
					    "Origin-AS: 0\n" .
					    "Prefix: NULL\n" .
					    "AS-Path: NULL\n" .
					    "Cache-Date: NULL";
		
				if($connections{$client}{'bulk_end'} == 0) {
					$response .= "\n";
				}
				
			}
		}
		else
		{				
			$response = "That IP address doesn't appear in the global routing table as of ". getDateTimeFormat($last_table_update);
		}
	}
	return $response;
}

sub load_data()
{
	my $cacheDate = time();
	
	# clear any cached data (if any)	
		
	%routes = ();
	undef $pt; $pt = new Net::Patricia;

	my $routes_found=0;

	my $sth;

	if(defined($opt{'router-id'})) {
		$sth = $select_from_bgp_routes_by_routerid_sth;
		$sth->execute(ipv4_quaddot_to_decimal($opt{'router-id'}));
	}
	else
	{
		$sth = $select_from_bgp_routes_sth;
		$sth->execute();
	}
	
	# get the data from the database (currently about 150K rows)
	while(my($id,$router_id, $network,$cidr,$nextHop,$asn,$asn_paths,$createDate,$modifyDate,$status,$best_route) =
		$sth->fetchrow_array()) {

		# polulate the internal cache table with data from the database
		# add the values to the hash
		my $network_quaddot = ipv4_decimal_to_quaddot($network);
		my $route;
		if(defined($cidr)) {
			$route = $network_quaddot .'/'. $cidr;
		}
		else
		{
			# if the route doesn't have a CIDR prefix, get the default prefix for classful routing
			$route = ipv4_parse($network_quaddot .'/'. 
					ipv4_msk2cidr(ipv4_dflt_netmask($network_quaddot)));
		}
		
		if(defined($routes{$route})) {
			$log->log(level=>"error", message=>"Route $route is already in FIB with id=". 
						$routes{$route}{'id'} ." ... $id is replacing it.  Why?\n") if $opt{'verbose'} >= 1;
		}
		
		$routes{$route}{'class'} = 'bgp';
		$routes{$route}{'route'} = $route;
		$routes{$route}{'status'} = $status;
		$routes{$route}{'id'} = $id;
		$routes{$route}{'router_id'} = $router_id;
		$routes{$route}{'asn'} = $asn;
		$routes{$route}{'path'} = $asn_paths;
		$routes{$route}{'createDate'} = $createDate;
		$routes{$route}{'modifyDate'} = $modifyDate;
		$routes{$route}{'cacheDate'} = $cacheDate;
		
		# update the table update date from one of the records
		$last_table_update = $modifyDate if $routes_found == 0;
		
		$pt->add_string($route, $route);
		
		$routes_found++;
		$log->log(level=>"debug", message=>"Loading (best) network route: $route with asn=$asn id=$id.\n") if $opt{'verbose'} >= 10;
	}

	$sth->finish();
        $log->log(level=>"info", message=> getCurrentSyslogDateTime() ." BGP routing table cache loaded with ". scalar(keys %routes) . " routes.\n") if $opt{'verbose'} >= 1;

	%requests = ();
	# load our pwhois ACL data
	$select_from_pwhois_acl_sth->execute();
	while(my($ipdec, $max_count, $acl) = $select_from_pwhois_acl_sth->fetchrow_array()) {
		  my $ip = ipv4_decimal_to_quaddot($ipdec);
		  $requests{$ip}{'limit'} = $max_count;
		  $requests{$ip}{'lastQuery'} = 0;
		  $requests{$ip}{'firstQuery'} = 0;
		  $requests{$ip}{'count'} = 0;
		  $requests{$ip}{'acl'} = $acl;
		  
		  $log->log(level=>"info", message=> getCurrentSyslogDateTime() ." Setting ACL for $ip limit=$max_count acl=$acl\n") if $opt{'verbose'} >= 1;

	}
	$select_from_pwhois_acl_sth->finish();

	%peers = ();

	if(defined($opt{'router-id'})) {
		$sth = $select_from_bgp_routes_next_hops_by_routerid_sth;
		$sth->execute(ipv4_quaddot_to_decimal($opt{'router-id'}));
	}
	else
	{
		$sth = $select_from_bgp_routes_next_hops_sth;
		$sth->execute();
	}

	while(my($next_hop) = $sth->fetchrow_array()) 
	{
		my $ip = ipv4_decimal_to_quaddot($next_hop);
		$peers{$ip} = 1;
	}
	$sth->finish();
		
        $log->log(level=>"info", message=> getCurrentSyslogDateTime() ." Loaded peer information ... ". scalar(keys %peers) ." peers in feed(s).\n") if $opt{'verbose'} >= 1;
}

sub startServer()
{
	$socket = Net::Socket::NonBlock::Nest->new(SelectT  => 0.1,
        						SilenceT => 0,
							debug    => $^W,
							BuffSize => 10240,
							) or die "Error creating sockets nest: $@\n";
	
	$socket->Listen(LocalAddr => $opt{'bind'},
			LocalPort => $opt{'port'},
			Proto     => 'tcp',
			Accept    => \&clientConnect,
			SilenceT  => 0,
			ClientsST => 10,
			Listen    => 10,
			MaxClients => 10000
			)
			or die "Could not listen on port ". $opt{port} .": $@\n";
	
	my $localaddr = "*" if $opt{'bind'} eq '';
	$localaddr = $opt{'bind'} if $opt{'bind'} ne '';
	$log->log(level=>"info", message=>getCurrentSyslogDateTime() ." Whois server listening on pwhois://$localaddr:". $opt{'port'} ."\n") if $opt{verbose} >= 1;


	# drop previledges
	POSIX::setuid($opt{'uid'});
	POSIX::setgid($opt{'gid'});

	while($socket->IO())
	{	      
		$log->log(level=>"debug", message=>"Selecting on socket\n") if $opt{'verbose'} >= 10;
		foreach my $ClnSock (sort keys %connections)
		{
			my $req = undef;
			my $client_ip = $socket->PeerAddr($ClnSock);
			$log->log(level=>"debug", message=>"Receiving data from $client_ip\n") if $opt{'verbose'} >= 9;
			while(($req = $socket->Gets($ClnSock)) and length($req))
			{
				$log->log(level=>"debug", message=>"Data received from $client_ip: $req\n") if $opt{'verbose'} >= 9;
				my($response) = do_query($req, $ClnSock);						
				$socket->Puts($ClnSock, $response . "\n");
				$socket->IO();
				if($connections{$ClnSock}{'bulk'})
				{
					if($connections{$ClnSock}{'bulk_end'})
					{
						$socket->Close($ClnSock);
						delete($connections{$ClnSock});
					}
					else
					{
						# wait for additional commands
						$log->log(level=>"debug", message=>"Waiting for additional commands from: $client_ip\n")
							if $opt{'verbose'} >= 9;
					}
				}
				else
				{
					$log->log(level=>"debug", message=>"Connection closed: $client_ip\n") if $opt{'verbose'} >= 9;
					$socket->Close($ClnSock);
					delete($connections{$ClnSock});
				}
			}
			if(!defined($req)) {
				$socket->Close($ClnSock);
				delete($connections{$ClnSock});
			}
		}
	}

	$socket->Close();
			
}

sub clientConnect 
{
        my $client = shift;
	if(defined($client)) {
		my $client_ip = $socket->PeerAddr($client);
		$log->log(level=>"debug", message=>"Got connection from: $client_ip\n") if $opt{'verbose'} >= 9;
		#$connections{$client}{'socket'} = \$client;
		$connections{$client}{'bulk'} = 0;
		$connections{$client}{'bulk_end'} = 0;
		$connections{$client}{'application'} = 'unknown';
		return 1;
	}
	else
	{
		return 0;
	}
}

sub getDateTimeFormat($) {
	return Time::Format::time_format('Mon dd yyyy hh:mm{in}:ss tz', shift);
}

sub getRpslDateFormat($) {
	return Time::Format::time_format('yyyymmdd', shift);
}

sub getSyslogDateTime($) {
	return Time::Format::time_format('Mon ?d yyyy hh:mm{in}:ss', shift);
}

sub getCurrentSyslogDateTime() {
	return getSyslogDateTime(time());
}



sub DESTROY {
   my $self = shift;
   # clean up lock and temp files
   #$self->SUPER::DESTROY
}

## check the routing table to see if the
# ip address is within one of the networks.  If so
# set the community name (class) and network route
# address that matches.
sub findInNetwork
{
        my $dest = shift;
		
        if($dest eq '')  {
                warn "No ip address specified.\n";
                return (0, '', '');
        }
	
	die "Ip address can not be null\n"  if !defined($dest);

	#	$log->log(level=>'debug', message=>"Attempting to find '$dest' in RIB.\n")
	#	if $opt{verbose} >= 9 and $DEBUG eq 'true';
  
        # parse the dest. address to determine netmask -- default to /32 if non-provided
        my ($ip,$cidr) = ipv4_parse($dest);

	my $node = $pt->match_string($dest);
	if(defined($node)) {
		my $network = $node;                
                my $asn;
		my $path;
		my $cache_date;
		my ($network_ip, $network_cidr) =  ipv4_parse($network);
		$network_cidr =  ipv4_msk2cidr( ipv4_dflt_netmask($network_ip)) if !defined($network_cidr);

		$log->log(level=>'debug', message=>"comparing $ip is in network $network ($network_ip/$network_cidr) ... \n") if $opt{verbose} >= 4 and $DEBUG eq 'true';
		if(ipv4_in_network($network_ip, $network_cidr, $ip, $cidr))
		{

                	if($routes{$network}{'class'} eq 'bgp') {

				$path = $routes{$network}{'path'};
                       		$asn = $routes{$network}{'asn'};
				$cache_date = $routes{$network}{'cacheDate'};
                	}                        
                	else
                	{
                        	$log->log(level=>'error',
                	          	  message=>" woah ... unknown routing table entry found for $network\n")
                            		if $opt{verbose} and $DEBUG eq 'true';
                	}

                	$log->log(level=>'debug',
                          	  message=>" found $ip in network $network with most specific asn $asn\n")
                		if $opt{verbose} >= 7 and $DEBUG eq 'true';
            		return (1, $network, $asn, $path, $cache_date);
		}
        }
        $log->log(level=>'debug', message=>"$ip was not found in the routing table.\n") if $opt{verbose} >= 7 and $DEBUG eq 'true';
        return (0,'','','','');
}

sub ipv4_is_valid_quaddot($)
{
	my $ip = shift;

	if($ip =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)
	{
		my (@octets) = split /\./, $ip;
		foreach (@octets) {
			return 0 if $_  < 0 or $_ > 255;
		}
	}
	elsif($ip =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/)
	{
		my ($ip_addr, $cidr) = split /\//, $ip;
	 	my (@octets) = split /\./, $ip_addr;
	        foreach (@octets) {
			return 0 if $_  < 0 or $_ > 255;
		}

		if($cidr < 0 or $cidr > 32) {
			return 0;	
		}
	}
	else
	{
		return 0;
	}
	# valid quaddot
	return 1;
}


sub contains 
{
	my $value = shift;
	my @array = @_;

        foreach my $av (@array) {
#		$log->log(level=>"debug", message=>"Comparing value $value eq $av\n") if $DEBUG eq 'true';
		return 1 if $value == $av;
	}
	return 0;
}


#  Some of these ipv4 functions are from the IPv4Addr module.  I had to modify
# some of the to work appropriately for our use here.
#
#    Author: Francis J. Lacoste <francis@Contre.COM>
#
#    Copyright (C) 1999 Francis J. Lacoste, iNsu Innovations Inc.
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms as perl itself.

my $ip_rgx = "\\d+\\.\\d+\\.\\d+\\.\\d+";

# Given an IPv4 address in host, ip/netmask or cidr format
# returns a ip / cidr pair.
sub ipv4_parse($;$) {
  my ($ip,$msk);
  # Called with 2 args, assume first is IP address
  if ( defined $_[1] ) {
    $ip = $_[0];
    $msk= $_[1];
  } else {
    ($ip)  = $_[0] =~ /($ip_rgx)/o;
    ($msk) = $_[0] =~ m!/(.+)!o;
  }

  # Remove white spaces
  $msk =~ s/\s//g if defined $msk;

  # Check Netmask to see if it is a CIDR or Network
  if (defined $msk ) {
    if ($msk =~ /^\d{1,2}$/) {
      # Check cidr
      warn ": invalid cidr: ". $msk ."\n"
        if $msk < 0 or $msk > 32;
    } elsif ($msk =~ /^$ip_rgx$/o ) {
      $msk = ipv4_msk2cidr($msk);
    } else {
      warn ": invalid netmask specification: ". $msk ."\n";
    }
  } else {
    # Host
    return $ip;
  }
  wantarray ? ($ip,$msk) : "$ip/$msk";
}

sub ipv4_dflt_netmask($) {
  my ($ip) = ipv4_parse($_[0]);

  my ($b1) = split /\./, $ip;

  return "255.0.0.0"	if $b1 <= 127;
  return "255.255.0.0"	if $b1 <= 191;
  return "255.255.255.0";
}

# Transform a netmask in a CIDR mask length
sub ipv4_msk2cidr($) {
  my $msk = $_[0];
  my @bytes = split /\./, $msk;
  my $cidr = 0;
  for (@bytes) {
    my $bits = unpack( "B*", pack( "C", $_ ) );
    $cidr +=  $bits =~ tr /1/1/;
  }
  return $cidr;
}

# Transform a CIDR mask length in a netmask
sub ipv4_cidr2msk($) {
  my $cidr = shift;
  my $bits = "1" x $cidr . "0" x (32 - $cidr);
  return join ".", (unpack 'CCCC', pack("B*", $bits ));
}

# Return the network address of
# an IPv4 address
sub ipv4_network($;$) {
  my ($ip,$cidr) = ipv4_parse( $_[0], $_[1] );

  # If only an host is given, use the default netmask
  unless ($cidr) {
    $cidr = ipv4_msk2cidr( ipv4_dflt_netmask($ip) );
  }
  my $u32 = unpack "N", pack "CCCC", split /\./, $ip;
  my $bits = "1" x $cidr . "0" x (32 - $cidr );

  my $msk = unpack "N", pack "B*", $bits;

  my $net = join ".", unpack "CCCC", pack "N", $u32 & $msk;

  wantarray ? ( $net, $cidr) : "$net/$cidr";
}

sub ipv4_in_network($$;$$) {
  my ($ip1,$cidr1,$ip2,$cidr2);
  if ( @_ >= 3) {
  	($ip1,$cidr1) = ipv4_parse( $_[0], $_[1] );
	($ip2,$cidr2) = ipv4_parse( $_[2], $_[3] );
  } else {
	($ip1,$cidr1) = ipv4_parse( $_[0]);
	($ip2,$cidr2) = ipv4_parse( $_[1]);
  }

  # Check for magic addresses.
  return 0 if $ip1 eq "255.255.255.255" or $ip1 eq "0.0.0.0";
  return 0 if $ip2 eq "255.255.255.255" or $ip2 eq "0.0.0.0";
			  
  # Case where first argument is really a host
  return $ip1 eq $ip2 unless ($cidr1);
  # Case where second argument is an host
  if ( not defined $cidr2) {
	return ipv4_network( $ip1, $cidr1) eq ipv4_network( $ip2, $cidr1 );
  } elsif ( $cidr2 > $cidr1 ) {
  	# Netmask 2 is more specific than netmask 1
 	 return ipv4_network( $ip1, $cidr2) eq ipv4_network( $ip2, $cidr2);
  } else {
	# Netmask 1 is more specific than netmask 2
	return ipv4_network( $ip1, $cidr1) eq ipv4_network( $ip2, $cidr2);
  }
}

# convert quaddot to decimal integer
sub ipv4_quaddot_to_decimal($) {
	my $ip = shift;
	warn "invalid ip address provided" if !defined($ip);
	return -1 if !defined($ip);
	warn "invalid ip address provided: $ip" if(! $ip =~ /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/);
	return "" if(! $ip =~ /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/);
       
	return unpack "N", pack "CCCC", split /\./, $ip;
}

# convert decimal integer to quaddot
sub ipv4_decimal_to_quaddot($) {
	my $decimal = shift;
	warn "invalid decimal value provided" if !defined($decimal);
	return "" if !defined($decimal);
	
	warn "invalid decimal value provided: $decimal" if($decimal < 0 or $decimal > 4294967295);
	return "" if($decimal < 0 or $decimal > 4294967295);
        return join ".", unpack "CCCC", pack "N", $decimal;
}
 
my $obj = new Pwhoisd(); 
$obj->load_data() if !defined($opt{'no-load'});
$obj->startServer();

1;
