From 923710803851b50991a2ce2569be3085ec31d4f2 Mon Sep 17 00:00:00 2001 From: Tom Smeding Date: Sun, 12 Jul 2020 21:39:11 +0200 Subject: Add simple client using the tomsg clientlib --- ssh/.gitignore | 1 + ssh/Makefile | 5 +- ssh/client.c | 492 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 497 insertions(+), 1 deletion(-) create mode 100644 ssh/client.c diff --git a/ssh/.gitignore b/ssh/.gitignore index fa45e11..1acd62d 100644 --- a/ssh/.gitignore +++ b/ssh/.gitignore @@ -2,5 +2,6 @@ ssh_host_key server_proxy client_proxy ssh_client +client *.o compile_commands.json diff --git a/ssh/Makefile b/ssh/Makefile index 487bb46..f33e02f 100644 --- a/ssh/Makefile +++ b/ssh/Makefile @@ -4,7 +4,7 @@ LDFLAGS = -pthread CFLAGS += $(shell pkg-config --cflags libssh) LDFLAGS += $(shell pkg-config --libs libssh) -TARGETS = server_proxy client_proxy ssh_client +TARGETS = server_proxy client_proxy ssh_client client .PHONY: all clean @@ -24,5 +24,8 @@ client_proxy: client_proxy.o sshnc.o util.o ssh_client: ssh_client.o sshnc.o util.o $(CC) -o $@ $^ $(LDFLAGS) +client: client.o tomsg_clientlib.o sshnc.o string_view.o + $(CC) -o $@ $^ $(LDFLAGS) + %.o: %.c $(wildcard *.h) $(CC) $(CFLAGS) -c -o $@ $< diff --git a/ssh/client.c b/ssh/client.c new file mode 100644 index 0000000..64f8f57 --- /dev/null +++ b/ssh/client.c @@ -0,0 +1,492 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "tomsg_clientlib.h" +#include "string_view.h" + + +static bool parse_address(const char *arg, char **hostp, int *portp) { + const char *const p = strchr(arg, ':'); + if (p == NULL) return false; + const size_t hostlen = p - arg; + + char *endp; + *portp = strtol(p + 1, &endp, 10); + if (!p[1] || *endp) return false; + + *hostp = malloc(hostlen + 1); + memcpy(*hostp, arg, hostlen); + (*hostp)[hostlen] = '\0'; + return true; +} + +struct readbuffer { + char *buffer; + size_t len, cap; + size_t newline_cursor; +}; + +static struct readbuffer readbuffer_new(void) { + return (struct readbuffer){ + .buffer = malloc(1024), + .len = 0, + .cap = 1024, + .newline_cursor = 0, + }; +} + +// Returns whether fd was closed +static bool read_more_data(int fd, struct readbuffer *rb) { + if (rb->cap - rb->len < 256) { + rb->cap *= 2; + rb->buffer = realloc(rb->buffer, rb->cap); + } + + const ssize_t nr = read(fd, rb->buffer + rb->len, rb->cap - rb->len); + if (nr < 0) { + if (errno == EINTR) return false; + perror("read"); + return true; + } + if (nr == 0) return true; + + rb->len += nr; + return false; +} + +static struct string_view extract_line(struct readbuffer *rb) { + for (size_t i = rb->newline_cursor; i < rb->len; i++) { + if (rb->buffer[i] == '\n') { + const struct string_view sv = string_view(rb->buffer, i); + rb->newline_cursor = i; + return sv; + } + } + + rb->newline_cursor = rb->len; + return string_view(NULL, 0); +} + +static void splice_scanned(struct readbuffer *rb) { + size_t start = rb->newline_cursor + 1; + if (start > rb->len) start = rb->len; + + const size_t restlen = rb->len - start; + memmove(rb->buffer, rb->buffer + start, restlen); + rb->len = restlen; + rb->newline_cursor = 0; +} + +static struct string_view tokenise_greedy(struct string_view *line) { + sv_skip_whitespace(line); + return sv_tokenise_word(line); +} + +static bool parse_args( + struct string_view line, char **args, int num, bool longlast) { + memset(args, 0, num * sizeof(char*)); + + for (int i = 0; i < num - longlast; i++) { + struct string_view word = tokenise_greedy(&line); + if (!word.s) { + printf("Not enough arguments given (%d expected)\n", num); + for (int j = 0; j < i; j++) free(args[j]); + memset(args, 0, num * sizeof(char*)); + return false; + } + sv_copy(word, &args[i]); + } + + if (longlast) sv_copy(line, &args[num - 1]); + else if (!sv_is_empty(line)) { + printf("Too many arguments given (%d expected)\n", num); + for (int i = 0; i < num; i++) free(args[i]); + memset(args, 0, num * sizeof(char*)); + return false; + } + + return true; +} + +struct state { + char **rooms; + size_t num_rooms; +}; + +static void autocomplete_roomname(const struct state *state, char **namep) { + const char *const name = *namep; + const size_t namelen = strlen(name); + if (strlen(name) == 0) return; + + size_t found_index = (size_t)-1; + + for (size_t i = 0; i < state->num_rooms; i++) { + const size_t roomnamelen = strlen(state->rooms[i]); + for (size_t j = 0; j < roomnamelen && j < namelen; j++) { + if (tolower(state->rooms[i][j]) != tolower(name[j])) goto nope_next; + } + + if (found_index == (size_t)-1) found_index = i; + else return; // ambiguous completion + + nope_next: ; + } + + if (found_index == (size_t)-1) return; // no completion found + + // apply completion + free(*namep); + *namep = strdup(state->rooms[found_index]); +} + +static bool parse_i64(const char *str, int64_t *output) { + const size_t len = strlen(str); + if (len > 20) return false; // strlen(itoa(INT64_MIN)) == 20 + char buf[21]; + memcpy(buf, str, len); + buf[len] = '\0'; + char *endp; + *output = strtoll(buf, &endp, 10); + return endp == buf + len; +} + +static bool handle_line( + struct state *state, + struct tomsg_client *client, + struct string_view line +) { + const struct string_view command = tokenise_greedy(&line); + char *args[10]; + int num_args = 0; + + enum tomsg_retval ret = TOMSG_OK; + bool quit_requested = false; + + if (sv_equals(command, "register")) { + if (parse_args(line, args, num_args = 2, false)) { + ret = tomsg_register(client, args[0], args[1]); + } + + } else if (sv_equals(command, "login")) { + if (parse_args(line, args, num_args = 2, false)) { + ret = tomsg_login(client, args[0], args[1]); + } + + } else if (sv_equals(command, "logout")) { + ret = tomsg_logout(client); + + } else if (sv_equals(command, "ls") || sv_equals(command, "list_rooms")) { + ret = tomsg_list_rooms(client); + + } else if (sv_equals(command, "lsm") || sv_equals(command, "list_members")) { + if (parse_args(line, args, num_args = 1, false)) { + autocomplete_roomname(state, &args[0]); + ret = tomsg_list_members(client, args[0]); + } + + } else if (sv_equals(command, "create_room")) { + ret = tomsg_create_room(client); + + } else if (sv_equals(command, "invite")) { + if (parse_args(line, args, num_args = 2, false)) { + autocomplete_roomname(state, &args[0]); + ret = tomsg_invite(client, args[0], args[1]); + } + + } else if (sv_equals(command, "s") || sv_equals(command, "send")) { + if (parse_args(line, args, num_args = 2, true)) { + autocomplete_roomname(state, &args[0]); + ret = tomsg_send(client, args[0], args[1], NULL); + } + + } else if (sv_equals(command, "ping")) { + ret = tomsg_ping(client); + + } else if (sv_equals(command, "hist") || sv_equals(command, "history")) { + int64_t count; + if (parse_args(line, args, num_args = 2, false) && + parse_i64(args[1], &count)) { + autocomplete_roomname(state, &args[0]); + ret = tomsg_history(client, args[0], count, -1); + } + + } else if (sv_equals(command, "histb") || + sv_equals(command, "history_before")) { + int64_t count, msgid; + if (parse_args(line, args, num_args = 3, false) && + parse_i64(args[1], &count) && + parse_i64(args[2], &msgid)) { + autocomplete_roomname(state, &args[0]); + ret = tomsg_history(client, args[0], count, msgid); + } + + } else if (sv_equals(command, "on") || + sv_equals(command, "is_online")) { + if (parse_args(line, args, num_args = 1, false)) { + ret = tomsg_is_online(client, args[0]); + } + + } else if (sv_equals(command, "act") || + sv_equals(command, "user_active")) { + if (parse_args(line, args, num_args = 1, false)) { + const bool active = strchr("1yY", args[0][0]); + ret = tomsg_user_active(client, active); + } + + } else if (sv_equals(command, "quit") || sv_equals(command, "exit")) { + quit_requested = true; + + } else if (sv_equals(command, "help")) { + printf( + "Commands:\n" + " register \n" + " login \n" + " logout\n" + " ls/list_rooms\n" + " lsm/list_members \n" + " create_room\n" + " invite \n" + " s/send \n" + " ping\n" + " hist/history \n" + " histb/history_before \n" + " on/is_online \n" + " act/user_active \n" + " help\n" + " quit/exit\n" + "Room names are autocompleted.\n" + ); + + } else { + char *str; + sv_copy(command, &str); + printf("Error: unknown command '%s'\n", str); + free(str); + } + + for (int i = 0; i < num_args; i++) free(args[i]); + + if (ret != TOMSG_OK) { + printf("Error: %s\n", tomsg_strerror(ret)); + return true; + } else { + return quit_requested; + } +} + +static const char* event_type_descr(enum tomsg_event_type type) { + switch (type) { + case TOMSG_EV_REGISTER: return "register"; + case TOMSG_EV_LOGIN: return "login"; + case TOMSG_EV_LOGOUT: return "logout"; + case TOMSG_EV_LIST_ROOMS: return "list_rooms"; + case TOMSG_EV_LIST_MEMBERS: return "list_members"; + case TOMSG_EV_CREATE_ROOM: return "create_room"; + case TOMSG_EV_INVITE: return "invite"; + case TOMSG_EV_SEND: return "send"; + case TOMSG_EV_HISTORY: return "history"; + case TOMSG_EV_PING: return "ping"; + case TOMSG_EV_IS_ONLINE: return "is_online"; + case TOMSG_EV_USER_ACTIVE: return "user_active"; + case TOMSG_EV_PUSH_ONLINE: return "push_online"; + case TOMSG_EV_PUSH_MESSAGE: return "push_message"; + case TOMSG_EV_PUSH_INVITE: return "push_invite"; + case TOMSG_EV_PUSH_JOIN: return "push_join"; + } + + return "?unknown type?"; +} + +static void print_history_message(const struct history_message msg) { + printf("%" PRIi64 " \x1B[90m%" PRIi64 "\x1B[0m <%s> %s\n", + msg.msgid, msg.timestamp, msg.username, msg.message); +} + +static void handle_event(struct state *state, const struct tomsg_event event) { + printf("Event(%s):\n", event_type_descr(event.type)); + if (event.error) { + printf(" Error: %s\n", event.error); + return; + } + + switch (event.type) { + case TOMSG_EV_REGISTER: + printf(" Successfully registered as %s\n", event.login.username); + break; + + case TOMSG_EV_LOGIN: + printf(" Successfully logged in as %s\n", event.login.username); + break; + + case TOMSG_EV_LOGOUT: + printf(" Successfully logged out\n"); + break; + + case TOMSG_EV_LIST_ROOMS: + for (int64_t i = 0; i < event.list_rooms.count; i++) { + printf(" - %s\n", event.list_rooms.rooms[i]); + } + + for (size_t i = 0; i < state->num_rooms; i++) free(state->rooms[i]); + if (event.list_rooms.count != (int64_t)state->num_rooms) { + state->num_rooms = event.list_rooms.count; + state->rooms = realloc( + state->rooms, state->num_rooms * sizeof(char*)); + } + + // Move the strings to state->rooms + for (size_t i = 0; i < state->num_rooms; i++) { + state->rooms[i] = event.list_rooms.rooms[i]; + event.list_rooms.rooms[i] = NULL; + } + + break; + + case TOMSG_EV_LIST_MEMBERS: + printf(" In room %s:\n", event.list_members.room_name); + for (int64_t i = 0; i < event.list_members.count; i++) { + printf(" - %s\n", event.list_members.members[i]); + } + break; + + case TOMSG_EV_CREATE_ROOM: + printf(" Created room %s\n", event.create_room.room_name); + break; + + case TOMSG_EV_INVITE: + printf(" Invited %s to %s\n", + event.join.username, event.join.room_name); + break; + + case TOMSG_EV_SEND: + printf(" Sent message %" PRIi64 "\n", event.send.msgid); + break; + + case TOMSG_EV_HISTORY: + printf(" Some history for %s:\n", event.history.room_name); + for (int64_t i = 0; i < event.history.count; i++) { + printf(" "); + print_history_message(event.history.messages[i]); + } + break; + + case TOMSG_EV_PING: + printf(" Pong\n"); + break; + + case TOMSG_EV_IS_ONLINE: + case TOMSG_EV_PUSH_ONLINE: + printf(" %s is ", event.is_online.username); + if (event.type == TOMSG_EV_PUSH_ONLINE) printf("now "); + if (event.is_online.online_count == 1) { + printf("online once\n"); + } else if (event.is_online.online_count > 1) { + printf("online %" PRIi64 " times\n", event.is_online.online_count); + } else { + printf("offline\n"); + } + break; + + case TOMSG_EV_USER_ACTIVE: + printf(" Marked %s\n", + event.user_active.active ? "active" : "inactive"); + break; + + case TOMSG_EV_PUSH_MESSAGE: + printf("[%s] ", event.push_message.room_name); + print_history_message(event.push_message.message); + break; + + case TOMSG_EV_PUSH_INVITE: + printf(" You've been invited to %s by %s\n", + event.push_invite.room_name, event.push_invite.inviter); + break; + + case TOMSG_EV_PUSH_JOIN: + printf(" %s has joined %s\n", + event.join.username, event.join.room_name); + break; + } +} + +int main(int argc, char **argv) { + if (argc != 2) { + printf("Usage: %s \n", argv[0]); + return 1; + } + + char *hostname = NULL; + int port; + if (!parse_address(argv[1], &hostname, &port)) { + printf("Cannot parse hostname:port from '%s'\n", argv[1]); + return 1; + } + + printf("Connecting...\n"); + + struct tomsg_client *client; + enum tomsg_retval ret = tomsg_connect(hostname, port, &client); + if (ret != TOMSG_OK) { + printf("Could not connect: %s\n", tomsg_strerror(ret)); + return 1; + } + + printf("Connected.\n"); + + struct readbuffer stdinbuf = readbuffer_new(); + + struct state state = (struct state){ + .rooms = NULL, + .num_rooms = 0, + }; + + struct pollfd pollfds[2]; + pollfds[0].fd = STDIN_FILENO; + pollfds[0].events = POLLIN; + pollfds[1].fd = tomsg_poll_fd(client); + pollfds[1].events = POLLIN; + + while (true) { + int ret = poll(pollfds, 2, -1); + if (ret < 0) { + perror("poll"); + tomsg_close(client); + return 1; + } + if (ret == 0) continue; + + if (pollfds[0].revents & (POLLIN | POLLHUP | POLLERR)) { + if (read_more_data(STDIN_FILENO, &stdinbuf)) break; + const struct string_view sv = extract_line(&stdinbuf); + + if (sv.s != NULL) { + if (handle_line(&state, client, sv)) break; + splice_scanned(&stdinbuf); + } + } + + if (pollfds[1].revents & (POLLIN | POLLHUP | POLLERR)) { + struct tomsg_event event; + // Get all events currently available + while (true) { + ret = tomsg_next_event(client, &event); + if (ret == TOMSG_ERR_AGAIN) break; + if (ret == TOMSG_OK) { + handle_event(&state, event); + tomsg_event_nullify(event); + } else { + printf("Tomsg error: %s\n", tomsg_strerror(ret)); + break; + } + } + } + } + + tomsg_close(client); +} -- cgit v1.2.3-54-g00ecf