5.3 Command line client
This example application is a classical ssh command line client for POSIX platforms.
Our client has to handle multiple data streams to communicate with the remote host and drive the terminal. In order not to get blocked by an I/O operation on one of the file descriptors, we need to use the poll system call. This example shows how the API of libassh can be used to deal with this programming model, polling in both directions, so that neither read nor write calls are blocking.
The details of the library initialization as well as other topics covered in the previous examples are not detailed again here.
The application is composed of two loops: the main loop that polls on the various file descriptors and the nested libassh event loop that handles the library events.
The main loop [link]
We have four file descriptors based data endpoints to deal with:
The incoming ssh network stream read from the sock socket file descriptor,
The outgoing ssh network stream transmitted over the same file descriptor,
The terminal input read from standard input file descriptor 0,
The terminal output written to the standard output file descriptor 1.
Before entering the main loop, we do declare the pollfd objects needed to monitor the file descriptors:
// code from examples/client.c:460
/* main IOs polling loop */
struct pollfd p[3];
p[POLL_STDIN].fd = 0;
p[POLL_STDOUT].fd = 1;
p[POLL_SOCKET].fd = sock;
We need to transfer data between those file descriptors and the following interfaces provided by the library:
The library network input pulled by the ASSH_EVENT_READ events,
The library network output pushed by the ASSH_EVENT_WRITE events,
The remote process output reported by the ASSH_EVENT_CHANNEL_DATA events,
The remote process input that is sent over the channel by the assh_channel_data_send function.
Because the interactive session is not started immediately, only the network stream needs to be transferred initially. Moreover, we do not always have data to write to the file descriptors. That's why the POLLIN and POLLOUT flags are set conditionally on each iteration of the main loop. The assh_channel_more_data and assh_transport_has_output functions are used to test if the library has some output data to report:
do {
p[POLL_STDIN].events = 0;
p[POLL_STDOUT].events = 0;
/* poll on terminal when the interactive session is open */
if (inter.state == ASSH_CLIENT_INTER_ST_OPEN)
{
p[POLL_STDIN].events = POLLIN;
if (assh_channel_more_data(session))
p[POLL_STDOUT].events = POLLOUT;
}
/* always poll on the ssh socket */
p[POLL_SOCKET].events = POLLIN;
if (assh_transport_has_output(session))
p[POLL_SOCKET].events |= POLLOUT;
We are almost ready to call the poll function. Because this system call is also able to return after a delay, it is useful to retrieve the next ssh2 protocol timeout delay from the library:
/* get the appropriate ssh protocol timeout */
int timeout = assh_session_delay(session, time(NULL)) * 1000;
if (poll(p, 3, timeout) <= 0)
continue;
As in the remote command execution example, the application relies on an helper state machine in order to manage the interactive session. Because we only want to run a single session, we have to initiate a disconnection from the remote server when the single session terminates:
/* disconnect on interactive session close. */
if (inter.state == ASSH_CLIENT_INTER_ST_CLOSED)
assh_session_disconnect(session, SSH_DISCONNECT_BY_APPLICATION, NULL);
We can then deal with data transfered from the local standard input to the remote process input, provided that the interactive session has started and some data are available. The data is written to the open ssh2 channel as explained in the previous example:
if (inter.state == ASSH_CLIENT_INTER_ST_OPEN)
{
/* write data from the terminal to the session channel */
if (p[POLL_STDIN].revents & POLLIN)
{
/* let the library allocate an output buffer for us */
uint8_t *buf;
size_t s = 256;
if (!assh_channel_data_alloc(inter.channel, &buf, &s, 1))
{
/* read data from the terminal directly in the
buffer of the outgoing packet then send it. */
ssize_t r = read(p[POLL_STDIN].fd, buf, s);
if (r > 0)
assh_channel_data_send(inter.channel, r);
}
}
If we are not able to read more data from the standard input, an end of file is reported to the remote host. In the same way, if we are not able to write, a channel close is requested. This will makes the library report the ASSH_EVENT_CHANNEL_CLOSE event.
/* close the session channel on stdio errors */
if (p[POLL_STDOUT].revents & (POLLERR | POLLHUP))
assh_channel_close(inter.channel);
else if (p[POLL_STDIN].revents & (POLLERR | POLLHUP))
assh_channel_eof(inter.channel);
}
As stated previously, the other data transfers are performed on library events. We then need to handle libassh events at this point. We use an other loop to process those events. This nested loop handle as many events as possible without blocking. This loop is in implemented in the separate ssh_loop function:
/* let our ssh event loop handle ssh stream io events, channel data
input events and any other ssh related events. */
} while (ssh_loop(session, &inter, p));
assh_session_release(session);
assh_context_release(context);
close(sock);
return 0;
}
The ssh_loop function returns 0 if there are no more events to report. This occurs at the end of the ssh2 session and breaks the main loop.
The assh event loop [link]
The ssh_loop function contains the libassh event loop, as stated previously:
static assh_bool_t
ssh_loop(struct assh_session_s *session,
struct asshh_client_inter_session_s *inter,
struct pollfd *p)
{
time_t t = time(NULL);
while (1)
{
struct assh_event_s event;
/* Get the next event from the assh library. */
if (!assh_event_get(session, &event, t))
return 0;
Only data transfer related events are shown here, as handling of other events mostly rely on helper functions that deal with user authentication and interactive session management. This is detailed in the remote command execution example.
Because the ASSH_EVENT_READ event wants data available from the socket file descriptor, the loop is interrupted if the poll system call did not allow us to read yet. We yield to the enclosing I/O polling loop in this case.
switch (event.id)
{
case ASSH_EVENT_READ:
/* return if we are not sure that we can read some ssh
stream from the socket without blocking */
if (!(p[POLL_SOCKET].revents & POLLIN))
{
assh_event_done(session, &event, ASSH_OK);
return 1;
}
/* let an helper function read ssh stream from socket */
asshh_fd_event(session, &event, p[POLL_SOCKET].fd);
p[POLL_SOCKET].revents &= ~POLLIN;
break;
The event processing loop is interrupted just before trying any I/O operation that may block. The POLLIN flag is cleared so that a single read is allowed between calls to the poll function.
The same mechanism is used for write system calls:
case ASSH_EVENT_WRITE:
/* return if we are not sure that we can write some ssh
stream to the socket without blocking */
if (!(p[POLL_SOCKET].revents & POLLOUT))
{
assh_event_done(session, &event, ASSH_OK);
return 1;
}
/* let an helper function write ssh stream to the socket */
asshh_fd_event(session, &event, p[POLL_SOCKET].fd);
p[POLL_SOCKET].revents &= ~POLLOUT;
break;
Transferring data from the channel to the terminal requires more work because no helper function is used, but the approach is still the same:
case ASSH_EVENT_CHANNEL_DATA: {
assh_status_t err = ASSH_OK;
/* return if we are not sure that we can write some data to
the standard output right now */
if (!(p[POLL_STDOUT].revents & POLLOUT))
{
assh_event_done(session, &event, ASSH_OK);
return 1;
}
p[POLL_STDOUT].revents &= ~POLLOUT;
struct assh_event_channel_data_s *ev = &event.connection.channel_data;
/* write to stdout */
ssize_t r = write(p[POLL_STDOUT].fd, ev->data.data, ev->data.size);
if (r < 0)
err = ASSH_ERR_IO;
else
ev->transferred = r;
assh_event_done(session, &event, err);
break;
}
The event handling loop ends as usual:
default:
ASSH_DEBUG("event %u not handled\n", event.id);
assh_event_done(session, &event, ASSH_OK);
}
}
}
This non-blocking event loop is sufficient to implement our poll based command line ssh2 client. Note that we rely on helper functions that query the user on the terminal about the validity of the host key. In order to obtain a truly non-blocking event loop, handling of the related events should be re-implemented as well, depending on the application user interface.