aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTom Smeding <tom.smeding@gmail.com>2020-07-09 18:30:13 +0200
committerTom Smeding <tom.smeding@gmail.com>2020-07-09 18:31:50 +0200
commit05a818bb65d00ef89cf97e59ebca867fcef5863a (patch)
tree31fcdfef52f40e32bdfc4bb0ff735dc9afa53592
parent4659374068eda6473feed06143433dce4e8eade2 (diff)
ssh: Abstract SSH communication in mini-library
-rw-r--r--ssh/Makefile2
-rw-r--r--ssh/client.c295
-rw-r--r--ssh/sshnc.c358
-rw-r--r--ssh/sshnc.h102
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
+);