#!/usr/bin/env expect
# vim: set filetype=expect ts=4 sw=4 sts=4 et:

# To debug this script, you may want to uncomment the two following lines:
#exp_internal 1
#strace 2

# we need at least 10 positional arguments (must be synchronized with osh.pl, our caller)
if { [llength $argv] < 10 } {
    puts {BASTION SAYS: autologin usage error, expected 10 positional args: <ssh|telnet> <login> <ip> <port> <file_with_password> <password_id> <timeout> <fallback_delay> <stty_options> <terminal> [passthrough arguments to ssh or telnet]}
    exit 1
}

# name our positional arguments
set arg_prog           [lindex $argv 0]
set arg_login          [lindex $argv 1]
set arg_ip             [lindex $argv 2]
set arg_port           [lindex $argv 3]
set arg_file           [lindex $argv 4]
set arg_password_id    [lindex $argv 5]
set arg_timeout        [lindex $argv 6]
set arg_fallback_delay [lindex $argv 7]
set arg_stty_options   [lindex $argv 8]
set arg_term           [lindex $argv 9]
set arg_remaining      [lrange $argv 10 end]

# set the TERM env to the wanted value, this is passed through ssh so it may modify the behavior of the remote device
set ::env(TERM) $arg_term

# start the program
if { $arg_prog == "ssh" } {
    lappend spawn_args -l $arg_login -p $arg_port $arg_ip
} elseif { $arg_prog == "telnet" } {
    lappend spawn_args $arg_ip $arg_port
} else {
    puts "BASTION SAYS: autologin usage error, program must be either 'ssh' or 'telnet'"
    exit 1
}

if { [llength $arg_remaining] > 0 } {
    set spawn_args [concat $spawn_args $arg_remaining]
}

# set the interactive timeout for expect{} blocks
set timeout $arg_timeout

# if success, doesn't return (calls interact then exit 0)
# if auth failed, return 100 (caller might retry with another password)
# if other non-critical error, return 101
# if critical error, exits
proc attempt_to_login args {
    set tryid              [lindex $args 0]
    set prog               [lindex $args 1]
    set login              [lindex $args 2]
    set file               [lindex $args 3]
    set arg_fallback_delay [lindex $args 4]
    set spawn_args         [lindex $args 5]
    set stty_options       [lindex $args 6]

    if { [file exists $file] == 0 } {
        if { $tryid == 0 } { puts "BASTION SAYS: file $file does not exist" }
        return 101
    }
    if { [file readable $file] == 0 } {
        if { $tryid == 0 } { puts "BASTION SAYS: file $file is not readable with our current rights" }
        return 101
    }

    if { $tryid > 0 } {
        puts "BASTION SAYS: trying with fallback password $tryid after sleeping for $arg_fallback_delay seconds..."
        sleep $arg_fallback_delay
    }

    # reading password (256 chars max)
    set pass_fh [open $file r]
    set pass [read $pass_fh 256]
    close $pass_fh

    # stty_init: if we have $stty_options, use it
    if { $stty_options != "" } {
        set stty_init "$stty_options"
    }
    spawn -noecho $prog {*}$spawn_args

    if { $prog == "telnet" } {
        # send login (only for telnet)
        expect {
            -re "login:|Username:" { send -- "$login\n" }
            eof     { puts "BASTION SAYS: connection failed"; exit 2 }
            timeout { puts "BASTION SAYS: timed out while waiting for login prompt"; exit 2 }
        }
    }

    if { $stty_options != "" } {
        # in that case, silence the "Password:" prompt, as our caller probably doesn't expect (sic) to see it
        log_user 0
    }

    # send password
    expect {
        -re {[Pp]assword:|Password for [a-zA-Z0-9@._-]+:} { send -- "$pass" }
        eof     { puts "BASTION SAYS: connection aborted"; exit 3 }
        timeout { puts "BASTION SAYS: timed out while waiting for password prompt"; exit 3 }
    }

    if { $stty_options != "" } {
        # restore log_user to its default value after the "Password:" prompt
        log_user 1
    }

    # do we have a login success with interactive prompt?
    expect {
        # prompts
        "#" { interact; exit 0 }
        ">" { interact; exit 0 }
        # 'enable' prompt on a network device
        "(enable)" { interact; exit 0 }
        # a successful login on a bastion (mainly for tests)
        -exact "the-bastion-" { interact; exit 0 }
        # login failure messages
        -re {[Pp]assword:|Password for [a-zA-Z0-9@_-]+:|Authentication failed|Permission denied|UNIX authentication refused} {
            if { $tryid == 0 } { puts "BASTION SAYS: authentication failed!" }
            close
            wait
            return 100
        }
        eof     { puts "BASTION SAYS: connection aborted"; exit 4 }
        timeout { puts "BASTION SAYS: timed out while waiting for interactive prompt on success login"; exit 4 }
    }
    # unreachable:
    exit 5
}

# if no specific password was requested, try to login with the main password file, then try the fallbacks
set tryid 0
if { $arg_password_id == -1 } {
    set last_attempt [attempt_to_login $tryid $arg_prog $arg_login $arg_file $arg_fallback_delay $spawn_args $arg_stty_options]
    while { $last_attempt == 100 && $tryid < 10 } {
        # auth failed, might want to try with the fallback
        incr tryid
        set last_attempt [attempt_to_login $tryid $arg_prog $arg_login "$arg_file.$tryid" $arg_fallback_delay $spawn_args $arg_stty_options]
    }
} elseif { $arg_password_id == 0 } {
    set last_attempt [attempt_to_login $tryid $arg_prog $arg_login $arg_file $arg_fallback_delay $spawn_args $arg_stty_options]
} else {
    set last_attempt [attempt_to_login $tryid $arg_prog $arg_login "$arg_file.$arg_password_id" $arg_fallback_delay $spawn_args $arg_stty_options]
}
exit $last_attempt
