From 535aaf7e3c4145df815916e1449818d52f357987 Mon Sep 17 00:00:00 2001 From: Tom Smeding Date: Sun, 12 Jul 2020 21:36:48 +0200 Subject: Add tomsg client lib for C --- ssh/string_view.c | 72 +++++ ssh/string_view.h | 35 +++ ssh/tomsg_clientlib.c | 756 ++++++++++++++++++++++++++++++++++++++++++++++++++ ssh/tomsg_clientlib.h | 168 +++++++++++ 4 files changed, 1031 insertions(+) create mode 100644 ssh/string_view.c create mode 100644 ssh/string_view.h create mode 100644 ssh/tomsg_clientlib.c create mode 100644 ssh/tomsg_clientlib.h 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 +#include +#include +#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 +#include +#include + + +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 +#include +#include +#include +#include +#include +#include +#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 +#include +#include + + +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); -- cgit v1.2.3-70-g09d2