6
\$\begingroup\$

At the moment, prtscn takes a server, and a list of port ranges to test (of the form x,y-z,a-c,d,e etc). The -v option will show all ports being scanned, and the -r option will return the first open port in the list. If -r is passed, the list of port ranges is optional. In this case, if the port list is missing, -r will just return the first open port on the target OS.

Going forward, the next option I would like to add is a pretty printer for the open ports (IE, if it returns 22, 80, 4200, 4201, 4202, then that should print 22, 80, 4200-4202) and potentially make this async.

#!/usr/bin/perl

# prtscn.pl - scan server for open

# TODO
# Make async
# if any ports are sequential, form a range (ie if 22, 4200, 4201, and 4202 are open, print 22, 4200-4202)

use strict;
use warnings;

use IO::Socket;
use Getopt::Long;

my $verbosity = undef;
my $use_first_port = undef;

GetOptions('v' => \$verbosity, 'u' => \$use_first_port);

sub scan {
    my $server = shift;
    my $start_port = shift;
    my $end_port = shift;
    my @open_ports = ();

    while($start_port-1 < $end_port) {
        if($verbosity) {
            print "scanning port $start_port.\n";
        }

        my $sock = IO::Socket::INET->new(PeerHost => $server, PeerPort => $start_port);

        if($sock) {
            push @open_ports, $start_port;

            if($use_first_port) {
                return @open_ports;
            }
        }

        $start_port += 1;
    }

    return @open_ports;
}

my $server = shift || die "err: need server.\n";
my $range_list = shift || "1-65536";
my @ranges = split /,/, $range_list || die "err: need range list.\n";
my @open_ports = ();

foreach my $range (@ranges) {
    my ($start, $end) = (undef, undef);

    if($range =~ /-/) {
        ($start, $end) = split /-/, $range;
    } else {
        ($start, $end) = ($range, $range);
    }

    push @open_ports, scan($server, $start, $end); 
}

if(scalar @open_ports != 0) {
    print "open port(s): ${\join ', ', sort @open_ports}\n";
} else {
    print "no ports listed are open.\n";
}
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Layout

Place the scan sub at the end of the file instead of in the middle of the procedural code.

use as little as possible

I prefer to only import the necessary functions form the modules (in the use lines). Since you are not using anything from IO::Socket, you can pass it the empty list: qw().

You only use one function from Getopt::Long.

Variable declarations

There is no need to explicitly initialize variables to undef or the empty list in your code.

It is not possible to hit the die statement on the @ranges line.

Comments

Remove the unnecessary TODO comments.

Style

for is simlper than foreach.

Add space between control keywords (if, while) and the opening paren (.

Use single quotes to avoid unnecessary variable interpolation for the $range_list default string.

There is no need for scalar.

The \join code is a little hard to understand.

Use ++ instead of += 1.

User documentation and argument checking

Add usage documentation with the standard Pod::Usage module and plain old documentation (POD). Move argument parsing and checking to it's own sub. Check the return value of GetOptions for enhanced argument checking.

Your question text mentions the -r option, but your code uses -u instead.


Here is new code with the suggestions above:

use strict;
use warnings;
use IO::Socket   qw();
use Getopt::Long qw(GetOptions);
use Pod::Usage   qw(pod2usage);

my $verbosity;
my $use_first_port;
my $server;
my $range_list;

parse_args();

my @ranges = split /,/, $range_list;
my @open_ports;

for my $range (@ranges) {
    my ($start, $end);

    if ($range =~ /-/) {
        ($start, $end) = split /-/, $range;
    } else {
        ($start, $end) = ($range, $range);
    }

    push @open_ports, scan($server, $start, $end);
}

if (@open_ports) {
    print 'open port(s): ', (join ', ', sort @open_ports), "\n";
} else {
    print "no ports listed are open.\n";
}
exit;

sub scan {
    my $server      = shift;
    my $start_port  = shift;
    my $end_port    = shift;
    my @open_ports;

    while (($start_port - 1) < $end_port) {
        if ($verbosity) {
            print "scanning port $start_port.\n";
        }

        my $sock = IO::Socket::INET->new(PeerHost => $server, PeerPort => $start_port);

        if ($sock) {
            push @open_ports, $start_port;

            if ($use_first_port) {
                return @open_ports;
            }
        }

        $start_port++;
    }

    return @open_ports;
}

sub parse_args {
    my $help;
    GetOptions(
        'help'  => \$help,
        'v'     => \$verbosity,
        'u'     => \$use_first_port,
    ) or pod2usage();

    $help and pod2usage(-verbose => 2);

    if (@ARGV) {
        $server = shift @ARGV;
        $range_list  = (shift @ARGV) || '1-65536';
    } else {
        pod2usage('Error: server required.');
    }

    @ARGV and pod2usage("Error: unexpected args: @ARGV");
}

=head1 NAME

prtscn

=head1 SYNOPSIS

prtscn [options] server [range]

    Options:
    -help   Verbose help
    -v      Verbose output for debug
    -u      Use first port

=head1 DESCRIPTION

Show ports for a server.

=head1 ARGUMENTS

=over 4

=item B<server>

A server is required.

=item B<range>

A range is optional.  The default range is C<1-65536>

=back

=head1 OPTIONS

All options can be abbreviated.

=over 4

=item B<-v>

Show verbose output on stdout

    prtscn -v 0.0.0.0

=item B<-u>

By default, show all ports in the range.
To show just the first port in the range, use the C<-u> option.

    prtscn -u 0.0.0.0 111-555

=item B<-help>

Show verbose usage information.

=back

=cut
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.