#! @EXPECT_PATH@ --
##
## @PACKAGE@ @VERSION@
@copyright@
#
# rivlogin - Riverstone (and Enterasys SSR) login
#
# Based upon rscmd (see nmops.org)
# rscmd - Riverstone Networks Automated login
# by Mike MacFaden, Kiran Addepalli
# Riverstone Networks, 2000
#
# Returned to the RANCID crowd by andrew fort

# Usage line
set usage "Error: Usage: $argv0 \[-diV\] \[-noenable\] \
\[-f cloginrc-file\] \[-c command\] \[-Evar=x\] \[-s script-file\] \
\[-x command-file\] \[-t timeout\] \[-o output-file\] \
router \[router...\]\n"

# program diagnostics
set verbose  0
set success  1
set config   0

# in seconds to wait for data back from device
set timeoutdflt 10
# Some CLIs having problems if we write too fast (Extreme, PIX, Cat)
set send_human {.2 .1 .4 .2 1}
set tempfile "/tmp/rivlogin.[exec date]"

# cli command prompt
set my_prompt    ">"
set enable_prompt "\#"

set default_user ""
set output_file  ""
set conf_prompt  "*\(config\)# "
set logging 0
set config_mode 0
# Save config, if prompted
set do_saveconfig 0
# cloginrc debugging knob
set do_cloginrcdbg 0
# Password file for routers to access
set password_file $env(HOME)/.cloginrc

# If no -c or -s specified, just automate router login ala rsh
set do_command 0
set do_interact 0
set do_script  0
set log_user   0

# The default CLI mode to login to is "enable" mode
set avenable	1

# The default is to look in the password file to find the passwords.  This
# tracks if we receive them on the command line.
set do_passwd    1
set do_enapasswd 1
#
set send_human {.4 .4 .7 .3 5}

# intialize cloginrc parsing stacks
set int_file {}
set int_lineno {}

# env(CLOGIN) may contain:
#	x == do not set xterm banner or name

# Find the user in the ENV, or use the unix userid.
if {![info exists default_user] && [info exists env(CISCO_USER)]} {
    if {[string length $env(CISCO_USER)]} {
	set default_user $env(CISCO_USER)
    }
}
if {![info exists default_user] && [info exists env(USER)]} {
    if {[string length $env(USER)]} {
	set default_user $env(USER)
    }
}
if {![info exists default_user] && [info exists env(LOGNAME)]} {
    if {[string length $env(LOGNAME)]} {
	set default_user $env(LOGNAME)
    }
}
if (![info exists default_user]) {
    # This uses "id" which I think is portable.  At least it has existed
    # (without options) on all machines/OSes I've been on recently -
    # unlike whoami or id -nu.
    if [catch {exec id} reason] {
	send_error "\nError: could not exec id: $reason\n"
	exit 1
    }
    regexp {\(([^)]*)} "$reason" junk default_user
}
if {[info exists env(CLOGINRC)]} {
    set password_file $env(CLOGINRC)
}

#
# Done configuration/variable setting.  Now run with it...
#

# Sets Xterm title if interactive...if its an xterm and the user cares
proc label { host } {
    global env
    # if CLOGIN has an 'x' in it, don't set the xterm name/banner
    if [info exists env(CLOGIN)] {
	if {[string first "x" $env(CLOGIN)] != -1} { return }
    }
    # take host from ENV(TERM)
    if [info exists env(TERM)] {
	if [regexp \^(xterm|vs) $env(TERM) ignore] {
	    send_user "\033]1;[lindex [split $host "."] 0]\a"
	    send_user "\033]2;$host\a"
	}
    }
}

# This is a helper function to make the password file easier to
# maintain.  Using this the password file has the form:
# add password sl*	pete cow
# add password at*	steve
# add password *	hanky-pie
proc add {var args} {
    global int_file int_lineno int_$var
    set file [lindex $int_file 0]
    set lineno [lindex $int_lineno 0]
    lappend int_$var "$var:$file:$lineno: $args"
}
proc include {args} {
    global env
    regsub -all "(^{|}$)" $args {} args
    if { [regexp "^/" $args ignore] == 0 } {
	set args $env(HOME)/$args
    }
    source_password_file $args
}

proc find {var router} {
    global do_cloginrcdbg
    upvar int_$var list
    if { [info exists list] } {
	foreach line $list {
	    if { [string match -nocase [lindex $line 1] $router] } {
		if { $do_cloginrcdbg > 0 } {
		    send_error -- [join [list [lindex $line 0] [lrange $line 1 end] "\r\n"]]
		}
		if { $do_cloginrcdbg == 2 } {
		    # save return value
		    if {! [info exists result]} {
			set result [lrange $line 2 end]
		    }
		} else {
		    return [lrange $line 2 end]
		}
	    }
	}
    }

    if { $do_cloginrcdbg == 2 } {
	if {[info exists result]} {
	    return $result
	}
    }
    return {}
}

# Loads the password file.  Note that as this file is tcl, and that
# it is sourced, the user better know what to put in there, as it
# could install more than just password info...  I will assume however,
# that a "bad guy" could just as easy put such code in the clogin
# script, so I will leave .cloginrc as just an extention of that script
proc source_password_file { file } {
    global env int_file int_lineno
    if { ! [file exists $file] } {
	send_user "\nError: password file ($file) does not exist\n"
	exit 1
    }
    file stat $file fileinfo
    if { [expr ($fileinfo(mode) & 007)] != 0000 } {
	send_user "\nError: $file must not be world readable/writable\n"
	exit 1
    }
    if [catch {set fd [open $file "r"]} reason] {
	send_user "\nError: $reason\n"
	exit 1
    }
    set int_file [linsert $int_file 0 $file]
    set int_lineno [linsert $int_lineno 0 0]
    while { [gets $fd line] >= 0 } {
	set tmp [lindex $int_lineno 0]; incr tmp
	lset int_lineno 0 $tmp
	eval $line
    }
    set int_file [lrange $int_file 1 end]
    set int_lineno [lrange $int_lineno 1 end]
    close $fd
}

# pre: login completed ok
# post: terminate login session by closing tcp connection
proc auto_exit { } {

    global telnet_id

    if { $verbose == 1 } {
	puts "DEBUG: auto_exit closing connection to pid $telnet_id\n"
    }
    close -i telnet_id
}

# perform login basic to a router
# pre: args are valid, router is reachable via network
# post: return 0 on successful login, else 1
#
# NOTE: a number of globals are setup: my_prompt, telnet_id are key
# and paging of cli output is disabled
proc login { router user userpswd passwd enapasswd } {

    global login_array
    global telnet_id
    global expect_out
    global spawn_id
    global verbose
    global config verbose my_prompt

    eval spawn -noecho [split "$telnetcmd $router"]
    set telnet_id $spawn_id

    if { $telnet_id == 0 } {
	puts "ERROR: login: spawn telnet session failed.\n"
	return 1
    }

    # wait for initial 'Press RETURN to...' response
    sleep 0.3

    expect "*"
    send "\r"

    # If password fails 3 times then expect again
    set pass_attempt  0

    expect {
	-re ".*> " { }
	"Password:" {
	    incr pass_attempt
	    send -- "$passwd\r"
	    exp_continue
	}
	"Username: " {
	    set pattempt 0

	    send -- "$user\r"
	    expect {
	    "Password: " {
		incr pattempt
		if {$pattempt == 1} {
		    send -- "$userpswd\r";
		} else {
		    send -- "$enapasswd\r";
		}
		exp_continue
	    }
	    -re ".*> " { exp_continue;}
	    }
	}
	"%TELNETD-W-BADPASSWD" {
	    puts "ERROR: bad userid or password to telnet."
	    return 1
	}
	"%CONS-W-AUTH_PASSWD" {
	    exp_continue
	}
	"% Authentication failed." {
	    puts "ERROR: bad userid or password to telnet."
	    return 1
	}
	"Authentication Failed:" {
	    puts "ERROR: bad userid or password to radius/tacacs+"
	    return 1
	}
	"Connection closed *" {
	    if {$pass_attempt == 3} {
		puts "ERROR: Maximum attempts for password reached. Check password. Exiting.";
		puts $expect_out(0,string);
		return 1
	    }
	}
	timeout {
	    puts "ERROR: Timeout on login. Exiting.";
	    return 1
	}
	eof {
	    puts "ERROR: device closed telnet connection during login"
	    return 1
	}
    }

    # save my_prompt for later use
    send "\r"
    expect -re ".*> "
    set abc "$expect_out(buffer)"
    set my_prompt "[lindex $abc 0]"
    regexp {(.*[^>])} $my_prompt my

    return 0;
}


# pre: login completed ok
# post: turn off paging of commands
proc disable_cli_paging { } {
    global my_prompt

    send "cli set terminal rows 0\r"

    expect {
	"$my_prompt" {return 0 }
    }
    return 1
}

# pre: login returned 0, prompt at top level
# post: turn off command completion return 0
# on error, return 1
proc disable_cmd_autocomplete { } {
    global my_prompt

    send "cli set command completion off\r"
    expect {
	$my_prompt { }

	timeout {
	    puts "ERROR:disable_cmd_autocomplete(TIMEOUT)";
	    return 0;
	}
    }

    return 0
}

# pre: login returned 0, do_enable returned 0, cli is in enable or config mode
# post: issues logout cli to device, returns 0
proc logout { prompt } {
    global config_mode enable_prompt

    # in case of not being at root cmd...
    # verify top level prompt state, move to it if necessary
    if { $config_mode == 1 } {
	send "exit\r"

	expect {
	    "Do you want*" {
		send "no\r"
	    }
	    "$enable_prompt" { }
	    timeout { puts "ERROR: logout: timeout from config mode\n" }
	    eof { puts "ERROR: device dropped connection\n" }
	}
	set config_mode 0
    }

    send "logout\r"
    expect {
	"Are you sure*" {
	    send "yes\r"
	    return 0
	}
    }
}

# pre: current mode allows transition to enable mode
# post: enable mode entered, my_prompt updated, return 0 else 1
proc do_enable { enauser enapasswd userpswd } {
    global expect_out verbose
    global my_prompt enable_prompt
    set enable_prompt [ string trimright $my_prompt ">" ]
    set enable_prompt $enable_prompt\#
    set uses_username 0;

    send "enable\r"

    expect {
	Username: {
	    set uses_username 1;
	    send -- "$enauser\r";
	    exp_continue
	}
	Password: {
	    if {$uses_username == 1} {
		send -- "$userpswd\r";
	    } else {
		send -- "$enapasswd\r";
	    }
	    exp_continue
	}
	"$my_prompt" {
	    puts "ERROR: do_enable failed to gain enable mode."
	    return 1
	}
	"CONS-W-AUTH_PASSWD" { send -- "$enapasswd\r"; }
	"$enable_prompt " { }
	"%SYS-W-NOPASSWD*" { }
	"Authentication Failed: Access Denied" {
	    puts "ERROR: Bad user or password for enable mode."
	    return 1
	}
    }

   set my_prompt $enable_prompt
   return 0
}

# pre: current mode allows transition to enable mode
# post: enable mode entered, my_prompt updated, return 0 else 1
proc do_configure { } {
    global expect_out verbose config_mode
    global my_prompt
    set config_prompt [ string trimright $my_prompt "\#" ]
    set config_prompt $config_prompt\(config\)\#

    send "configure\r"
    expect {
	"$config_prompt " { }
	"$my_prompt" { }
	eof { return 1}
	timeout { return 1}
    }

    set config_mode 1
    set my_prompt $config_prompt
    return 0
}

# track sent/received from device to output_file
# pre: outut_file is valid filename w/write access
# post: logfile open, global var logging == 1, return 0 , else 1
proc start_logfile { output_file } {
    global logging

    if { [ string length $output_file ] != 0 } {
	set rc [ catch { log_file -noappend $output_file } errMsg ]

	if { $rc != 0 } {
	    puts "ERROR: open file $output_file for write access failed. $errMsg\n"
	    return 1
	}
	set logging 1
    }

    return 0
}

proc run_commands { prompt cmdstring } {
    global do_interact sendstring

    # handle escaped ;s in commands, and ;; and ^;
    regsub -all {([^\\]);} $command "\\1\u0002;" esccommand
    regsub -all {([^\\]);;} $esccommand "\\1;\u0002;" command
    regsub {^;} $command "\u0002;" esccommand
    regsub -all {[\\];} $esccommand ";" command
    regsub -all {\u0002;} $command "\u0002" esccommand
    set sep "\u0002"
    set commands [split $esccommand $sep]
    set num_commands [llength $cmdstring]

    for {set i 0} {$i < $num_commands} { incr i} {
	regsub -- {[ ]*([^\.]*)} [subst -nocommands [lindex $commands $i]] {\1} sendstring

	if {[ run_single_command $prompt $sendstring ] == 1} {
	    puts "ERROR: command '$sendstring' not processed by device.  Check previous error messages."
	    return 1
	}
    }

    if { $do_interact == 1 } {
	interact
	return 0
    }

    return 0
}

# Run commands given on the command line
# pre: prompt is current system cli prompt, cmdstring is command to execute
# post: return 0 on success else 1
#  NOTE: output from router ends up in output_file if specified
#  expect internal input buffer is reset to "" after each command
proc run_single_command { prompt cmdstring } {
    global verbose
    set rc 0
    set seen_prompt 0
    set seen_timeout 0
    set need_ays 0
    set delay 0

    # ays == "are you sure" - must send back yes
    if { [string compare $cmdstring "save startup" ]  == 0 } {
	set need_ays 1
	set delay 1
    }

    # TODO: add case for copy command to startup, also prompts for ok

    # TODO: if we see config command: system set name note it
    # if we see a save active, then update system prompts as well

    send "$cmdstring\r"

    if { $delay == 1} {
	sleep 1
    }

    expect {
	"%CLI-E-IVCMD*" {
	    puts "ERROR: run_commands(command rejected by device)\n"
	    set rc 1
	}
	"%CLI-E-FACUNKNWN*" {
	    puts "ERROR: run_commands(command rejected by device)\n"
	    set rc 1
	}
	"%SYS-I-ADDFAILED*" {
	    puts "ERROR: run_commands(command rejected by device)\n"
	    set rc 1
	}
	"%TFTP-E-REMOTE,*" {
	    puts "ERROR: run_commands(command rejected by device)\n"
	    set rc 1
	}
	"%SYS-E-PRIMARY_NO_SUCH_IMAGE*" {
	    puts "ERROR: run_commands(command rejected by device)\n"
	    set rc 1
	}
	"want to overwrite " {
	    send "yes\r"
	    set need_ays 0
	}
	"%CONFIG-E-DUPLICATE,*" { }
	"$prompt" {
	    if { $seen_prompt == 0 } {
		set seen_prompt 1
	    }

	    if { $need_ays == 1 } {
		exp_continue
	    }
	}
	-re ".* More: m,<space> --- Quit: q --- One line: <return> ---" {
	    send "q"
	    exp_continue
	}
	timeout {
	    if {$verbose == 1} {
		puts "DEBUG: timeout occured for the $seen_time time\n"
	    }

	    if { $seen_timeout == 0 } {
		set seen_timeout 1
		send "\r\r"
		exp_continue
	    }

	    puts "ERROR:run_commands(TIMEOUT)"
	    set rc 1
	}
	eof {
	    puts "ERROR:run_commands(connection closed by device)\n"
	    set rc 1
	}
	"\n" { exp_continue }
    }

    # clear input buffer of any remaining data
    expect "*"
    return $rc
}


# pre: RSTONE_USER env var is set
# post: update global "default_user" to this string
proc init_userid { } {
  global default_user

    if {[ info exists env(RSTONE_USER) ] } {
	set default_user $env(RSTONE_USER)
    } else {
	# This uses "id" which I think is portable.  At least it has existed
	# (without options) on all machines/OSes I've been on recently -
	# unlike whoami or id -nu.
	regexp {\(([^)]*)} [exec id] junk default_user
    }
}

proc source_script_file { filename } {
    global my_prompt

    expect -re "$my_prompt" {}

    source $filename
}

# pre: login completed ok, filename contains set of cli commands one per line
# post: each command is extracted from filename and sent to device
# return 0 on success, return 1 on error
# NOTE: for scripts that begin with "configure", change the mode to configure
# before executing the following commands
proc process_script_file { filename } {
    global my_prompt verbose
    set rc 0
    set ifile ""

    set rc [ catch { set ifile [ open $filename r] } errMsg ]

    if { $rc != 0 } {
	puts "ERROR: process_script_file: open script file $filename for read access failed. $errMsg\n"
	return 1
    }

    set line_cnt 0

    while { [eof $ifile]  != 1 } {

	set bytes [ gets $ifile cmd ]
	incr line_cnt

	if { $bytes < 0  } {
	    break
	} elseif { $bytes == 0 } {
	    continue
	}

	# skip comments in script files
	if { [regexp "^\#" $cmd] != 1  } {

	   # puts "$cmd rc = [string compare $cmd "configure" ]\n"

	    if { [string compare $cmd "configure" ]  == 0 } {

		do_configure

	    } else {
		if {[ run_commands $my_prompt $cmd ] == 1} {
		    puts "ERROR: line $line_cnt in $filename not processed by device. Check previous error msgs."
		    set rc 1
		    break
		}
	    }
	}
    }

    close $ifile
    return $rc
}

#  pre: filename is valid file
#  post: remove extended ascii sequences and other cruft
#  and prepend a header: rscmd: ip-addr : date
#  TODO: should watch all file commands more closely
proc strip_log { filename router } {
    global tempfile

    set rc [ catch { set ifile [ open $filename r] } errMsg ]

    if { $rc != 0 } {
	puts "ERROR: strip_log: open script file $filename for read access failed. $errMsg\n"
	return 1
    }
    set rc [ catch { set ofile [ open $tempfile w] } errMsg ]

    if { $rc != 0 } {
	puts "ERROR: strip_log: open temp file $tempfile for write access failed. $errMsg\n"
	return 1
    }

    set nl 0

    puts $ofile "rscmd: $router : [exec date]"

    while { [eof $ifile]  != 1 } {
	set bytes [ gets $ifile cmd ]
	if { $bytes <= 0  } {
	    break
	}
	incr nl
	if { $nl <= 2 } {
	    continue
	}

	regsub -all -- "\r" $cmd "" newcmd
	puts $ofile $newcmd
    }

    close $ifile
    close $ofile
    set rc 0
    file copy -force $tempfile $filename
    file delete $tempfile
    return $rc
}

#
# main section
#
# send input like in a fast and consistent human style
set send_human {.1 .3 1 .05 2}

# initialize default_user variable
init_userid

# Parse Command Line
for {set idx 0} {$idx < $argc} {incr idx} {
    set arg [lindex $argv $idx]

    switch  -glob -- $arg {
	-c* {
	    if {! [  regexp .\[cC\](.+) $arg ignore command]} {
		incr idx
		set command [ lindex $argv $idx ]
	    }
	    set do_command 1
	# Expect debug mode
	} -d* {
	    exp_internal 1
	# Environment variable to pass to -s scripts
	} -E*
	{
	    if {[ regexp .\[E\](.+)=(.+) $arg ignore varname varvalue]} {
		set E$varname $varvalue
	    } else {
		send_user "Error: invalid format for -E in $arg\n"
		exit 1
	    }
	# Expect script to run
	} -s* {
	    if {! [  regexp .\[sS\](.+) $arg ignore sfile]} {
		incr idx
		set sfile [ lindex $argv $idx ]
	    }

	    if { ! [ file exists $sfile ] } {
		puts "ERROR: invalid argument script file \"$sfile\" does not exist.\n"
		exit 1
	    }
	    if { ! [ file readable $sfile ] } {
		puts "ERROR: invalid argument script file \"$sfile\" permissions disallow read access.\n"
		exit 1
	    }

	    set do_script 1
	# save config on exit
	} -S* {
	    set do_saveconfig 1
	# Version string
	} -V* {
	    send_user "@PACKAGE@ @VERSION@\n"
	    exit 0
	# Command file
	} -x* {
	    if {! [  regexp .\[xX\](.+) $arg ignore cmd_file]} {
		incr idx
		set cmd_file [ lindex $argv $idx ]
	    }
	    if [ catch {set cmd_fd [open $cmd_file r]} reason ] {
		send_user "\nError: $reason\n"
		exit 1
	    }
	    set cmd_text [read $cmd_fd]
	    close $cmd_fd
	    set command [join [split $cmd_text \n] \;]
	    set do_command 1
	} -f* {
	    if {! [ regexp .\[fF\](.+) $arg ignore password_file]} {
		incr idx
		set password_file [ lindex $argv $idx ]
	    }
	} -o* {
	    if {! [ regexp .\[fF\](.+) $arg ignore password_file]} {
		incr idx
		set output_file [ lindex $argv $idx ]
	    }
	# Timeout
	} -t* {
	    incr idx
	    set timeoutdflt [ lindex $argv $idx ]
	# Do we enable?
	} -noenable {
	    set avenable 0
	} -* {
	    send_user "Error: Unkown argument! $arg\n"
	    send_user $usage
	    exit 1
	} default {
	    break
	}
    }
}

# Verify at least one router is specified
#
if { $idx == $argc } {
    puts "\n$usage"
    exit 1
}

# main loop
set exitval 0
foreach router [lrange $argv $idx end] {
    set router [string tolower $router]
    send_user -- "$router\n"

    # device timeout
    set timeout [find timeout $router]
    if { [llength $timeout] == 0 } {
	set timeout $timeoutdflt
    }

    # look for noenable option in .cloginrc
    if { [find noenable $router] == "1" } {
	set enable 0
    } else {
	set enable $avenable
    }

    # Figure out passwords
    if { $do_passwd || $do_enapasswd } {
	set pswd [find password $router]
	if { [llength $pswd] == 0 } {
	    send_user -- "\nError: no password for $router in $password_file.\n"
	    exit 1
	}
	if { $do_enapasswd && [llength $pswd] < 2 } {
	    send_user -- "\nError: no enable password for $router in $password_file."
	    exit 1
	}
	if { $do_passwd } {
	    set passwd [join [lindex $pswd 0] ""]
	} else {
	    set passwd $userpasswd
	}
	if { $do_enapasswd } {
	    set enapasswd [join [lindex $pswd 1] ""]
	} else {
	    set enapasswd $enapasswd
	}
    } else {
	set passwd $userpasswd
	set enapasswd $enapasswd
    }

    # Figure out user to login with if necessary
    if {[info exists username]} {
	# command line username
	set user $username
    } else {
	set user [join [find user $router] ""]
	if { "$user" == "" } { set user $default_user }
    }

    # Figure out username's password
    if {[info exists userpasswd]} {
	# command line username
	set userpswd $userpasswd
    } else {
	set userpswd [join [find userpassword $router] ""]
	if { "$userpswd" == "" } { set userpswd $passwd }
    }

    # Figure out enable username
    if {[info exists enausername]} {
	# command line enausername
	set enauser $enausername
    } else {
	set enauser [join [find enauser $router] ""]
	if { "$enauser" == "" } { set enauser $user }
    }

    # Login to the router, set my_prompt to router's cmd prompt
    if {[login $router $user $userpswd $passwd $enapasswd ]} {
	incr exitval
	if { $verbose == 1 } {
	    puts "DEBUG: login to $router failed\n"
	}
	exit 1
    }

    if { $enable == 1 } {
	if { [do_enable $enauser $enapasswd $userpswd] == 1} {
	    incr exitval
	    if { $verbose == 1 } {
		puts "DEBUG: switch to enable mode on $router failed\n"
	    }
	    exit 1
	}
    }

    # Figure out the telnet executable name
    set telnetcmd [join [lindex [find telnetcmd $router] 0] ""]
    if { "$telnetcmd" == "" } { set telnetcmd "@TELNET_CMD@" }

    # run in one of three modes
    if { $do_command } {
	disable_cmd_autocomplete
	disable_cli_paging

	if { [start_logfile $output_file] != 0 } {
    	    exit 1
	}

	if {[ run_commands $my_prompt $command ]} {
	    incr exitval
    	    log_file
	    exit 1
	} else {
	    logout $my_prompt
	}
    } elseif { $do_script } {
	disable_cmd_autocomplete
	disable_cli_paging

	if {[ start_logfile $output_file] != 0 } {
	    exit 1
	}

	source_script_file $sfile

	logout $my_prompt
    } else {
	label $router
	log_user 1

	if {[ start_logfile $output_file] != 0 } {
	    exit 1
	}
	interact
	log_file
    }

    if { $logging == 1} {
	log_file
	strip_log $output_file $router
    }

    # End of for each router
    catch {wait};
    sleep 0.3
}
exit $exitval
