#!/usr/bin/perl
#
# $Id: IPAddress.pm,v 1.3 2001/11/24 02:55:56 levine Exp $
#
#
# Copyright (C) 2000, 2001  James D. Levine (jdl@vinecorp.com)
#
#
#   This program is free software; you can redistribute it and/or
#   modify it under the terms of the GNU General Public License
#   as published by the Free Software Foundation; either version 2
#   of the License, or (at your option) any later version.
# 
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
# 
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 
#   02111-1307, USA.
#
####################################################################


#
# Some useful static utility methods for IP addresses
#


use strict;
use Getopt::Long;
use PortScan::ScannedHost;
use PortScan::ScanContext;
use PortScan::DataStore;
use PortScan::ScanSet;

package PortScan::IPAddress;

use vars qw(@ISA);

@ISA = qw( Exporter );



# private.
# Evaluates a user-specified octet spec from and IP address to a listref.
# A numeric evaluates to itself; a-b evaluates to the list of all values from
# a to b, inclusive.  * evaluates to 0-255 inclusive.
# Returns a listref.

sub expand_ip
{
    my $s = shift;
    
    $s =~ s/\s//g;

    return [0..255] if $s eq '*';

    ($s =~ /(\d*)-(\d*)/) && return [$1 .. $2];

    ($s =~ /(\d*)/) && return [$1];

    return [];
}

#
# Expands a dotted-quad to all specified hosts.  
# For a given octed in the quad, a numeric evaluates to itself; a-b evaluates
# to the list of all values from a to b, inclusive.  * evaluates to 0-255
# inclusive.  CIDR notation is also supported.  (TODO: further test CIDR expansion)
# Returns a listref.


sub expand_host_range
{
    my $cidr = shift;
    $cidr =~ s/\s//g;

    my ($ip_string, $netbits) = split /\//, $cidr;
    my ($a, $b, $c, $d) = split /\./, $ip_string;


    return [] if ( !length($a) || !length($b) || !length($c) || !length($d) );

    $netbits = 32 if !length($netbits);

    my $net_whole_octets = int($netbits / 8);
    my $remainder = $netbits % 8;
    my $host_whole_octets = int ((32 - $netbits) / 8);

    my @final_octets = ();

    foreach my $octet ($a, $b, $c, $d)
    {
	if ($net_whole_octets > 0) 
	{
	    push @final_octets, $octet;
	    -- $net_whole_octets;
	}
	elsif ($remainder)
	{			
	    my $f =  ((255 << $remainder)   & $octet);
	    my $l = $f +((1 << (8 - $remainder))-1) ;
	    push @final_octets,  "${f}-${l}" ;

	    $remainder = 0;
	}
	elsif ($host_whole_octets > 0)
	{
	    push @final_octets, "1-255";
	    -- $host_whole_octets;
	}
    }

    my ($af, $bf, $cf, $df) = @final_octets;

    my $range = [];
    
    my $al = expand_ip($af);
    my $bl = expand_ip($bf);
    my $cl = expand_ip($cf);
    my $dl = expand_ip($df);

    foreach my $ah (@$al) {
	foreach my $bh (@$bl) {
	    foreach my $ch (@$cl) {
		foreach my $dh (@$dl) {
		    push @$range, "$ah.$bh.$ch.$dh";
		}
	    }
	}
    }

    $range;
} 



#
# Evaluates a text port specification, returning a listref of
# PortSpec instances.  A port may be specified as a number N
# or a range N-M, which evaluates to all between N and M
# inclusive.  The protocol and state are specified with modifiers:
#  o = open;  c = closed;  x = unfiltered; f = filtered;  u = udp; t = tcp
#
#  default: open, tcp
#
# Sample:   
#   23/t         tcp port 23, open
#   1-100/uf     udp ports 1-100, filtered

sub expand_port
{
    my ($string) = shift;

    $string =~ s/\s//g;

    # ports . [o | f | u | t | x] open, filtered, unfiltered, tcp, udp
#    my ($ports, $flags) = split /\./,$string; 

#    $string =~ /(\d*)(\w*)/;
    $string =~ /([\W-]*)(\w*)/;
    my ($ports, $flags) = ($1, $2);

    my @range = ();
    my $named_port = "";
#print "expanding $string \n"
#    ;
    ($string =~ /(\d+)-(\d+)(\w*)/) && (@range = ($1 .. $2), $flags = $3,  goto NEXT);

    ($string =~ /(\d+)(\w*)/) && (@range = ($1), $flags = $2,  goto NEXT);

#    ($string =~ /([\w\-]+)\.(\w+)/ ) && ($named_port = $1, $flags = $2, goto NEXT);
    ($string =~ /\./ ) && ( ($named_port, $flags) = split (/\./, $string) , goto NEXT);

    ($string =~ /([\w\-]+)$/ )  && ($named_port = $1, $flags = "", goto NEXT);


NEXT:
#print "flags $flags named_port $named_port\n"
#    ;
    my ($state, $proto) = ("open","tcp");
    
    ($flags =~ /o/) && ($state = "open");
    ($flags =~ /c/) && ($state = "closed");
    ($flags =~ /x/) && ($state = "unfiltered");
    ($flags =~ /f/) && ($state = "filtered");
    ($flags =~ /u/) && ($proto = "udp");
    ($flags =~ /t/) && ($proto = "tcp");

    @range = PortScan::PortSpec::known_port_by_name($named_port, $proto) if $named_port ne "";
#print "range @range\n"
#    ;
    my $result = [];

    return [] if $#range < 0;

    foreach my $port (@range)
    {
	push @$result, new PortScan::PortSpec($port, $state, $proto, "", "", "", "");
    }
    $result;
}


#
# Generates a listref of PortSpecs from a comma-separated string
# of text port specifications, as detailed for expand_port() above.
#

sub expand_port_range
{
    my $port_string = shift;	# comma-separated, port numbers

    my @list = split /,/,$port_string; 

    my $ports = [];

    foreach my $portspec (@list)
    {
	my $portlist = &expand_port($portspec);
	push @$ports, @$portlist;
    }

    $ports;
}



#
# Given a listref of text hostspecs and portspecs, create a full-blown
# ScanSet.  Each ScannedHost in the set gets the same set of PortSpecs,
# and has the default_ignored_state set as specified.

sub make_scanset
{
    my ($hspecs, $pspecs, $default_ignored_state) = @_;

    my $ports_listref;

    if (length $pspecs)
    { 
	$ports_listref =  expand_port_range($pspecs);
    }
    else
    {
	$ports_listref = PortScan::PortSpec::expand_all_known_ports();
    }
	
    my $scanned_ports = {};

    foreach my $p (@$ports_listref)
    {
	$scanned_ports->{$p->key_for()} = $p;
    }

    my $results_set = new PortScan::ScanSet("", undef, undef, $scanned_ports);
    foreach my $spec (@$hspecs)
    {
	my $negate = ($spec =~ /\!/) ? 1 : 0;

	$spec =~ s/\!//g;

	my ($host_spec, $port_spec, $ignored_state) = split /:/,$spec;

	my $host_addrs = &expand_host_range($host_spec);
	my $ports = &expand_port_range($port_spec);

	my @hosts = ();

	foreach my $host_addr (@$host_addrs)
	{
	    my $host_obj = new PortScan::ScannedHost;
	    $host_obj->scan_set($results_set);
	    $host_obj->addr($host_addr);
	    $host_obj->default_state( $default_ignored_state );

	    foreach my $pspec (@$ports)
	    {
		$host_obj->add_port($pspec);
	    }

	    $host_obj->default_state( $ignored_state ) if length $ignored_state;

	    push @hosts, $host_obj;
	}

	if ($negate)
	{
	    foreach my $host (@hosts)
	    {
		my $result_host = $results_set->get_host($host->addr());

		if (defined $result_host)
		{
		    my $port_specs = $host->port_specs();


		    if (keys %$port_specs)
		    { # if host has ports, try to delete each port from the host in results
			foreach my $pkey (keys %$port_specs)
			{
			    $result_host->remove_port($pkey);
			}
		    }
		    else
		    { # if host has no ports, try to delete the host from results
			$results_set->remove_host($host->addr());
		    }
		}
	    }

	}
	else
	{
	    foreach my $host (@hosts)
	    {
		$results_set->add_host($host);
	    }
	}

    }

    $results_set;
}


1;


















