diff options
| author | Nicolas James <Eele1Ephe7uZahRie@tutanota.com> | 2025-02-13 18:00:17 +1100 |
|---|---|---|
| committer | Nicolas James <Eele1Ephe7uZahRie@tutanota.com> | 2025-02-13 18:00:17 +1100 |
| commit | 98cef5e9a772602d42acfcf233838c760424db9a (patch) | |
| tree | 5277fa1d7cc0a69a0f166fcbf10fd320f345f049 /comp3331/server/src/client/client.cc | |
initial commit
Diffstat (limited to 'comp3331/server/src/client/client.cc')
| -rw-r--r-- | comp3331/server/src/client/client.cc | 519 |
1 files changed, 519 insertions, 0 deletions
diff --git a/comp3331/server/src/client/client.cc b/comp3331/server/src/client/client.cc new file mode 100644 index 0000000..9ec2ab2 --- /dev/null +++ b/comp3331/server/src/client/client.cc @@ -0,0 +1,519 @@ +#include "client/client.hh" + +namespace client { + +// Args passed to handle functions, the input line split by spacebars. Similar +// to argv, the first is the program name (or in this case command name). + +static std::string read_cin(const char* const msg) noexcept { + std::cout << msg << ": "; + std::string line; + std::getline(std::cin, line); + return line; +} + +static std::pair<std::string, std::string>& +get_address_port(const char* const address = nullptr, + const char* const port = nullptr) { + static std::pair<std::string, std::string> pair = [&]() { + return std::make_pair<std::string, std::string>(address, port); + }(); + return pair; +} + +static shared::contents_t urecv_contents(shared::connection& connection) { + const auto ret = [&]() { + for (;;) { + const auto ret = shared::urecv_packet(connection.get_socket()); + if (connection.should_discard_packet(ret->packet)) { + continue; + } + return ret; + } + }(); + const shared::contents_t contents = shared::packet_to_contents(ret->packet); + + // error checking early out + const auto find_it = contents.find("success"); + if (find_it != std::end(contents)) { + if (find_it->second == "error") { + throw std::runtime_error(contents.find("message")->second); + } + } + + return contents; +} + +static shared::socket_t get_new_rsock() { + const auto& addr_port = get_address_port(); + const auto info = [&]() { + addrinfo a{}; + a.ai_flags = AI_PASSIVE; + a.ai_family = AF_INET; + a.ai_socktype = SOCK_STREAM; + return shared::make_addrinfo(addr_port.first.c_str(), + addr_port.second.c_str(), std::move(a)); + }(); + const shared::socket_t socket = shared::make_socket(info); + shared::connect_socket(socket, info); + return socket; +} + +using args_t = std::vector<std::string>; +static void handle_create_thread(const args_t& args, + shared::connection& connection) { + if (args.size() != 2) { + throw std::invalid_argument("invalid syntax"); + } + + const std::string& thread_title = args[1]; + { // send request + const shared::contents_t contents{ + {"thread_title", thread_title}, + }; + connection.send_packet(shared::contents_to_packet(contents, "CRT")); + } + + const shared::contents_t contents = urecv_contents(connection); + + const auto message_it = contents.find("message"); + const auto success_it = contents.find("success"); + if (message_it == std::end(contents) || success_it == std::end(contents)) { + std::cout << "\treceived bad response from the server, try again\n"; + return; + } + + std::cout << message_it->second; +} + +static void handle_post_message(const args_t& args, + shared::connection& connection) { + if (args.size() < 3) { + throw std::invalid_argument("invalid syntax"); + } + + const std::string& thread_title = args[1]; + const std::string message = [&]() { + std::string message = args[2]; + for (auto i = 3ul; i < args.size(); ++i) { + message += ' ' + args[i]; + } + return message; + }(); + + connection.send_packet(shared::contents_to_packet( + {{"thread_title", thread_title}, {"message", message}}, "MSG")); + + const shared::contents_t contents = urecv_contents(connection); + const auto message_it = contents.find("message"); + if (message_it == std::end(contents)) { + std::cout << "\treceived bad response from the server, try again\n"; + return; + } + + std::cout << message_it->second; +} + +static void handle_delete_message(const args_t& args, + shared::connection& connection) { + if (args.size() != 3) { + throw std::invalid_argument("invalid syntax"); + } + + const std::string& thread_title = args[1]; + const std::string& message_number = args[2]; + + try { + std::stoi(message_number); + } catch (...) { + throw std::invalid_argument{"failed to parse \"" + message_number + + "\" as an integer"}; + } + + connection.send_packet(shared::contents_to_packet( + {{"thread_title", thread_title}, {"message_number", message_number}}, + "DLT")); + + const shared::contents_t contents = urecv_contents(connection); + const auto message_it = contents.find("message"); + if (message_it == std::end(contents)) { + std::cout << "\treceived bad response from the server, try again\n"; + return; + } + + std::cout << message_it->second; +} + +static void handle_edit_message(const args_t& args, + shared::connection& connection) { + if (args.size() < 4) { + throw std::invalid_argument("invalid syntax"); + } + + const std::string& thread_title = args[1]; + const std::string& message_number = args[2]; + const std::string message = [&]() { + std::string message = args[3]; + for (auto i = 4ul; i < args.size(); ++i) { + message += ' ' + args[i]; + } + return message; + }(); + + try { + std::stoi(message_number); + } catch (...) { + throw std::invalid_argument{"failed to parse \"" + message_number + + "\" as an integer"}; + } + + connection.send_packet( + shared::contents_to_packet({{"thread_title", thread_title}, + {"message_number", message_number}, + {"message", message}}, + "EDT")); + + const shared::contents_t contents = urecv_contents(connection); + const auto message_it = contents.find("message"); + if (message_it == std::end(contents)) { + std::cout << "\treceived bad response from the server, try again\n"; + return; + } + + std::cout << message_it->second; +} + +static void handle_list_threads(const args_t& args, + shared::connection& connection) { + if (args.size() != 1) { + throw std::invalid_argument("invalid syntax"); + } + + { connection.send_packet(shared::contents_to_packet({}, "LST")); } + + const shared::contents_t contents = urecv_contents(connection); + const auto message_it = contents.find("message"); + if (message_it == std::end(contents)) { + std::cout << "\treceived bad response from the server, try again\n"; + return; + } + + std::cout << message_it->second; +} + +static void handle_read_thread(const args_t& args, + shared::connection& connection) { + if (args.size() != 2) { + throw std::invalid_argument("invalid syntax"); + } + + const std::string& thread_title = args[1]; + { // send request + const shared::contents_t contents{ + {"thread_title", thread_title}, + }; + connection.send_packet(shared::contents_to_packet(contents, "RDT")); + } + + const shared::contents_t contents = urecv_contents(connection); + + const auto message_it = contents.find("message"); + const auto success_it = contents.find("success"); + if (message_it == std::end(contents) || success_it == std::end(contents)) { + std::cout << "\treceived bad response from the server, try again\n"; + return; + } + + std::cout << message_it->second; +} + +static void handle_upload_file(const args_t& args, + shared::connection& connection) { + if (args.size() != 3) { + throw std::invalid_argument("invalid syntax"); + } + + const std::string& thread_title = args[1]; + const std::string& filename = args[2]; + const std::string file_contents = [&]() -> std::string { + std::ifstream in{"./" + filename}; + if (!in.is_open()) { + throw std::invalid_argument("file not found"); + } + std::stringstream ss; + ss << in.rdbuf(); + return ss.str(); + }(); + + { + const shared::contents_t contents{{"thread_title", thread_title}, + {"filename", filename}}; + connection.send_packet(shared::contents_to_packet(contents, "UPD")); + } + + { + const shared::contents_t contents = urecv_contents(connection); + + const auto message_it = contents.find("message"); + const auto success_it = contents.find("success"); + if (message_it == std::end(contents) || + success_it == std::end(contents)) { + std::cout << "\treceived bad response from the server, try again\n"; + return; + } + std::cout << message_it->second; + if (success_it->second != "true") { + return; + } + } + + { // send file + const shared::socket_t rsock = get_new_rsock(); + shared::scoped_function close_rsock{[rsock]() { close(rsock); }}; + const shared::contents_t contents{{"file_contents", file_contents}}; + const shared::packet packet = + shared::contents_to_packet(contents, "UPD"); + shared::send_packet(packet, rsock); + } + + { + const shared::contents_t contents = urecv_contents(connection); + + const auto message_it = contents.find("message"); + const auto success_it = contents.find("success"); + if (message_it == std::end(contents) || + success_it == std::end(contents)) { + std::cout << "\treceived bad response from the server, try again\n"; + return; + } + std::cout << message_it->second; + } +} + +static void handle_download_file(const args_t& args, + shared::connection& connection) { + if (args.size() != 3) { + throw std::invalid_argument("invalid syntax"); + } + + const std::string& thread_title = args[1]; + const std::string& filename = args[2]; + { + const shared::contents_t contents{{"thread_title", thread_title}, + {"filename", filename}}; + connection.send_packet(shared::contents_to_packet(contents, "DWN")); + } + + { + const shared::contents_t contents = urecv_contents(connection); + + const auto message_it = contents.find("message"); + const auto success_it = contents.find("success"); + if (message_it == std::end(contents) || + success_it == std::end(contents)) { + std::cout << "\treceived bad response from the server, try again\n"; + return; + } + std::cout << message_it->second; + if (success_it->second != "true") { + return; + } + } + + const auto file_contents = []() { + const shared::socket_t rsock = get_new_rsock(); + shared::scoped_function close_rsock{[rsock]() { close(rsock); }}; + const auto packet = shared::rrecv_packet(rsock); + return packet_to_contents(*packet); + }(); + + const auto data_it = file_contents.find("file_contents"); + const auto message_it = file_contents.find("message"); + if (data_it == std::end(file_contents) || + message_it == std::end(file_contents)) { + return; + } + + std::ofstream out{filename, std::ios_base::trunc}; + if (!out.is_open()) { + throw std::runtime_error{"failed to write file " + filename}; + } + out << data_it->second; + std::cout << message_it->second; +} + +static void handle_remove_thread(const args_t& args, + shared::connection& connection) { + if (args.size() != 2) { + throw std::invalid_argument("invalid syntax"); + } + + const auto& thread_title = args[1]; + { + const shared::contents_t contents{ + {"thread_title", thread_title}, + }; + connection.send_packet(shared::contents_to_packet(contents, "RMV")); + } + + const shared::contents_t contents = urecv_contents(connection); + + const auto message_it = contents.find("message"); + const auto success_it = contents.find("success"); + if (message_it == std::end(contents) || success_it == std::end(contents)) { + std::cout << "\treceived bad response from the server, try again\n"; + return; + } + + std::cout << message_it->second; +} + +static void authenticate(shared::connection& connection) { + const auto send_auth_var = [&](const char* const varname) { + for (;;) { // send username + if (shared::should_exit) { + throw shared::should_exit_exception{}; + } + + const std::string var = read_cin(varname); + if (var.length() == 0) { + continue; + } + + { // request server for status on username + const shared::contents_t contents{{varname, var}}; + connection.send_packet( + shared::contents_to_packet(contents, "ATH")); + } + + const shared::contents_t contents = urecv_contents(connection); + const auto success_it = contents.find("success"); + const auto message_it = contents.find("message"); + if (message_it == std::end(contents) || + success_it == std::end(contents)) { + std::cout + << "\treceived bad response from the server, try again\n"; + continue; + } + std::cout << message_it->second; + + if (success_it->second == "true") { + break; + } + } + }; + + send_auth_var("username"); + send_auth_var("password"); +} + +static void interact(shared::connection& connection) { + static const std::unordered_map<std::string, + decltype(&handle_create_thread)> + commands{{"CRT", handle_create_thread}, {"MSG", handle_post_message}, + {"DLT", handle_delete_message}, {"EDT", handle_edit_message}, + {"LST", handle_list_threads}, {"RDT", handle_read_thread}, + {"UPD", handle_upload_file}, {"DWN", handle_download_file}, + {"RMV", handle_remove_thread}}; + const auto print_commands = [&]() { + std::cout << "\tavailable commands: "; + for (const auto& pair : commands) { + const auto& key = pair.first; + std::cout << key << ' '; + } + std::cout << "(or \'XIT\' to quit)\n"; + }; + + print_commands(); + for (;;) { + if (shared::should_exit) { + throw shared::should_exit_exception{}; + } + const std::string input = read_cin("command"); + if (input.length() <= 0) { + continue; + } + + const std::vector<std::string> args = [&]() { // string split args + std::vector<std::string> ret; + + std::stringstream ss{std::move(input)}; + std::string arg; + while (std::getline(ss, arg, ' ')) { + if (arg.size() <= 0) { + continue; + } + ret.emplace_back(std::move(arg)); + } + + return ret; + }(); + + if (args.size() <= 0) { + continue; + } + + const std::string command = [&]() { + std::string command; + std::transform(std::begin(args[0]), std::end(args[0]), + std::back_inserter(command), + [](const char c) { return toupper(c); }); + return command; + }(); + + if (command == "XIT") { + break; + } + + const auto find_it = commands.find(command); + if (find_it == std::end(commands)) { // unknown command, print commands + print_commands(); + continue; + } + + try { + const auto& func = find_it->second; + func(args, connection); + } catch (const std::invalid_argument& e) { + std::cout << "\tbad arguments: " << e.what() << '\n'; + } catch (const std::runtime_error& e) { + std::cout << "\truntime error: " << e.what() << '\n'; + } + } +} + +void do_client(const char* const address, const char* const port) { + const shared::socket_t usock = [&]() -> shared::socket_t { + get_address_port(address, port); + const auto info = [&]() { // no designated initialisers :( + addrinfo a{}; + a.ai_flags = AI_PASSIVE; + a.ai_family = AF_INET; + a.ai_socktype = SOCK_DGRAM; + return shared::make_addrinfo("0.0.0.0", 0, std::move(a)); + }(); + const shared::socket_t socket = shared::make_socket(info); + shared::bind_socket(socket, info); + return socket; + }(); + + shared::connection connection = [&]() { + sockaddr_in dest{}; + dest.sin_family = AF_INET; + dest.sin_port = htons(static_cast<std::uint16_t>(std::stoi(port))); + inet_aton(address, &dest.sin_addr); + return shared::connection{usock, std::move(dest)}; + }(); + + try { + authenticate(connection); + interact(connection); + } catch (const shared::should_exit_exception&) { + // exit gracefully on should_exit flag + } + + // upload exit before leaving + connection.send_packet(shared::contents_to_packet({}, "XIT")); +} + +} // namespace client |
