#! /usr/pkg/bin/perl
##
## $Id: lg.cgi.in 3465 2016-09-13 22:11:23Z heas $
##
## rancid 3.6.2
## Copyright (c) 1997-2016 by Henry Kilmer and John Heasley
## All rights reserved.
##
## This code is derived from software contributed to and maintained by
## Henry Kilmer, John Heasley, Andrew Partan, Pete Whiting, Austin Schutz,
## and Andrew Fort.
##
## Redistribution and use in source and binary forms, with or without
## modification, are permitted provided that the following conditions
## are met:
## 1. Redistributions of source code must retain the above copyright
##    notice, this list of conditions and the following disclaimer.
## 2. Redistributions in binary form must reproduce the above copyright
##    notice, this list of conditions and the following disclaimer in the
##    documentation and/or other materials provided with the distribution.
## 3. All advertising materials mentioning features or use of this software
##    must display the following acknowledgement:
##        This product includes software developed by Henry Kilmer and John
##        Heasley and its contributors for RANCID.
## 4. Neither the name of RANCID nor the names of its
##    contributors may be used to endorse or promote products derived from
##    this software without specific prior written permission.
## 5. It is requested that non-binding fixes and modifications be contributed
##    back to RANCID.
##
## THIS SOFTWARE IS PROVIDED BY Henry Kilmer, John Heasley AND CONTRIBUTORS
## ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
## TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
## PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COMPANY OR CONTRIBUTORS
## BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
## CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
## SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
## INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
## CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
## ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
## POSSIBILITY OF SUCH DAMAGE.
##
## It is the request of the authors, but not a condition of license, that
## parties packaging or redistributing RANCID NOT distribute altered versions
## of the etc/rancid.types.base file nor alter how this file is processed nor
## when in relation to etc/rancid.types.conf.  The goal of this is to help
## suppress our support costs.  If it becomes a problem, this could become a
## condition of license.
#
# The original original lookingglass s/w was written by Ed Kern.  It was
# a single script that used to be available at http://nitrous.digex.net/.
# Provided by permission and modified beyond recognition.
#
# Looking glass
# vars: query, router, args

BEGIN {
    $me = $0;
    $me =~ s/.*\/(\S+)$/$1/;
}

use CGI qw/:standard escapeHTML/;
use POSIX qw(strftime);
use Sys::Syslog;
use LockFile::Simple qw(lock trylock unlock);

my($BASEDIR) = "/usr/pkg";
my($SYSCONFDIR) = "/usr/pkg/etc/rancid";
my($LOCALSTATEDIR) = "/var/rancid";
my($pingcmd) = "/sbin/ping -c 1";

my($query, $max_time_diff, $cache_dir, $cloginrc, @results);
my($type, $router_param, $remote_user, $arg, $router, $mfg);

my($LG_CACHE_DIR, $LG_CLOGINRC, $LG_IMAGE, $LG_LOG, $LG_ROUTERDB, $LG_AS_REG);
my($LG_BGP_RT, $LG_CACHE_TIME, $LG_SINGLE, $LG_STRIP);

if (!defined($ENV{HOME})) { $ENV{HOME} = "."; }

# note: the following functions are duplicated between lgform.cgi and lg.cgi
# to avoid the need for module inclusion headaches from within a httpd context.
# it is just easier to be self-contained.
# SO, ANY CHANGES HERE SHOULD BE REFLECTED IN THE OTHER .cgi.

# logging
sub dolog
{
    my($level, $msg) = @_;

    if (defined($LG_LOG) && $LG_LOG !~ /\//) {
	openlog($me, "pid", $LG_LOG);
	syslog($level, "%s", $msg);
	closelog;
    } else {
	local(*LOG);
	my($file);
	if (defined($LG_LOG)) {
	    $file = $LG_LOG;
	} else {
	    $file = "$cache_dir/lg.log";
	}
	#  log date, hostname, query, addr
	if (open(LOG, ">>$file") == 0) {
	    # stderr, if all else fails
	    printf(STDERR "[" . strftime("%a %b %e %H:%M:%S %Y", gmtime) .
			"] could not open log file $file: $!\n");
	    printf(STDERR $msg);
	} else {
	    printf(LOG $msg);
	    close(LOG);
	}
    }
    return;
}

# read LG configuration file
sub readconf
{
    my($conffile, $cmds);
    local(*CONF);
    if (defined($ENV{LG_CONF})) {
	$conffile = $ENV{LG_CONF};
    } elsif (-e "lg.conf") {
	$conffile = "lg.conf";
    } else {
	$conffile = "$SYSCONFDIR/lg.conf";
    }

    if (! -f $conffile) {
	return;
    }

    if (open(CONF, "< $conffile")) {
	while (<CONF>) {
	    next if (/^\s*(#|$)/);
	    $cmds .= $_;
	}
	close(CONF);
	eval $cmds;
    } else {
	printf(STDERR "ERROR: couldn\'t open the configuration file: " .
	       "$conffile: $!\n");
	exit(1);
    }

    return;
}

# read router.db file
sub readrouters
{
    my($rtrdb);
    local(*RTR);

    if (defined($LG_ROUTERDB)) {
	$rtrdb = $LG_ROUTERDB;
    } else {
	$rtrdb = "$SYSCONFDIR/router.db";
    }

    if (! -f $rtrdb) {
	my(@dirs, $dir);
	# if the router.db file does not exist, try to compile the list from
	# the rancid group router.db files.
	local(*DIR);
	if (! opendir(DIR, $LOCALSTATEDIR)) {
	    dolog(LOG_ERR, "ERROR: couldn\'t read $LOCALSTATEDIR: $!\n");
	} else {
	    while ($dir = readdir(DIR)) {
		next if ($dir =~ /^(\.|\.\.|\.ssh|CVS|bin|etc|logs|util)$/);
		push(@dirs, $dir) if (-d "$LOCALSTATEDIR/$dir");
	    }
	    closedir(DIR);

	    foreach $dir (@dirs) {
		if (! opendir(DIR, "$LOCALSTATEDIR/$dir")) {
		    dolog(LOG_ERR,
			  "ERROR: couldn\'t read $LOCALSTATEDIR/$dir: $!\n");
		    next;
		}
		closedir(DIR);
		next if (! -f "$LOCALSTATEDIR/$dir/router.db");
		if (open(RTR, "< $LOCALSTATEDIR/$dir/router.db")) {
		    while (<RTR>) {
			next if (/^\s*(#|$)/);
			# fqdn;mfg;state
			@record = split('\;', $_);
			next if ($record[2] !~ /up/i || $record[1] !~ /(cisco|foundry|juniper)/);
			push(@rtrlist, join(':', ($record[0], $record[1])));
			$rtrlabels{join(':', ($record[0], $record[1]))} = $record[0];
		    }
		    close(RTR);
		} else {
		    dolog(LOG_ERR, "ERROR: couldn\'t open the router.db " .
			  "file: $LOCALSTATEDIR/$dir/router.db: $!\n");
		}
	    }
	}
    } else {
	if (open(RTR, "< $rtrdb")) {
	    while (<RTR>) {
		next if (/^\s*(#|$)/);
		# fqdn;mfg;state
		@record = split('\;', $_);
		next if ($record[2] !~ /up/i || $record[1] !~ /(cisco|foundry|juniper)/);
		push(@rtrlist, join(':', ($record[0], $record[1])));
		$rtrlabels{join(':', ($record[0], $record[1]))} = $record[0];
	    }
	    close(RTR);
	} else {
	    dolog(LOG_ERR, "ERROR: couldn\'t open the router.db file: " .
		  "$rtrdb: $!\n");
	    exit(1);
	}
    }

    return;
}

# the remaining functions are particular to lg.cgi.

# return true if $router is a member of @rtrlist
sub arraymember {
    my($rtrlist) = shift;
    my($router) = shift;
    my($r);

    foreach $r (@$rtrlist) {
	$r = (split(':', $r))[0];
	return(1) if ($r eq $router);
    }

    return(0);
}

# check reachability and lock file before attempting to connect to device
# return non-zero on error.
sub DoRsh
{
    my ($router, $mfg, $cmd, $arg) = @_;
    my($ctime) = time();
    my($val);

    my($lckobj) = LockFile::Simple->make(-delay => $lock_int,
		-max => $max_lock_wait, -hold => $max_lock_hold);

    if ($pingcmd =~ /\d$/) {
	`$pingcmd $router`;
    } else {
	`$pingcmd $router 56 1`;
    }
    if ($?) {
	print "$router is unreachable.  Try again later.\n";
	return(-1);
    }
    if ($LG_SINGLE) {
	if (! $lckobj->lock("$cache_dir/$router")) {
	    print "$router is busy.  Try again later.\n";
	    return(-1);
	}
    }
    $val = &DoCmd($router, $mfg, $cmd, $arg);
    if ($LG_SINGLE) {
	$lckobj->unlock("$cache_dir/$router");
    }
    return($val);
}

# run commands on the router.  return non-zero on error.
sub DoCmd
{
    my($rtr, $mfg, $cmd, $arg) = @_;
    local(*CMD);

    if ($mfg =~ /foundry/i) {
	open(CMD, "sh -c \"flogin -f $cloginrc -c \'$cmd $arg\' $rtr\" 2>&1 |");
    } elsif ($mfg =~ /juniper/i) {
	open(CMD, "sh -c \"jlogin -f $cloginrc -c \'$cmd $arg\' $rtr\" 2>&1 |");
    } else {
	open(CMD, "sh -c \"clogin -noenable -f $cloginrc -c \'$cmd $arg\' $rtr\" 2>&1 |");
    }
    while (<CMD>) {
	tr/\015//d;
	if (/^error:/i) {
	    dolog(LOG_ERR, $_);
	    if ($LG_STRIP) { undef(@results); }
	    push(@results, $_);
	    print @results;
	    return(-1);
	}
	push(@results, $_);
	if (/$cmd/) {
	    ($prompt) = /^(\S*)[\#>]/;
	    if ($LG_STRIP) {
		undef(@results);
	    } else {
		print @results;
	    }
	    last;
	}
    }

    while (<CMD>) {
	last if /^$prompt[\#>]/;
	tr/\015//d;
	print $_;
	push(@results, $_);
    }
    while (<CMD>) {}
    close(CMD);

    return(0);
}

##
#       Usage:  &Error("msg"));
# Description:  displays an error and exits.
##
sub Error {
    my($msg) = @_;

    my($q) = new CGI();
    print $q->header;
    if ($LG_STYLE) {
	print $query->start_html(-title => "LookingGlass Results - $router",
						-style => {'src' => $LG_STYLE});
    } else {
	print $query->start_html(-title => "LookingGlass Results - $router");
    }

    # add the company image, LG_IMAGE
    print $LG_IMAGE;


    print <<EOF ;
	<br>
	<B><FONT SIZE=+2>Looking Glass Error:</FONT></B>
	<p>
	$msg
	<br>
	<hr>
	$LG_INFO
EOF

    print $q->end_html;
    exit(0);
}

# convert an ipv4 address mask to prefix length
sub mask2len {
    my($mask) = shift;
    my($a, $b, $c, $d) = split('\.', $mask);
    my($p, $len);

    $p = ~ (($a << 24) + ($b << 16) + ($c << 8) + $d);
    for ($len = 32; $p > 0; $len --) {
        $p = $p >> 1;
    }

    return($len);
}

# end the page and exit.
sub end_page {

    print <<END ;
	</pre>
	<!--- end page content --->
	</body>
END

    print $query->end_html;
    exit(0);
}

# start the page and log the transaction...
sub start_page {
    my($mfg) = @_;
    my($cmd);

    my($timestr) = strftime("%a %b %e %H:%M:%S %Y", gmtime);
    dolog(LOG_INFO, sprintf("%s %s %s %s\n",
	$ENV{REMOTE_HOST}, $ENV{REMOTE_ADDR}, $ENV{REMOTE_USER},
	"- - [$timestr] $type $router $arg"));
    print $query->header;
    if ($LG_STYLE) {
	print $query->start_html(-title =>"LookingGlass form",
						-style => {'src' => $LG_STYLE});
    } else {
	print $query->start_html(-title =>"LookingGlass from");
    }

    $timestr = strftime("%a %b %e %H:%M:%S %Y %Z", gmtime);

    # add the company image, LG_IMAGE
    print $LG_IMAGE;

    if ($mfg =~ /foundry/i) {
	$cmd = $foundryCmd{$type};
    } elsif ($mfg =~ /juniper/i) {
	$cmd = $juniperCmd{$type};
    } else {
	$cmd = $ciscoCmd{$type};
    }

    print <<HEAD ;
	</b></font>
	<font size=+3><b><h1>Looking Glass Results - $router
	</b></h1></font>
	<hr>

	<center>
	<b>Date:</b> $timestr
	<p>
	<b>Query:</b> $cmd
	<br>
HEAD

    if ($arg) { print "<b>Argument(s):</b> $arg\n"; }
    print "</center>\n";

    print <<END ;
	<!--$cached-->
	</center>
	<p>
	<pre>
END

    return;
}


# Main()
# read the configuration file if it exists.
readconf();

# The script will now cache the results as simple files in the $cache_dir,
# named after the type of query (queries must, of course, be one word with
# no spaces).  Modify $LG_CACHE_TIME to set the lifetime for cache entries.
# for most web servers, cache_dir must be writable by uid nobody
if (defined($LG_CACHE_DIR)) {
    $cache_dir = $LG_CACHE_DIR;
} else {
    $cache_dir = "./tmp";
}

# read routers table to get @rtrlist
readrouters();

# when to display cache?  max time difference (in seconds)
if (defined($LG_CACHE_TIME)) {
    $max_time_diff = $LG_CACHE_TIME;
} else {
    $max_time_diff = "600" ;
}

# serialize queries?
if (!defined($LG_SINGLE)) {
    $LG_SINGLE = 1;
}

# max seconds to wait for a 'router' lock to free up
$max_lock_wait = 30;
$lock_int = 5;
$max_lock_hold = 300;

# clogin setup
if (defined($LG_CLOGINRC)) {
    $cloginrc = $LG_CLOGINRC;
} else {
    $cloginrc = $ENV{HOME} . "/.cloginrc";
}

$query = new CGI;

# get form data and validate
$type = ($query->param('query'))[0];
$router_param = ($query->param('router'))[0];
$remote_user = $ENV{REMOTE_USER};
$arg = ($query->param('args'))[0];
# handle multiple args
$arg =~ s/["'`]//g;			# these are BS in any arg for any query
@arg = split(' ', $arg);

# verify router, commands, arguments, etc.
($router, $mfg) = split(':', lc($router_param));
if (!defined($type) || !defined($router) || $router eq "") {
    &Error("You must at least choose a Query and a router.  Try buying " .
								"a clue.\n");
}

if ($arg !~ /^[-A-Za-z0-9|_\/: \.^\$]*$/) {
    &Error("Funny characters in argument; ignoring.\n");
}
if (length($arg) >= 50) {
    &Error("Argument string too long; ignoring. \n");
}

if (! arraymember(\@rtrlist, $router)) {
    my($timestr) = strftime("%a %b %e %H:%M:%S %Y", gmtime);
    dolog(LOG_WARNING, sprintf("%s %s %s %s\n",
	$ENV{REMOTE_HOST}, $ENV{REMOTE_ADDR}, $ENV{REMOTE_USER},
	"- - [$timestr] lg.cgi: attempt to access $router\n"));
    Error("access to $router not permitted");
}

# conversion of command "type" passed from lgform.cgi to the vendor's syntax.
if ($mfg eq "cisco") {
    %mfgCmd = (
		# Debug Queries
		log => "show logging",
		# Interface Queries
		framerelay => "show frame-relay pvc",
		intbrief => "show ip interface",	# switch in {interface}
		intdescr => "show interfaces description",
		interface => "show interface",
		#intoptics => "show controllers",	# XXX whats cmd on ios?
		#intwanphy => "show controllers wanphy",# XXX whats cmd on ios?
		# Routing Queries
		damp => "show ip bgp dampened-paths",
		neighbor => "show ip bgp neighbor",
		# Multicast Queries
		mbgp => "show ip mbgp",
		mbgpsum => "show ip mbgp summary",
		mneighbor => "show ip bgp neighbor",
		msdp => "show ip msdp summary",
		msdpsa => "show ip msdp sa-cache",
		msess => "show ip sdr",
		mroute => "show ip mroute",
		pim_interface => "show ip pim interface",
		pim_neighbor => "show ip pim neighbor",
		pim_rp => "show ip pim rp mapping",
		# IPv6 Queries
		v6_bgp => "show bgp ipv6",
		v6_interface => "show ipv6 interface",
		#v6_route => "show route ipv6",
		v6_summary => "show bgp ipv6 summary",
		#
		#acl => "show access-list",
		#aspath => "show ip as-path-access-list",
		#communitylist => "show ip community-list",
		ping => "ping",
		prefix => "show ip bgp",
		prefixlist => "show ip prefix-list",
		regex => "show ip bgp regex",
		route => "show ip route",
		routemap => "show route-map",
		rpf => "show ip rpf",
		summary => "show ip bgp summary",
		trace => "traceroute"
    );
} elsif ($mfg eq "cisco-xr") {
    %mfgCmd = (
		# Debug Queries
		log => "show logging",
		# Interface Queries
		framerelay => "show frame-relay pvc",
		intbrief => "show ipv4 interface",
		intdescr => "show interfaces description",
		interface => "show interface",
		intoptics => "show controllers",
		intwanphy => "show controllers wanphy",
		# Routing Queries
		damp => "show bgp all all dampened-paths",
		neighbor => "show bgp neighbor",
		# Multicast Queries
		mbgp => "show ip mbgp",
		mbgpsum => "show bgp all multicast summary",
		#mneighbor => "show ip bgp neighbor",
		#msdp => "show ip msdp summary",
		#msdpsa => "show ip msdp sa-cache",
		#msess => "show ip sdr",
		#mroute => "show ip mroute",
		#pim_interface => "show ip pim interface",
		#pim_neighbor => "show ip pim neighbor",
		#pim_rp => "show ip pim rp mapping",
		# IPv6 Queries
		v6_bgp => "show bgp ipv6 all",
		v6_interface => "show ipv6 interface",
		v6_route => "show route ipv6",
		v6_summary => "show bgp ipv6 unicast summary",
		#
		#acl => "show access-list",
		#aspath => "show ip as-path-access-list",
		#communitylist => "show ip community-list",
		ping => "ping",
		prefix => "show ip bgp",
		prefixlist => "show ip prefix-list",
		regex => "show ip bgp regex",
		route => "show route",
		routemap => "show route-map",
		rpf => "show ip rpf",
		summary => "show ip bgp summary",
		trace => "traceroute"
    );
} elsif ($mfg eq "foundry") {
    %mfgCmd = (
		# Debug Queries
		log => "show log",
		ping => "ping",
		trace => "traceroute",
		# Interface Queries
		#framerelay => "show frame-relay pvc",	# no compatible command
		interface => "show interface",
		#intoptics => "show controllers",	# XXX whats cmd on fdry?
		#intwanphy => "show controllers wanphy",# XXX whats cmd on fdry?
		# Routing Queries
		damp => "show ip bgp dampened-paths",
		neighbor => "show ip bgp neighbor",
		#regex => "show ip bgp aspath-regex",
		route => "show ip route",
		summary => "show ip bgp summary",
		# Multicast Queries
		#mbgp => "show ip mbgp",
		#mbgpsum => "show bgp summary",
		#mneighbor => "show ip bgp neighbor",
		mroute => "show ip mroute",
		msdp => "show ip msdp summary",
		msdpsa => "show ip msdp sa-cache",
		msess => "show ip sdr",
		pim_interface => "show ip pim interface",
		pim_neighbor => "show ip pim neighbor",
		pim_rp => "show ip pim rp mapping",
		rpf => "show ip rpf",
		# IPv6 Queries
		# v6_bgp => "show bgp ipv6",
		# v6_interface => "show ipv6 interface",
		# v6_summary => "show bgp ipv6 summary"
		#
		#acl => "show access-list",
		#aspath => "show ip as-path-access-list",
		#communitylist => "show ip community-list",
		routemap => "show route-map",
		prefix => "show ip bgp",
		prefixlist => "show ip prefix-list"
	);
} elsif ($mfg eq "juniper") {
    %mfgCmd = (
		# Debug Queries
		log => "show log messages",
		ping => "ping rapid count 5",
		trace => "traceroute",
		# Interface Queries
		framerelay => "show frame-relay pvc",
		intbrief => "show ip interface",	# switch in {interface}
		intdescr => "show interfaces descriptions",
		interface => "show interface",
		intoptics => "show interfaces diagnostics optics", # <intf>
		intwanphy => "show interface",		# "<intf> media detail"
		# Routing Queries
		damp => "show route damping suppressed terse table inet.0",
		neighbor => "show bgp neighbor",
		regex => "show route table inet.0 aspath-regex",
		summary => "show bgp summary",
		# Multicast Queries
		mbgp => "show route table inet.2 terse",
		mbgpsum => "show bgp summary",
		mneighbor => "show bgp neighbor",
		mroute => "show multicast route extensive",
		msdp => "show msdp",
		msdpsa => "show msdp source-active",
		msess => "show multicast sessions",
		pim_interface => "show pim interface",
		pim_neighbor => "show pim neighbors",
		pim_rp => "show pim rps",
		pim_join => "show pim join",
		rpf => "show multicast rpf",
		# IPv6 Queries
		v6_bgp => "show route table inet6.0",
		v6_interface => "show interface",
		v6_summary => "show bgp summary",
		#
		#acl => "show access-list",
		#aspath => "show ip as-path-access-list",
		#communitylist => "show ip community-list",
		prefix => "show route table inet.0",
		prefixlist => "show policy",
		route => "show route table inet.0 terse",
		routemap => "show policy"
	);
} else {
    &Error("Unknown device type: $mfg\n");
}

# construct Display command from configuration
%cmdDisp=();
foreach $qtype (sort keys(%$queries)) {
    next if (! scalar(%{$queries->{$qtype}}));
    foreach $sub_type (sort keys(%{$queries->{$qtype}})) {
	$cmdDisp{$sub_type} = $queries->{$qtype}->{$sub_type};
    }
}

# make sure the command is not disabled
if (! defined($cmdDisp{$type})) {
    &Error("Unknown command type: $type\n");
}

# not all cmds/queries are implemented for all platforms
if (! defined($mfgCmd{$type})) {
    Error("$cmdDisp{$type} not implemented for $mfg or no suitable " .
						"equivalent exists.  sorry.\n");
}
$cmd = $mfgCmd{$type};

# handle each query/command type
if ($type eq "prefix" || $type eq "mbgp" || $type eq "route" ) {
    if ($arg[0] !~ /^\d+\.\d+\.\d+\.\d+$/) {
	&Error("The IP address \"$arg[0]\" is not valid and lacking an " .
				"address would over-burden our router.\n");
    } elsif (defined($arg[1]) && $arg[1] !~ /^\d+\.\d+\.\d+\.\d+$/) {
	&Error("The IP netmask \"$arg[1]\" is not valid.\n");
    }
    if ($mfg =~ /juniper/i && defined($arg[1])) {
	$arg = $arg[0] . "/" . mask2len($arg[1]);
    }
} elsif ($type eq "v6_route" ){
    # XXX: is this check of the address arg correct and pedantic?
    if ($arg[0] !~ /[0-9a-fA-F:]+$/) {
	&Error("The IPv6 address \"$arg[0]\" is not valid.\n");
    }
} elsif ($type eq "framerelay") {
    if ($mfg =~ /juniper/) {
	&Error("Juniper does not have a show frame-relay pvc command.  " .
						"Use show interface.\n");
    }
    if ($arg[0] > 15 && $arg[0] < 1024) {
	$arg = $arg[0];
    } else {
	undef($arg);
    }
} elsif ($type eq "interface" || $type eq "v6_interface") {

	# XXX: wtf is arg[1]?
#    if ($arg[1] =~ /[-\/0-9:.]+/) {
#       $arg = $arg[0] . " " . $arg[1];
#    } else {

    if ($mfg =~ /(cisco|foundry)/) {
	if ($arg[0] !~ /^b[^ ]+[0-9]/i && $arg[0] =~ /^b/i) {
	    $type = "intbrief";
	    $arg = "brief";
	} else {
	    $arg = $arg[0];
	}
    } elsif ($mfg =~ /juniper/) {
	my($optind) = 0;
	# arg 0 may be an intf name or a display option, but there can
	# only be 2 args
	$arg = "";
	while ($optind <= $#arg && $optind < 2) {
	    $arg[$optind] =~ s/brief/terse/;
	    if ($arg[$optind] =~ /^([a-z0-9]{2}\-\d+\/\d+\/\d+(:\d+)?)/i) {
		$arg .= " $1";
	    } elsif ($arg[$optind] =~ /^det/i) {
		$arg .= " detail";
	    } elsif ($arg[$optind] =~ /^ter/i) {
		$arg .= " terse";
	    } elsif ($arg[$optind] =~ /^ext/i) {
		$arg .= " extensive";
	    }
	    $optind += 1;
	}
    }
} elsif ($type eq "intoptics") {
    # arg 0 may be an intf name or a display option, but there can
    # only be 2 args
    if ($#arg < 0 || $arg[0] =~ /^\s*$/) {
&Error("show interface optics requires an interface argument.\n");
    }
    if ($mfg =~ /cisco-xr/) {
	if ($arg[0] =~ /^([^0-9]* ?\d+[^[:space:]]*)/i) {
	    $arg = "$1 phy";
	} else {
	    &Error("Interface argument $arg[0] did not match interface regex.\n");
	}
    } elsif ($mfg =~ /juniper/) {
	if ($arg[0] =~ /^([a-z0-9]{2}\-\d+\/\d+\/\d+(:\d+)?)/i) {
	    $arg = "$1";
	} else {
	    &Error("Interface argument $arg[0] did not match interface regex.\n");
	}
    }
} elsif ($type eq "intwanphy") {
    # arg 0 may be an intf name or a display option, but there can
    # only be 2 args
    if ($#arg < 0 || $arg[0] =~ /^\s*$/) {
	&Error("show interface wanphy requires an interface argument.\n");
    }
    if ($mfg =~ /(cisco-xr)/) {
	if ($arg[0] =~ /^[^0-9]*(\d+[^[:space:]]*)/i) {
	    $arg = "$1 all";
	} else {
	    &Error("Interface argument $arg[0] did not match interface regex.\n");
	}
    } elsif ($mfg =~ /juniper/) {
	if ($arg[0] =~ /^([a-z0-9]{2}\-\d+\/\d+\/\d+(:\d+)?)/i) {
	    $arg = "$1 media detail";
	} else {
	    &Error("Interface argument $arg[0] did not match interface regex.\n");
	}
    }
} elsif ($type eq "log") {
    if ($arg[0] =~ /^\s*\|?$/) {
	shift(@arg);
    }
    $arg[0] =~ s/^\s*\|?//;
    if ($arg[0] !~ /^\s*$/) {
	if ($mfg =~ /cisco/i) {
	    $arg = " | include " . join(' ', @arg);
	} elsif ($mfg =~ /juniper/i) {
	    $arg = " | match \\\"" . join(' ', @arg) . "\\\"";
	} else {
	    undef($arg);
	}
    } else {
	undef($arg);
    }
} elsif ($type eq "ping" || $type eq "trace") {
    # XXX: is this check of the address arg correct and pedantic?
    if ($arg[0] !~ /^\d+\.\d+\.\d+\.\d+$/ && $arg[0] !~ /[0-9a-fA-F:]+$/) {
	if ($arg[0] !~ /^[A-Za-z0-9._-]+$/) {
	    &Error("That argument ($arg[0]) is not valid.\n");
	}
    }
    $arg = $arg[0];
} elsif ($type eq "aspath" || $type eq "communitylist") {
    if ($arg[0] !~ /^\d+$/ || ($arg[0] < 1 && $arg[0] > 199)) {
	&Error("That argument ($arg[0]) is not valid.\n");
    }
    $arg = $arg[0];
} elsif ($type eq "acl") {
    if ($arg[0] !~ /^\d+$/ || ($arg[0] < 100 && $arg[0] > 199) ||
					($arg[0] < 1300 && $arg[0] > 2699)) {
	&Error("That argument ($arg[0]) is not valid.\n");
    }
    $arg = $arg[0];
    # don't show the jewels
    # XXX: this error msg is useless, but show acl is un-implemented.
    &Error($mfg) if ($arg == 98 || $arg == 99);
} elsif ($type eq "prefixlist" || $type eq "routemap") {
    if ($arg[0] !~ /^[0-9A-Za-z][^\s\"]*$/) {
	&Error("That argument ($arg[0]) is not valid.\n");
    }
    $arg = $arg[0];
} elsif ($type eq "regex") {
    # bgp as-path regex
    $arg = $arg[0];
    if ($#arg >= 1) {
	for ($n = 1; $n <= $#arg; $n++) { $arg .= " " . $arg[$n]; }
    }
    # remove leading/trailing whitespace
    $arg =~ s/^\s*//; $arg =~ s/\s*$//;
    if ($arg !~ /^[0-9_ ^.*+?[\])\(-]*\$?$/ || $arg =~ /^\s*$/) {
	&Error("That argument ($arg[0]) is not valid.\n");
    }
    # pathetic excuses for lookups
    if ($arg =~ /^[_.* ^]*(\*|1|701|1239|1280|1740|3561|5462|10303)+[_\$]*$/ ||
	$arg =~ /^[_.* ^]*(1|701|1239|1280|1740|3561|5462|10303)+[_ .]*[\[*.]/) {
	&Error("Get real.  Such a query has potential to over-burden our " .
		"router.\nLook that up on your own router.\n");
    }
    if ($mfg =~ /juniper/) {
	$arg =~ s/_/ /g;
	# pre-junos 4.4 does not allow anchors
	if ($arg =~ /\^\$/) {
	    $arg =~ "()";
	} else {
	    $arg =~ s/[\$^]/ /g;
	}
	$arg = "\\\"$arg\\\"";
    }
    # escape any ()s
    $arg =~ s/([\(\)])/\\$1/g;
} elsif ($type eq "neighbor") {
    if ($arg[0] !~ /^\d+\.\d+\.\d+\.\d+$/) {
	if ($arg[0] !~ /([A-Za-z0-9-]*.)*[A-Za-z0-9-]*.(com|edu|net|org)/) {
	    &Error("That argument ($arg[0]) is not valid.\n");
	}
    }
    $arg = $arg[0];
    if (defined($arg[1]) && $arg[1] =~ /^(a|ro|f|re)/) {
	if ($mfg =~ /juniper/) {
	    if ($arg[1] =~ /^a/) {
		if (defined($LG_BGP_RT)) {
		    $cmd = "show route table inet.0 all advertising-protocol ".
			"bgp";
		}
	    } elsif ($arg[1] =~ /^f/) {
		if (defined($LG_BGP_RT)) {
		    $cmd = "show route damping table inet.0 all ".
			"receive-protocol bgp";
		}
	    } elsif ($arg[1] =~ /^r/) {
		if (defined($LG_BGP_RT)) {
		    $cmd = "show route table inet.0 all receive-protocol bgp";
		}
	    }
	} else {
	    if ($arg[1] =~ /^a/) {
		if (defined($LG_BGP_RT)) { $arg .= " advertised-routes"; }
	    } elsif ($arg[1] =~ /^f/) {
		$arg .= " flap-statistics";
	    } elsif ($arg[1] =~ /^ro/) {
		if (defined($LG_BGP_RT)) { $arg .= " routes"; }
	    } elsif ($arg[1] =~ /^re/) {
		if (defined($LG_BGP_RT)) { $arg .= " received-routes"; }
	    }
	}
    }
} elsif ($type eq "mneighbor") {
    if ($arg[0] !~ /^\d+\.\d+\.\d+\.\d+$/) {
	if ($arg[0] !~ /([A-Za-z0-9-]*.)*[A-Za-z0-9-]*.(com|edu|net|org)/) {
	    &Error("That argument ($arg[0]) is not valid.\n");
	}
    }
    $arg = $arg[0];
    if (defined($arg[1]) && $arg[1] =~ /^(a|ro|f|re)/) {
	if ($mfg =~ /juniper/) {
	    if ($arg[1] =~ /^a/) {
		$cmd .= " advertised-routes";
	    } elsif ($arg[1] =~ /^f/) {
		$cmd .= " flap-statistics";
	    } elsif ($arg[1] =~ /^ro/) {
		$cmd .= " routes";
	    } elsif ($arg[1] =~ /^re/) {
		$cmd .= " received-routes";
	    }
	} else {
	    if ($arg[1] =~ /^a/) {
		$arg .= " advertised-routes";
	    } elsif ($arg[1] =~ /^f/) {
		$arg .= " flap-statistics";
	    } elsif ($arg[1] =~ /^ro/) {
		$arg .= " routes";
	    } elsif ($arg[1] =~ /^re/) {
		$arg .= " received-routes";
	    }
	}
    }
} elsif ($type eq "damp" || $type eq "summary" || $type eq "mbgpsum") {
    undef($arg);
}

# make stdout unbuffered, so result page streams.
$| = 1;
start_page();

# cache the following
if ($type eq "summary" || $type eq "mbgpsu" || $type eq "damp" ||
    $type eq "log") {
    if (!$arg) {
	# cache requests with no addr/argument
	local(*CACHE);

	my($file) = "$cache_dir/$type" ;
	$file =~ s/\s+/_/g;
	$file .= "_$router";

	if (-e $file) {
	    # see if cache exists
	    @stat = stat($file);
	    $ftime = $stat[9];
	    $dtime = time() - $stat[9];

	    # see if we are within cache time
	    if ($dtime <= $max_time_diff) {
		if (open(CACHE, "<$file") == 0) {
		    dolog(LOG_ERR, "couldnt open cache file $file: $!\n");
		} else {
		    print "<b>From cache (number of seconds old (max " .
			"$max_time_diff)):</b> $dtime\n\n";
		    while (<CACHE>) { print $_; }
		    close(CACHE);
		    &end_page();
		}
	    }
	}

	# else, execute command and save to a new cache file
	if (! &DoRsh($router, $mfg, $cmd, $arg)) {
	    if (open(CACHE, ">$file") == 0) {
		dolog(LOG_ERR, "couldnt create cache file $file: $!\n");
		exit(1);
	    } else {
		printf(CACHE "@results");
		close(CACHE);
	    }
	}
    } else {
	&DoRsh($router, $mfg, $cmd, $arg);
    }
    &end_page();
} else {
    &DoRsh($router, $mfg, $cmd, $arg);
    &end_page();
}

exit(0);
