Monday, December 16, 2013

Java, expect and groovy

If you need the TCL expect functionality in your java software, you basically have 4 options


TCL-Java bridges such as Jacl/TclBlend (http://tcljava.sourceforge.net) are useful to run small pieces of TCL code, but since expect contains some native C libraries, jacl is not an option. In the best case, you can write a TCL wrapper with TclBlend that calls your java code instead of the opposite.

So, from these 4 options, only ExpectJ supports expect "interact" mode. In expect, the "interact" command gives back the session control to the user. So, if the idea is to connect to a remote site after multiple hops, and get the session back (instead of running fire-and-forget scripts), ExpectJ is the way to go.

Now, you can also expose ExpectJ directives in a nice way (DSL) using groove. This is specially useful if you want to let the user write his/her own "expect" scripts.

Allowing the user to run his/her own scripts from inside an application also has security issues. For example, would you allow the user to add a "system.exit()" command? Not a good idea. We have to limit this too.

Groovy has some syntactic sugar features that lets the developer to write DSLs in a much faster and simpler way that it would be if you decide to write a full language from the scratch, with tools like ANTLR and JavaCC. You can also use eclipse to edit and run your Groovy code in a nice IDE and the learning curve, for Java developers, is nice.

To demonstrate a simple proof-of-concept, let's assume these 4 steps

  • Wrap ExpectJ engine in a java class
  • Wrap java class into a Groovy class
  • Add Groovy syntatic sugar
  • Sandbox Groovy script interpreter

We're also using Jsch (http://www.jcraft.com/jsch/), a pure java-based SSH client.

Step #1 - Wrap ExpectJ engine in a java class

    public class Expect {
       
        private ExpectJ expectinator;
        private Spawn spawn;
        private int lastPos;

        public Expect() throws IOException{
            expectinator = new ExpectJ(5);
            spawn = expectinator.spawn("localhost", 22, "leoks", "xyz");
        }
       
        public void send(String s) throws IOException{
            spawn.send(s+"\n");
        }
       
        public String expect(String s) throws IOException, TimeoutException{
            spawn.expect(s);
            return getReply();
        }
       
        private String getReply(){
            String all = spawn.getCurrentStandardOutContents();
            int newLastPos = all.length();
            String reply = all.substring(lastPos);
            lastPos = newLastPos;
            return reply;
        }
       
    }

Step #2 - Wrap java class into a Groovy class

    class ExpectGroovy {
       
        def Expect expectJava = new Expect()
       
        def expect(String expression){
            expectJava.expect(expression)
        }
       
        def send(String command){
            return expectJava.send(command)
        }
    }

Step #3 - Add Groovy syntatic sugar

    class Example {
        static main(args) {
            def binding = new Binding(exp: new ExpectGroovy())
           
            ExpectSandbox sandbox = new ExpectSandbox();
           
            def config = new CompilerConfiguration()
            config.scriptBaseClass = ExpectBaseScriptClass.class.name
            config.addCompilationCustomizers(new SandboxTransformer())
            sandbox.register()
           
            def shell = new GroovyShell(this.class.classLoader, binding, config)
            String script = ...
            try{
                shell.evaluate(script)
            }catch(e){e.printStackTrace()}
           
            sandbox.unregister()
            print "OK"
        }
    }
    abstract class ExpectBaseScriptClass extends Script{
        void expect(String expression){
            this.binding.exp.expect expression
        }
       
        void send(String command){
            this.binding.exp.send command
        }
    }

Step #4 - Sandbox Groovy script interpreter

Now, the groovy script may look like this

    expect "\\$"

    //try to uncomment - the script does not kill the process, instead this instruction
    //is blocked by groovy security restrictions
    //see http://groovy.codehaus.org/Advanced+compiler+configuration
    //System.exit(0)

    x = 0
    while (x < 5){
        result = send "hostname"
        expect "\\$"
        x++
    }

Please notice that

  •     The “expect” object is implicit (binding)
  •     Groovy loops and variable assignment for free (mallet for example needed a special code just for loop evaluation)
  •     Groovy allows to restrict certain undesired commands, as well as java packages restrictions
  •     No explicit grammar defined
  •     Groovy can be coded with eclipse, with code completion, etc

References


1 comment:

  1. If you are considering using an Expect tool, give a try to yet another 'Expect for Java' implementation:
    https://github.com/Alexey1Gavrilov/expectit

    It doesn't depend on third-party libraries, available on the Maven central and Apache licensed.

    Here is a code example of interacting with a public ssh service capturing the server output using regular expressions. You can compare the code below to Expect4J / expectj examples.

    ====================
    JSch jSch = new JSch();
    Session session = jSch.getSession("new", "sdf.org");
    Properties config = new Properties();
    config.put("StrictHostKeyChecking", "no");
    session.setConfig(config);
    session.connect();
    Channel channel = session.openChannel("shell");
    // jsch is ready
    Expect expect = new ExpectBuilder()
    .withOutput(channel.getOutputStream())
    .withInputs(channel.getInputStream(), channel.getExtInputStream())
    // trace all the I/O activity to the standard output stream
    .withEchoOutput(adapt(System.out))
    // remove ANSI color escape sequences and non-printable chars
    .withInputFilters(removeColors(), removeNonPrintable())
    .build();
    channel.connect();
    expect.expect(contains("[RETURN]"));
    expect.sendLine();
    String ipAddress = expect.expect(regexp("Trying (.*)\\.\\.\\.")).group(1);
    System.out.println("Captured IP: " + ipAddress);
    session.disconnect();
    expect.close();
    ===================

    ReplyDelete