5.5 POSIX server

This example application is a toy server able to handle connection sharing. It allows the client user to login and execute commands remotely. Multiple interactive sessions on the server side run separate processes started by a single client. The server code executes in the parent process and relies on libassh in order to manage the connection with the client and the children I/O streams.

We first introduce a few functions designed to execute a child process and setup related I/O redirections. This is the interactive session management code, provided by the application. Then we detail code of the event loops used to run the ssh server session. Those loops are similar to what is implemented in the client example application, but multiple channels and I/O streams are involved here.

Interactive session code [link] 

The states of interactive sessions are stored in instances of the its_s structure declared below. Our server application has to create those objects when execution of a new interactive session is requested by the client.

This structure contains a state field along with a reference to the associated libassh channel as well as some child process handles:

// code from examples/server.c:93

/* our interactive session context */
struct its_s
{
enum interactive_session_state_e state;

struct assh_channel_s *channel;

int child_pid;
int child_stderr_fd;
int child_stdout_fd;
int child_stdin_fd;
int poll_index;
};

Our small finite state machine is designed to track the status of an interactive session and is updated by the application functions presented in this section. Here are the existing states:

/* our interactive session state */
enum interactive_session_state_e
{
ITS_PIPE, /* use pipe() to redirect child IOs in its_exec */
ITS_PTY, /* use a pty to redirect child IOs in its_exec */
ITS_OPEN, /* channel is open and child is running */
ITS_HALF_NO_SEND, /* channel half closed, sending is not allowed */
ITS_HALF_NO_RECV, /* channel half closed, receiving is not possible */
ITS_CLOSED, /* channel is closed */
};

Because the main loop needs to poll on file descriptors associated with all child processes, we have to keep track of existing its_s objects in a table:

#define MAX_ITS_COUNT 10
#define MAX_POLL_ENTRIES (/* socket */ 1 + /* childs IOs */ MAX_ITS_COUNT * 3)

static struct its_s *its_table[MAX_ITS_COUNT];
static size_t its_table_count = 0;

The application functions described below manage the its_s instances. This consists in calling POSIX functions in order to create the child process and redirect its I/O. The content of these functions is not detailed here because an explanation of their behavior is enough to understand the libassh related part of the application that follows.

The its_open function allocates a new its_s object, set its state to ITS_PIPE and store a reference to the channel associated to the interactive session:

static struct its_s *
its_open(struct assh_channel_s *ch);

The its_pty function changes the state from ITS_PIPE to ITS_PTY. This is an optional step before calling the its_exec function that allows using a pseudo terminal device to redirect the child process I/O instead of relying on some file descriptor pipes .

static int
its_pty(struct its_s *its);

The its_exec function forks the current process, executes the requested child command and setups redirections of its I/O. This also makes the state of our interactive session object change to ITS_OPEN.

static int
its_exec(struct its_s *its,
const char *cmd);

The following three functions are used to manage the flow of data between the ssh2 channels and the child processes.

The its_poll_setup function is used to append some pollfd entries for monitoring child I/O file descriptors prior to invoking poll in the main loop:

static void
its_poll_setup(struct its_s *its,
struct assh_session_s *session,
struct pollfd p[], int *poll_i);

The its_child2channel function is then used to handle POLLIN events reported by poll. It processes child inputs and pushes data to the interactive session associated channel. This function is also responsible for closing and half closing the channel when an I/O error is reported by poll. In this case, the state is changed to ITS_HALF_NO_SEND or ITS_CLOSED.

static void
its_child2channel(struct its_s *its,
const struct pollfd p[]);

Data flow in the other direction is handled when libassh reports incoming channel data from the remote host. This is handled by the its_channel2child function which writes the incoming data to the standard input stream of the child process.

static assh_bool_t
its_channel2child(struct its_s *its, struct pollfd *p,
struct assh_event_channel_data_s *ev,
assh_status_t *err);

Finally, the its_eof and its_close functions are called when the library reports that the remote client has closed its I/O streams. The its_eof function changes the state of our interactive session related object to ITS_HALF_NO_RECV or ITS_CLOSED, depending on its previous state. The its_close function simply releases the its_s instance.

static void
its_eof(struct its_s *its,
struct assh_event_channel_eof_s *ev);

static void
its_close(struct its_s *its,
struct assh_event_channel_close_s *ev);

The assh event loop [link] 

The assh event loop is invoked from the I/O event loop. It is designed to handle as many library events as possible without blocking.

The event loop has the same layout as in the client example:

static assh_bool_t
ssh_loop(struct assh_session_s *session,
struct pollfd *p)
{
time_t t = time(NULL);

while (1)
{
struct assh_event_s event;

/* get events from the core. */
if (!assh_event_get(session, &event, t))
return 0;

switch (event.id)
{

Handling of the ASSH_EVENT_READ, ASSH_EVENT_WRITE and ASSH_EVENT_SESSION_ERROR events has already been covered in previous examples.

The library queries the server application about user authentication methods that must be made available to the remote client. This occurs before the first authentication attempt as well as before retries.

case ASSH_EVENT_USERAUTH_SERVER_METHODS:
/* wait 3 seconds after a failed password attempt */
if (event.userauth_server.methods.failed &
ASSH_USERAUTH_METHOD_PASSWORD)
sleep(3);

/* report the user authentication methods we accept. */
event.userauth_server.methods.methods =
ASSH_USERAUTH_METHOD_PUBKEY |
ASSH_USERAUTH_METHOD_PASSWORD;
assh_event_done(session, &event, ASSH_OK);
break;

The user authentication relies on helper functions. When the authentication is successful, the user ID of the current process is changed. This is handled by this simple code:

case ASSH_EVENT_USERAUTH_SERVER_USERKEY:
case ASSH_EVENT_USERAUTH_SERVER_PASSWORD:
/* let an helper function handle user authentication from
system password file and user authorized_keys file. */

asshh_server_event_auth(session, &event);
break;

case ASSH_EVENT_USERAUTH_SERVER_SUCCESS: {
/* change the process user id when user authentication is over */
uid_t uid;
gid_t gid;
if (asshh_server_event_user_id(session, &uid, &gid, &event) ||
#ifdef CONFIG_ASSH_POSIX_SETGROUPS
setgroups(0, NULL) ||
#endif
setgid(gid) ||
setuid(uid))
abort();
break;
}

Then comes the channels and requests related events. This is where our interactive session objects are created and managed thanks to the functions defined above.

Our server has to accept channel open messages from the client when starting of an interactive session is requested. This is the case when the channel type reported by the event is "session". The library then keeps the channel object alive until the ASSH_EVENT_CHANNEL_CLOSE event is reported.

case ASSH_EVENT_CHANNEL_OPEN: {
struct assh_event_channel_open_s *ev =
&event.connection.channel_open;

/* only accept session channels */
if (!assh_buffer_strcmp(&ev->type, "session"))
{
struct its_s *its = its_open(ev->ch);
if (its != NULL)
{
assh_channel_set_pv(ev->ch, its);
ev->reply = ASSH_CONNECTION_REPLY_SUCCESS;
}
}

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

Because requests are used to let the client select the command it wants to run on the server, we have to handle the ASSH_EVENT_REQUEST event as well. Any such request can only be related to one of the channel we accepted to open. That's why we are able retrieve the pointer to the associated its_s object we have attached previously. Global requests are rejected.

case ASSH_EVENT_REQUEST: {
struct assh_event_request_s *ev =
&event.connection.request;
assh_status_t err = ASSH_OK;

/* handle some standard requests associated to our session,
relying on request decoding helper functions. */

if (ev->ch != NULL)
{
struct its_s *its = assh_channel_pv(ev->ch);

The "pty-req" request may optionally be used by the client. This requires us to call our its_pty function discussed previously. The asshh_inter_decode_pty_req helper function is used here to validate and decode the data attached to the request but its content is not used in this simple server.

/* PTY request from the remote client */
if (!assh_buffer_strcmp(&ev->type, "pty-req"))
{
#ifdef HAVE_POSIX_OPENPT
struct asshh_inter_pty_req_s rqi;
err = asshh_inter_decode_pty_req(&rqi, ev->rq_data.data,
ev->rq_data.size);

if (!err && !its_pty(its))
ev->reply = ASSH_CONNECTION_REPLY_SUCCESS;
#endif
}

When the client eventually requests execution of a "shell" process, the its_exec function is called:

/* shell exec from the remote client */
else if (!assh_buffer_strcmp(&ev->type, "shell"))
{
if (!its_exec(its, "/bin/sh"))
ev->reply = ASSH_CONNECTION_REPLY_SUCCESS;
}

Note that we do not query the operating system about the actual shell binary to execute in this toy server.

When the client requests execution of an arbitrary command instead of a shell, a similar action is performed. In this case we need to parse the data attached to the request in order to get the command string before calling the its_exec function:

/* command exec from the remote client */
else if (!assh_buffer_strcmp(&ev->type, "exec"))
{
struct asshh_inter_exec_s rqi;
err = asshh_inter_decode_exec(&rqi, ev->rq_data.data,
ev->rq_data.size);

if (!err)
{
/* we need a null terminated string */
char *cmd = assh_buffer_strdup(&rqi.command);
if (cmd && !its_exec(its, cmd))
ev->reply = ASSH_CONNECTION_REPLY_SUCCESS;
free(cmd);
}
}

Finally, this event needs to be acknowledged, as usual:

}

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

The client may request half-closing the channel if it has no more data to transmit. When this occurs, the ASSH_EVENT_CHANNEL_EOF event is reported and we rely on our its_eof function to handle it:

case ASSH_EVENT_CHANNEL_EOF: {
struct assh_event_channel_eof_s *ev =
&event.connection.channel_eof;
struct its_s *its = assh_channel_pv(ev->ch);

/* handle session EOF */
its_eof(its, ev);
assh_event_done(session, &event, ASSH_OK);

break;
}

In any case, libassh will report the ASSH_EVENT_CHANNEL_CLOSE event at some point. This may be due to the client properly closing a single channel or due to a broken connection. This behavior of libassh ensures that we will always be able to properly release resources we have attached to channels by just handling the reported events.

case ASSH_EVENT_CHANNEL_CLOSE: {
struct assh_event_channel_close_s *ev =
&event.connection.channel_close;
struct its_s *its = assh_channel_pv(ev->ch);

/* handle session close */
its_close(its, ev);
assh_event_done(session, &event, ASSH_OK);
break;
}

Incoming channel data is handled by our its_channel2child function described previously. If we are not sure it is possible to write to the child input stream without blocking according to the pollfd array, the function returns 1. This interrupts the libassh event loop and yield to the enclosing I/O event loop:

case ASSH_EVENT_CHANNEL_DATA: {
struct assh_event_channel_data_s *ev =
&event.connection.channel_data;
struct its_s *its = assh_channel_pv(ev->ch);
assh_status_t err;

assh_bool_t wait = its_channel2child(its, p, ev, &err);
assh_event_done(session, &event, err);

if (wait)
return 1;
break;
}

Any other unhandled event is acknowledged, as usual:

default:
assh_event_done(session, &event, ASSH_OK);
}
}
}

The I/O event loop [link] 

The I/O event loop is the main loop of the process that handle a single incoming client connection.

The function below is executed in a new process when a client connects to the server. It creates a new libassh session and starts the I/O polling loop:

static int
server_connected(struct assh_context_s *context,
int conn, const struct sockaddr_in *con_addr)
{
printf("[%u] Client connected\n", getpid());

/* init a session for the incoming connection */
struct assh_session_s *session;

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

The poll system call is used to monitor the network connection to the client as well as I/O of the child processes. Our its_poll_setup function will fill the pollfd array with interactive session related entries.

struct pollfd p[MAX_POLL_ENTRIES];

do {
/* always poll on the ssh socket */
p[0].fd = conn;
p[0].events = POLLIN;
if (assh_transport_has_output(session))
p[0].events |= POLLOUT;

/* also register file descriptors related to child processes */
unsigned i;
int poll_i = 1;
for (i = 0; i < its_table_count; i++)
its_poll_setup(its_table[i], session, p, &poll_i);

As shown previously, the poll function is also used to let the library check for ssh2 protocol timeouts:

/* get the appropriate ssh protocol timeout */
assh_time_t timeout = assh_session_delay(session, time(NULL)) * 1000;

if (poll(p, poll_i, timeout) <= 0)
continue;

Our its_child2channel function may now perform the transfer of data from the child processes to the remote client:

/* read from childs and transmit over ssh */
for (i = 0; i < its_table_count; i++)
its_child2channel(its_table[i], p);

The libassh event loop described above is then executed. It may wait for more I/O events or terminate the connection:

/* let our ssh event loop handle ssh stream io events, channel data
input events and any other ssh related events. */

} while (ssh_loop(session, p));

printf("[%u] Client disconnected\n", getpid());

assh_session_release(session);
close(conn);

return 0;
}

The main loop [link] 

Our server main loop implements a simple forking server. Note that the maximum number to connections is not limited.

while (1)
{
/* handle incoming connections */
struct sockaddr_in con_addr;
socklen_t addr_size = sizeof(con_addr);

int conn = accept(sock, (struct sockaddr*)&con_addr, &addr_size);

if (conn < 0)
break;

/* handle incoming connection in a child process */
if (fork())
{
close(conn);
}
else
{
close(sock);
server_connected(context, conn, &con_addr);
break;
}
}
Valid XHTML 1.0 StrictGenerated by diaxen on Sun Oct 25 23:30:45 2020 using MkDoc