#!/usr/bin/expect # # rrr - remote remote remote. # # Usage: rrr "first command" ["second command" ...] # # Example: rrr "rlogin portal" "ssh1 joe@door.moose.com" "ssh mary@noc.noc" "telnet marysmachine" # # The problem this is trying to solve: suppose I am trying to get # an interactive shell on a remote machine, possibly requiring # several hops through rlogin, ssh, telnet, ... . # # Doing this in the usual way, you suffer the worst kind of interaction: # you type in the command for the first hop, # wait several seconds (or minutes) for the first password prompt, # enter the first password (while the program waits for you), # wait for the first shell prompt, # type in the second hop command, wait for the second password prompt, ... . # This wait-a-long-time, give-me-something, wait-a-long-time, # give-me-something behavior can cause your brain to thrash and overheat. # # This script solves all that by gathering all the passwords up front, # with no pauses, and then doling them out to the program as they are needed, # dropping into an interactive loop at the final prompt. # I use this even for a single ssh (instead of waiting for n seconds # for the password prompt). # # Assumptions: # - all shell prompts end in "% ", "# ", "> ", ": ", or "$ " # - anything with "assword" in it is a password prompt # - all programs require passwords # - all programs do echoing properly, i.e. they turn off echoing # *before* they prompt for the password. (this script # doesn't hesitate or try to cover up accidentally echoed passwords.) # Bugs: # - doesn't suspend properly # - too complicated! maybe there is a simpler way... # think about what would happen if we wait for the password # even if the user hasn't given it to us yet. # - works with scp but timeout is too small for large copies # # Author: Don Hatch # Last modified: Fri May 4 05:30:26 PDT 2001 # # This software may be used for any purpose # as long as it is good and not evil. # proc send_error_debug {messagelevel message} { global debuglevel if {$messagelevel <= $debuglevel} { send_error $message } } # # Main stuff... # # To change debuglevel, use "expect -c 'set debuglevel 8'" (or whatever). # Levels: # 0: nothing # 1 (default): SIGWINCH changes before child is spawned # 2: and SIGWINCH changes propagated to child # 3: and trace expect/interact calls and stty change # 4: and trace the buffer holding child output # 5: and print password lengths if {! [info exists debuglevel] } { set debuglevel 1 } set timeout 60; # change timeout for expect from 10 (default) to 60 seconds (this doesn't affect interact) # propagate window size changes to child... trap { set rows [stty rows] set cols [stty columns] # XXX this exists test has a race condition I think :-( if {[info exists spawn_out(slave,name)]} { send_error_debug 2 "\[\[WINCH $rows $cols\]\]\n" stty rows $rows columns $cols < $spawn_out(slave,name) } else { send_error_debug 1 "\[\[WINCH $rows $cols but no child\]\]\n" } } WINCH if {$argc < 1} { send_error "Usage: rrr \"first command\" \[\"second command\" ... \]\n" exit 1 } if {1} {; # new way of doing things, will replace old maybe # # Gather passwords from the user, # at the same time feeding commands and passwords to the # child when prompted by it. # The only time we don't give something immediately to the child # when prompted (i.e. the only time the child blocks) # is when it needs a password the user hasn't given yet, # in which case we will send it as soon as the user gives it. # The user never blocks (this is one of the main requirements of # this program); sie is allowed to give many passwords in advance # of when the child needs them. # set n [llength $argv]; # number of commands set i 0; # number of passwords got set j 0; # number of commands started set k 0; # number of passwords sent to child # # Spawn first command... # XXX how to trap failure? # eval spawn -noecho [lindex $argv $j] incr j set thebuffer "" # XXX would like to interact with user in cooked mode, # XXX but that doesn't seem to be possible. So, # XXX in the cases when the password is being gathered # XXX using interact rather than expect, it may contain # XXX backspaces and stuff, which I guess is okay, # XXX but sort of screwy. set oldmode [stty -echo]; # changed to non-echo cooked mode send_error_debug 3 "oldmode = $oldmode\n" while {$i < $n} { send_user "password for \"[lindex $argv $i]\": " # assertions: # $k <= $i < $n # $j <= $n # $k == $j or $k == $j-1 # Keep interacting until the user enters the password... while {1} { if {$k < $j && $k < $i} { # started child $k but haven't sent password $k, # and password $k is ready send_error_debug 3 "\[\[interacting awaiting password prompt $k\]\]" interact { -re "(.*)\r" {; # \r since raw send_user "\n" lappend passwords $interact_out(1,string) send_error_debug 5 "\[\[pw length = [string length $interact_out(1,string)]\]\]" incr i break; # from while {1} } -o -re ".*assword:" { send_error_debug 3 "\[\[got password prompt $k\]\]" append thebuffer $interact_out(0,string) send_error_debug 4 "\[\[thebuffer is now: \"$thebuffer\"\]\]" send "[lindex $passwords $k]\r" incr k return; # from interact } } } elseif {$k == $j} { # sent password $k, waiting for prompt $k send_error_debug 3 "\[\[interacting awaiting prompt $k\]\]" interact { -re "(.*)\r" {; # \r since raw send_user "\n" lappend passwords $interact_out(1,string) send_error_debug 5 "\[\[pw length = [string length $interact_out(1,string)]\]\]" incr i break; # from while {1} } -o -re ".*(%|#|\\$|>) $" { # # We know $j == $k <= $i < $n # so the following is in bounds # send_error_debug 3 "\[\[got shell prompt $k\]\]" append thebuffer $interact_out(0,string) send_error_debug 4 "\[\[thebuffer is now: \"$thebuffer\"\]\]" send "[lindex $argv $j]\n" incr j return; # from interact } } } else {; # $k < $j && $k == $i # started child $k but user hasn't given us # its password yet; can't do anything except # wait for the password from the user. send_error_debug 3 "\[\[interacting solely for password\]\]" expect_user { -re "(.*)\n" {; # \n since cooked send_user "\n" lappend passwords $expect_out(1,string) send_error_debug 5 "\[\[pw length = [string length $expect_out(1,string)]\]\]" incr i break; # from while {1} } } } }; # while {1} }; # while {$i < $n} eval stty $oldmode # # All passwords are now gathered; # display all buffered output from the child, # and all subsequent output will be displayed as it comes. # #send_error_debug 100 "\[\[got all passwords: $passwords\]\]" send_error_debug 3 "\[\[begin released buffer\]\]" send_user $thebuffer send_error_debug 3 "\[\[end released buffer\]\]" # # Do this after all password-gathering calls to expect_user, # otherwise it will apply to those expect_user calls # and if the child dies during an expect_user # then this script will exit and the password (or part of it) that the # user has entered so far will be spewed to stdout, which is bad. # expect_after { timeout {send_user "\[\[timed out\]\]\n"; exit} eof {send_user "\[\[got eof from child\]\]\n"; exit} } # # Keep automating the child until all passwords are sent # and last prompt appears. # Since we use expect rather than interact for this, output # gets sent to the terminal. # # # We are now in one of two states: # 1) $k==$j-1: waiting for password prompt $j-1 # 2) $k==$j: waiting for prompt $j-1 # if {$k < $j} { send_error_debug 3 "\[\[waiting for password prompt\]\]" expect "assword:" send_error_debug 3 "\[\[got password prompt\]\]" send "[lindex $passwords $k]\r" incr k } # Now $k==$j and it will stay that way... while {$k < $n} { send_error_debug 3 "\[\[waiting for shell prompt\]\]" expect -re "(%|#|\\$|>) $" send_error_debug 3 "\[\[got shell prompt\]\]" send "[lindex $argv $j]\r" incr j send_error_debug 3 "\[\[waiting for password prompt\]\]" expect "assword:" send_error_debug 3 "\[\[got password prompt\]\]" send "[lindex $passwords $k]\r" incr k } send_error_debug 3 "\[\[waiting for final shell prompt\]\]" expect { -re "(%|#|\\$|>) $" {} } } else { # # Old straightforward way, but only spawns the first command # before prompting for all passwords, so it wastes time # when the first password is ready and the first command is # ready for it but we're blocked waiting for the second (or more) # password from the user # # # Spawn first command... # XXX how to trap failure? # eval spawn [lindex $argv 0] # # Get passwords from user... # set oldmode [stty -echo -raw]; # no-echo, cooked for {set i 0} {$i < [llength $argv]} {incr i} { send_user "password for \"[lindex $argv $i]\": " expect_user -re "(.*)\n" send_user "\n" lappend passwords $expect_out(1,string) send_error_debug 5 "\[\[pw length = [string length $expect_out(1,string)]\]\]" } eval stty $oldmode # # Send passwords one by one as we are prompted for them, # and start successive commands # for {set i 0} {$i < [llength $argv]} {incr i} { if {$i > 0} { send "[lindex $argv $i]\r" } send_error_debug 3 "\[\[waiting for password prompt\]\]" expect "assword:" send_error_debug 3 "\[\[got password prompt\]\]" send "[lindex $passwords $i]\r" send_error_debug 3 "\[\[waiting for shell prompt\]\]" expect -re "(%|#|\\$|>) $" send_error_debug 3 "\[\[got shell prompt\]\]" } } send_user "\[\[you're on your own\]\] " interact send_user "\[\[bye\]\]\n"