diff options
Diffstat (limited to 'ssh')
| -rw-r--r-- | ssh/string_view.c | 72 | ||||
| -rw-r--r-- | ssh/string_view.h | 35 | ||||
| -rw-r--r-- | ssh/tomsg_clientlib.c | 756 | ||||
| -rw-r--r-- | ssh/tomsg_clientlib.h | 168 | 
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); | 
