Telnet Scripting Language (TSL) v0.1.0

Overview
--------

TSL is a tool that was created to allow Telnet sessions (such as Cisco network 
equipment and Linux computers) to be scripted.  The language should be 
flexible enough to allow almost any simple network protocol to be scripted 
(SMTP, HTTP, etc).

TSL does not allow complex processing of information received from the remote 
connection, but it does allow the network I/O to be captured to a disk file 
for external processing.

TSL is driven from a TSL command file, which is a plain-text file with a set 
of "if you see this" "then do this" type syntax.

Invoking TSL
---------------

tsl <host> [-p port] [-f telnet.tsl]

Where:  host is the IP address or DNS name of the remote host

        port is an optional destination TCP port address, defaulting to 23

        -f specifies the name of the script to execute, defaulting to 
         telnet.tsl

TSL Script Syntax
-----------------

A TSL file is a set of text commands, one per line, that instructs the TSL 
"engine" on how to behave.  The TSL file is broken up into "sections", which 
are usually blocks of code that loop to get something done.  A simple TSL file 
that logs in to a remote Linux computer and checks how long the Linux computer 
has been up might look something like:

   verbose on
   search
   on "login:"
     send tig "\n"
   on "password:"
     send "changeme\n"
   on "]$"
     done

   label LoggedIn
   send "who\n"
   on "]$"
     done

The above TSL file has two sections, the first section (which has no name) and 
the second section called "LoggedIn".  Sections are delineated using the 
keyword "label".  All whitespace on a line is ignored, and everything is case 
sensitive. The indentions above are for clarity, and will be ignored by TSL.  
Quotes can be used to delineate strings and is required when specifying a 
space or special (newline/tab) character.

The keyword "search" indicates that the section is looking to match text.  If 
no search command is present in a given section, the commands in the section 
will be run and execution will fall through to the next section.  The search 
command indicates that the section will be attempting to match text, and 
addition sections will not be executed until a match has occurred.

Once some text is found a series of commands will be executed.  The keyword 
"on" is used to specify the pattern that you are looking for.  You can look 
for multiple patterns at once.  In the first section we are attempting to look 
for the patterns "login:", "password:" and "]$" (the bash shell prompt).  

A section that is attempting to pattern match will continue to read characters 
from the remote end until a pattern matches (or until we run out of time - 
discussed later).  Once a match is found, the statements that follow it are 
executed in order, up to the next "on" line or until the end of the section (a 
"label" keyword).

To send characters to the remote end, use the "send" keyword.  One or more 
arguments can follow.  All arguments are sent as plain text (except for 
variables, discussed later).  If multiple arguments are specified, they are 
concatenated and then sent.  A carriage-return will not be automatically sent. 
To send a space or a special character (such as a newline), encapsulate the 
string in quotes.

When the "done" command is encountered, execution continues at the next 
section. Without the "done" command, execution will continue at the "search" 
command in the same section.  This behavior allows a single section to handle 
a number of patterns.  In the very last section, the "done" command will exit 
the program.

With the above script, the first section looks for the login, password, and 
bash shell prompt.  When we receive "login:", we send our username (and a 
newline).  The username being sent is 'tig', equally correct would have been 
to put it in quotes.  When we receive "password:", we send our password (and a 
newline).  When the bash prompt is detected, we continue processing at the 
next section.  The second section executes the "who" command on the remote 
system, waits for the bash prompt to be returned, then exits the program.  
Notice that the newline can be embedded with other strings.  Also notice that 
we wait for the bash prompt after sending "who\n", this ensures that we do not 
exit the program before all of the output from the who command is received.

Comments can be added to the TSL file for clarity.  Any line that starts with 
a hash (#) is considered a comment.  Blank lines are also treated as comments. 
Comments are automatically stripped-out during the TSL parsing and will not 
result in slower interpretation of the TSL script.

By default, network I/O is not displayed on the local console.  To change this 
default behavior, as we did in the above example, use the following command:

    verbose on

To suppress network I/O from displaying locally (the default behavior) use:

    verbose off

Note that verboseness can be changed any number of times during run-time, as 
can all other variables and settings.  To create a variable, use the "var" 
keyword.  Example:

    var username tig

To use the variable, specify leading and trailing underscores:

    send "My name is " _username_

Although variables are of limited usefulness as they can't be manipulated 
(much) when set, their primary purpose is to establish username/password 
information in the first section.  All other sections can then use the 
variables instead of hardcoding this information -- making the TSL script much 
more conducive to changes.

The only way to modify variables is to treat the variable as an integer.  Two 
operators exist to modify variables - they can be incremented or decremented 
using the following syntax:

    # This line increases the "counter" variable by 1
    ++ counter

    # This line decreases the "counter" variable by 1
    -- counter

Increment/decrement operations are currently the ONLY operations that can take 
place on variables.  In general variables are fixed.  An example of where the 
increment operation would be useful is setting all (the exact # is unknown at 
runtime) of the interfaces on a Cisco IOS switch to use spanning-tree portfast:

    var interface 1
    search
    on "#"
      send "interface fastEthernet 0/" _interface_ "\n"
      send "spanning-tree portfast\n"
      ++ interface
    on "Invalid input"
      done
      
Note:  Cisco responds with a hash mark when it is waiting for input.  When we 
specify an invalid interface, it responds with a "% Invalid input" message.  
In this case we know we have configured all of the interfaces so we continue 
to the next section of the TSL file (via the done command).

All variables have "global" scope.  Once created, they can be used in any 
section.  A variable can be defined any number of times.  Each time it is 
defined, it overwrites any previous value it may have had.  Variables that 
have not been defined are treated as blank strings if they are used.  If a 
variable that is not a valid number is incremented or decremented, it will be 
treated as containing the value 0.  For example:

    var username tig
    ++ username
    # username will now contain the value (string) 1

		var counter 1
		++ counter
    # counter will now contain the value (string) 2
		send _counter_ "\n"

Environment variables can be used by using the dollar sign ($) instead of an 
underscore (_).  To send the remote system the name of the user executing TSL 
you would use the following command:

    send $USER$

Special variables, called rotary variables, can hold multiple values.  Each 
time the variable is used it automatically switches to the next value.  Once 
all of the values have been referenced, it "loops" back to the first value.  
This is primary intended to accommodate logging in to a remote system using a 
number of different username/password combinations.  Rotary variables are 
referenced using the at (@) symbol.  An example of a rotary variable in action:

   verbose on
   rotary username root tig guest
   rotary password changeme password ""
   search
   on "login:"
     send @username@ "\n"
   on "password:"
     send @password@ "\n"
   on "]$"
     done

   label LoggedIn
   ...

The above will attempt to log in using the username "root" and password 
"password" the first time it encounters the login prompt.  If we can't log in, 
we won't receive the familiar bash prompt, we will receive the "login:" prompt 
again.  Without rotary variables we would continuously send the same 
username/password information to the remote system indefinitely or until the 
Linux session dropped the connection (actually, this isn't quite true as a 
number of measures are built-in to prevent this).  If we receive the "login:" 
prompt a second time, a different username/password will be used to log in - 
in the above case it will be tig/changeme.

By default, TSL will stay in any given section for only 30 seconds before 
assuming that something is wrong.  Once TSL has spent 30 seconds in a section, 
it terminates the connection.  This behavior is ideal to prevent TSL from 
looking for a pattern that won't ever be received.  30 seconds is plenty for 
most operations, but in some circumstances it may need to be increased (for 
example, if you send a command to a remote Linux computer to rebuild the 
locate database (send "updatedb\n") it could be more than 30 seconds before
you receive your bash prompt back).  To change the default from 30 seconds:

    # This section might take a long time, we'll set it for one hour
    waitfor 3600

    # This section could take any length of time, we'll disable the timer 
    # entirely
    waitfor 0

As with all other settings and variables, once the "waitfor" time has been it 
remains in effect for all sections after that.  If the new setting isn't 
intended for all sections be sure to change the waitfor time back to 30 
seconds (before the "done" command or before the "search" in the next section).

To start a new section, use the "label" command.  Sections should be named if 
it will be referenced from some other part of the TSL file directly.  If a 
section isn't referenced directly by any other section (such as only being 
executed due to a "done" statement being run in the previous section) it does 
not require a name.

    # This is the start of a new section that doesn't have a name
    label  
    ...

    # This section has a name so that it can be referenced by other parts of 
    # the TSL script
    label LOGIN
    ...

    # This section will continue execution at the "LOGIN" section if we 
    # receive a specific pattern
    ...
    search
    on "bad error message"
       send "logout\n"
       goto LOGIN

Notice that you can use the "goto" command to jump directly to a specific 
section.  You can only do this if the section has a name.

To send a message to the local console, use the "write" command.  It operates 
identical to the "send" command, except that instead of sending the text 
and/or variables to the remote system, it sends them to stdout (assuming 
verbose is on, of course).  If verbose is off, the default behavior, the write 
command will not be sent to the local console.

    search
    on "]$"
       write "We are now logged in.\n"

To capture all network traffic sent by the remote end to a file, use the 
"capture" command.  The capture command takes one or more arguments, which 
when concatenated make up the filename that will be used to store the output:

    capture $USER$ ".log"

Most remote systems will "echo" what you "send" back to TSL, so you will end 
up logging that too.  When capturing to a file, any "write" commands will be 
sent to the file as well, even if verbose is off.  This allows you to place 
comments in the log file for easier searching/parsing later:

    capture output.log
    verbose off
    search
    on "error"
       # This will be sent only to the capture file
       write "ERROR ENCOUNTERED\n"

To stop capturing to the file, use the "endcap" command.  Note that if you are 
already capturing to a file, and execute the "capture" command again, it 
closes the first log file automatically (an implicit endcap).  If the 
specified capture file already exists, logging will be appended to it:

    #
    # This will capture the contents of the "ls -l" command to a local file
    #
    capture output.log
    send "ls -l\n"
    search
    on "]$"
       endcap
       done

In addition to the "waitfor" variable, another mechanism is available to help 
you avoid endless loops.  "maxloops" defines the number of matches that a 
given section can have.  If a section matches more than the specified number 
of times, the connection is automatically closed.  By default this feature is 
disabled (maxloops 0) as "waitfor" will usually be able to detect a problem, 
if given enough time.

    # This will allow for a maximum of 5 login attempts (there are two matches 
    # per login attempt - one for "login:" and one for "password:" )
    maxloops 10
    search
    on "login:"
       ...
    on "password:"
       ... 

To put TSL to sleep for a given period of time use the "sleep" command.  This 
could be useful when a command should be executed on a remote system at a 
given interval:

    # Run keep a log of system load, at 10 second intervals
    search
    on "]$"
       send "uptime\n"
       sleep 10

When "sleep" is executed, the normal "waitfor" timer is disabled and restarted
after sleeping.

Instead of using multiple "send" statements, it is possible to send the 
contents of a local file to the remote system.  This condenses the TSL script 
and makes changes much easier in some cases:

    search
    on "]$"
       sendfile ~/localfile.txt
			 done

To execute a system script from TSL, use the "system" command.  This is 
intended primarily for use after "endcap" to allow the local system to process 
the log file.

    # This example assumes that the name of the capture file is set in the 
    # variable LOGFILE.
    search
    on "]$"
       system "~/processfile output.log"

In certain cases it is convenient to have a dedicated TSL file for things like 
username/passwords that can be used by multiple TSL scripts.  To include 
commands from a second TSL script into the current TSL script, use the 
"include" command.  The "include" statement is executed only once (during the
initial parsing of the TSL script).  

    # This example includes text from an "include" file.  It works much the
    # same as C's #include directive
    include $USER$ .tsl
    ...

Currently TSL syntax is at version 1, however TSL is written with the
assumption that changes/additions to the syntax are going to happen.  The
"version" command indicates the minimum required interpreter version that is
required to parse the TSL file.  Version 1 is assumed, so this command is
currently optional.  Future versions of TSL may change their behavior (to
become backward-compatible) based on the version specified.  All versions of
TSL will exit if the TSL file version is above what the interpreter can
understand.

    # This TSL script requires a version 1 (or greater) interpreter
    version 1

It is possible that an event could arise that warrants immediately closing the 
remote connection and terminating TSL operation.  In these cases, it is 
possible to use the "exit" command to terminate TSL:

   # This TSL script is an infinite loop that checks for utilization.  If the 
   # remote system starts shutting down, we will close the connection
   capture remote.log
   search
   on "]$"
      send "uptime\n"
      sleep 10
   on "Shutting down"
      endcap
      exit 0

Any other command, other than those mentioned above, will be totally ignored 
by the runtime interpreter.  This behavior is by design, where an invalid 
command is probably due to a typo and it's better to "keep on keepin' on" than 
to bomb out because of a problem with a single line.

