From 8db1456fb8f6c645131a449e84b95a507598c2cb Mon Sep 17 00:00:00 2001 From: Benedict Gaster <benedict.gaster@uwe.ac.uk> Date: Thu, 22 Feb 2024 16:49:59 +0000 Subject: [PATCH] setup for worksheet 2 part 2 --- .gitignore | 3 + Makefile | 54 ++++++ README.md | 92 +--------- chat_client.cpp | 268 +++++++++++++++++++++++++++ chat_server.cpp | 469 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 795 insertions(+), 91 deletions(-) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 chat_client.cpp create mode 100644 chat_server.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d513fe4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.o +chat_client +chat_server \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b10d763 --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +# Simple Makefile for IoT socket library and examples + +CC = clang++ +AR = ar +LD = clang++ +CPPFLAGS = -std=c++17 -I./ -I/opt/iot/include -D__DEBUG__=1 + +LDFLAGS = -lpthread -lncurses -L/opt/iot/lib -liot + +ROOTDIR = ./ + +CP = cp +ECHO = echo + +BUILD_DIR = . + +CPP_SOURCES_CLIENT = ./chat_client.cpp +CPP_SOURCES_SERVER = ./chat_server.cpp + +CPP_HEADERS = +C_SOURCES = + +APP = chat_client +SERVER = chat_server + +OBJECTS_CLIENT = $(addprefix $(BUILD_DIR)/,$(notdir $(CPP_SOURCES_CLIENT:.cpp=.o))) +OBJECTS_SERVER = $(addprefix $(BUILD_DIR)/,$(notdir $(CPP_SOURCES_SERVER:.cpp=.o))) + +vpath %.cpp $(sort $(dir $(CPP_SOURCES_CLIENT))) +vpath %.cpp $(sort $(dir $(CPP_SOURCES_SERVER))) +vpath %.cpp ./ + +# OBJECTS += $(addprefix $(BUILD_DIR)/,$(notdir $(C_SOURCES:.c=.o))) +# vpath %.c $(sort $(dir $(C_SOURCES))) + +$(BUILD_DIR)/%.o: %.cpp $(CPP_HEADERS) Makefile | $(BUILD_DIR) + $(ECHO) compiling $< + $(CC) -c $(CPPFLAGS) $< -o $@ + +$(BUILD_DIR)/%.o: %.c Makefile | $(BUILD_DIR) + $(ECHO) compiling $< + clang -c $(CFLAGS) $< -o $@ + +all: $(BUILD_DIR)/$(APP) $(BUILD_DIR)/$(SERVER) + +$(BUILD_DIR)/$(APP): $(OBJECTS_CLIENT) Makefile + $(ECHO) linking $< + $(CC) -o $@ $(OBJECTS_CLIENT) $(LDFLAGS) + $(ECHO) successs + +$(BUILD_DIR)/$(SERVER): $(OBJECTS_SERVER) Makefile + $(ECHO) linking $< + $(CC) -o $@ $(OBJECTS_SERVER) $(LDFLAGS) + $(ECHO) successs \ No newline at end of file diff --git a/README.md b/README.md index c9f1093..5f9aa58 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,3 @@ # iot_ws2_part2 - - -## Getting started - -To make it easy for you to get started with GitLab, here's a list of recommended next steps. - -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! - -## Add your files - -- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: - -``` -cd existing_repo -git remote add origin https://gitlab.uwe.ac.uk/br-gaster/iot_ws2_part2.git -git branch -M main -git push -uf origin main -``` - -## Integrate with your tools - -- [ ] [Set up project integrations](https://gitlab.uwe.ac.uk/br-gaster/iot_ws2_part2/-/settings/integrations) - -## Collaborate with your team - -- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) - -## Test and Deploy - -Use the built-in continuous integration in GitLab. - -- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) - -*** - -# Editing this README - -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template. - -## Suggestions for a good README - -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. - -## Name -Choose a self-explaining name for your project. - -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. - -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. - -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. - -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. - -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. - -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. - -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. - -## Contributing -State if you are open to contributions and what your requirements are for accepting them. - -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. - -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. - -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. - -## License -For open source projects, say how it is licensed. - -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. +Your words go here... \ No newline at end of file diff --git a/chat_client.cpp b/chat_client.cpp new file mode 100644 index 0000000..33b09e5 --- /dev/null +++ b/chat_client.cpp @@ -0,0 +1,268 @@ + +#include <arpa/inet.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> +#include <cstdlib> + +#include <atomic> +#include <iostream> + +// IOT socket api +#include <iot/socket.hpp> + +#include <chat.hpp> +#include <gui.hpp> +#include <colors.hpp> +#include <util.hpp> + +namespace { +std::atomic<bool> sent_leave{false}; +}; + +//--------------------------------------------------------------------------------------- + +/** + * @brief Convert a string command from the UI into a chat command. + * NOTE: It is only a subset of all command types. + * + * @param cmd command to convert + * @return command type ID representing the passed in command +*/ +chat::chat_type to_type(std::string cmd) { + switch(string_to_int(cmd.c_str())) { + // case string_to_int("join"): return chat::JOIN; + // case string_to_int("bc"): return chat::BROADCAST; + // case string_to_int("dm"): return chat::DIRECTMESSAGE; + case string_to_int("list"): return chat::LIST; + case string_to_int("leave"): return chat::LEAVE; + case string_to_int("exit"): return chat::EXIT; + default: + return chat::UNKNOWN; + } + + return chat::UNKNOWN; // unknowntype +} + +//---------------------------------------------------------------------------------------- + +std::pair<std::thread, Channel<chat::chat_message>> make_receiver(uwe::socket* sock) { + auto [tx, rx] = make_channel<chat::chat_message>(); + + std::thread receiver_thread{[](Channel<chat::chat_message> tx, uwe::socket* sock) { + try { + for (;;) { + chat::chat_message msg; + + // you need to fill in + // receive message from server + // send it over channel (tx) to main UI thread + + // exit receiver thread + if (msg.type_ == chat::EXIT || (msg.type_ == chat::LACK && sent_leave)) { + break; + } + } + } + catch(...) { + DEBUG("caught exception\n"); + }; + }, std::move(tx), sock}; + + return {std::move(receiver_thread), std::move(rx)}; +} + +int main(int argc, char ** argv) { + if (argc != 4) { + printf("USAGE: %s <ipaddress> <port> <username>\n", argv[0]); + exit(0); + } + + std::string username{argv[3]}; + // Set client IP address + uwe::set_ipaddr(argv[1]); + + const char* server_name = "192.168.1.8"; + + const int server_port = SERVER_PORT; + + sockaddr_in server_address; + memset(&server_address, 0, sizeof(server_address)); + server_address.sin_family = AF_INET; + + // creates binary representation of server name and stores it as sin_addr + inet_pton(AF_INET, server_name, &server_address.sin_addr); + + // htons: port in network order format + server_address.sin_port = htons(server_port); + + // open socket + uwe::socket sock{AF_INET, SOCK_DGRAM, 0}; + + // port for client + const int client_port = std::atoi(argv[2]); + + // socket address used for the client + struct sockaddr_in client_address; + memset(&client_address, 0, sizeof(client_address)); + client_address.sin_family = AF_INET; + client_address.sin_port = htons(client_port); + inet_pton(AF_INET, uwe::get_ipaddr().c_str(), &client_address.sin_addr); + + sock.bind((struct sockaddr *)&client_address, sizeof(client_address)); + + chat::chat_message msg = chat::join_msg(username); + + // send data + int len = sock.sendto( + reinterpret_cast<const char*>(&msg), sizeof(chat::chat_message), 0, + (sockaddr*)&server_address, sizeof(server_address)); + + DEBUG("Join message (%s) sent, waiting for JACK\n", username.c_str()); + // wait for JACK + sock.recvfrom(reinterpret_cast<char*>(&msg), sizeof(chat::chat_message), 0, nullptr, nullptr); + + if (msg.type_ == chat::JACK) { + DEBUG("Received jack\n"); + + // create GUI thread and communication channels + auto [gui_thread, gui_tx, gui_rx] = chat::make_gui(); + auto [rec_thread, rec_rx] = make_receiver(&sock); + + // going to need recv thread for messages from server + + bool exit_loop = false; + for(;!exit_loop;) { + // check and see if any GUI messages to handle + if (!gui_rx.empty() && !sent_leave) { + auto result = gui_rx.recv(); + if (result) { + auto cmds = split(*result, ':'); + if (cmds.size() > 1) { + chat::chat_type type = to_type(cmds[0]); + switch(type) { + case chat::EXIT: { + DEBUG("Received Exit from GUI\n"); + // you need to fill in + break; + } + case chat::LEAVE: { + DEBUG("Received LEAVE from GUI\n"); + // you need to fill in + break; + } + case chat::LIST: { + DEBUG("Received LIST from GUI\n"); + // you need to fill in + break; + } + default: { + // the default case is that the command is a username for DM + // <username> : message + if (cmds.size() == 2) { + DEBUG("Received message from GUI\n"); + } + break; + } + } + } + else { + // message to broadcast to everyone online + chat::chat_message msg = chat::broadcast_msg(username, *result); + // send data + int len = sock.sendto( + reinterpret_cast<const char*>(&msg), sizeof(chat::chat_message), 0, + (sockaddr*)&server_address, sizeof(server_address)); + } + } + } + //check to see if any messages received from the server + if (!rec_rx.empty() && !exit_loop) { + auto result = rec_rx.recv(); + if (result) { + switch ((*result).type_) { + case chat::LEAVE: { + chat::display_command cmd{chat::GUI_USER_REMOVE}; + cmd.text_ = std::string{(char*)(*result).username_}; + gui_tx.send(cmd); + break; + } + case chat::EXIT: { + DEBUG("Received EXIT\n"); + exit_loop = true; + break; + } + case chat::LACK: { + DEBUG("Received LACK\n"); + if (sent_leave) { + exit_loop = true; + break; + } + } + case chat::BROADCAST: { + std::string msg{(char*)(*result).username_}; + msg.append(": "); + msg.append((char*)(*result).message_); + chat::display_command cmd{chat::GUI_CONSOLE, msg}; + gui_tx.send(cmd); + break; + } + case chat::DIRECTMESSAGE: { + std::string msg{"dm("}; + msg.append((char*)(*result).username_); + msg.append("): "); + msg.append((char*)(*result).message_); + chat::display_command cmd{chat::GUI_CONSOLE, msg}; + gui_tx.send(cmd); + break; + } + case chat::LIST: { + bool end = false; + auto users = split(std::string{(char*)(*result).username_}, ':'); + for (auto u: users) { + if (u.compare("END") == 0) { + end = true; + break; + } + chat::display_command cmd{chat::GUI_USER_ADD, u}; + gui_tx.send(cmd); + } + + if (!end) { + auto users = split(std::string{(char*)(*result).message_}, ':'); + for (auto u: users) { + if (u.compare("END") == 0) { + break; + } + } + } + + break; + } + case chat::ERROR: { + break; + } + default: { + + } + } + } + } + } + + DEBUG("Exited loop\n"); + // send message to GUI to exit + chat::display_command cmd{chat::GUI_EXIT}; + gui_tx.send(cmd); + gui_thread.join(); + rec_thread.join(); + + // so done... + DEBUG("Time to rest\n"); + } + else { + DEBUG("Received invalid jack\n"); + } + + return 0; +} \ No newline at end of file diff --git a/chat_server.cpp b/chat_server.cpp new file mode 100644 index 0000000..4f7dc28 --- /dev/null +++ b/chat_server.cpp @@ -0,0 +1,469 @@ +#include <map> +// IOT socket api +#include <iot/socket.hpp> + +#include <arpa/inet.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> + +#include <chat.hpp> + +#define USER_ALL "__ALL" +#define USER_END "END" + +/** + * @brief map of current online clients +*/ +typedef std::map<std::string, sockaddr_in *> online_users; + +void handle_list( + online_users& online_users, std::string username, std::string, + struct sockaddr_in& client_address, uwe::socket& sock, bool& exit_loop); + +/** + * @brief Send a given message to all clients + * + * @param msg to send + * @param username used if not to send to that particular user + * @param online_users current online users + * @param sock socket for communicting with client + * @param send_to_username determines also to send to username +*/ +void send_all( + chat::chat_message& msg, std::string username, online_users& online_users, + uwe::socket& sock, bool send_to_username = true) { + for (const auto user: online_users) { + if ((send_to_username && user.first.compare(username) == 0) || user.first.compare(username) != 0) { + int len = sock.sendto( + reinterpret_cast<const char*>(&msg), sizeof(chat::chat_message), 0, + (sockaddr*)user.second, sizeof(struct sockaddr_in)); + } + } +} + +/** + * @brief handle sending an error and incoming error messages + * + * Note: there should not be any incoming errors messages! + * + * @param err code for error + * @param client_address address of client to send message to + * @param sock socket for communicting with client + * @parm exit_loop set to true if event loop is to terminate +*/ +void handle_error(uint16_t err, struct sockaddr_in& client_address, uwe::socket& sock, bool& exit_loop) { + auto msg = chat::error_msg(err); + int len = sock.sendto( + reinterpret_cast<const char*>(&msg), sizeof(chat::chat_message), 0, + (sockaddr*)&client_address, sizeof(struct sockaddr_in)); +} + +/** + * @brief handle broadcast message + * + * @param online_users map of usernames to their corresponding IP:PORT address + * @param username part of chat protocol packet + * @param msg part of chat protocol packet + * @param client_address address of client to send message to + * @param sock socket for communicting with client + * @parm exit_loop set to true if event loop is to terminate +*/ +void handle_broadcast( + online_users& online_users, std::string username, std::string msg, + struct sockaddr_in& client_address, uwe::socket& sock, bool& exit_loop) { + DEBUG("Received broadcast\n"); + + // send message to all users, except the one we received it from + for (const auto user: online_users) { + DEBUG("username %s\n", user.first.c_str()); + if (strcmp(inet_ntoa(client_address.sin_addr), inet_ntoa(user.second->sin_addr)) == 0 && + client_address.sin_port != user.second->sin_port) { + // send BC + auto m = chat::broadcast_msg(username, msg); + int len = sock.sendto( + reinterpret_cast<const char*>(&m), sizeof(chat::chat_message), 0, + (sockaddr*)user.second, sizeof(struct sockaddr_in)); + } + else { + DEBUG("Not sending message to self: %s\n", msg.c_str()); + } + } +} + +/** + * @brief handle join messageß + * + * @param online_users map of usernames to their corresponding IP:PORT address + * @param username part of chat protocol packet + * @param msg part of chat protocol packet + * @param client_address address of client to send message to + * @param sock socket for communicting with client + * @parm exit_loop set to true if event loop is to terminate +*/ +void handle_join( + online_users& online_users, std::string username, std::string, + struct sockaddr_in& client_address, uwe::socket& sock, bool& exit_loop) { + DEBUG("Received join\n"); + + // first check user not already online + if (auto search = online_users.find(username); search != online_users.end()) { + handle_error(ERR_USER_ALREADY_ONLINE, client_address, sock, exit_loop); + } + else { + // add new user to known user map + + // send back JACK message to client that has joined + + // send broadcast (use handle broadcast) to all other clients + + // send list message (use handle_list) set 2nd argument to "__ALL" + + } +} + +/** + * @brief handle jack message + * + * @param online_users map of usernames to their corresponding IP:PORT address + * @param username part of chat protocol packet + * @param msg part of chat protocol packet + * @param client_address address of client to send message to + * @param sock socket for communicting with client + * @parm exit_loop set to true if event loop is to terminate +*/ +void handle_jack( + online_users& online_users, std::string username, std::string, + struct sockaddr_in& client_address, uwe::socket& sock, bool& exit_loop) { + DEBUG("Received jack\n"); + handle_error(ERR_UNEXPECTED_MSG, client_address, sock, exit_loop); +} + +/** + * @brief handle direct message + * + * @param online_users map of usernames to their corresponding IP:PORT address + * @param username part of chat protocol packet + * @param msg part of chat protocol packet + * @param client_address address of client to send message to + * @param sock socket for communicting with client + * @parm exit_loop set to true if event loop is to terminate +*/ +void handle_directmessage( + online_users& online_users, std::string username, std::string message, + struct sockaddr_in& client_address, uwe::socket& sock, bool& exit_loop) { + DEBUG("Received directmessage to %s\n", username.c_str()); + + // handle sending direct message + if (auto search = online_users.find(username); search != online_users.end()) { + DEBUG("found user dm\n"); + for (const auto user: online_users) { + // remember you need to translate the client IO address with inet_ntoa, + // and also the user you are comparing + + // check if user matches the incoming client address, using strcmp, and + // and if so send message with chat::dm_msg() + } + } + else { + // TODO: error message + } +} + +/** + * @brief handle list message + * + * @param online_users map of usernames to their corresponding IP:PORT address + * @param username part of chat protocol packet + * @param msg part of chat protocol packet + * @param client_address address of client to send message to + * @param sock socket for communicting with client + * @parm exit_loop set to true if event loop is to terminate +*/ +void handle_list( + online_users& online_users, std::string username, std::string, + struct sockaddr_in& client_address, uwe::socket& sock, bool& exit_loop) { + DEBUG("Received list\n"); + + int username_size = MAX_USERNAME_LENGTH; + int message_size = MAX_MESSAGE_LENGTH; + + char username_data[MAX_USERNAME_LENGTH] = { '\0' }; + char * username_ptr = &username_data[0]; + char message_data[MAX_MESSAGE_LENGTH] = { '\0' }; + char * message_ptr = &message_data[0]; + + bool using_username = true; + bool full = false; + + for (const auto user: online_users) { + if (using_username) { + if (username_size - (user.first.length()+1) >= 0) { + memcpy(username_ptr, user.first.c_str(), user.first.length()); + *(username_ptr+user.first.length()) = ':'; + username_ptr = username_ptr+user.first.length()+1; + username_size = username_size - (user.first.length()+1); + username_data[MAX_USERNAME_LENGTH - username_size] = '\0'; + } + else { + using_username = false; + } + } + + // otherwise we fill the message field + if(!using_username) { + if (message_size - (user.first.length()+1) >= 0) { + memcpy(message_ptr, user.first.c_str(), user.first.length()); + *(message_ptr+user.first.length()) = ':'; + message_ptr = message_ptr+user.first.length()+1; + message_size = message_size - (user.first.length()+1); + } + else { + // we are full and we need to send packet and start again + chat::chat_message msg{chat::LIST, '\0', '\0'}; + username_data[MAX_USERNAME_LENGTH - username_size] = '\0'; + memcpy(msg.username_, &username_data[0], MAX_USERNAME_LENGTH - username_size ); + message_data[MAX_MESSAGE_LENGTH - message_size] = '\0'; + memcpy(msg.message_, &message_data[0], MAX_MESSAGE_LENGTH - message_size ); + + // + if (username.compare("__ALL") == 0) { + send_all(msg, "__ALL", online_users, sock); + } + else { + int len = sock.sendto( + reinterpret_cast<const char*>(&msg), sizeof(chat::chat_message), 0, + (sockaddr*)&client_address, sizeof(struct sockaddr_in)); + } + + username_size = MAX_USERNAME_LENGTH; + message_size = MAX_MESSAGE_LENGTH; + + username_ptr = &username_data[0]; + message_ptr = &message_data[0]; + + using_username = false; + } + } + } + + if (using_username) { + if (username_size >= 4) { + // enough space to store end in username + memcpy(&username_data[MAX_USERNAME_LENGTH - username_size], USER_END, strlen(USER_END) ); + username_size = username_size - (strlen(USER_END)+1); + } + else { + username_size = username_size + 1; // this enables overwriting the last ':' + using_username = false; + } + } + + if (!using_username) { + + } + + chat::chat_message msg{chat::LIST, '\0', '\0'}; + username_data[MAX_USERNAME_LENGTH - username_size] = '\0'; + DEBUG("username_data = %s\n", username_data); + memcpy(msg.username_, &username_data[0], MAX_USERNAME_LENGTH - username_size ); + message_data[MAX_MESSAGE_LENGTH - message_size] = '\0'; + memcpy(msg.message_, &message_data[0], MAX_MESSAGE_LENGTH - message_size ); + + if (username.compare("__ALL") == 0) { + send_all(msg, "__ALL", online_users, sock); + } + else { + int len = sock.sendto( + reinterpret_cast<const char*>(&msg), sizeof(chat::chat_message), 0, + (sockaddr*)&client_address, sizeof(struct sockaddr_in)); + } +} + +/** + * @brief handle leave message + * + * @param online_users map of usernames to their corresponding IP:PORT address + * @param username part of chat protocol packet + * @param msg part of chat protocol packet + * @param client_address address of client to send message to + * @param sock socket for communicting with client + * @parm exit_loop set to true if event loop is to terminate +*/ +void handle_leave( + online_users& online_users, std::string username, std::string, + struct sockaddr_in& client_address, uwe::socket& sock, bool& exit_loop) { + DEBUG("Received leave\n"); + + username = ""; + // find username + for (const auto user: online_users) { + if (strcmp(inet_ntoa(client_address.sin_addr), inet_ntoa(user.second->sin_addr)) == 0 && + client_address.sin_port == user.second->sin_port) { + username = user.first; + } + } + DEBUG("%s is leaving the sever\n", username.c_str()); + + if (username.length() == 0) { + // this should never happen + handle_error(ERR_UNKNOWN_USERNAME, client_address, sock, exit_loop); + } + else if (auto search = online_users.find(username); search != online_users.end()) { + // first free memory for sockaddr + struct sockaddr_in * addr = search->second; + delete addr; + + // now delete from username map + online_users.erase(search); + + // finally send back LACK + auto msg = chat::lack_msg(); + int len = sock.sendto( + reinterpret_cast<const char*>(&msg), sizeof(chat::chat_message), 0, + (sockaddr*)&client_address, sizeof(struct sockaddr_in)); + + //handle_broadcast(online_users, username, "has left the server", client_address, sock, exit_loop); + msg = chat::chat_message{chat::LEAVE, '\0', '\0'}; + memcpy(msg.username_, username.c_str(), username.length()+1); + send_all(msg, username, online_users, sock, false); + } + else { + handle_error(ERR_UNKNOWN_USERNAME, client_address, sock, exit_loop); + } +} + +/** + * @brief handle lack message + * + * @param online_users map of usernames to their corresponding IP:PORT address + * @param username part of chat protocol packet + * @param msg part of chat protocol packet + * @param client_address address of client to send message to + * @param sock socket for communicting with client + * @parm exit_loop set to true if event loop is to terminate +*/ +void handle_lack( + online_users& online_users, std::string username, std::string, + struct sockaddr_in& client_address, uwe::socket& sock, bool& exit_loop) { + DEBUG("Received lack\n"); + handle_error(ERR_UNEXPECTED_MSG, client_address, sock, exit_loop); +} + +/** + * @brief handle exit message + * + * @param online_users map of usernames to their corresponding IP:PORT address + * @param username part of chat protocol packet + * @param msg part of chat protocol packet + * @param client_address address of client to send message to + * @param sock socket for communicting with client + * @parm exit_loop set to true if event loop is to terminate +*/ +void handle_exit( + online_users& online_users, std::string username, std::string, + struct sockaddr_in& client_address, uwe::socket& sock, bool& exit_loop) { + + DEBUG("Received exit\n"); + + // send exit message (chat::exit_msg()) to each user, and clear up memory for them + + + // leave this code as it is required for exiting + exit_loop = true; +} + +/** + * @brief + * + * @param online_users map of usernames to their corresponding IP:PORT address + * @param username part of chat protocol packet + * @param msg part of chat protocol packet + * @param client_address address of client to send message to + * @param sock socket for communicting with client + * @parm exit_loop set to true if event loop is to terminate +*/ +void handle_error( + online_users& online_users, std::string username, std::string, + struct sockaddr_in& client_address, uwe::socket& sock, bool& exit_loop) { + DEBUG("Received error\n"); +} + +/** + * @brief function table, mapping command type to handler. +*/ +void (*handle_messages[9])(online_users&, std::string, std::string, struct sockaddr_in&, uwe::socket&, bool& exit_loop) = { + handle_join, handle_jack, handle_broadcast, handle_directmessage, + handle_list, handle_leave, handle_lack, handle_exit, handle_error +}; + +/** + * @brief server for chat protocol +*/ +void server() { + // keep track of online users + online_users online_users; + + // port to start the server on + + // socket address used for the server + struct sockaddr_in server_address; + memset(&server_address, 0, sizeof(server_address)); + server_address.sin_family = AF_INET; + + // htons: host to network short: transforms a value in host byte + // ordering format to a short value in network byte ordering format + server_address.sin_port = htons(SERVER_PORT); + + // htons: host to network long: same as htons but to long + // server_address.sin_addr.s_addr = htonl(INADDR_ANY); + // creates binary representation of server name and stores it as sin_addr + inet_pton(AF_INET, uwe::get_ipaddr().c_str(), &server_address.sin_addr); + + // create a UDP socket + uwe::socket sock{AF_INET, SOCK_DGRAM, 0}; + + sock.bind((struct sockaddr *)&server_address, sizeof(server_address)); + + // socket address used to store client address + struct sockaddr_in client_address; + size_t client_address_len = 0; + + char buffer[sizeof(chat::chat_message)]; + DEBUG("Entering server loop\n"); + bool exit_loop = false; + for (;!exit_loop;) { + int len = sock.recvfrom( + buffer, sizeof(buffer), 0, (struct sockaddr *)&client_address, &client_address_len); + + + // DEBUG("Received message:\n"); + if (len == sizeof(chat::chat_message)) { + // handle incoming packet + chat::chat_message * message = reinterpret_cast<chat::chat_message*>(buffer); + auto type = static_cast<chat::chat_type>(message->type_); + std::string username{(const char*)&message->username_[0]}; + std::string msg{(const char*)&message->message_[0]}; + + if (is_valid_type(type)) { + DEBUG("handling msg type %d\n", type); + // valid type, so dispatch message handler + handle_messages[type](online_users, username, msg, client_address, sock, exit_loop); + } + } + else { + DEBUG("Unexpected packet length\n"); + } + } +} + +/** + * @brief entry point for chat server application +*/ +int main(void) { + // Set server IP address + uwe::set_ipaddr("192.168.1.8"); + server(); + + return 0; +} \ No newline at end of file -- GitLab