#!/usr/bin/kermit +
#!/opt/local/bin/kermit +
#!/p/kd/fdc/solaris9/wermit +

# IMPORTANT: In Unix, the top line must indicate the path of the C-Kermit 
# binary that is to execute this script, and it must be followed by a
# space and a plus sign.  And this file itself must have execute permission.

.version = 2.05

# General-purpose Rolm CBX access script.
# Must be run from the directory where it is kept.
# Command-line aruments:
#  1. CBX Nodename
#  2. Command or commands to execute on that node
#  3. Tag to use in generated filenames that identifies the task
#
# [2.04]
# If only the first argument is given, an interactive session is attempted.
# If the second argument is given the third one is required too.
#
# Dependencies:
#  1. C-Kermit 8.0 or later or Kermit 95 2.0 or later
#  2. Shared master config script in same directory that defines
#     access info for each CBX node.
#  3. Not required but handy: shell script in same directory for launching.

.loginonly = 0
if ( == \v(argc) 2 ) {
     echo LOGINONLY
    .loginonly = 1
} else if ( < \v(argc) 4 ) {
    exit 1 {Usage: \fbasename(\%0) cbxnodename "cbx command(s)" tag}
}
.configfile := ./config			# Config file path is hardwired
.nodename := \%1			# Name of CBX node to access
if not \m(loginonly) {
    void \fsplit(\%2,&c,\44)		# Get command list (one or more)
    if < \fdim(&c) 1 exit 1 {FATAL - Command list empty}
}
.tag := \%3				# ID for output files
.encryptedconfig = 0			# 1 for encrypted config file
.production = 1                         # 1 for production runs
.quiet = 1                              # 1 for silent running as in cron
.debug = 0                              # 1 for verbose debugging
undef \%c				# Logfile channel
.multinode = 0				# Most switches don't have > 1 node
if equal "\m(nodename)" "morningside" .multinode = 1 # But Morningside does

if def \$(DEBUG) .debug = 1

# Development / debugging vs production settings...

if \m(quiet) {                          # Be quiet?
    set quiet on                        # Be quiet
    set input echo off                  # Be quiet
    set take echo off                   # Be quiet
} else {                                # Not quiet...
    set input echo on                   # Watch all dialogs
}
if ( \m(debug) || \m(loginonly) ) {	# Debugging or interactive
    ; set take echo on                  # Print each command upon execution
    ; set macro echo on                 # Print each command upon execution
    set input echo on
}
# Macro definitions...

# Macro to make a timestamped entry in the history log
define logentry {
    if \m(loginonly) end 0
    if not def \%c end 0 WARNING HISTORY LOG FILE NOT OPEN
    fwrite /line \%c "\v(timestamp): \%1"
    if fail fatal "History log write error - \f_errmsg()"
}
# Macro to handle fatal errors (C-Kermit should exit, K95 should not)
define fatal {
    if numeric \%c {
        logentry "FATAL - \%1"		# First log the error
        fclose \%c			# and close the log.
    }        
    if eq "\v(system)" "win32" stop 1 "FATAL - \%1" # Windows: Stop, don't exit
    writeln error "FATAL - \%1"         # Unix: write message to stderr
    exit 1                              # and exit with failure code
}
# Macro to read configuration file
define readconfig {			# Read configuration file
    if not \m(encryptedconfig) {	# Clear-text config 
        take \m(configfile) \m(nodename) # Execute it directly
        end \v(status)			# and we're done

    }    
    # Encrypted config file.
    # User must enter decryption key from keyboard.  Then we feed it to crypt
    # interactively through a pty so it can't be seen on a 'ps' or 'w'
    # listing.  Then we read the contents of the file from crypt's stdout
    # through the same pty, so the file never has to be stored in clear text
    # on disk even for an instant.

    undef key				# Prompt for key
    while not def key { askq key "Enter key: " } # Until we have one

    set host /pty {sh -c "cat config.x | crypt"} # Start pty
    if fail exit 1 "Can't start pty"
    input 3 "Enter key:"		# Get key prompt from crypt
    if fail exit 1 "No key prompt from crypt" 
    output \m(key)\13			# Give crypt the key
    undef key				# erase the key

    while true {			# Loop through decrypted lines
	clear input			# Clear INPUT buffer
	input 2 \10			# Get next line (up to linefeed)
	if fail {			# Check for error
	    if = \v(instatus) 4 break	# I/O error = EOF - finished with file
	    inerr "Problem reading config file" # Other error - fatal
	}
	.line := \ftrim(\v(input),\32\9\10\13)  # Trim junk chars from line
	{ \m(line) }                            # Execute the line as a command
	if fail exit 1 "BAD COMMAND [\m(line)]" # Maybe wrong decryption key?
    }
    close connection
}
# Default definitions for some variables that usually don't vary
# but that can be overridden in the configuration file.
#
define setdefaults {
    undef site
    undef telnet_ip
    undef telnet_username_prompt
    undef telnet_username
    undef telnet_password_prompt
    undef telnet_password
    undef digi_selection_prompt
    undef digi_selection
    undef digi_username_prompt
    undef digi_username
    undef digi_password_prompt
    undef digi_password

    def telnet_username_prompt "login: "
    def telnet_password_prompt "password: "
    def digi_selection_prompt "Selection > "
    def digi_selection 1
    def digi_username_prompt "Digi UserName:"
    def digi_password_prompt "Digi Password "

    # The next ones are not used since all CBXs should be the same but if it
    # turns out that some are different we'll use these too.

    def cbx_username_prompt "USERNAME: "
    def cbx_password_prompt "PASSWORD: "
    def cbx_command_prompt "COMMAND: "
}
define check_var {
    if not def \%1 end 0
    if not def \m(\%1) fatal "\%1: not defined"
    if \m(debug) show mac \%1
}
define checkconfig {			# These variables must be defined
    check_var site
    check_var telnet_ip
    check_var telnet_username_prompt
    check_var telnet_username
    check_var telnet_password_prompt
    check_var telnet_password
    check_var digi_selection_prompt
    check_var digi_selection
    check_var digi_username_prompt
    check_var digi_username
    check_var digi_password_prompt
    check_var digi_password
    check_var cbx_username_prompt
#   check_var cbx_username
#   check_var cbx_password_prompt
#   check_var cbx_password
    if not def datadirectory def datadirectory .
}
# Execution starts here...

set network type tcp/ip                 # (as opposed to SSH)
set exit warning off                    # Don't hassle me if I try to exit
.rc = 1                                 # Default exit status code is failure
setdefaults				# Set default running parameters
readconfig				# Read execution parameters
if fail fatal "Configuration file \m(configfile) - \v(errstring)"
checkconfig				# Check that all variables are defined

.logfile := \m(tag)-\m(site).log	# History log file name
if \m(loginonly) {
    .sessionlog := \m(site)_\v(user)_\v(timestamp).ses # Interactive log
    .sessionlog := \fsubstitute(\m(sessionlog),\32:,_.)
} else {
    .sessionlog := \m(tag)-\m(site).ses	# Session log just for login sequence
}
if open session-log close session-log
log session \m(sessionlog)		# Always do this just in case
if open session-log echo LOGGING TO \m(sessionlog)

cd \m(datadirectory)			# Change to data directory.
if fail fatal -
"CD to datafile directory \fpathname(\m(datadirectory) - \v(errstring)"

if not \m(loginonly) {
    fopen /append \%c \m(logfile)	# Open history log.
    if fail fatal "History log file - \m(logfile) - \f_errmsg()"
    logentry "BEGIN SCRIPT \v(cmdfile) v\m(version)"

# Check if data file already exists
    .datafile := \m(tag)_\m(site).cap	# File containing captured switch data
    if exist \m(datafile) {		# Check in advance before logging in
	if not writeable \m(datafile) {
	    fatal "Capture file \m(datafile) not writeable"
	}
    }
}

# Don't negotiate TELNET COM-PORT-CONTROL - The Digi Box supports this 
# but then gets confused if it is negotiated.  Disabling this keeps the 
# negotiations identical to system telnet.
#
set telopt com-port refused		# Don't even offer to do this.

# Make the connection without entering terminal mode.

set host \m(telnet_ip) telnet		# Use "set hose" instead of "telnet"
if fail fatal "Telnet \m(telnet_ip) failed - \v(errstring)"

for \%i 1 5 1 {                         # Try up to 5 times to get login prompt
   input \%i \m(telnet_username_prompt) # Use loop index for increasing timeout
   if success break                     # Got it - break from loop
    output \13                          # Send carriage return to wake it up
}
if > \%i 5 {
    fatal "Timeout waiting for Digi login prompt [\m(telnet_username_prompt)]"
}
# From here we have to avoid Telnet NVT mode (CR -> CRLF).  Trial and error
# shows that commands must be terminated with LF rather than CR, because
# sometimes CR is treated like two CRs, no matter what has been agreed upon
# in the telnet protocol negotiations, and then the script gets hopelessly out
# of sync.

set telnet newline binary               # This disables NVT mode on our side
.eol := \10				# Line terminator to use for commands

set output pacing 10

output \m(telnet_username)\m(eol)	# Send user ID
input 8 \m(telnet_password_prompt)      # Wait up to 8 sec for Password prompt
if fail fatal "Timed out waiting for Digi password prompt"
output \m(telnet_password)\m(eol)	# Send password
minput 10 "\m(digi_selection_prompt)" "\m(telnet_username_prompt)"
if ( fail || == \v(minput) 2 ) fatal "Digi authentication failure"
if fail fatal "Timed out waiting for Digi Selection prompt"
msleep 500                              # Wait 500 msec then send response
output \m(digi_selection)\m(eol)	# Select the desired service
minput 10 "\m(digi_username_prompt)" "Cannot open this port or hunt group."
if fail fatal "Timed out waiting for Digi response"
if == \v(minput) 2 fatal "\{13}\{10}Port \m(digi_selection) is busy"

msleep 100				# Connected to desired service
output \m(digi_username)\m(eol)		# Send Digi Username
input 5 \m(digi_password_prompt)
if fail fatal "Timed out waiting for service password prompt"
msleep 100
output \m(digi_password)\m(eol)

# Once we arrive at the switch console it can be in any of several states,
# depending on how it was left last time.  The following code allows
# for each possibility.
# 
# In a typical scenario, the CBX has already printed its banner and USERNAME:
# prompt before the Digi makes the connection.  We don't see any of this, so
# in the first round we time out and send a LF, which causes the CBX to print
# its PASSWORD: prompt.  But since we never sent a username, we have to send
# an empty response to get the "INVALID USERNAME-PASSWORD PAIR" response and
# then a new USERNAME: prompt, and then we're in sync.

# set output pacing 100

set input timeout proceed               # So we can test MINPUT result

.state = 1
for \%i 1 8 1 {                         # Try this up to 8 times
    minput 3 "COMMAND: " "USERNAME: " "PASSWORD: " "1% \29" "% \29" "NOUN:"
    switch \v(minput) {
      :1, forward SKIPAHEAD
      :2, if == \m(state) 3 exit 1 "CBX authentication failure"
          msleep 100
          output \m(cbx_username)\m(eol)
          .state = 2
          break 
      :3, msleep 100
          if == \m(state) 2 {
              output \m(cbx_password)
              .state = 3
          }
          output \m(eol)
          break 
      :4, msleep 100, output cn\m(eol), break
      :5, msleep 100
          if \m(multinode) {		# If multinode switch
              output logon node 1\m(eol) # must log on to a specific node
          } else {			# if single-node switch
              output cn\m(eol)    	# we're already there.
          }
          break
      :6, output \27			# Stuck at NOUN: prompt
          msleep 10			# send ESC ESC
          output \27
          continue
      :default, output \m(eol)		# Maybe also ^Q
    }
}
fatal "Failure to get CBX Console login prompts"  # Loop count exhausted


:SKIPAHEAD

if \m(loginonly) {
    set telnet bug binary-me-means-u-too
    set telopt binary-mode requested
    set telnet newline binary
    connect
    stop
}

if open session-log close session-log   # (in case we were debugging)

# At this point we should have the COMMAND prompt

set input timeout quit
if \m(production) {                     # If it's a production run
    for \%j 1 \fdim(&c) 1 {		# Do each command (one or more)
        .command := \fltrim(\&c[\%j])
        .job := \freplace(\m(command),\32,_)
        .datafile := \m(tag)-\m(site)-\m(job).cap
	if exist \m(datafile) {		# Delete previous datafile
	    .df := \m(datafile)		# (so next line will be shorter)
	    logentry "DELETE \m(df) \fsize(\m(df)) \fdate(\m(df))"
	    delete \m(datafile)		# (Or rename it for backup)
	} else {
	    logentry "DELETE (no previous datafile found)"
	}
	logentry "BEGIN CAPTURE \m(datafile)"
	log session \m(datafile)	# Start a new datafile.
	# Maybe put a PAGE OFF command here
	msleep 100			# Capture the listing.
	output "\m(command)\m(eol)"	# Execute the command that was given.
        # Terminate upon ERROR or next COMMAND prompt.
        minput 9999 {NOUN: \29} {COMMAND: \29}
	close session-log		# Close datafile.
	if eq "\m(tag)" "fac" {		# Finish the event log.
	  grep /output:/dev/null /count:\%n ^DS \m(datafile)
	  logentry -
"END CAPTURE \m(datafile) size=\fsize(\m(datafile)) records=\%n"
	} else {
	  grep /output:/dev/null /count:\%n * \m(datafile) # Count lines
	  logentry -
"END CAPTURE \m(datafile) size=\fsize(\m(datafile)) lines=\%n"
	}
        if == \v(minput) 1 {		# Command error
    	    logentry "STATUS: FAILURE (CBX COMMAND ERROR) [\m(command)]"
            .rc = 1			# Exit status is failure
            output {\27}		# Send ESC ESC to get COMMAND prompt
            msleep 10			# Maybe with a little pause
            output {\27}		# Finish sending ESC ESC
            input 5 {COMMAND: \29}	# Get command prompt
            if fail forward fin		# If we can't then bail out
        } else {
	    logentry "STATUS: SUCCESS"
            .rc = 0			# Exit status is success
        }
    }
}
# Now exit the CBX console, the Digi, and the Telnet host.  If any of the
# following INPUTs fails we just exit, which closes the Telnet connection
# automatically, which presumably should also shut the others.  Exit status is
# success even if any of the following fails because log is already captured.

:exit
output "bye\m(eol)"
if \m(multinode) {			# Multinode switches have this
    input 20 "1% \29"			# extra level of egress.
    if fail exit 0
    msleep 100
    output "bye\m(eol)"
}
input 20 "% \29"
if fail exit 0
msleep 100
output "bye\m(eol)"
output \m(eol)
minput 20 "USERNAME" "PASSWORD"
if fail exit 0
output "&\m(eol)"
input 20 "Selection > "
if fail exit 0
lineout "0"
input 2 \10
if fail exit 0
:fin
logentry "END SCRIPT status=\m(rc)"	# Make final log entry
fclose \%c
exit \m(rc)

; Local Variables:
; comment-column:40
; comment-start:"# "
; End:
