Updated 2013-11-13 15:54:20 by RFox

CMcC: Here are a couple of procs to feed tcl commands to a tclsh on another machine via ssh, and collect the result. Requires a recent tclsh8.4 with extended return and dict commands. -- Lars H, 2007-06-25: Don't you mean a tclsh8.5?

How it Works:

We use ssh to log into a remote machine as a remote user whose shell is tclsh. We then send a little command loop command to the remote tclsh, which catch/evals code sent to it via stdin (which is connected to the ssh write fd on the local machine.)

The result, with any errors, are strung together and output by remote on stdout, to be collected up on the ssh read fd on the local machine, and returned as if they were executed locally.

I'm concerned that the protocol breaks down if stdin is directly written to by the code sent to the remote, so that's a limitation. I am also concerned that if the remote's result contains special characters, they could interfere with the protocol ... any insights into that problem are welcome.

(tomas 2012-01-09) Disabling the escape character (option -e none) should make the transport fully transparent, according to the manual page (OpenSSH 5.9)

Security:

Caveat! This code enables anyone who can ssh connect to user@machine to run whatever code they like. Clearly this is not entirely safe, but it is no less safe than any ssh connection ... a safe interp version shouldn't be too hard to come up with, but is at the moment left as a future exercise. I wouldn't run it as root, but then I don't think I'd run any automated process with remote access as root without a lot of forethought, and a chroot jail.

Possible Extensions:

  • safe interp for the remote, using Safe to provide policy and such - main impediment being to correctly and flexibly implement policy setting at the remote end.
  • some protection against the puts stderr problem mentioned above.
  • asynchronous command execution - it's not too hard, actually, it only requires a sequence number to match requests and responses, and a fileevent-driven command interpreter.
  • chroot jail. TclX provides chroot, but actually setting up a chroot jail is a fairly complex process. Perhaps tclsh + chroot, working in with a Safe interp would do the trick.

The Code:

connect fred@host will connect as user fred on host host. Note that fred should be a user whose login shell is tclsh, and for whom login is not permitted (I know this sounds contradictory, but it works under unix.) The user running these procs has to have keys sufficient to log in without password and without passphrase.
   proc connect {where} {
        global ssh
        set ssh [open "|ssh $where" "r+"] ;# start up the remote shell, should be tclsh
        fconfigure $ssh -buffering line -blocking 1

        # set up a command loop which processes and returns our commands
        puts $ssh {
                fconfigure stdout -buffering line
                while {![eof stdin]} {
                    set cmd [gets stdin]
                    set code [catch $cmd result opt]
                    puts [string map [list \\ \\\\ \n \\n] $opt]
                    puts [string map [list \\ \\\\ \n \\n] $result]
                }
        }

        # return the ssh connection
        return $ssh
    }

I (CJL) found the code above introduces a lag between command and response, such that command-N sees the response to command-(N-1). I eventually tracked it down to the injection of a false null command by the puts $ssh ... statement. The code below removes that behaviour (the double closing brace, rather than two single braces is what makes the difference):
        puts $ssh {
                fconfigure stdout -buffering line
                while {![eof stdin]} {
                    set cmd [gets stdin]
                    set code [catch $cmd result opt]
                    puts [string map [list \\ \\\\ \n \\n] $opt]
                    puts [string map [list \\ \\\\ \n \\n] $result]
                }}

remote passes the expression to the remote via ssh, and returns the result.
    proc remote {arg} {
        global ssh

        # execute the command in the remote tclsh
        #puts stderr "CMD: $arg"
        puts $ssh $arg

        # get the error option dict
        set opt [string map [list \\n \n \\\\ \\] [gets $ssh]]
        #puts "OPT: $opt"

        # get the remote execution result
        set result [string map [list \\n \n \\\\ \\] [gets $ssh]]
        #puts "RESULT: $result"

        # return the result or error from the remote
        return -options [eval dict create $opt] $result
    }

remotes passes several commands to the remote ssh, returning the last result
    proc remotes {arg} {
        foreach line [split $arg \n] {
            #puts "CMDL: $line"
            remote $line
        }
    }

This is just a little bitttt of test code. There's a problem with quoting, if someone can show me how to do it better, I'd be grateful.
    if {[info exists argv0] && ($argv0 == [info script])} {
        connect user@host
        puts [remote set mumble 100]
        puts [remote incr mumble]
        puts [remote set mumble]
        puts [remote return "Woo\\nWoo\\n"]
        puts [remote set blerf]        ;# expect an error here
        puts [remote set mumble]
    }

Changes:

  • Changed [connect] to permit multi-line commands.

ZB 22.06.2008. BTW: there is an utility SecPanel [1], which is a SSH gui for unix like systems, written in pure TCL (last version 0.6.1 from 18.08.2010.). Wrote this here, because it's "very a'propos".

Tclssh is a package of pure Tcl functions for executing Tcl scripts in a remote host.

RFox - 2013-11-13 15:54:20

Would have thought this was an ideal job for expect?