Single File SSH Client Library

Introduction

This library supports the following features within SSH: X11 forwarding is not implemented but the basic mechanism is in there, I just didn't do it because I don't need it.

Shell execution doesn't support environment variable setting prior to starting up the shell. I found out that the latest OpenSSH version doesn't allow it. This makes it useless because you don't know whether an environment variable setting will succeed or not and you have to work around if it fails (i.e. setting it via shell commands). So, the best option is to simply skip that step and then do it with shell commands in all cases.

File transfers are done using SFTP, but not all of the SFTP features are exposed. SFTP is not really a file transfer protocol, but a remote file system access protocol. The library provides a higher level interface as found in proper file transfer protocols.

The library doesn't depend on any external libraries. It contains all cryptographic functions needed. Being such a standalone library, it has limited support for various algorithms. It supports the following encryption, hash and key exchange algorithms.

Adding more is easy, it's just a matter of finding a suitably licensed implementation and plugging it into the framework.

The code is written on a GNU/Linux box, but should work with Android/Linux as well. Eventually, I will make a Windows port. The framework is ready for it.

The library internally uses a cooperative thread library. This limits its use to x86-32, x86-64 and ARM-32 CPUs. I will add ARM-64 at some point. I'm not going to target anything old or esoteric.

Download

Here is the latest single file release: tssh.c.

My code is distributed under the BSD license. I have imported some code from elsewhere, namely the encryption functions. These are mostly public domain and some have the BSD license. I have kept their license declarations intact and the overall package should be compatible with the BSD license but you should check it yourself if you want to use it commercially. Please contact cinarus at yahoo dot com if there are any problems.

API

The library can be used simply by including tssh.c in a source file. Currently, there is no way to separate header information from the code. This will change..

tssh is almost completely asynchronous. It has a separate thread for the control loop as well as two other threads for I/O. Results and incoming data are provided as events to the user thread. In addition, tssh is thread-safe. You can call any function from any user thread you want, but it's best to use only one thread to watch for events.

Making a Connection

  tssh_t *tssh_init (char *alias, char *host, int port,
                     char *user, char *passwd);
This function creates a tssh_t object and starts the connection process. Please note that the connection process also includes a key exchange, which can take some time. When the function returns, the object is ready to supply events but you can't do anything else with it until you get a TSSH_EVENT_CONNECT_SUCCESS; the control thread is still working on the background calculating and exchanging keys with the server.

The alias argument is any arbitrary string, which will be used in a later release. You can just as well supply NULL here.

This function will generate one of the following events:

None of these events have any data associated with them.

Receiving Events

In order to receive events, you need to call
  tssh_event_t* tssh_get_event(tssh_t *T);
The returned event structure is pretty complex, I will get into its details later. The first field you need to know is the type field:
  typedef struct
  {
    int type;
    ...
  } tssh_event_t;    
This tells us what kind of event we have received. Here is a complete list of events:
enum {
  TSSH_EVENT_LOG= 1,
  TSSH_EVENT_CONNECT_BANNER,
  TSSH_EVENT_CONNECT_BANNER_END,
  TSSH_EVENT_PASSWORD_BANNER,
  TSSH_EVENT_CONNECT_SUCCESS,
  TSSH_EVENT_CONNECT_FAILURE,
  TSSH_EVENT_SHELL_OK,
  TSSH_EVENT_SHELL_FAILURE,
  TSSH_EVENT_CHANNEL_DATA,
  TSSH_EVENT_CHANNEL_EXTENDED_DATA,
  TSSH_EVENT_CHANNEL_WRITE_OK,
  TSSH_EVENT_CHANNEL_CLOSED,
  TSSH_EVENT_PORT_FORWARD_SUCCESS,
  TSSH_EVENT_PORT_FORWARD_FAILURE,
  TSSH_EVENT_PORT_FORWARD_CONNECT,

  TSSH_EVENT_FTP_DISCONNECTED,
  TSSH_EVENT_FTP_SUCCESS,
  TSSH_EVENT_FTP_FAILURE,               

  TSSH_EVENT_FTP_CONNECT_SUCCESS,
  TSSH_EVENT_FTP_CONNECT_FAILURE, 
};
I will describe each event as we progress. OK, so how to tell when there is an event to be read? After all, this is an asychronous library and waiting at get_event() wouldn't be productive at all.

The library creates a socket which can be select()ed or poll()ed to monitor for new events. You shouldn't read from this socket at all, the get_event() function does that. You can obtain the socket descriptor by calling:

  int tssh_event_fd(tssh_t *T);
If you don't need asynchronous behaviour, you can simply call get_event() and let your user thread block until an event arrives.

After you're done with an event, you shouldn't try to deallocate it. get_event() will do that on the next call.

Handling Errors

The library never gives out detailed error codes. Each operation can either succeed or fail, that's it. Doing it otherwise could be overwhelmingly complex because there are a lot of places where an operation can go wrong. As an exercise, let's think about these places: Coding for each of these scenarios would be quite challenging, so I opted to give a yes/no result for each operation.

Prior to reporting the failure of a command, the library will emit a log message thru the TSSH_EVENT_LOG event. This event has the D.str field, as in:

  typedef struct
  {
    union {
    ...
    char *str;
    ...
    } D;
  } tssh_event_t;
You can print this string, or store it in some text file. Log messages don't contain any user data except for file names. Ignoring these messages right out is another option, although this can make it hard to troubleshoot errors because there is no other indication.

Starting a Shell

The function
   void tssh_shell(tssh_t *T, char **env);
starts a remote shell after a connection is made. This will result in one of the following events: The environment pointer should be NULL, until I figure out a way to implement it properly. As I mentioned in the introduction, the latest OpenSSH server doesn't allow me to set environment variables before executing a shell.

This function starts the default shell for the user. The failure event doesn't have any data associated with it. The success event has one data field: the channel.

  typedef struct {
  ...
   tssh_channel_t *channel;
  ...
  } tssh_event_t;
When you get the channel from this event, you can then proceed writing to and reading from it. A shell channel has no further special features.

Channels

The SSH protocol allows multiple streams of data to flow thru one connection. From one connection to an SSH server, you can run a shell, do file transfers, forward sockets etc. Each of these streams is called a channel. A channel is created by executing a shell or as a result of some other process connecting to a forwarded port.

When a channel is closed, the system sends the event TSSH_EVENT_CHANNEL_CLOSED. This event has the channel field and nothing else. When you receive this event, you should call:

  void tssh_channel_destroy(tssh_channel_t *ch);
This function simply frees the memory associated with the channel. Do not call this function to close a channel. A function to do that will be implemented later.

Writing to a Channel

You can send data to a channel (i.e. keyboard input for a shell) using the following function:
  int tssh_channel_write(tssh_channel_t *ch,
                         uint8_t *data, uint32_t len);
The return value is non-zero if there was a yet-undiscovered problem with the channel. Remember, the library runs in the background in a different thread. Therefore, it can discover that the server has closed a channel before it gets a chance to tell you thru get_event(). This is useful in that, you can stop sending data immediately if you get a non-zero result from channel_write().

This function copies the data given in (data,len). When it returns, you may deallocate data if necessary.

If you have more substantial amount of data, it's more efficient to allocate write buffer and fill that in:

  typedef struct tssh_write_buffer
  {
    uint32_t size;
    uint32_t len;
    uint8_t *data;
    struct tssh_write_buffer *next;
  } tssh_write_buffer_t;    

  tssh_write_buffer_t *tssh_alloc_write_buffer(uint32_t size);
The size field tells us how big the buffer is, it's copied from the call to alloc_write_buffer(). The len field holds the number of bytes used in the buffer. The next field will be NULL when allocated. It's used internally by the library.

In any case, after you have filled in the data you want (updating len along the way of course) you may send the data using:

  int tssh_channel_write_buffer
     (tssh_channel_t *ch, tssh_write_buffer_t *B);
The return value has the same semantics as for channel_write(). The write buffer is deallocated automatically when the system is done with it.

In general, a channel write will not block. It's done in another thread and the user thread simply sends a message to that thread. However, if the writing thread is busy at that time, the message will be buffered. Instead of allowing unlimited writes to go unchecked, I implemented a limited buffer in order to stall the user thread. Ideally, such flow control should be done at a higher protocol.

If you want to make sure that the user thread never stalls, you may delay each of your writes (after the first write) until you get the event TSSH_EVENT_CHANNEL_WRITE_OK. This event contains a valid channel pointer and means that a write to the channel would probably not block the calling thread. This event is generated after a chunk of data is sent over the wire and thus some buffer space is freed. Chunk here means an argument to write() or write_block().

Reading From a Channel

You don't need any explicit code to read from a channel. When some data is received for a channel, it's reported as part of an event. The type of this event is TSSH_EVENT_CHANNEL_DATA. This event contains a valid buffer pointer:
  typedef struct
  {
   ...
    tssh_buffer_t *buffer;
   ...
  } tssh_event_t;
As with other objects, the buffer is free()d on the next call to get_event(). If you'd like to keep the buffer for longer, just set the buffer field of the event object to NULL.

After you get the buffer, you can modify its fields however you like, it doesn't effect deallocation. It's a very simple struct:

  typedef struct
  {
    uint8_t *data;
    uint32_t len;
  } tssh_buffer_t;           
Channel data can also be reported as part of an event with type TSSH_EVENT_CHANNEL_EXTENDED_DATA. This event is used for what's called extended data for a channel. The SSH RFC describes this as a separate stream of data for the channel, in addition to the main stream, as reported by CHANNEL_DATA events. It's supposed to handle standard error streams but in practice I haven't encountered its use. When you start a shell with a virtual tty, all output from the shell is directed to that tty stream, whether it originates from the standard output or standard error stream. Maybe it's used for other command invocations other than the shell.

Port Forwarding

In order to forward a port from the remote machine to the local machine, you first need to make a request to the server:
  void tssh_request_port_forward(tssh_t *T, uint32_t port);
If port is given as zero, then the server will pick an available port number. The server then binds a socket to the given/picked port and responds. When it does, the library produces one of the following events: The failure event doesn't have any data. The success event has the port field.
  typedef struct
  {
    ...
    uint32_t port;
    ...
  } tssh_event_t;
When there is a connection to the forwarded port on the server machine, the system generates the TSSH_EVENT_PORT_FORWARD_CONNECT event. This event contains the following fields:
tssh_channel_t *channel;
The channel to be used for communication with the remote peer.
uint32_t connected_port;
Port number of the socket that was created as a result of the connection.
char* connected_address;
Address of the socket mentioned above.
uint32_t originator_port;
Port number of the socket for the remote peer; the process originating the connection.
char *originator_address;
Address of the socket mentioned above.
When you get the channel from this event, you can use it to communicate with the remote peer as described in the previous section.

File Transfer

File transfers are done using SFTP. Not all features of SFTP are supported. The supported operations are more like a traditional FTP operations set. In order to start file transfers, you need to enable the SFTP channel using:
  void tssh_ftp_connect(tssh_t *T);
This will generate one of the following events: None of these events have any data. When it's time to disconnect the SFTP channel, you may call:
  void tssh_ftp_disconnect(tssh_t *T);
This will generate the event TSSH_EVENT_FTP_DISCONNECTED. The same event is generated when the server decides to severe the connection.

FTP operations don't return immediate results, they generate one of the following events when the operation completes:

Both of these events have a common field, the ftp_op. This field tells us which operation has failed. After getting connected to the SFTP server, you can make multiple SFTP requests one after the other without waiting for results. Later on, you will get their status via these events. Therefore, you need to look inside this struct to identify which request has succeeded or failed. Let's look at this struct now:
  typedef struct tssh_ftop
  {
    int cmd;
    char *fn; char *oldfn;
    char *localfn;
    uint8_t *data;
    uint32_t size;
    struct tssh_ftop *next;
    uint8_t *udata;
  } tssh_ftop_t;      
Here, cmd is one of Along with the field 'fn', this should be enough to identify which operation has finished.

Reading Files

In order to download a file to a local file, you may use:
  void tssh_ftp_download_file(tssh_t *T, char *fn,
                              char *localfn);
This generates the usual failure/success events with no additional data. It's also possible to read a file directly into memory using:
  void tssh_ftp_download_mem(tssh_t *T, char *fn); 
When this finishes successfully, the generated success event will have a valid 'buffer' field. Its use is the same as described in the section describing channels.

Writing Files

Similar to reading, a remote file can be written from a local file or memory:
  void tssh_ftp_upload_file
     (tssh_t *T, char *fn, char *localfn);
  void tssh_ftp_upload_mem
     (tssh_t *T, char *fn, uint8_t *data, uint32_t len);
For both of these functions, the generated events have no additional data. When you use upload_mem(), the data must not be deallocated until the operation has finished. If you're doing multiple operations without waiting for a response, you may recover this pointer by accessing
   E->ftp_op->udata
where E is an event pointer.

Listing Directories

The function
  void tssh_ftp_list(tssh_t *T, char *dir_name);
lists the contents of a directory. The generated success event for this operation has the data in its n_fileinfo and fileinfo fields:
  typedef struct
  {
   ...
   uint32_t n_fileinfo;
   tssh_fileinfo_t *fileinfo; 
   ...
  } tssh_event_t;
The fileinfo array gets deallocated (names included) when you call get_event() again, so make sure to create copies if needed.
  typedef struct
  {
    char *name;
    int type;
    uint32_t perm;
    uint64_t size;
    uint32_t mtime;
  } tssh_fileinfo_t;
The type field is not used for now. This will change.. Remaining fields are pretty self explanatory. If a server doesn't report a field such as perm or mtime, the corresponding field will be all zeros.

Other Operations

  void tssh_ftp_create_dir(tssh_t *T, char *fn);
  void tssh_ftp_remove_dir(tssh_t *T, char *fn);
  void tssh_ftp_remove_file(tssh_t *T, char *fn);
  void tssh_ftp_rename(tssh_t *T, char *oldfn, char* fn); 
These are all simple commands which generate basic success/failure events with no data.

The query function


will get the fileinfo for a given file name. The returned success event
has one fileinfo structure in it, with n_fileinfo=1.

Change Log

Below are the development packages. I don't develop the whole thing as a single file, of course!
1.0 : 6apr20
Initial.

Things to Do and Bugs

Make a tssh_channel_close function.

Make a port_forward_cancel function.

Create the ftp_query() entry function.

Store longnames in fileinfo structures so that the user can make traditional unix listings if desired.

Rewrite KEX routines to use the cothread framework.

Test re-keying.

Split up connection into two parts. When the keys have been exchanged, report to the user the server key and ask for them to authenticate with a password.

user-defined limits for pend_max etc.

rewrite queue code to use tssh_lock etc.

closing a tssh connection cleanly

free() function for kex

option to disable logging completely

collect the 'connection banner' lines into a buffer and report them all in one go, instead of going line by line

for both connection banners and authentication banners, use just one event.

Add pthread option to coth, so that I can use valgrind on this to verify memory usage.

Make proper header and code sections.

Hide the coth and buffer code as static.

Allow all functions to be static, for great inclusion.

Make sure imported code doesn't leave behind any macros defined, for great inclusion.