diff options
author | Tom Smeding <tom.smeding@gmail.com> | 2020-07-09 18:30:13 +0200 |
---|---|---|
committer | Tom Smeding <tom.smeding@gmail.com> | 2020-07-09 18:31:50 +0200 |
commit | 05a818bb65d00ef89cf97e59ebca867fcef5863a (patch) | |
tree | 31fcdfef52f40e32bdfc4bb0ff735dc9afa53592 | |
parent | 4659374068eda6473feed06143433dce4e8eade2 (diff) |
ssh: Abstract SSH communication in mini-library
-rw-r--r-- | ssh/Makefile | 2 | ||||
-rw-r--r-- | ssh/client.c | 295 | ||||
-rw-r--r-- | ssh/sshnc.c | 358 | ||||
-rw-r--r-- | ssh/sshnc.h | 102 |
4 files changed, 535 insertions, 222 deletions
diff --git a/ssh/Makefile b/ssh/Makefile index 68113cd..d77dc67 100644 --- a/ssh/Makefile +++ b/ssh/Makefile @@ -16,7 +16,7 @@ clean: server: server.o util.o $(CC) -o $@ $^ $(LDFLAGS) -client: client.o util.o +client: client.o sshnc.o util.o $(CC) -o $@ $^ $(LDFLAGS) %.o: %.c $(wildcard *.h) diff --git a/ssh/client.c b/ssh/client.c index fc4ad96..b9bd52a 100644 --- a/ssh/client.c +++ b/ssh/client.c @@ -3,12 +3,10 @@ #include <stdlib.h> #include <string.h> #include <ctype.h> -#include <errno.h> -#include <assert.h> -#include <libssh/callbacks.h> -#include <sys/select.h> #include <poll.h> +#include <unistd.h> #include "util.h" +#include "sshnc.h" static bool prompt_yn(const char *text) { @@ -47,86 +45,17 @@ static bool prompt_yn(const char *text) { return response; } -struct session_data { - ssh_session session; - ssh_channel channel; - bool should_close; -}; +static bool hostkey_checker(const unsigned char *hash, size_t length) { + printf("Server host key hash: %s\n", sshnc_print_hash(hash, length)); -void channel_close_cb(ssh_session session, ssh_channel channel, void *sesdata_) { - (void)session; (void)channel; - struct session_data *sesdata = (struct session_data*)sesdata_; - sesdata->should_close = true; -} - -void channel_eof_cb(ssh_session session, ssh_channel channel, void *sesdata_) { - (void)session; (void)channel; - struct session_data *sesdata = (struct session_data*)sesdata_; - sesdata->should_close = true; -} - -int channel_data_cb(ssh_session session, ssh_channel channel, void *data, uint32_t len, int is_stderr, void *sesdata_) { - (void)session; (void)channel; (void)is_stderr; - struct session_data *sesdata = (struct session_data*)sesdata_; - - const char *start = (const char*)data; - const char *cursor = start; - const char *end = cursor + len; - - while (cursor < end) { - ssize_t nw = write(STDOUT_FILENO, cursor, end - cursor); - if (nw < 0) { - if (errno == EINTR) continue; - perror("write(stdout)"); - sesdata->should_close = true; - return cursor - start; - } - assert(nw > 0); - cursor += nw; + bool response = prompt_yn( + "Does this hash match the one given to you by the server administrator, or by a\n" + "member that you trust and is already connected to the server? [y/n] "); + if (!response) { + printf("Disconnecting.\n"); } - return len; -} - -int channel_write_wontblock_cb(ssh_session session, ssh_channel channel, size_t bytes, void *sesdata_) { - (void)session; (void)channel; (void)sesdata_; - fprintf(stderr, "(write won't block for %zu bytes)\n", bytes); - return 0; -} - -int stdin_data_cb(int fd, int revents, void *sesdata_) { - (void)fd; - struct session_data *sesdata = (struct session_data*)sesdata_; - - if (revents & POLLIN) { - char buffer[1024]; - ssize_t nr = read(STDIN_FILENO, buffer, sizeof buffer); - if (nr < 0) { - if (errno == EINTR) return 0; - perror("read(stdin)"); - sesdata->should_close = true; - return 0; - } - - if (nr == 0) { // eof - sesdata->should_close = true; - return 0; - } - - const char *cursor = buffer; - while (cursor < buffer + nr) { - int ret = ssh_channel_write(sesdata->channel, cursor, buffer + nr - cursor); - if (ret == SSH_ERROR) { - fprintf(stderr, "Error writing to channel: %s\n", - ssh_get_error(sesdata->channel)); - sesdata->should_close = true; - return 0; - } - assert(ret > 0); - cursor += ret; - } - } - return 0; + return response; } int main(int argc, char **argv) { @@ -144,156 +73,80 @@ int main(int argc, char **argv) { return 1; } - ssh_session session = ssh_new(); - if (!session) { - fprintf(stderr, "Could not open SSH session\n"); - goto cleanup_unconnected; - } - - const char *ciphers_str = "aes256-gcm@openssh.com,aes256-ctr,aes256-cbc"; - bool procconfig = false; - bool ok = true; - ok &= ssh_options_set(session, SSH_OPTIONS_PROCESS_CONFIG, &procconfig) == SSH_OK; - ok &= ssh_options_set(session, SSH_OPTIONS_USER, "tomsg") == SSH_OK; - ok &= ssh_options_set(session, SSH_OPTIONS_HOST, server_host) == SSH_OK; - ok &= ssh_options_set(session, SSH_OPTIONS_PORT, &port) == SSH_OK; - ok &= ssh_options_set(session, SSH_OPTIONS_CIPHERS_C_S, ciphers_str) == SSH_OK; - ok &= ssh_options_set(session, SSH_OPTIONS_CIPHERS_S_C, ciphers_str) == SSH_OK; - // int loglevel = SSH_LOG_PROTOCOL; - // ok &= ssh_options_set(session, SSH_OPTIONS_LOG_VERBOSITY, &loglevel) == SSH_OK; - - if (!ok) { - fprintf(stderr, "Could not set options on SSH session: %s\n", ssh_get_error(session)); - goto cleanup_unconnected; - } - - if (ssh_connect(session) != SSH_OK) { - fprintf(stderr, "Could not connect to %s:%d: %s\n", - server_host, port, ssh_get_error(session)); - goto cleanup_unconnected; - } - - ssh_key host_key; - if (ssh_get_server_publickey(session, &host_key) != SSH_OK) { - fprintf(stderr, "Could not get host key from session: %s\n", ssh_get_error(session)); - goto cleanup_connected; - } - - unsigned char *host_key_hash = NULL; - size_t host_key_hash_length = 0; - if (ssh_get_publickey_hash(host_key, SSH_PUBLICKEY_HASH_SHA256, &host_key_hash, &host_key_hash_length) != SSH_OK) { - fprintf(stderr, "Failed to hash host key!\n"); - goto cleanup_connected; - } - - printf("Server host key hash: "); - fflush(stdout); - ssh_print_hash(SSH_PUBLICKEY_HASH_SHA256, host_key_hash, host_key_hash_length); - - bool response = prompt_yn( - "Does this hash match the one given to you by the server administrator, or by a\n" - "member that you trust and is already connected to the server? [y/n] "); - if (!response) { - printf("Disconnecting.\n"); - goto cleanup_connected; - } - - printf("Connected.\n"); - -retry_userauth: - switch (ssh_userauth_none(session, NULL)) { - case SSH_AUTH_ERROR: - fprintf(stderr, "Error authenticating: %s\n", ssh_get_error(session)); - return 1; - - case SSH_AUTH_DENIED: - case SSH_AUTH_PARTIAL: - fprintf(stderr, "Server denied authentication.\n"); - return 1; - - case SSH_AUTH_SUCCESS: - break; - - case SSH_AUTH_AGAIN: - if (ssh_get_status(session) & (SSH_CLOSED | SSH_CLOSED_ERROR)) { - fprintf(stderr, "Socket unexpectedly closed!\n"); - return 1; - } - goto retry_userauth; - } - - printf("Authenticated.\n"); - - ssh_channel channel = ssh_channel_new(session); - if (!channel) { - fprintf(stderr, "Failed to allocate channel: %s\n", ssh_get_error(session)); - goto cleanup_connected; - } + struct sshnc_client *client; + enum sshnc_retval ret = sshnc_connect( + server_host, port, "tomsg", "tomsg", hostkey_checker, &client); - printf("Created channel\n"); - - if (ssh_channel_open_session(channel) != SSH_OK) { - fprintf(stderr, "Failed to open channel: %s\n", ssh_get_error(channel)); - goto cleanup_connected; - } - - printf("Opened channel\n"); - - if (ssh_channel_request_subsystem(channel, "tomsg") != SSH_OK) { - fprintf(stderr, "Server did not allow opening 'tomsg' channel: %s\n", ssh_get_error(channel)); - goto cleanup_connected; - } - - printf("Obtained tomsg subsystem on channel\n"); - - struct session_data *sesdata = malloc(sizeof(struct session_data)); - if (!sesdata) { - fprintf(stderr, "Out of memory (allocating session_data)!\n"); + if (ret != SSHNC_OK) { + fprintf(stderr, "Could not connect: %s\n", sshnc_strerror(ret)); return 1; } - sesdata->session = session; - sesdata->channel = channel; - sesdata->should_close = false; + struct pollfd polls[2]; + polls[0] = (struct pollfd){ + .fd = sshnc_poll_fd(client), + .events = POLLIN, + }; + polls[1] = (struct pollfd){ + .fd = STDIN_FILENO, + .events = POLLIN, + }; - struct ssh_channel_callbacks_struct chan_cb; - memset(&chan_cb, 0, sizeof chan_cb); - ssh_callbacks_init(&chan_cb); - chan_cb.userdata = sesdata; - chan_cb.channel_close_function = channel_close_cb; - chan_cb.channel_eof_function = channel_eof_cb; - chan_cb.channel_data_function = channel_data_cb; - chan_cb.channel_write_wontblock_function = channel_write_wontblock_cb; - - if (ssh_set_channel_callbacks(channel, &chan_cb) != SSH_OK) { - fprintf(stderr, "Failed to set channel callbacks\n"); - goto cleanup_connected; - } + while (true) { + int pollret = poll(polls, sizeof polls / sizeof polls[0], -1); + if (pollret < 0) { + perror("poll"); + goto cleanup; + } - printf("Set callbacks\n"); + if (polls[0].revents & (POLLERR | POLLNVAL)) { + fprintf(stderr, "Error reading from SSH socket\n"); + goto cleanup; + } + if (polls[1].revents & (POLLERR | POLLNVAL)) { + fprintf(stderr, "Error reading from stdin\n"); + goto cleanup; + } - ssh_event event = ssh_event_new(); - if (!event - || ssh_event_add_session(event, session) != SSH_OK - || ssh_event_add_fd(event, STDIN_FILENO, POLLIN, stdin_data_cb, sesdata) != SSH_OK) { - fprintf(stderr, "Failed to create ssh event context\n"); - goto cleanup_connected; - } + if (polls[0].revents & (POLLIN | POLLHUP)) { + char buffer[4096]; + size_t length = 0; + ret = sshnc_maybe_recv(client, sizeof buffer, buffer, &length); + if (ret == SSHNC_OK) { + fwrite(buffer, 1, length, stdout); + } else if (ret == SSHNC_EOF) { + break; + } else if (ret != SSHNC_AGAIN) { + fprintf(stderr, "Error on SSH recv: %s\n", sshnc_strerror(ret)); + goto cleanup; + } + } - printf("Created event object\n"); + if (polls[1].revents & (POLLIN | POLLHUP)) { + char buffer[4096]; + ssize_t nr = read(STDIN_FILENO, buffer, sizeof buffer); + if (nr < 0) { + perror("Error reading from stdin"); + goto cleanup; + } + if (nr == 0) { + break; + } - while (!sesdata->should_close) { - // printf("poll loop...\n"); - ssh_event_dopoll(event, -1); - int status = ssh_get_status(session); - if (status & (SSH_CLOSED | SSH_CLOSED_ERROR)) goto cleanup_connected; - if (status & SSH_READ_PENDING) { - printf("read pending?\n"); + ret = sshnc_send(client, buffer, nr); + if (ret == SSHNC_EOF) { + break; + } else if (ret != SSHNC_OK) { + fprintf(stderr, "Error on SSH send: %s\n", sshnc_strerror(ret)); + goto cleanup; + } } } -cleanup_connected: - ssh_disconnect(session); -cleanup_unconnected: - ssh_free(session); + sshnc_close(client); + return 0; + +cleanup: + sshnc_close(client); + return 1; } diff --git a/ssh/sshnc.c b/ssh/sshnc.c new file mode 100644 index 0000000..3a13e08 --- /dev/null +++ b/ssh/sshnc.c @@ -0,0 +1,358 @@ +#include <stdio.h> +#include <stdlib.h> +#include <errno.h> +#include <threads.h> +#include <assert.h> +#include <libssh/callbacks.h> +#include "sshnc.h" + + +// - We never use ssh_disconnect, just ssh_free. I believe the only added value +// of ssh_disconnect is that it sends a polite quit message to the server, +// but it has the disadvantage of then closing the socket immediately -- +// without checking on the channels, which ssh_free closes neatly. + + +static thread_local char libssh_additional_error_description[1024]; + +static size_t min_size_t(size_t a, size_t b) { return a < b ? a : b; } + +static void clear_additional_error(void) { + libssh_additional_error_description[0] = '\0'; +} + +static void store_additional_error(const ssh_session session) { + const char *const ptr = ssh_get_error(session); + const size_t available = sizeof libssh_additional_error_description; + const size_t length = min_size_t(strlen(ptr), available - 1); + memcpy(libssh_additional_error_description, ptr, length); + libssh_additional_error_description[length] = '\0'; +} + +struct session_data { + ssh_session session; + ssh_channel channel; + + bool should_close; + enum sshnc_retval close_reason; + + // Data is appended whenever some is received + unsigned char recvdata[4096]; + size_t recvlen; +}; + +struct sshnc_client { + // If is_closed, all fields in this structure have been freed. This is to + // facilitate closing the connection on error, but later still accomodate + // sshnc_close(). + bool is_closed; + ssh_event event; + struct session_data *sesdata; + + const char *libssh_error_descr; + + struct ssh_channel_callbacks_struct chan_cb; +}; + +const char* sshnc_print_hash(const unsigned char *hash, size_t length) { + static char buffer[64]; + + char *const fingerprint = ssh_get_fingerprint_hash( + SSH_PUBLICKEY_HASH_SHA256, (unsigned char*)hash, length); + if (fingerprint == NULL) return NULL; + + const size_t fingerprint_len = strlen(fingerprint); + assert(fingerprint_len + 1 <= sizeof buffer); + memcpy(buffer, fingerprint, fingerprint_len + 1); + + ssh_string_free_char(fingerprint); + + return buffer; +} + +const char* sshnc_strerror(enum sshnc_retval code) { + const char *description = NULL; + + switch (code) { +#define CASE(code, descr) case code: description = #code ": " descr; break; + CASE(SSHNC_OK, "Success") + CASE(SSHNC_EOF, "EOF") + CASE(SSHNC_AGAIN, "Non-blocking read with no data, try again") + CASE(SSHNC_ERR_CONNECT, "Could not connect to host") + CASE(SSHNC_ERR_UNTRUSTED, "Hostkey checker rejected key") + CASE(SSHNC_ERR_AUTH, "Error authenticating to server") + CASE(SSHNC_ERR_DENIED, "Server did not accept 'none' authentication") + CASE(SSHNC_ERR_CLOSED, "Server unexpectedly closed connection") + CASE(SSHNC_ERR_SUBSYSTEM, "Server did not accept the subsystem channel") + CASE(SSHNC_ERR_SESSION, "Could not open libssh session") + CASE(SSHNC_ERR_CHANNEL, "Could not open libssh channel") + CASE(SSHNC_ERR_OPTIONS, "Could not set libssh options") + CASE(SSHNC_ERR_GETKEY, "Could not get key from libssh") + CASE(SSHNC_ERR_CALLBACKS, "Sshnc would not accept our callbacks structure") + CASE(SSHNC_ERR_EVENT, "Could not create libssh event poller") + CASE(SSHNC_ERR_WRITE, "Could not write to ssh channel") + CASE(SSHNC_ERR_POLL, "Could not poll the socket for activity") +#undef CASE + } + + static char buffer[2048]; + if (description == NULL) { + snprintf(buffer, sizeof buffer, "sshnc_strerror: unknown code %d", code); + } else if (libssh_additional_error_description[0] != '\0') { + snprintf(buffer, sizeof buffer, "%s (%s)", + description, libssh_additional_error_description); + } else { + strcpy(buffer, description); + } + return buffer; +} + +static void channel_close_cb(ssh_session session, ssh_channel channel, void *sesdata_) { + (void)session; (void)channel; + struct session_data *const sesdata = (struct session_data*)sesdata_; + sesdata->should_close = true; + sesdata->close_reason = SSHNC_EOF; +} + +static void channel_eof_cb(ssh_session session, ssh_channel channel, void *sesdata_) { + (void)session; (void)channel; + struct session_data *const sesdata = (struct session_data*)sesdata_; + sesdata->should_close = true; + sesdata->close_reason = SSHNC_EOF; +} + +static int channel_data_cb(ssh_session session, ssh_channel channel, void *data, uint32_t len, int is_stderr, void *sesdata_) { + (void)session; (void)channel; (void)is_stderr; + struct session_data *const sesdata = (struct session_data*)sesdata_; + + const size_t remaining_space = sizeof sesdata->recvdata - sesdata->recvlen; + const size_t consumed = min_size_t(len, remaining_space); + + memcpy(sesdata->recvdata + sesdata->recvlen, data, consumed); + sesdata->recvlen += consumed; + return consumed; +} + +enum sshnc_retval sshnc_connect( + const char *hostname, + int port, + const char *username, + const char *subsystem, + sshnc_hostkey_checker_t checker, + struct sshnc_client **clientp // output +) { + clear_additional_error(); + + const ssh_session session = ssh_new(); + if (!session) { + return SSHNC_ERR_SESSION; + } + +#define RETURN(errorcode) \ + do { ssh_free(session); *clientp = NULL; return (errorcode); } while(0) + + const char *ciphers_str = "aes256-gcm@openssh.com,aes256-ctr,aes256-cbc"; + const bool procconfig = false; + bool ok = true; + ok &= ssh_options_set(session, SSH_OPTIONS_PROCESS_CONFIG, &procconfig) == SSH_OK; + ok &= ssh_options_set(session, SSH_OPTIONS_USER, username) == SSH_OK; + ok &= ssh_options_set(session, SSH_OPTIONS_HOST, hostname) == SSH_OK; + ok &= ssh_options_set(session, SSH_OPTIONS_PORT, &port) == SSH_OK; + ok &= ssh_options_set(session, SSH_OPTIONS_CIPHERS_C_S, ciphers_str) == SSH_OK; + ok &= ssh_options_set(session, SSH_OPTIONS_CIPHERS_S_C, ciphers_str) == SSH_OK; + // int loglevel = SSH_LOG_PROTOCOL; + // ok &= ssh_options_set(session, SSH_OPTIONS_LOG_VERBOSITY, &loglevel) == SSH_OK; + + if (!ok) { + store_additional_error(session); + RETURN(SSHNC_ERR_OPTIONS); + } + + if (ssh_connect(session) != SSH_OK) { + store_additional_error(session); + RETURN(SSHNC_ERR_CONNECT); + } + + ssh_key host_key; + if (ssh_get_server_publickey(session, &host_key) != SSH_OK) { + store_additional_error(session); + RETURN(SSHNC_ERR_GETKEY); + } + + unsigned char *host_key_hash = NULL; + size_t host_key_hash_length = 0; + if (ssh_get_publickey_hash(host_key, SSH_PUBLICKEY_HASH_SHA256, &host_key_hash, &host_key_hash_length) != SSH_OK) { + store_additional_error(session); + RETURN(SSHNC_ERR_GETKEY); + } + + if (!checker(host_key_hash, host_key_hash_length)) { + RETURN(SSHNC_ERR_UNTRUSTED); + } + + // Now we're connected, let's do authentication. + +retry_userauth: + switch (ssh_userauth_none(session, NULL)) { + case SSH_AUTH_ERROR: + store_additional_error(session); + RETURN(SSHNC_ERR_AUTH); + + case SSH_AUTH_DENIED: + case SSH_AUTH_PARTIAL: + RETURN(SSHNC_ERR_DENIED); + + case SSH_AUTH_SUCCESS: + break; + + case SSH_AUTH_AGAIN: + if (ssh_get_status(session) & (SSH_CLOSED | SSH_CLOSED_ERROR)) { + RETURN(SSHNC_ERR_CLOSED); + } + goto retry_userauth; + } + + // We're authenticated; open channel and set it up. + + const ssh_channel channel = ssh_channel_new(session); + if (!channel) { + store_additional_error(session); + RETURN(SSHNC_ERR_CHANNEL); + } + + if (ssh_channel_open_session(channel) != SSH_OK || + ssh_channel_request_subsystem(channel, subsystem) != SSH_OK) { + store_additional_error(session); + RETURN(SSHNC_ERR_SUBSYSTEM); + } + + // Fully connected, now set up libssh to our wishes. + + struct sshnc_client *const client = malloc(sizeof(struct sshnc_client)); + client->is_closed = false; + client->libssh_error_descr = NULL; + + client->sesdata = malloc(sizeof(struct session_data)); + *client->sesdata = (struct session_data){ + .session = session, + .channel = channel, + .should_close = false, + .close_reason = SSHNC_OK, + .recvlen = 0, + }; + + memset(&client->chan_cb, 0, sizeof client->chan_cb); + ssh_callbacks_init(&client->chan_cb); + client->chan_cb.userdata = client->sesdata; + client->chan_cb.channel_close_function = channel_close_cb; + client->chan_cb.channel_eof_function = channel_eof_cb; + client->chan_cb.channel_data_function = channel_data_cb; + + if (ssh_set_channel_callbacks(channel, &client->chan_cb) != SSH_OK) { + store_additional_error(session); + RETURN(SSHNC_ERR_CALLBACKS); + } + + client->event = ssh_event_new(); + if (!client->event || ssh_event_add_session(client->event, session) != SSH_OK) { + store_additional_error(session); + RETURN(SSHNC_ERR_EVENT); + } + + *clientp = client; + return SSHNC_OK; + +#undef RETURN +} + +static void sshnc_close_nofree(struct sshnc_client *client) { + ssh_event_free(client->event); + ssh_free(client->sesdata->session); + free(client->sesdata); + client->is_closed = true; +} + +void sshnc_close(struct sshnc_client *client) { + if (!client->is_closed) { + sshnc_close_nofree(client); + } + free(client); +} + +int sshnc_poll_fd(struct sshnc_client *client) { + if (client->is_closed) return -1; + return ssh_get_fd(client->sesdata->session); +} + +enum sshnc_retval sshnc_send( + struct sshnc_client *client, + const char *data, + size_t length +) { + clear_additional_error(); + + if (client->is_closed) return SSHNC_EOF; + + size_t cursor = 0; + while (cursor < length) { + const int ret = ssh_channel_write(client->sesdata->channel, data + cursor, length - cursor); + + if (client->sesdata->should_close) { + return client->sesdata->close_reason; + } + + if (ret == SSH_ERROR) { + if (ssh_channel_is_closed(client->sesdata->channel) + || !ssh_is_connected(client->sesdata->session)) { + return SSHNC_EOF; + } else { + store_additional_error(client->sesdata->session); + return SSHNC_ERR_WRITE; + } + } + + cursor += ret; + } + + return SSHNC_OK; +} + +enum sshnc_retval sshnc_maybe_recv( + struct sshnc_client *client, + size_t capacity, + char *data, // output + size_t *length // output +) { + clear_additional_error(); + + *length = 0; // in case we error along the way + + if (client->is_closed) return SSHNC_EOF; + + const int ret = ssh_event_dopoll(client->event, -1); + if (ret == SSH_ERROR) { + return SSHNC_ERR_POLL; + } + + const int status = ssh_get_status(client->sesdata->session); + if (client->sesdata->should_close + || (status & (SSH_CLOSED | SSH_CLOSED_ERROR))) { + sshnc_close_nofree(client); + return SSHNC_EOF; + } + + if (client->sesdata->recvlen == 0) { + return SSHNC_AGAIN; + } + + struct session_data *const ses = client->sesdata; + const size_t consumed = min_size_t(ses->recvlen, capacity); + + memcpy(data, ses->recvdata, consumed); + *length = consumed; + + memmove(ses->recvdata, ses->recvdata + consumed, ses->recvlen - consumed); + ses->recvlen -= consumed; + + return SSHNC_OK; +} diff --git a/ssh/sshnc.h b/ssh/sshnc.h new file mode 100644 index 0000000..3e4bcfe --- /dev/null +++ b/ssh/sshnc.h @@ -0,0 +1,102 @@ +#pragma once + +#include <stddef.h> +#include <stdbool.h> + + +// This "SSH netcat" library is very specifically oriented on the use of SSH in +// the tomsg protocol. That is: it is assumed that what you want is a +// connection with one simple channel with a particular subsystem, and that you +// need no authentication at all (i.e. authentication type 'none'). For +// anything more involved, use libssh directly. + + +struct sshnc_client; + +// Should return 'true' if the key is trusted, 'false' otherwise. The hash is +// sha256 in byte form, not yet encoded in hexadecimal or similar. +typedef bool (*sshnc_hostkey_checker_t)(const unsigned char *hash, size_t length); + +// Convenience function to convert a hash to a human-readable form. Returns a +// reference to an internal static buffer. +const char* sshnc_print_hash(const unsigned char *hash, size_t length); + +enum sshnc_retval { + // Successful result + SSHNC_OK = 0, + + // Other status codes + SSHNC_EOF, // connection closed (sshnc_send, sshnc_maybe_recv) + SSHNC_AGAIN, // no data now, try again later (sshnc_maybe_recv) + + // Error codes + SSHNC_ERR_CONNECT, // could not connect to host + SSHNC_ERR_UNTRUSTED, // hostkey checker rejected key + SSHNC_ERR_AUTH, // error authenticating to server + SSHNC_ERR_DENIED, // server did not accept 'none' authentication + SSHNC_ERR_CLOSED, // server unexpectedly closed connection + SSHNC_ERR_SUBSYSTEM, // server did not accept the subsystem channel + + // Internal error codes + SSHNC_ERR_SESSION, // could not open libssh session + SSHNC_ERR_CHANNEL, // could not open libssh channel + SSHNC_ERR_OPTIONS, // could not set libssh options + SSHNC_ERR_GETKEY, // could not get key from libssh + SSHNC_ERR_CALLBACKS, // sshnc would not accept our callbacks structure + SSHNC_ERR_EVENT, // could not create libssh event poller + SSHNC_ERR_WRITE, // could not write to ssh channel + SSHNC_ERR_POLL, // could not poll the socket for activity +}; + +// Returns reference to internal static buffer. All error codes are negative. +// Additional error info may be stored in an internal thread_local buffer, and +// returned as part of the description. +const char* sshnc_strerror(enum sshnc_retval code); + +// If successful, stores a new connection structure in 'client' and returns +// SSHNC_OK. On error, stores NULL in 'client' and returns an error code. +enum sshnc_retval sshnc_connect( + const char *hostname, + int port, + const char *username, + const char *subsystem, + sshnc_hostkey_checker_t checker, + struct sshnc_client **client // output +); + +// Close the connection. This also frees the client structure. Note that even +// in case of an error, you must still call this function to prevent a memory +// leak. +void sshnc_close(struct sshnc_client *client); + +// Returns a file descriptor that can be listened for read-ready events (using +// e.g. select(2) or poll(2)). Whenever it becomes ready for reading, you +// should call sshnc_maybe_recv(). If the connection was already closed +// internally due to an error, returns -1. +int sshnc_poll_fd(struct sshnc_client *client); + +// Returns SSHNC_OK if successful, SSHNC_EOF if the connection was closed, or +// an error code otherwise. +enum sshnc_retval sshnc_send( + struct sshnc_client *client, + const char *data, + size_t length +); + +// Retrieves up to 'capacity' bytes from the connection, and writes them to the +// buffer pointed to by 'data', and the number of bytes received to 'length'. +// Returns SSHNC_OK if successful, SSHNC_EOF if the connection was closed +// before any data was received, SSHNC_AGAIN if no data was available without +// blocking, or an error code otherwise. +// If the return value is not SSHNC_OK, 0 is stored in 'length'. +// Note that because this operation is non-blocking, the caller should only +// call this if it has a reason for suspecting there might be data (e.g. +// because poll(2) reported as such). +// This function also handles general ssh protocol messages, and must thus +// ALWAYS be called if there is readable data on the socket. +enum sshnc_retval sshnc_maybe_recv( + struct sshnc_client *client, + size_t capacity, + char *data, // output + size_t *length // output +); |