5.1 Remote command execution

The simple client application presented in this section is designed to connect to an ssh2 server and request execution of a command. It can be invoked like this:

$ examples/client localhost ls

The first thing to do in the program main function is initialization of the external libraries. libassh does not actually need this, but some other libraries like Libgcrypt and OpenSSL used by modules may require calling a global initialization function. If you do not want to bother about which third party libraries are involved in your build of libassh, just call the assh_deps_init function:

// code from examples/rexec.c:70

int main(int argc, char **argv)
{
/* perform initialization of third party libraries */
if (assh_deps_init())
ERROR("initialization error\n");

If you know that your build of libassh on your specific platform do not need this, you can skip this step.

Our application then parses command line arguments and setups a TCP socket to the remote server. This part of the program is not quoted here. All it does is proper initialization of the sock file descriptor and of the command string variables from command line arguments.

An struct assh_session_s object is needed in order to store the state of the whole ssh2 session. This requires an struct assh_context_s object used to store resources that may be shared by multiple sessions. That's why initializing a new library context is the first thing to do. Modules can then be registered on the new context:

/* initialize an assh context, register services and algorithms */
struct assh_context_s *context;

if (assh_context_create(&context, ASSH_CLIENT,
NULL, NULL, NULL, NULL) ||
assh_service_register_default(context) ||
assh_algo_register_default(context, ASSH_SAFETY_WEAK))
ERROR("Unable to create an assh context.\n");

Note that error handling is minimalist here as we just make the application exit if something goes wrong.

Our struct assh_session_s object can then be initialized and attached to the context:

/* initialize an assh session object */
struct assh_session_s *session;

if (assh_session_create(context, &session))
ERROR("Unable to create an assh session.\n");

Our application will need to handle the user authentication related events originating from the server and reported by the library. That's why we have to keep track of user authentication methods we want to use. A variable will be update to reflect methods that have not been already tried:

/* specify user authentication methods to use */
enum assh_userauth_methods_e auth_methods =
ASSH_USERAUTH_METHOD_PASSWORD |
ASSH_USERAUTH_METHOD_PUBKEY |
ASSH_USERAUTH_METHOD_KEYBOARD;

The interactive session part of the connection protocol runs on the highest layer of ssh2. This component is used to request execution of a process on the remote server. Note that despite his name, the interactive session concept is not related to the struct assh_session_s type presented above.

Even if more advanced applications will want to implement interactive sessions directly, the library provides a small helper state machine that performs basic handling of client interactive sessions. This is used below to handle request and channel related events that allow remote execution of the user command. The code of the helper is not an internal part of the library. Feel free to study and paste the code of this helper state machine directly in your application, as a starting point for more complex constructs.

At this point, we just need to initializes a state object for this helper:

/* initializes an interactive session state machine object */
struct asshh_client_inter_session_s inter;
asshh_client_init_inter_session(&inter, command, NULL);

Then comes the event loop, as described in the Event based API section:


/** get events from the core. */
struct assh_event_s event;

while (assh_event_get(session, &event, time(NULL)))
{
switch (event.id)
{

The I/O events are reported by the library in order to let the application transfer the ssh2 stream over the network. In this example, this is left to an helper function designed to read and write the ssh2 network stream using the application provided socket file descriptor:

case ASSH_EVENT_READ:
case ASSH_EVENT_WRITE:
/* use helpers to read/write the ssh stream from/to our
socket file descriptor */

asshh_fd_event(session, &event, sock);
break;

Error notifications are also reported as events by the library. This event is handled here without relying on an helper function. That's why the application has to take care of acknowledging the event by making a direct call to the assh_event_done function:

case ASSH_EVENT_SESSION_ERROR:
/* report any error to the terminal */
fprintf(stderr, "SSH error: %s\n",
assh_error_str(event.session.error.code));
assh_event_done(session, &event, ASSH_OK);
break;

Note that this event is only used for notification and must not result in exiting the event processing loop. The library may still report more events, allowing the application to clean open channels and other resources properly.

Then come events that are only reported during specific phases of the protocol. Those are related to the key-exchange process and to the currently running service.

When the server sends its host key during the key-exchange process, our client needs to check if it is a known key or if the user accept the new key. An helper function handles this by looking in the ~/.ssh/known_hosts file. This helper also queries the user on the terminal if needed.

case ASSH_EVENT_KEX_HOSTKEY_LOOKUP:
/* let an helper function lookup host key in openssh
standard files and query the user */

asshh_client_event_hk_lookup(session, stderr, stdin, hostname, &event);
break;

The application could have handled this in a custom way in order to adapt the usage or support a specific target platform API.

Once again an helper function is used to handle the user authentication events. It takes care of querying the system user database and reads user public key files:

case ASSH_EVENT_USERAUTH_CLIENT_BANNER:
case ASSH_EVENT_USERAUTH_CLIENT_USER:
case ASSH_EVENT_USERAUTH_CLIENT_METHODS:
case ASSH_EVENT_USERAUTH_CLIENT_PWCHANGE:
case ASSH_EVENT_USERAUTH_CLIENT_KEYBOARD:
/* let an helper function handle user authentication events */
asshh_client_event_auth(session, stderr, stdin, user, hostname,
&auth_methods, asshh_client_user_key_default, &event);
break;

Then comes execution of the interactive sessions FSM that allows starting a process on a remote host. The ssh2 protocol is able to multiplex many application channels over a session. However, our simple tool only wants to start a single command, that's why we choose to properly shutdown the ssh2 session when our single interactive session ends:

case ASSH_EVENT_SERVICE_START:
case ASSH_EVENT_CHANNEL_CONFIRMATION:
case ASSH_EVENT_CHANNEL_FAILURE:
case ASSH_EVENT_REQUEST_SUCCESS:
case ASSH_EVENT_REQUEST_FAILURE:
case ASSH_EVENT_CHANNEL_CLOSE:
/* let an helper function start and manage an interactive
session. */

asshh_client_event_inter_session(session, &event, &inter);

/* terminate the connection when we are done with this session */
if (inter.state == ASSH_CLIENT_INTER_ST_CLOSED)
assh_session_disconnect(session, SSH_DISCONNECT_BY_APPLICATION, NULL);
break;

Because the library still needs to exchange packets in order to properly terminate the connection, we still have to run the loop after calling the assh_session_disconnect function. The assh_event_get function will stop reporting more events at some point, breaking our main loop as appropriate.

Then comes handling of data transferred over the interactive session channel. When the remote command writes data to its standard output, the remote server forwards it to our client through the only existing struct assh_channel_s object we have. We may then write the data to the standard output on the local side:

case ASSH_EVENT_CHANNEL_DATA: {
struct assh_event_channel_data_s *ev = &event.connection.channel_data;
assh_status_t err = ASSH_OK;

/* write remote command output sent over the channel to the
standard output. */

ssize_t r = write(1, ev->data.data, ev->data.size);
if (r < 0)
err = ASSH_ERR_IO;
else
ev->transferred = r;

assh_event_done(session, &event, err);
break;
}

We do not want to handle any other type of event that the library may report, but we still have to acknowledge them:

default:
/* acknowledge any unhandled event */
assh_event_done(session, &event, ASSH_OK);
}
}

When we eventually exit from the loop, the struct assh_session_s and struct assh_context_s objects can be released:

printf("Connection closed\n");

assh_session_release(session);
assh_context_release(context);

return 0;
}
Valid XHTML 1.0 StrictGenerated by diaxen on Sun Oct 25 23:30:45 2020 using MkDoc