aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTom Smeding <tom.smeding@gmail.com>2020-07-12 21:36:48 +0200
committerTom Smeding <tom.smeding@gmail.com>2020-07-12 21:37:29 +0200
commit535aaf7e3c4145df815916e1449818d52f357987 (patch)
tree36ecc9624845880f8c8de5e798df63c0058273ea
parente539ab7a082854892c8f7722d2ec5b6b14d3cf24 (diff)
Add tomsg client lib for C
-rw-r--r--ssh/string_view.c72
-rw-r--r--ssh/string_view.h35
-rw-r--r--ssh/tomsg_clientlib.c756
-rw-r--r--ssh/tomsg_clientlib.h168
4 files changed, 1031 insertions, 0 deletions
diff --git a/ssh/string_view.c b/ssh/string_view.c
new file mode 100644
index 0000000..2f4f22a
--- /dev/null
+++ b/ssh/string_view.c
@@ -0,0 +1,72 @@
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include "string_view.h"
+
+
+struct string_view string_view(const char *s, size_t len) {
+ return (struct string_view){.s = s, .len = len};
+}
+
+bool sv_equals(const struct string_view s, const char *cmp) {
+ return s.s && s.len == strlen(cmp) && memcmp(s.s, cmp, s.len) == 0;
+}
+
+bool sv_is_empty(const struct string_view s) {
+ return !s.s || s.len == 0;
+}
+
+bool sv_copy(const struct string_view s, char **output) {
+ if (s.s != NULL) {
+ char *buffer = malloc(s.len + 1);
+ if (!buffer) return false; // really should return TOMSG_ERR_MEMORY, but this will do
+ memcpy(buffer, s.s, s.len);
+ buffer[s.len] = '\0';
+ *output = buffer;
+ return true;
+ } else {
+ *output = NULL;
+ return false;
+ }
+}
+
+bool sv_parse_i64(const struct string_view s, int64_t *output) {
+ if (!s.s || s.len > 20) return false; // strlen(itoa(INT64_MIN)) == 20
+ char buf[21];
+ memcpy(buf, s.s, s.len);
+ buf[s.len] = '\0';
+ char *endp;
+ *output = strtoll(buf, &endp, 10);
+ return endp == buf + s.len;
+}
+
+struct string_view sv_tokenise_word(struct string_view *line) {
+ if (!line->s) return string_view(NULL, 0);
+
+ for (size_t i = 0; i < line->len; i++) {
+ if (line->s[i] == ' ') {
+ const struct string_view word = string_view(line->s, i);
+ line->s += i + 1;
+ line->len -= i + 1;
+ return word;
+ }
+ }
+
+ // No space found; if line is not empty, this is the final word
+ if (line->len > 0) {
+ const struct string_view word = *line;
+ *line = string_view(NULL, 0);
+ return word;
+ }
+
+ // Line is empty, so return NULL.
+ return string_view(NULL, 0);
+}
+
+void sv_skip_whitespace(struct string_view *line) {
+ if (!line->s) return;
+ while (line->len > 0 && isspace(line->s[0])) {
+ line->s++;
+ line->len--;
+ }
+}
diff --git a/ssh/string_view.h b/ssh/string_view.h
new file mode 100644
index 0000000..659dad4
--- /dev/null
+++ b/ssh/string_view.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+
+struct string_view {
+ const char *s; // if NULL, len must be 0
+ size_t len;
+};
+
+
+struct string_view string_view(const char *s, size_t len);
+
+bool sv_equals(const struct string_view s, const char *cmp);
+
+bool sv_is_empty(const struct string_view s);
+
+// If 's' is not NULL, writes a newly allocated copy to *output and returns
+// true. Otherwise, stores NULL in *output and returns false.
+bool sv_copy(const struct string_view s, char **output);
+
+// If 's' is not NULL and fully parses as a decimal integer, writes that
+// integer to *output and returns true. Otherwise, returns false.
+bool sv_parse_i64(const struct string_view s, int64_t *output);
+
+// Returns the word starting at the beginning of 'line', and modifies 'line' to
+// point to the start of the next word. If there is no next word, sets line to
+// NULL.
+// If 'line' is empty or NULL, returns NULL.
+struct string_view sv_tokenise_word(struct string_view *line);
+
+// Skips all isspace() at the beginning of the string
+void sv_skip_whitespace(struct string_view *line);
diff --git a/ssh/tomsg_clientlib.c b/ssh/tomsg_clientlib.c
new file mode 100644
index 0000000..0108a38
--- /dev/null
+++ b/ssh/tomsg_clientlib.c
@@ -0,0 +1,756 @@
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <inttypes.h>
+#include <assert.h>
+#include <poll.h>
+#include "tomsg_clientlib.h"
+#include "sshnc.h"
+#include "string_view.h"
+
+
+struct inflight {
+ int64_t tag;
+
+ // Partially filled in, depending on the event type
+ struct tomsg_event event;
+};
+
+struct tomsg_client {
+ struct sshnc_client *conn;
+
+ // Receive buffer for the ssh connection
+ size_t buffer_len, buffer_cap;
+ char *buffer;
+ size_t buffer_newline_cursor; // no newlines before this index
+
+ int64_t next_tag;
+
+ size_t inflight_num, inflight_cap;
+ struct inflight *inflight;
+};
+
+static size_t min_size_t(size_t a, size_t b) { return a < b ? a : b; }
+
+static bool hostkey_checker(const unsigned char *hash, size_t length, void *userdata) {
+ (void)userdata;
+ static const char *preload = "SHA256:ppz/McaESpOQy0O3kbaIi1LPZ37/YtrdC+y9102Y0+I";
+ const char *fingerprint = sshnc_print_hash(hash, length);
+ return strcmp(fingerprint, preload) == 0;
+}
+
+static bool hasspacelf(const char *string) {
+ for (size_t i = 0; string[i]; i++)
+ if (string[i] == ' ' || string[i] == '\n') return true;
+ return false;
+}
+
+static bool haslf(const char *string) {
+ for (size_t i = 0; string[i]; i++)
+ if (string[i] == '\n') return true;
+ return false;
+}
+
+__attribute__((warn_unused_result))
+static enum tomsg_retval add_inflight(
+ struct tomsg_client *client, int64_t tag, struct tomsg_event event) {
+ if (client->inflight_num == client->inflight_cap) {
+ client->inflight_cap *= 2;
+ struct inflight *new_inflight = realloc(
+ client->inflight, client->inflight_cap * sizeof(struct inflight));
+ if (!new_inflight) {
+ // Just don't send the message; hope the application can handle that
+ tomsg_event_nullify(event);
+ return TOMSG_ERR_MEMORY;
+ }
+ client->inflight = new_inflight;
+ }
+
+ client->inflight[client->inflight_num++] = (struct inflight){
+ .tag = tag,
+ .event = event,
+ };
+
+ return TOMSG_OK;
+}
+
+static enum tomsg_retval receive_more_data(struct tomsg_client *client) {
+ if (client->buffer_cap - client->buffer_len < 256) {
+ client->buffer_cap *= 2;
+ char *new_buffer = realloc(client->buffer, client->buffer_cap);
+ if (!new_buffer) {
+ // Cannot safely continue; we also can't "just" clear the buffer
+ // and start parsing again, since we may detect commands in the
+ // middle of messages, which, even if not an RCE vector, would not
+ // be good. Thus, throw everything away and close the connection
+ // now, before stuff goes wrong.
+ free(client->buffer);
+ client->buffer = NULL;
+ sshnc_close(client->conn);
+ client->conn = NULL;
+ return TOMSG_ERR_MEMORY;
+ }
+ client->buffer = new_buffer;
+ }
+
+ size_t recvlen = 0;
+ const enum sshnc_retval ret = sshnc_maybe_recv(
+ client->conn,
+ client->buffer_cap - client->buffer_len,
+ client->buffer + client->buffer_len,
+ &recvlen
+ );
+
+ if (ret == SSHNC_EOF) return TOMSG_ERR_CLOSED;
+ if (ret == SSHNC_AGAIN) return TOMSG_ERR_AGAIN;
+ if (ret != SSHNC_OK) return TOMSG_ERR_TRANSPORT;
+
+ client->buffer_len += recvlen;
+
+ return TOMSG_OK;
+}
+
+// Returns the first line in the buffer, if any. Updates buffer_newline_cursor
+// to point at the newline after that line, or at the end of the buffer
+// otherwise.
+static struct string_view extract_next_line(struct tomsg_client *client) {
+ for (size_t i = client->buffer_newline_cursor; i < client->buffer_len; i++) {
+ if (client->buffer[i] == '\n') {
+ struct string_view sv = string_view(client->buffer, i);
+
+ client->buffer_newline_cursor = i;
+ return sv;
+ }
+ }
+
+ client->buffer_newline_cursor = client->buffer_len;
+ return string_view(NULL, 0);
+}
+
+// Assumes buffer_newline_cursor points at a newline; removes the part ending
+// at that newline from the buffer.
+static void splice_scanned_buffer_part(struct tomsg_client *client) {
+ const size_t start = min_size_t(client->buffer_newline_cursor + 1, client->buffer_len);
+ const size_t restlen = client->buffer_len - start;
+ memmove(client->buffer, client->buffer + start, restlen);
+ client->buffer_len = restlen;
+ client->buffer_newline_cursor = 0;
+}
+
+const char* tomsg_strerror(enum tomsg_retval code) {
+ switch (code) {
+ case TOMSG_OK: return "Success";
+ case TOMSG_ERR_CONNECT: return "Server refused connection";
+ case TOMSG_ERR_CLOSED: return "Server connection unexpectedly closed";
+ case TOMSG_ERR_VERSION: return "Server protocol version incompatible";
+ case TOMSG_ERR_TRANSPORT: return "Error in the underlying SSH transport";
+ case TOMSG_ERR_SPACE: return "Argument contained space or LF, while it could not";
+ case TOMSG_ERR_AGAIN: return "No events for now, poll(2) and try again";
+ case TOMSG_ERR_PARSE: return "Could not parse line from server, line ignored";
+ case TOMSG_ERR_MEMORY: return "Error allocating memory";
+ }
+
+ return "?unknown error?";
+}
+
+static enum tomsg_retval version_negotiation(struct tomsg_client *client) {
+ if (!client->conn) return TOMSG_ERR_CLOSED;
+
+ const enum sshnc_retval retssh = sshnc_send(client->conn, "ver version 1\n", 14);
+ if (retssh == SSHNC_EOF) return TOMSG_ERR_CLOSED;
+ if (retssh != SSHNC_OK) return TOMSG_ERR_TRANSPORT;
+
+ struct pollfd pfd;
+ pfd.fd = sshnc_poll_fd(client->conn);
+ pfd.events = POLLIN;
+
+ while (true) {
+ const int retpoll = poll(&pfd, 1, -1);
+ if (retpoll < 0) return TOMSG_ERR_TRANSPORT;
+ if (retpoll == 0) continue;
+
+ const enum tomsg_retval retmsg = receive_more_data(client);
+ if (retmsg == TOMSG_ERR_AGAIN) continue;
+ if (retmsg != TOMSG_OK) return retmsg;
+
+ const struct string_view line = extract_next_line(client);
+ if (line.s != NULL) {
+ if (sv_equals(line, "ver ok")) {
+ splice_scanned_buffer_part(client);
+ return TOMSG_OK;
+ } else {
+ sshnc_close(client->conn);
+ client->conn = NULL;
+ return TOMSG_ERR_VERSION;
+ }
+ }
+ }
+}
+
+enum tomsg_retval tomsg_connect(
+ const char *hostname, int port, struct tomsg_client **clientp) {
+ struct sshnc_client *conn;
+ enum sshnc_retval ret = sshnc_connect(
+ hostname, port, "tomsg", "tomsg", hostkey_checker, NULL, &conn);
+
+ if (ret == SSHNC_ERR_CONNECT) return TOMSG_ERR_CONNECT;
+ if (ret != SSHNC_OK) return TOMSG_ERR_TRANSPORT;
+
+ struct tomsg_client *client = malloc(sizeof(struct tomsg_client));
+ if (!client) return TOMSG_ERR_MEMORY;
+ client->conn = conn;
+ client->buffer_len = 0;
+ client->buffer_cap = 1024;
+ client->buffer = malloc(client->buffer_cap);
+ if (!client->buffer) { free(client); return TOMSG_ERR_MEMORY; }
+ client->buffer_newline_cursor = 0;
+ client->next_tag = 1;
+ client->inflight_num = 0;
+ client->inflight_cap = 2;
+ client->inflight = malloc(client->inflight_cap * sizeof(struct inflight));
+ if (!client->inflight) { free(client->buffer); free(client); return TOMSG_ERR_MEMORY; }
+
+ *clientp = client;
+
+ return version_negotiation(client);
+}
+
+void tomsg_close(struct tomsg_client *client) {
+ if (client->conn) sshnc_close(client->conn);
+ free(client->buffer);
+ if (client->inflight) {
+ for (size_t i = 0; i < client->inflight_num; i++) {
+ tomsg_event_nullify(client->inflight[i].event);
+ }
+ }
+ free(client->inflight);
+ free(client);
+}
+
+int tomsg_poll_fd(const struct tomsg_client *client) {
+ if (!client->conn) return -1;
+ return sshnc_poll_fd(client->conn);
+}
+
+static void history_message_nullify(struct history_message message) {
+ free(message.username); message.username = NULL;
+ free(message.message); message.message = NULL;
+}
+
+void tomsg_event_nullify(struct tomsg_event event) {
+ if (event.error) free(event.error);
+ event.error = NULL;
+
+ switch (event.type) {
+ case TOMSG_EV_LOGOUT:
+ case TOMSG_EV_PING:
+ case TOMSG_EV_USER_ACTIVE:
+ case TOMSG_EV_SEND:
+ break;
+
+ case TOMSG_EV_REGISTER:
+ case TOMSG_EV_LOGIN:
+ free(event.login.username); event.login.username = NULL;
+ break;
+
+ case TOMSG_EV_INVITE:
+ case TOMSG_EV_PUSH_JOIN:
+ free(event.join.room_name); event.join.room_name = NULL;
+ free(event.join.username); event.join.username = NULL;
+ break;
+
+ case TOMSG_EV_IS_ONLINE:
+ case TOMSG_EV_PUSH_ONLINE:
+ free(event.is_online.username); event.is_online.username = NULL;
+ break;
+
+ case TOMSG_EV_CREATE_ROOM:
+ free(event.create_room.room_name); event.create_room.room_name = NULL;
+ break;
+
+ case TOMSG_EV_LIST_ROOMS:
+ if (event.list_rooms.rooms != NULL) {
+ for (int64_t i = 0; i < event.list_rooms.count; i++) {
+ free(event.list_rooms.rooms[i]);
+ }
+ }
+ free(event.list_rooms.rooms);
+ event.list_rooms.rooms = NULL;
+ break;
+
+ case TOMSG_EV_LIST_MEMBERS:
+ if (event.list_members.members != NULL) {
+ for (int64_t i = 0; i < event.list_members.count; i++) {
+ free(event.list_members.members[i]);
+ }
+ }
+ free(event.list_members.members);
+ event.list_members.members = NULL;
+ break;
+
+ case TOMSG_EV_HISTORY:
+ free(event.history.room_name); event.history.room_name = NULL;
+ if (event.history.messages != NULL) {
+ for (int64_t i = 0; i < event.history.count; i++) {
+ history_message_nullify(event.history.messages[i]);
+ }
+ }
+ free(event.history.messages);
+ event.history.messages = NULL;
+ break;
+
+ case TOMSG_EV_PUSH_MESSAGE:
+ free(event.push_message.room_name); event.push_message.room_name = NULL;
+ history_message_nullify(event.push_message.message);
+ break;
+
+ case TOMSG_EV_PUSH_INVITE:
+ free(event.push_invite.room_name); event.push_invite.room_name = NULL;
+ free(event.push_invite.inviter); event.push_invite.inviter = NULL;
+ break;
+ }
+}
+
+static bool find_extract_inflight(struct tomsg_client *client, int64_t tag, struct inflight *output) {
+ for (size_t i = 0; i < client->inflight_num; i++) {
+ if (client->inflight[i].tag == tag) {
+ *output = client->inflight[i];
+ if (i < client->inflight_num - 1) {
+ client->inflight[i] = client->inflight[client->inflight_num - 1];
+ }
+ client->inflight_num--;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// Returns TOMSG_ERR_AGAIN in case the line generated no output event.
+static enum tomsg_retval handle_line(
+ struct tomsg_client *client,
+ struct string_view line,
+ struct tomsg_event *eventp
+) {
+ const struct string_view tagstr = sv_tokenise_word(&line);
+ const struct string_view command = sv_tokenise_word(&line);
+
+ // These parse macro's directly write to eventp.
+#define PARSE_I64(memb_) \
+ do { if (!sv_parse_i64(sv_tokenise_word(&line), &eventp -> memb_)) return TOMSG_ERR_PARSE; } while (0)
+#define PARSE_WORD(memb_) \
+ do { if (!sv_copy(sv_tokenise_word(&line), &eventp -> memb_)) return TOMSG_ERR_PARSE; } while (0)
+#define PARSE_RESTSTRING(memb_) \
+ do { if (!sv_copy(line, &eventp -> memb_)) return TOMSG_ERR_PARSE; } while (0)
+#define EXPECT_FINISH() \
+ do { if (!sv_is_empty(line)) return TOMSG_ERR_PARSE; } while (0)
+
+ if (sv_equals(tagstr, "_push")) {
+ // Ordered by rought expected occurrence probability
+ if (sv_equals(command, "ping")) {
+ return TOMSG_ERR_AGAIN;
+ } else if (sv_equals(command, "message")) {
+ eventp->type = TOMSG_EV_PUSH_MESSAGE;
+ PARSE_WORD(push_message.room_name);
+ PARSE_WORD(push_message.message.username);
+ PARSE_I64(push_message.message.timestamp);
+ PARSE_I64(push_message.message.msgid);
+ PARSE_RESTSTRING(push_message.message.message);
+ return TOMSG_OK;
+ } else if (sv_equals(command, "online")) {
+ eventp->type = TOMSG_EV_PUSH_ONLINE;
+ PARSE_I64(is_online.online_count);
+ PARSE_WORD(is_online.username);
+ EXPECT_FINISH();
+ return TOMSG_OK;
+ } else if (sv_equals(command, "invite")) {
+ eventp->type = TOMSG_EV_PUSH_INVITE;
+ PARSE_WORD(push_invite.room_name);
+ PARSE_WORD(push_invite.inviter);
+ EXPECT_FINISH();
+ return TOMSG_OK;
+ } else if (sv_equals(command, "join")) {
+ eventp->type = TOMSG_EV_PUSH_JOIN;
+ PARSE_WORD(join.room_name);
+ PARSE_WORD(join.username);
+ EXPECT_FINISH();
+ return TOMSG_OK;
+ } else {
+ // Unknown command, let's just ignore
+ return TOMSG_ERR_AGAIN;
+ }
+ }
+
+ // These parse functions are now not valid anymore, since we can't directly
+ // write to eventp anymore, and additionally need to manage the event
+ // structure we will have around in 'inflight'.
+#undef PARSE_I64
+#undef PARSE_WORD
+#undef PARSE_RESTSTRING
+#undef EXPECT_FINISH
+
+ int64_t tag;
+ struct inflight inflight;
+ if (!sv_parse_i64(tagstr, &tag) || !find_extract_inflight(client, tag, &inflight)) {
+ // Unknown tag, let's just ignore
+ return TOMSG_ERR_AGAIN;
+ }
+
+ // These macros correctly dispose of, or persist, the event structure before returning.
+#define CLEANUP_RETURN(ret_) do { tomsg_event_nullify(inflight.event); return (ret_); } while (0)
+#define SUCCESS_RETURN() do { *eventp = inflight.event; return TOMSG_OK; } while (0)
+
+ if (sv_equals(command, "error")) {
+ if (!sv_copy(line, &inflight.event.error)) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ SUCCESS_RETURN();
+ }
+
+ // Since the response is not 'error', at least we're not in an error case.
+ // Thus, we can assert the other, successful response type in each of the
+ // event arms below.
+ inflight.event.error = NULL;
+
+ switch (inflight.event.type) {
+ case TOMSG_EV_REGISTER:
+ case TOMSG_EV_LOGIN:
+ case TOMSG_EV_LOGOUT:
+ case TOMSG_EV_INVITE:
+ if (!sv_equals(command, "ok")) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ SUCCESS_RETURN();
+
+ case TOMSG_EV_LIST_ROOMS:
+ case TOMSG_EV_LIST_MEMBERS: {
+ if (!sv_equals(command, "list")) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+
+ int64_t count;
+ char **words;
+ if (!sv_parse_i64(sv_tokenise_word(&line), &count)) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ if (count < 0 || count > (1 << 30)) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+
+ words = calloc(count, sizeof(char*));
+ if (!words) CLEANUP_RETURN(TOMSG_ERR_MEMORY);
+ for (int64_t i = 0; i < count; i++) {
+ if (!sv_copy(sv_tokenise_word(&line), &words[i])) {
+ for (int64_t j = 0; j < i; j++) free(words[j]);
+ CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ }
+ }
+
+ // There might be words after the last one in the list, but we'll
+ // just ignore them for simplicity's sake.
+
+ if (inflight.event.type == TOMSG_EV_LIST_ROOMS) {
+ inflight.event.list_rooms.count = count;
+ inflight.event.list_rooms.rooms = words;
+ } else {
+ inflight.event.list_members.count = count;
+ inflight.event.list_members.members = words;
+ }
+
+ SUCCESS_RETURN();
+ }
+
+ case TOMSG_EV_CREATE_ROOM:
+ if (!sv_equals(command, "name")) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ if (!sv_copy(sv_tokenise_word(&line), &inflight.event.create_room.room_name)) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ if (!sv_is_empty(line)) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ SUCCESS_RETURN();
+
+ case TOMSG_EV_SEND:
+ if (!sv_equals(command, "number")) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ if (!sv_parse_i64(sv_tokenise_word(&line), &inflight.event.send.msgid)) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ if (!sv_is_empty(line)) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ SUCCESS_RETURN();
+
+ case TOMSG_EV_HISTORY:
+ if (sv_equals(command, "history")) {
+ int64_t count;
+ if (!sv_parse_i64(sv_tokenise_word(&line), &count)) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ if (count < 0 || count > (1 << 30)) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ inflight.event.history.count = count;
+
+ // Use calloc() to ensure the embedded pointers are NULL, in case we free it early
+ inflight.event.history.messages = calloc(count, sizeof(struct history_message));
+ if (!inflight.event.history.messages) CLEANUP_RETURN(TOMSG_ERR_MEMORY);
+
+ // Re-add the tag for the history_message events
+ enum tomsg_retval ret = add_inflight(client, tag, inflight.event);
+ if (ret != TOMSG_OK) return ret;
+ return TOMSG_ERR_AGAIN; // don't cleanup, we re-submitted the event!
+ } else if (sv_equals(command, "history_message")) {
+ int64_t index;
+ if (!sv_parse_i64(sv_tokenise_word(&line), &index)) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ if (index < 0 || index >= inflight.event.history.count) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+
+ // Note that early-exiting in case of parse errors is valid here
+ // (and everywhere): the messages buffer has been allocated with
+ // calloc(), so tomsg_event_nullify() will cleanup up precisely
+ // everything that we haven't filled in yet.
+ struct history_message *msg = &inflight.event.history.messages[index];;
+ if (!sv_equals(sv_tokenise_word(&line), inflight.event.history.room_name)) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ if (!sv_copy(sv_tokenise_word(&line), &msg->username)) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ if (!sv_parse_i64(sv_tokenise_word(&line), &msg->timestamp)) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ if (!sv_parse_i64(sv_tokenise_word(&line), &msg->msgid)) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ if (!sv_copy(line, &msg->message)) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+
+ // If there are more history_message events coming, re-add the tag for them
+ if (index == inflight.event.history.count - 1) {
+ SUCCESS_RETURN(); // done with the whole history listing
+ } else {
+ enum tomsg_retval ret = add_inflight(client, tag, inflight.event);
+ if (ret != TOMSG_OK) return ret;
+ return TOMSG_ERR_AGAIN; // don't cleanup, we re-submitted the event!
+ }
+ } else {
+ CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ }
+
+ case TOMSG_EV_PING:
+ if (!sv_equals(command, "pong")) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ if (!sv_is_empty(line)) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ SUCCESS_RETURN();
+
+ case TOMSG_EV_IS_ONLINE:
+ if (!sv_equals(command, "number")) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ if (!sv_parse_i64(sv_tokenise_word(&line), &inflight.event.is_online.online_count)) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ if (!sv_is_empty(line)) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ SUCCESS_RETURN();
+
+ case TOMSG_EV_USER_ACTIVE:
+ if (!sv_equals(command, "ok")) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ if (!sv_is_empty(line)) CLEANUP_RETURN(TOMSG_ERR_PARSE);
+ SUCCESS_RETURN();
+
+ case TOMSG_EV_PUSH_ONLINE:
+ case TOMSG_EV_PUSH_MESSAGE:
+ case TOMSG_EV_PUSH_INVITE:
+ case TOMSG_EV_PUSH_JOIN:
+ // What? Why did we put a push event in the inflight list? That's a bug.
+ assert(false);
+ }
+
+#undef CLEANUP_RETURN
+#undef SUCCESS_RETURN
+
+ // Should not be reachable; either the switch above missed a 'return'
+ // statement somewhere, which is a bug, or the event type was not in the
+ // tomsg_event_type enum, which is also a bug.
+ assert(false);
+}
+
+// If the buffer contains a full line, handles that line, stores the received
+// event in eventp, and removes the line from the buffer. Otherwise, returns
+// TOMSG_ERR_AGAIN.
+static enum tomsg_retval find_handle_next_line(
+ struct tomsg_client *client, struct tomsg_event *eventp) {
+
+ // Loop while a line was handled without producing an output event
+ while (true) {
+ struct string_view line = extract_next_line(client);
+ if (line.s == NULL) return TOMSG_ERR_AGAIN; // out of data
+
+ const enum tomsg_retval ret = handle_line(client, line, eventp);
+ splice_scanned_buffer_part(client);
+ if (ret == TOMSG_ERR_AGAIN) continue; // handled but no output
+ return ret; // handled with output, or error thrown
+ }
+}
+
+enum tomsg_retval tomsg_next_event(struct tomsg_client *client, struct tomsg_event *eventp) {
+ if (!client->conn) return TOMSG_ERR_CLOSED;
+
+ // If we already have a line waiting in the buffer, handle it now.
+ enum tomsg_retval ret = find_handle_next_line(client, eventp);
+ if (ret != TOMSG_ERR_AGAIN) return ret;
+
+ // Otherwise, try to receive more data.
+ ret = receive_more_data(client);
+ if (ret != TOMSG_OK) return ret;
+
+ // And then handle any line that might be completed at this point.
+ return find_handle_next_line(client, eventp);
+}
+
+#define SEND_FMT0(client, tag, fmt) do { \
+ char buffer_[128]; \
+ int length_ = snprintf(buffer_, sizeof buffer_, "%" PRIi64 " " fmt "\n", (tag)); \
+ assert(length_ < (int)sizeof buffer_); \
+ enum sshnc_retval ret_ = sshnc_send((client)->conn, buffer_, length_); \
+ if (ret_ == SSHNC_EOF) return TOMSG_ERR_CLOSED; \
+ if (ret_ != SSHNC_OK) return TOMSG_ERR_TRANSPORT; \
+ } while (0)
+
+#define SEND_FMT(client, tag, fmt, ...) do { \
+ char *buffer_; \
+ int length_ = asprintf(&buffer_, "%" PRIi64 " " fmt "\n", (tag), __VA_ARGS__); \
+ if (length_ < 0) return TOMSG_ERR_MEMORY; \
+ enum sshnc_retval ret_ = sshnc_send((client)->conn, buffer_, length_); \
+ free(buffer_); \
+ if (ret_ == SSHNC_EOF) return TOMSG_ERR_CLOSED; \
+ if (ret_ != SSHNC_OK) return TOMSG_ERR_TRANSPORT; \
+ } while (0)
+
+enum tomsg_retval tomsg_register(
+ struct tomsg_client *client, const char *username, const char *password) {
+ if (!client->conn) return TOMSG_ERR_CLOSED;
+ if (hasspacelf(username)) return TOMSG_ERR_SPACE;
+ if (haslf(password)) return TOMSG_ERR_SPACE;
+ const int64_t tag = client->next_tag++;
+ SEND_FMT(client, tag, "register %s %s", username, password);
+ const struct tomsg_event event = (struct tomsg_event){
+ .type = TOMSG_EV_REGISTER,
+ .login.username = strdup(username),
+ };
+ return add_inflight(client, tag, event);
+}
+
+enum tomsg_retval tomsg_login(
+ struct tomsg_client *client, const char *username, const char *password) {
+ if (!client->conn) return TOMSG_ERR_CLOSED;
+ if (hasspacelf(username)) return TOMSG_ERR_SPACE;
+ if (haslf(password)) return TOMSG_ERR_SPACE;
+ const int64_t tag = client->next_tag++;
+ SEND_FMT(client, tag, "login %s %s", username, password);
+ const struct tomsg_event event = (struct tomsg_event){
+ .type = TOMSG_EV_LOGIN,
+ .login.username = strdup(username),
+ };
+ return add_inflight(client, tag, event);
+}
+
+enum tomsg_retval tomsg_logout(struct tomsg_client *client) {
+ if (!client->conn) return TOMSG_ERR_CLOSED;
+ const int64_t tag = client->next_tag++;
+ SEND_FMT0(client, tag, "logout");
+ const struct tomsg_event event = (struct tomsg_event){
+ .type = TOMSG_EV_LOGOUT,
+ };
+ return add_inflight(client, tag, event);
+}
+
+enum tomsg_retval tomsg_list_rooms(struct tomsg_client *client) {
+ if (!client->conn) return TOMSG_ERR_CLOSED;
+ const int64_t tag = client->next_tag++;
+ SEND_FMT0(client, tag, "list_rooms");
+ const struct tomsg_event event = (struct tomsg_event){
+ .type = TOMSG_EV_LIST_ROOMS,
+ .list_rooms.count = 0,
+ .list_rooms.rooms = NULL,
+ };
+ return add_inflight(client, tag, event);
+}
+
+enum tomsg_retval tomsg_list_members(struct tomsg_client *client, const char *room_name) {
+ if (!client->conn) return TOMSG_ERR_CLOSED;
+ if (hasspacelf(room_name)) return TOMSG_ERR_SPACE;
+ const int64_t tag = client->next_tag++;
+ SEND_FMT(client, tag, "list_members %s", room_name);
+ const struct tomsg_event event = (struct tomsg_event){
+ .type = TOMSG_EV_LIST_MEMBERS,
+ .list_members.room_name = strdup(room_name),
+ .list_members.count = 0,
+ .list_members.members = NULL,
+ };
+ return add_inflight(client, tag, event);
+}
+
+enum tomsg_retval tomsg_create_room(struct tomsg_client *client) {
+ if (!client->conn) return TOMSG_ERR_CLOSED;
+ const int64_t tag = client->next_tag++;
+ SEND_FMT0(client, tag, "create_room");
+ const struct tomsg_event event = (struct tomsg_event){
+ .type = TOMSG_EV_CREATE_ROOM,
+ .create_room.room_name = NULL,
+ };
+ return add_inflight(client, tag, event);
+}
+
+enum tomsg_retval tomsg_invite(
+ struct tomsg_client *client, const char *room_name, const char *username) {
+ if (!client->conn) return TOMSG_ERR_CLOSED;
+ if (hasspacelf(room_name)) return TOMSG_ERR_SPACE;
+ if (hasspacelf(username)) return TOMSG_ERR_SPACE;
+ const int64_t tag = client->next_tag++;
+ SEND_FMT(client, tag, "invite %s %s", room_name, username);
+ const struct tomsg_event event = (struct tomsg_event){
+ .type = TOMSG_EV_INVITE,
+ .join.room_name = strdup(room_name),
+ .join.username = strdup(username),
+ };
+ return add_inflight(client, tag, event);
+}
+
+enum tomsg_retval tomsg_send(
+ struct tomsg_client *client, const char *room_name, const char *message,
+ int64_t *tagp //output
+) {
+ if (!client->conn) return TOMSG_ERR_CLOSED;
+ if (hasspacelf(room_name)) return TOMSG_ERR_SPACE;
+ if (haslf(message)) return TOMSG_ERR_SPACE;
+ const int64_t tag = client->next_tag++;
+ SEND_FMT(client, tag, "send %s %s", room_name, message);
+ const struct tomsg_event event = (struct tomsg_event){
+ .type = TOMSG_EV_SEND,
+ .send.tag = tag,
+ };
+ if (tagp) *tagp = tag;
+ return add_inflight(client, tag, event);
+}
+
+enum tomsg_retval tomsg_history(
+ struct tomsg_client *client, const char *room_name,
+ int64_t count, int64_t before_msgid) {
+ if (!client->conn) return TOMSG_ERR_CLOSED;
+ if (hasspacelf(room_name)) return TOMSG_ERR_SPACE;
+ if (count < 0) count = 0;
+ const int64_t tag = client->next_tag++;
+ const struct tomsg_event event = (struct tomsg_event){
+ .type = TOMSG_EV_HISTORY,
+ .history.room_name = strdup(room_name),
+ .history.count = 0, // filled in when the first response is received
+ .history.messages = NULL,
+ };
+ if (before_msgid < 0) {
+ SEND_FMT(client, tag, "history %s %" PRIi64, room_name, count);
+ } else {
+ SEND_FMT(client, tag, "history_before %s %" PRIi64 " %" PRIi64,
+ room_name, count, before_msgid);
+ }
+ return add_inflight(client, tag, event);
+}
+
+enum tomsg_retval tomsg_ping(struct tomsg_client *client) {
+ if (!client->conn) return TOMSG_ERR_CLOSED;
+ const int64_t tag = client->next_tag++;
+ SEND_FMT0(client, tag, "ping");
+ const struct tomsg_event event = (struct tomsg_event){
+ .type = TOMSG_EV_PING,
+ .send.tag = tag,
+ };
+ return add_inflight(client, tag, event);
+}
+
+enum tomsg_retval tomsg_is_online(struct tomsg_client *client, const char *username) {
+ if (!client->conn) return TOMSG_ERR_CLOSED;
+ if (hasspacelf(username)) return TOMSG_ERR_SPACE;
+ const int64_t tag = client->next_tag++;
+ SEND_FMT(client, tag, "is_online %s", username);
+ const struct tomsg_event event = (struct tomsg_event){
+ .type = TOMSG_EV_IS_ONLINE,
+ .is_online.username = strdup(username),
+ };
+ return add_inflight(client, tag, event);
+}
+
+enum tomsg_retval tomsg_user_active(struct tomsg_client *client, bool active) {
+ if (!client->conn) return TOMSG_ERR_CLOSED;
+ const int64_t tag = client->next_tag++;
+ SEND_FMT(client, tag, "user_active %d", active);
+ const struct tomsg_event event = (struct tomsg_event){
+ .type = TOMSG_EV_USER_ACTIVE,
+ .user_active.active = active,
+ };
+ return add_inflight(client, tag, event);
+}
diff --git a/ssh/tomsg_clientlib.h b/ssh/tomsg_clientlib.h
new file mode 100644
index 0000000..f560728
--- /dev/null
+++ b/ssh/tomsg_clientlib.h
@@ -0,0 +1,168 @@
+#pragma once
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+
+struct tomsg_client;
+
+enum tomsg_retval {
+ // Successful result
+ TOMSG_OK = 0,
+
+ // Error codes
+ TOMSG_ERR_CONNECT, // Server refused connection
+ TOMSG_ERR_CLOSED, // Server connection unexpectedly closed
+ TOMSG_ERR_VERSION, // Server protocol version incompatible
+ TOMSG_ERR_TRANSPORT, // Error in the underlying SSH transport
+ TOMSG_ERR_SPACE, // Argument contained space or LF, while it could not
+ TOMSG_ERR_AGAIN, // (tomsg_next_event) no events for now, poll(2) and try again
+ TOMSG_ERR_PARSE, // (tomsg_next_event) could not parse line from server, line ignored
+ TOMSG_ERR_MEMORY, // Error allocating memory
+};
+
+const char* tomsg_strerror(enum tomsg_retval code);
+
+enum tomsg_retval tomsg_connect(
+ const char *hostname, int port,
+ struct tomsg_client **client // output
+);
+
+// Will also free the client structure
+void tomsg_close(struct tomsg_client *client);
+
+int tomsg_poll_fd(const struct tomsg_client *client);
+
+enum tomsg_event_type {
+ // Data in union fields indicated (but check 'error' first)
+ TOMSG_EV_REGISTER, // login
+ TOMSG_EV_LOGIN, // login
+ TOMSG_EV_LOGOUT, // -
+ TOMSG_EV_LIST_ROOMS, // list_rooms
+ TOMSG_EV_LIST_MEMBERS, // list_members
+ TOMSG_EV_CREATE_ROOM, // create_room
+ TOMSG_EV_INVITE, // join
+ TOMSG_EV_SEND, // send
+ TOMSG_EV_HISTORY, // history
+ TOMSG_EV_PING, // -
+ TOMSG_EV_IS_ONLINE, // is_online
+ TOMSG_EV_USER_ACTIVE, // user_active
+ TOMSG_EV_PUSH_ONLINE, // is_online
+ TOMSG_EV_PUSH_MESSAGE, // push_message
+ TOMSG_EV_PUSH_INVITE, // push_invite
+ TOMSG_EV_PUSH_JOIN, // join
+};
+
+struct history_message {
+ char *username;
+ int64_t timestamp;
+ int64_t msgid;
+ char *message;
+};
+
+struct tomsg_event {
+ enum tomsg_event_type type;
+ // If the server returned an error, 'error' is non-NULL; otherwise it is
+ // NULL, and the union fields describe the server's response.
+ // Actually, if 'error' is non-NULL, the union fields that can be
+ // filled with request data will already be filled.
+ char *error;
+
+ union {
+ struct {
+ char *username;
+ } login;
+ struct {
+ int64_t count;
+ char **rooms;
+ } list_rooms;
+ struct {
+ char *room_name;
+ int64_t count;
+ char **members;
+ } list_members;
+ struct {
+ char *room_name;
+ } create_room;
+ struct {
+ char *room_name;
+ char *username;
+ } join;
+ struct {
+ int64_t tag; // the tag of the send request, returned by tomsg_send()
+ int64_t msgid;
+ } send;
+ struct {
+ char *room_name;
+ int64_t count;
+ struct history_message *messages;
+ } history;
+ struct {
+ char *username;
+ int64_t online_count;
+ } is_online;
+ struct {
+ bool active;
+ } user_active;
+ struct {
+ char *room_name;
+ struct history_message message;
+ } push_message;
+ struct {
+ char *room_name;
+ char *inviter;
+ } push_invite;
+ };
+};
+
+void tomsg_event_nullify(struct tomsg_event event);
+
+// Will return TOMSG_ERR_AGAIN if no events are available at present. In that
+// case, use poll(2) on the file descriptor from tomsg_poll_fd().
+// NOTE: when calling this function, you must call it as long as it gives
+// TOMSG_OK, because there might be multiple messages in one batch.
+enum tomsg_retval tomsg_next_event(
+ struct tomsg_client *client,
+ struct tomsg_event *event // output
+);
+
+enum tomsg_retval tomsg_register(
+ struct tomsg_client *client,
+ const char *username, const char *password
+);
+
+enum tomsg_retval tomsg_login(
+ struct tomsg_client *client,
+ const char *username, const char *password
+);
+
+enum tomsg_retval tomsg_logout(struct tomsg_client *client);
+
+enum tomsg_retval tomsg_list_rooms(struct tomsg_client *client);
+
+enum tomsg_retval tomsg_list_members(struct tomsg_client *client, const char *room_name);
+
+enum tomsg_retval tomsg_create_room(struct tomsg_client *client);
+
+enum tomsg_retval tomsg_invite(
+ struct tomsg_client *client, const char *room_name, const char *username);
+
+// If 'tag' is not NULL, will write the message request tag to the referenced
+// location. This tag will also be given in the TOMSG_EV_SEND response, so that
+// the response can be linked to the original message.
+enum tomsg_retval tomsg_send(
+ struct tomsg_client *client, const char *room_name, const char *message,
+ int64_t *tag // output
+);
+
+// pass -1 to 'before_msgid' to return the latest 'count'
+enum tomsg_retval tomsg_history(
+ struct tomsg_client *client, const char *room_name,
+ int64_t count, int64_t before_msgid);
+
+enum tomsg_retval tomsg_ping(struct tomsg_client *client);
+
+enum tomsg_retval tomsg_is_online(struct tomsg_client *client, const char *username);
+
+enum tomsg_retval tomsg_user_active(struct tomsg_client *client, bool active);