Communicating with Tcl applications

Often, our application needs to be able to accept commands from a local and/or remote application or troubleshooting utility. In many cases it is enough to set up a service that will accept connections and evaluate commands sent over them.

We can do this by using the comm package that offers both client and server functionality. This package allows many use cases from a simple server that evaluates all commands to a system that allows only certain tasks to be performed. This package uses TCP connections so it is possible to handle both local and remote connections. However, it is configurable whether connections from other computers are allowed.

Using a comm package we can also connect to our application from applications such as TclDevKit inspector from ActiveState, which can help in troubleshooting daemon-like applications. This is described in more detail in chapter 4.

Comm client and server

Using the comm protocol requires the use of the comm::comm command by providing an actual command to perform as its first argument.

One of the subcommands is config, which allows us to get and set the configuration. It accepts either no arguments, in which case it returns list of name-value pairs specifying all options and their values, or one or more arguments. Specifying only the name of an option returns its current value. Specifying one or more name-value pairs causes those to be set to specific values.

The most important options are -port and -local. The first one allows us to specify the TCP port to listen on, or to retrieve the port currently used by default the system assigns an available port. The second option allows us to specify whether only local connections should be accepted or if both local and remote connections are allowed by default only local connections are accepted.

In most cases it is best to assign a port either hardcoded or read from a configuration file. However, in some cases it makes sense to have it assigned by the operating system in this case we can simply load comm package and get port that was assigned by default. We can then read it using the self subcommand, which returns the currently used port.

For example:

package require comm
puts "Comm port: [comm::comm self]"

If we want to define which port to use, we can simply set the -port option. For example:

package require comm
comm::comm config -port 1991

For the purpose of this section we'll use port 1991 throughout all further examples.

Assuming that our application is in an event loop for example by invoking vwait forever, we can now send commands to it. We can do this by invoking the send subcommand, specifying identifier of the application and command to run. In the case of processes on the same system identifier is simply the port number 1991 in our case. In case of remote systems, identifier is a list containing port and hostname for example {1991 192.168.2.12}.

For example, in order to calculate 1+1 using remote interpreter we can do:

package require comm
set result [comm::comm send 1991 expr {1+1}]
puts "1 + 1 = $result"

Similarly calculating the same value using a remote computer can be done by changing our client's code to:

set result [comm::comm send {1991 192.168.1.12} expr {1+1}]

The remaining parts of the example are the same. In addition, the server needs to be configured to accept remote connections:

comm::comm config -port 1991 remote 1

We can also perform asynchronous operations that will not block the client until the server completes the operation. One case is when our application only needs to send a command without needing its result. In this case, we simply need to provide the -async flag before the identifier of the remote system. For example:

comm::comm send -async 1991 {after 3000}

This will cause the remote system to wait for 3 seconds, but the send command returns instantly.

Asynchronous commands can also be used to perform long running operations, or for actions that will require user input. In this case we need to specify the -command option followed by the command to be run when the results have been received.

This command will be run with name-value pairs containing information about both the channel and the result appended to it. The item -result contains the result from the command or the error description for errors. The item -code will specify result of the command as 0 indicating successful evaluation of the command and 1 indicating an error. In case of an error -errorinfo and -errorcode are set respectively to indicate error information. More details on error information can be found in Chapter 4. These items can be retrieved using dict command.

For example, in order to calculate the MD5 checksum of a large remote file we can run:

comm::comm send -command oncallback 1991 
{md5::md5 -hex [string repeat "Tcl Book" 16777216]}

And in order to define the command that handles the result we can do:

proc oncallback {args} {
if {[dict get $args -code] != 0} {
puts "Error while evaluating command"
} else {
puts "Result: [dict get $args -result]"
}
}

This callback checks whether the command returned an error and if not, prints out the result from the command.

Note

Source code for both synchronous and asynchronous communication is located in the 04comm-basics directory in the source code examples for this chapter.

Performing commands asynchronously

In many cases, we want our remote procedure to send a result that needs operations which might not happen immediately, such as passing a request to another machine or application.

A typical example can be sending a request to an other Tcl application over comm or sending a request over HTTP. In such cases we can receive the result after long period of time and we might want to return information to our caller when our event handler returns.

In such cases we can use the return_async subcommand from the comm::comm command in the Tcl interpreter that is the server for a particular connection. This command returns a new command, usually called future that we can invoke later on in order to pass the result.

In order to return a value we can invoke the return subcommand of the future object. It accepts a value to return, optionally preceded by the return code, which can be supplied by providing the -code option before the actual result.

For example, we can return the result after several seconds by doing:

proc returnAfter {after code value} {
set future [comm::comm return_async]
after $after [list $future return -code $code $value]
}

In this case, whatever we return from within the procedure will be ignored. Invoking the return subcommand also cleans up the future object.

We can invoke the command from the client in the same way regardless if it uses asynchronous return or if it returns instantly. For example we can run:

set v [comm::comm send 1991 returnAfter 3000 0 1]

Asynchronous returns are usually used when we need to invoke asynchronous operations, such as sending an HTTP request. A good example might be how to implement a comm proxy service:

proc proxy {id args} {
set future [comm::comm return_async]
comm::comm send -command [list proxyReturn $future] 
$id {*}$args
}

This command takes the id of the next hop as an argument and the remaining arguments are treated as commands to pass on to the next hop. It also sets up asynchronous return command which is used in proxyReturn procedure. It can look like this:

proc proxyReturn {future args} {
$future return -code [dict get $args -code] 
[dict get $args -result]
}

This simply passes the result back to the client calling the proxy command. For example assuming our proxy comm server is listening on port 1992, we can pass the query to actual server on port 1991 by doing:

set v [comm::comm send 1992 proxy 1991 
returnAfter 3000 0 1]

We can also use multiple hops and over network assuming we need to send all requests first via comm on host 192.168.2.12 and further via host 10.0.0.1, we can do:

set v [comm::comm send 
{1991 192.168.2.12} proxy 
{1991 10.0.0.1} proxy 
returnAfter 3000 0 1]

All of the proxy comm servers will perform this operation without waiting so it is possible to proxy multiple requests at a time this way, regardless of how much it takes to complete the operation.

Note

Source code for proxy based communication is located in the 05comm-proxy directory in the source code examples for this chapter.

Security aspects

An obvious concern is that this allows everyone to send commands to be run either only from local machine or from the entire network. While for some operations this is perfectly acceptable, there are cases where such a model might not work. An example could be that if our system is shared among users in this case anyone knowing the port might send commands to our application. Another case is when our application accepts commands from remote machines, unless access to the network is strictly limited.

The comm package allows the specification of hooks that are evaluated whenever an event occurs. These can be used for both comm clients and servers.

We can set up a script that will be evaluated when an event occurs by invoking the hook subcommand of the comm::comm command. It accepts event type and script as its arguments. These can be used to authenticate and optionally reject incoming connections, validating commands to evaluate as well as their responses.

Hook scripts can read various information using variables that are accessible while it is running. These variables are:

Variable

Description

chan

Comm command that is associated with the event, by default it is comm::comm; additional channels are described further in the section

id

Id of the remote interpreter

fid

Tcl channel associated with connection

addr

IP address of the remote peer; used for authenticating remote connection

remport

Port used by the remote peer; used for authenticating remote connection

cmd

Command type for evaluating a script -sync or async

buffer

Tcl command to evaluate

host

Host to connect to; used by client hooks

port

Port to connect to; used by client hooks

Not all variables are accessible for all hooks and availability is described along with each of the events.

The event type incoming is run for listening connections whenever a remote interpreter is connecting. It can be used to log incoming connections and/or reject connections based on remote address. This hook can reject a connection by throwing an error. This hook allows access to chan, fid, addr and remport variables.

For example, in order to accept connections only from hosts matching 127.* or 192.168.* pattern, we can add the following hook:

comm::comm hook incoming {
puts "comm—Connection from $addr"
if {(![string match "127.*" $addr])
&& (![string match "192.168.*" $addr])} {
error "Connection from unknown host $addr"
}
}

This hook prints information about incoming connections to standard output. Then if the addr variable (provided by comm package) does not match any of defined patterns, an error is thrown. This error causes comm to close the connection and pass this error to client.

Event type eval is run whenever a command is to be evaluated. It provides information in chan, id, cmd and buffer variables. In this case it is possible to modify command to evaluate, return any result from within the hook or throw an error in case a specified command should not be run.

If the eval hook returns with an error, this error is passed back to the original client. For example, in order to refuse all commands except for the known ones we can do:

comm::comm hook eval {
if {[lsearch $::validcommands [lindex $buffer 0]] < 0} {
error "Command not authorized"
}
}

In this case, we search for first element of the command in global validcommands variable if it contains the command we allow execution of the command.

Usually it is a better idea to either pass the command to a procedure that will handle it, which can do additional checks or make sure that only a subset of functionality is exposed. For example we can rewrite the buffer variable to pass actual command as argument by doing:

comm::comm hook eval {
set buffer [list [list myhandler $buffer]]
}

This way we pass the original buffer as first argument to myhandler command. As the comm package tries to concatenate all elements into a single list, we need to build a list of lists so that our myhandler is invoked with just one argument. We can then write a simple function that writes the actual command and result by doing:

proc myhandler {command} {
puts "Evaluating '$buffer'"
set rc [eval $command]
puts "Result: '$rc'"
return $rc
}

This command prints out the command to execute, evaluates it and prints out results.

Another option is to evaluate the command on our own instead of having it done by the comm package. We can use the return command inside the hook to force the comm package to pass our result back to the client. For example, we can invoke myhandler in the same way by doing:

comm::comm hook eval {
return [myhandler $buffer]
}

It is also possible to use an asynchronous handler for commands that are evaluated by our hook. For example, we can do the following:

proc myhandler {command} {
if {$command == "asynctest"} {
set future [comm::comm return_async]
after 5000 [list $future return "After 5 seconds"]
return
}
set rc [eval $command]
return $rc
}

This command has a special case where if the command invoked is asynctest, we return the result after 5 seconds. Otherwise we just evaluate whatever command user provided and return it.

Note

Source code for server side hooks is located in the 06comm-hooks directory in the source code examples for this chapter.

Limiting available commands

In many cases it is good practice to limit the operations remote applications can perform provide only a set of commands that user can access and allow only access to a subset that user has access to. We can even introduce an idea of a session identifier which is given when authenticating a user and can expire after some time.

For example we can work in the following way first operation a user should perform is authorizing using username and password. Next the server provides a session identifier that can be used to invoke other commands. We can do this by sending a session identifier as the first argument and assume that if session is not set, remaining arguments are username and password. If the session is valid, we can assume that the second argument is a command name and the remaining arguments are passed to the command.

We can create a command that handles buffer in this way by doing:

proc so::handle {buffer} {
set session [lindex $buffer 0]
if {$session == ""} {
lassign $buffer session user pass
set sid [authorize $user $pass]
return $sid
} elseif {[check $session [lindex $buffer 1]]} {
return [so::service::[lindex $buffer 1] 
{*}[lrange $buffer 2 end]]
} else {
error "Authorization failed"
}
}

What the procedure does is check first element of a list if it is empty, we assign the second and third argument as the username and password and invoke the authorize command, which returns a new session identifier or an empty string. If the session is set, and if the user is authorized to run specified command, we run it assuming it is in the so::service namespace to avoid running commands outside of the specified namespace. If the session is not valid or the user is not authorized to run a specified command, we throw an error.

We also need to set up a hook for eval event:

comm::comm hook eval {
return [so::handle [lrange $buffer 0 end]]
}

We're using the lrange command to make sure the user input is a valid list, just as a precaution.

Our sample authorization command should authorize the user, generate a session ID (for example using uuid package) and return it. A simple one, with a hardcoded username and password could be as follows:

proc so::authorize {user password} {
variable sessions
if {($user == "admin") && ($password == "pass")} {
set sid [uuid::uuid generate]
set sessions($sid) $sid
}
return $sid
}

This checks if the username is admin and the password is pass if so, it sets up a session and stores it in namespace variable sessions. We can also implement a trivial checking by doing:

proc so::check {sid command} {
variable sessions
if {[info exists sessions($sid)]} {
return true
}
return false
}

And now we have a safe comm based service that can be used to offer services. We can also add a simple command on top of this infrastructure, for example:

proc so::service::add {a b} {
return [expr {$a + $b}]
}

In order to use it we can now use the following code on the client:

set sid [comm::comm send 1991 {} admin pass]
if {$sid == ""} {
error "Unable to authorize"
}
set v [comm::comm send 1991 $sid 
add 1 2]

The main issue we need to implement is support for sessions to expire, for example if no operation is performed for some time, session is removed from sessions variable.

Note

Source code for examples related to providing services on top of the comm package is located in the 07comm-service directory in the source code examples for this chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.136.142