#define BOOST_ASIO_HAS_CO_AWAIT #include #include #include #include #include #include #include #include #include using namespace boost; std::string_view streambufAsString(asio::streambuf& buffer) { return std::string_view{reinterpret_cast(buffer.data().data()), buffer.data().size()}; } bool isPrintable(std::string_view str) { for (const auto c : str) { if (!isprint(c)) { return false; } } return true; } enum class Event { join, leave, message }; class User { std::vector>& users; asio::ip::tcp::socket socket; std::string nickname; bool good = true; public: User(asio::io_context& ioc, std::vector>& users) : socket(ioc), users(users) {} auto& getSocket() { return socket; } const auto& getNickname() const { return nickname; } bool isGood() const { return good; } bool isLoggedOn() const { return good && !nickname.empty(); } bool wasLoggedOn() { return !nickname.empty(); } void markAsBad() { good = false; } asio::awaitable sendString(std::string_view str) { co_await asio::async_write(socket, asio::buffer(str), asio::use_awaitable); } asio::awaitable recvLine() { asio::streambuf buffer; co_await asio::async_read_until(socket, buffer, '\n', asio::use_awaitable); auto fres = std::string(streambufAsString(buffer)); fres.pop_back(); co_return fres; } asio::awaitable connect() { try { // Ask for nickname co_await sendString("Welcome to pyChat! Please type your nickname: "); while (true) { // Get nickname as string auto proposedNickname = co_await recvLine(); // Check that nickname has a healthy length if (proposedNickname.empty() || proposedNickname.size() > 30) { co_await sendString("Your nickname has a bad length. Please choose a different one: "); continue; } // Check that nickname is printable if (!isPrintable(proposedNickname)) { co_await sendString("Your nickname is invalid. Please choose a different one: "); continue; } // Check that nickname is not already taken if (std::find_if(users.begin(), users.end(), [&, this] (const auto& o) {return o->getNickname() == proposedNickname;}) != users.end()) { co_await sendString("This nickname has already been taken. Please choose a different one: "); continue; } // Use this nickname nickname = std::move(proposedNickname); break; } std::cout << "User has connected as " << nickname << std::endl; // Broadcast join co_await broadcastEvent(Event::join); // Wait loop for messages while (true) { auto message = co_await recvLine(); // Check kind of message if (message.starts_with('/')) { // Command if (message == "/quit") { markAsBad(); throw std::runtime_error("User initiated disconnect"); } else { co_await sendString("Invalid command: "+message+'\n'); } } else { // Message // Make sure message is printable if (!isPrintable(message)) { co_await sendString("Message could not be sent because it contains non-printable characters.\n"); continue; } // Broadcast message co_await broadcastEvent(Event::message, message); } } } catch (std::exception& e) { std::cout << "User " << (nickname.empty() ? "" : nickname) << " has disconnected: " << e.what() << std::endl; markAsBad(); } // Broadcast user disconnect if (wasLoggedOn()) { co_await broadcastEvent(Event::leave); } // Delete user from list auto it = std::find_if(users.begin(), users.end(), [this] (const auto& o) { return o.get() == this; }); users.erase(it); } asio::awaitable onEvent(User& sender, Event event, std::string_view param = "") { switch (event) { case Event::join: co_await sendString("* "+sender.nickname+" has joined.\n"); break; case Event::leave: co_await sendString("* "+sender.nickname+" has left.\n"); break; case Event::message: co_await sendString(" <"+sender.nickname+"> "+std::string(param)+'\n'); break; } } asio::awaitable broadcastEvent(Event event, std::string_view param = "") { for (auto& user : users) { if (user->isLoggedOn()) { co_await user->onEvent(*this, event, param); } } } }; class Server { asio::io_context& ioc; std::vector> users; public: Server(asio::io_context& ioc) : ioc(ioc) {} asio::awaitable listen(int port) { try { // Create acceptor asio::ip::tcp::acceptor acceptor(ioc, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port)); // Handle all incoming connections while (true) { auto user = std::make_shared(ioc, users); co_await acceptor.async_accept(user->getSocket(), asio::use_awaitable); asio::co_spawn(ioc, user->connect(), asio::detached); users.push_back(std::move(user)); } } catch (std::exception& e) { std::cerr << "Failed to stay alive: " << e.what() << std::endl; } } }; int main() { try { asio::io_context ioc; Server server(ioc); int port = 22080; asio::co_spawn(ioc, server.listen(port), asio::detached); std::cout << "Server has initialized and will now be listening on port " << port << '.' << std::endl; ioc.run(); } catch (std::exception& e) { std::cerr << "Failed to launch: " << e.what() << std::endl; } }