aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ssh/.gitignore1
-rw-r--r--ssh/Makefile5
-rw-r--r--ssh/client.c492
3 files changed, 497 insertions, 1 deletions
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 <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <ctype.h>
+#include <inttypes.h>
+#include <poll.h>
+#include <errno.h>
+#include <unistd.h>
+#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 <user> <pass>\n"
+ " login <user> <pass>\n"
+ " logout\n"
+ " ls/list_rooms\n"
+ " lsm/list_members <room>\n"
+ " create_room\n"
+ " invite <room> <user>\n"
+ " s/send <room> <message...>\n"
+ " ping\n"
+ " hist/history <room> <count>\n"
+ " histb/history_before <room> <count> <before_msgid>\n"
+ " on/is_online <user>\n"
+ " act/user_active <y/n>\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 <hostname:port>\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);
+}