diff options
Diffstat (limited to 'src/network')
-rw-r--r-- | src/network/CMakeLists.txt | 16 | ||||
-rw-r--r-- | src/network/network.cpp | 50 | ||||
-rw-r--r-- | src/network/network.h | 33 | ||||
-rw-r--r-- | src/network/packet.cpp | 262 | ||||
-rw-r--r-- | src/network/packet.h | 165 | ||||
-rw-r--r-- | src/network/room.cpp | 1110 | ||||
-rw-r--r-- | src/network/room.h | 151 | ||||
-rw-r--r-- | src/network/room_member.cpp | 696 | ||||
-rw-r--r-- | src/network/room_member.h | 318 | ||||
-rw-r--r-- | src/network/verify_user.cpp | 17 | ||||
-rw-r--r-- | src/network/verify_user.h | 45 |
11 files changed, 2863 insertions, 0 deletions
diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt new file mode 100644 index 000000000..382a69e2f --- /dev/null +++ b/src/network/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(network STATIC + network.cpp + network.h + packet.cpp + packet.h + room.cpp + room.h + room_member.cpp + room_member.h + verify_user.cpp + verify_user.h +) + +create_target_directory_groups(network) + +target_link_libraries(network PRIVATE common enet Boost::boost) diff --git a/src/network/network.cpp b/src/network/network.cpp new file mode 100644 index 000000000..0841e4134 --- /dev/null +++ b/src/network/network.cpp @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/assert.h" +#include "common/logging/log.h" +#include "enet/enet.h" +#include "network/network.h" + +namespace Network { + +RoomNetwork::RoomNetwork() { + m_room = std::make_shared<Room>(); + m_room_member = std::make_shared<RoomMember>(); +} + +bool RoomNetwork::Init() { + if (enet_initialize() != 0) { + LOG_ERROR(Network, "Error initalizing ENet"); + return false; + } + m_room = std::make_shared<Room>(); + m_room_member = std::make_shared<RoomMember>(); + LOG_DEBUG(Network, "initialized OK"); + return true; +} + +std::weak_ptr<Room> RoomNetwork::GetRoom() { + return m_room; +} + +std::weak_ptr<RoomMember> RoomNetwork::GetRoomMember() { + return m_room_member; +} + +void RoomNetwork::Shutdown() { + if (m_room_member) { + if (m_room_member->IsConnected()) + m_room_member->Leave(); + m_room_member.reset(); + } + if (m_room) { + if (m_room->GetState() == Room::State::Open) + m_room->Destroy(); + m_room.reset(); + } + enet_deinitialize(); + LOG_DEBUG(Network, "shutdown OK"); +} + +} // namespace Network diff --git a/src/network/network.h b/src/network/network.h new file mode 100644 index 000000000..e4de207b2 --- /dev/null +++ b/src/network/network.h @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <memory> +#include "network/room.h" +#include "network/room_member.h" + +namespace Network { + +class RoomNetwork { +public: + RoomNetwork(); + + /// Initializes and registers the network device, the room, and the room member. + bool Init(); + + /// Returns a pointer to the room handle + std::weak_ptr<Room> GetRoom(); + + /// Returns a pointer to the room member handle + std::weak_ptr<RoomMember> GetRoomMember(); + + /// Unregisters the network device, the room, and the room member and shut them down. + void Shutdown(); + +private: + std::shared_ptr<RoomMember> m_room_member; ///< RoomMember (Client) for network games + std::shared_ptr<Room> m_room; ///< Room (Server) for network games +}; + +} // namespace Network diff --git a/src/network/packet.cpp b/src/network/packet.cpp new file mode 100644 index 000000000..0e22f1eb4 --- /dev/null +++ b/src/network/packet.cpp @@ -0,0 +1,262 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifdef _WIN32 +#include <winsock2.h> +#else +#include <arpa/inet.h> +#endif +#include <cstring> +#include <string> +#include "network/packet.h" + +namespace Network { + +#ifndef htonll +static u64 htonll(u64 x) { + return ((1 == htonl(1)) ? (x) : ((uint64_t)htonl((x)&0xFFFFFFFF) << 32) | htonl((x) >> 32)); +} +#endif + +#ifndef ntohll +static u64 ntohll(u64 x) { + return ((1 == ntohl(1)) ? (x) : ((uint64_t)ntohl((x)&0xFFFFFFFF) << 32) | ntohl((x) >> 32)); +} +#endif + +void Packet::Append(const void* in_data, std::size_t size_in_bytes) { + if (in_data && (size_in_bytes > 0)) { + std::size_t start = data.size(); + data.resize(start + size_in_bytes); + std::memcpy(&data[start], in_data, size_in_bytes); + } +} + +void Packet::Read(void* out_data, std::size_t size_in_bytes) { + if (out_data && CheckSize(size_in_bytes)) { + std::memcpy(out_data, &data[read_pos], size_in_bytes); + read_pos += size_in_bytes; + } +} + +void Packet::Clear() { + data.clear(); + read_pos = 0; + is_valid = true; +} + +const void* Packet::GetData() const { + return !data.empty() ? &data[0] : nullptr; +} + +void Packet::IgnoreBytes(u32 length) { + read_pos += length; +} + +std::size_t Packet::GetDataSize() const { + return data.size(); +} + +bool Packet::EndOfPacket() const { + return read_pos >= data.size(); +} + +Packet::operator bool() const { + return is_valid; +} + +Packet& Packet::Read(bool& out_data) { + u8 value{}; + if (Read(value)) { + out_data = (value != 0); + } + return *this; +} + +Packet& Packet::Read(s8& out_data) { + Read(&out_data, sizeof(out_data)); + return *this; +} + +Packet& Packet::Read(u8& out_data) { + Read(&out_data, sizeof(out_data)); + return *this; +} + +Packet& Packet::Read(s16& out_data) { + s16 value{}; + Read(&value, sizeof(value)); + out_data = ntohs(value); + return *this; +} + +Packet& Packet::Read(u16& out_data) { + u16 value{}; + Read(&value, sizeof(value)); + out_data = ntohs(value); + return *this; +} + +Packet& Packet::Read(s32& out_data) { + s32 value{}; + Read(&value, sizeof(value)); + out_data = ntohl(value); + return *this; +} + +Packet& Packet::Read(u32& out_data) { + u32 value{}; + Read(&value, sizeof(value)); + out_data = ntohl(value); + return *this; +} + +Packet& Packet::Read(s64& out_data) { + s64 value{}; + Read(&value, sizeof(value)); + out_data = ntohll(value); + return *this; +} + +Packet& Packet::Read(u64& out_data) { + u64 value{}; + Read(&value, sizeof(value)); + out_data = ntohll(value); + return *this; +} + +Packet& Packet::Read(float& out_data) { + Read(&out_data, sizeof(out_data)); + return *this; +} + +Packet& Packet::Read(double& out_data) { + Read(&out_data, sizeof(out_data)); + return *this; +} + +Packet& Packet::Read(char* out_data) { + // First extract string length + u32 length = 0; + Read(length); + + if ((length > 0) && CheckSize(length)) { + // Then extract characters + std::memcpy(out_data, &data[read_pos], length); + out_data[length] = '\0'; + + // Update reading position + read_pos += length; + } + + return *this; +} + +Packet& Packet::Read(std::string& out_data) { + // First extract string length + u32 length = 0; + Read(length); + + out_data.clear(); + if ((length > 0) && CheckSize(length)) { + // Then extract characters + out_data.assign(&data[read_pos], length); + + // Update reading position + read_pos += length; + } + + return *this; +} + +Packet& Packet::Write(bool in_data) { + Write(static_cast<u8>(in_data)); + return *this; +} + +Packet& Packet::Write(s8 in_data) { + Append(&in_data, sizeof(in_data)); + return *this; +} + +Packet& Packet::Write(u8 in_data) { + Append(&in_data, sizeof(in_data)); + return *this; +} + +Packet& Packet::Write(s16 in_data) { + s16 toWrite = htons(in_data); + Append(&toWrite, sizeof(toWrite)); + return *this; +} + +Packet& Packet::Write(u16 in_data) { + u16 toWrite = htons(in_data); + Append(&toWrite, sizeof(toWrite)); + return *this; +} + +Packet& Packet::Write(s32 in_data) { + s32 toWrite = htonl(in_data); + Append(&toWrite, sizeof(toWrite)); + return *this; +} + +Packet& Packet::Write(u32 in_data) { + u32 toWrite = htonl(in_data); + Append(&toWrite, sizeof(toWrite)); + return *this; +} + +Packet& Packet::Write(s64 in_data) { + s64 toWrite = htonll(in_data); + Append(&toWrite, sizeof(toWrite)); + return *this; +} + +Packet& Packet::Write(u64 in_data) { + u64 toWrite = htonll(in_data); + Append(&toWrite, sizeof(toWrite)); + return *this; +} + +Packet& Packet::Write(float in_data) { + Append(&in_data, sizeof(in_data)); + return *this; +} + +Packet& Packet::Write(double in_data) { + Append(&in_data, sizeof(in_data)); + return *this; +} + +Packet& Packet::Write(const char* in_data) { + // First insert string length + u32 length = static_cast<u32>(std::strlen(in_data)); + Write(length); + + // Then insert characters + Append(in_data, length * sizeof(char)); + + return *this; +} + +Packet& Packet::Write(const std::string& in_data) { + // First insert string length + u32 length = static_cast<u32>(in_data.size()); + Write(length); + + // Then insert characters + if (length > 0) + Append(in_data.c_str(), length * sizeof(std::string::value_type)); + + return *this; +} + +bool Packet::CheckSize(std::size_t size) { + is_valid = is_valid && (read_pos + size <= data.size()); + + return is_valid; +} + +} // namespace Network diff --git a/src/network/packet.h b/src/network/packet.h new file mode 100644 index 000000000..e69217488 --- /dev/null +++ b/src/network/packet.h @@ -0,0 +1,165 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <array> +#include <vector> +#include "common/common_types.h" + +namespace Network { + +/// A class that serializes data for network transfer. It also handles endianess +class Packet { +public: + Packet() = default; + ~Packet() = default; + + /** + * Append data to the end of the packet + * @param data Pointer to the sequence of bytes to append + * @param size_in_bytes Number of bytes to append + */ + void Append(const void* data, std::size_t size_in_bytes); + + /** + * Reads data from the current read position of the packet + * @param out_data Pointer where the data should get written to + * @param size_in_bytes Number of bytes to read + */ + void Read(void* out_data, std::size_t size_in_bytes); + + /** + * Clear the packet + * After calling Clear, the packet is empty. + */ + void Clear(); + + /** + * Ignores bytes while reading + * @param length THe number of bytes to ignore + */ + void IgnoreBytes(u32 length); + + /** + * Get a pointer to the data contained in the packet + * @return Pointer to the data + */ + const void* GetData() const; + + /** + * This function returns the number of bytes pointed to by + * what getData returns. + * @return Data size, in bytes + */ + std::size_t GetDataSize() const; + + /** + * This function is useful to know if there is some data + * left to be read, without actually reading it. + * @return True if all data was read, false otherwise + */ + bool EndOfPacket() const; + + explicit operator bool() const; + + /// Overloads of read function to read data from the packet + Packet& Read(bool& out_data); + Packet& Read(s8& out_data); + Packet& Read(u8& out_data); + Packet& Read(s16& out_data); + Packet& Read(u16& out_data); + Packet& Read(s32& out_data); + Packet& Read(u32& out_data); + Packet& Read(s64& out_data); + Packet& Read(u64& out_data); + Packet& Read(float& out_data); + Packet& Read(double& out_data); + Packet& Read(char* out_data); + Packet& Read(std::string& out_data); + template <typename T> + Packet& Read(std::vector<T>& out_data); + template <typename T, std::size_t S> + Packet& Read(std::array<T, S>& out_data); + + /// Overloads of write function to write data into the packet + Packet& Write(bool in_data); + Packet& Write(s8 in_data); + Packet& Write(u8 in_data); + Packet& Write(s16 in_data); + Packet& Write(u16 in_data); + Packet& Write(s32 in_data); + Packet& Write(u32 in_data); + Packet& Write(s64 in_data); + Packet& Write(u64 in_data); + Packet& Write(float in_data); + Packet& Write(double in_data); + Packet& Write(const char* in_data); + Packet& Write(const std::string& in_data); + template <typename T> + Packet& Write(const std::vector<T>& in_data); + template <typename T, std::size_t S> + Packet& Write(const std::array<T, S>& data); + +private: + /** + * Check if the packet can extract a given number of bytes + * This function updates accordingly the state of the packet. + * @param size Size to check + * @return True if size bytes can be read from the packet + */ + bool CheckSize(std::size_t size); + + // Member data + std::vector<char> data; ///< Data stored in the packet + std::size_t read_pos = 0; ///< Current reading position in the packet + bool is_valid = true; ///< Reading state of the packet +}; + +template <typename T> +Packet& Packet::Read(std::vector<T>& out_data) { + // First extract the size + u32 size = 0; + Read(size); + out_data.resize(size); + + // Then extract the data + for (std::size_t i = 0; i < out_data.size(); ++i) { + T character; + Read(character); + out_data[i] = character; + } + return *this; +} + +template <typename T, std::size_t S> +Packet& Packet::Read(std::array<T, S>& out_data) { + for (std::size_t i = 0; i < out_data.size(); ++i) { + T character; + Read(character); + out_data[i] = character; + } + return *this; +} + +template <typename T> +Packet& Packet::Write(const std::vector<T>& in_data) { + // First insert the size + Write(static_cast<u32>(in_data.size())); + + // Then insert the data + for (std::size_t i = 0; i < in_data.size(); ++i) { + Write(in_data[i]); + } + return *this; +} + +template <typename T, std::size_t S> +Packet& Packet::Write(const std::array<T, S>& in_data) { + for (std::size_t i = 0; i < in_data.size(); ++i) { + Write(in_data[i]); + } + return *this; +} + +} // namespace Network diff --git a/src/network/room.cpp b/src/network/room.cpp new file mode 100644 index 000000000..3fc3a0383 --- /dev/null +++ b/src/network/room.cpp @@ -0,0 +1,1110 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <algorithm> +#include <atomic> +#include <iomanip> +#include <mutex> +#include <random> +#include <regex> +#include <shared_mutex> +#include <sstream> +#include <thread> +#include "common/logging/log.h" +#include "enet/enet.h" +#include "network/packet.h" +#include "network/room.h" +#include "network/verify_user.h" + +namespace Network { + +class Room::RoomImpl { +public: + // This MAC address is used to generate a 'Nintendo' like Mac address. + const MacAddress NintendoOUI; + std::mt19937 random_gen; ///< Random number generator. Used for GenerateMacAddress + + ENetHost* server = nullptr; ///< Network interface. + + std::atomic<State> state{State::Closed}; ///< Current state of the room. + RoomInformation room_information; ///< Information about this room. + + std::string verify_uid; ///< A GUID which may be used for verfication. + mutable std::mutex verify_uid_mutex; ///< Mutex for verify_uid + + std::string password; ///< The password required to connect to this room. + + struct Member { + std::string nickname; ///< The nickname of the member. + std::string console_id_hash; ///< A hash of the console ID of the member. + GameInfo game_info; ///< The current game of the member + MacAddress mac_address; ///< The assigned mac address of the member. + /// Data of the user, often including authenticated forum username. + VerifyUser::UserData user_data; + ENetPeer* peer; ///< The remote peer. + }; + using MemberList = std::vector<Member>; + MemberList members; ///< Information about the members of this room + mutable std::shared_mutex member_mutex; ///< Mutex for locking the members list + + UsernameBanList username_ban_list; ///< List of banned usernames + IPBanList ip_ban_list; ///< List of banned IP addresses + mutable std::mutex ban_list_mutex; ///< Mutex for the ban lists + + RoomImpl() + : NintendoOUI{0x00, 0x1F, 0x32, 0x00, 0x00, 0x00}, random_gen(std::random_device()()) {} + + /// Thread that receives and dispatches network packets + std::unique_ptr<std::thread> room_thread; + + /// Verification backend of the room + std::unique_ptr<VerifyUser::Backend> verify_backend; + + /// Thread function that will receive and dispatch messages until the room is destroyed. + void ServerLoop(); + void StartLoop(); + + /** + * Parses and answers a room join request from a client. + * Validates the uniqueness of the username and assigns the MAC address + * that the client will use for the remainder of the connection. + */ + void HandleJoinRequest(const ENetEvent* event); + + /** + * Parses and answers a kick request from a client. + * Validates the permissions and that the given user exists and then kicks the member. + */ + void HandleModKickPacket(const ENetEvent* event); + + /** + * Parses and answers a ban request from a client. + * Validates the permissions and bans the user (by forum username or IP). + */ + void HandleModBanPacket(const ENetEvent* event); + + /** + * Parses and answers a unban request from a client. + * Validates the permissions and unbans the address. + */ + void HandleModUnbanPacket(const ENetEvent* event); + + /** + * Parses and answers a get ban list request from a client. + * Validates the permissions and returns the ban list. + */ + void HandleModGetBanListPacket(const ENetEvent* event); + + /** + * Returns whether the nickname is valid, ie. isn't already taken by someone else in the room. + */ + bool IsValidNickname(const std::string& nickname) const; + + /** + * Returns whether the MAC address is valid, ie. isn't already taken by someone else in the + * room. + */ + bool IsValidMacAddress(const MacAddress& address) const; + + /** + * Returns whether the console ID (hash) is valid, ie. isn't already taken by someone else in + * the room. + */ + bool IsValidConsoleId(const std::string& console_id_hash) const; + + /** + * Returns whether a user has mod permissions. + */ + bool HasModPermission(const ENetPeer* client) const; + + /** + * Sends a ID_ROOM_IS_FULL message telling the client that the room is full. + */ + void SendRoomIsFull(ENetPeer* client); + + /** + * Sends a ID_ROOM_NAME_COLLISION message telling the client that the name is invalid. + */ + void SendNameCollision(ENetPeer* client); + + /** + * Sends a ID_ROOM_MAC_COLLISION message telling the client that the MAC is invalid. + */ + void SendMacCollision(ENetPeer* client); + + /** + * Sends a IdConsoleIdCollison message telling the client that another member with the same + * console ID exists. + */ + void SendConsoleIdCollision(ENetPeer* client); + + /** + * Sends a ID_ROOM_VERSION_MISMATCH message telling the client that the version is invalid. + */ + void SendVersionMismatch(ENetPeer* client); + + /** + * Sends a ID_ROOM_WRONG_PASSWORD message telling the client that the password is wrong. + */ + void SendWrongPassword(ENetPeer* client); + + /** + * Notifies the member that its connection attempt was successful, + * and it is now part of the room. + */ + void SendJoinSuccess(ENetPeer* client, MacAddress mac_address); + + /** + * Notifies the member that its connection attempt was successful, + * and it is now part of the room, and it has been granted mod permissions. + */ + void SendJoinSuccessAsMod(ENetPeer* client, MacAddress mac_address); + + /** + * Sends a IdHostKicked message telling the client that they have been kicked. + */ + void SendUserKicked(ENetPeer* client); + + /** + * Sends a IdHostBanned message telling the client that they have been banned. + */ + void SendUserBanned(ENetPeer* client); + + /** + * Sends a IdModPermissionDenied message telling the client that they do not have mod + * permission. + */ + void SendModPermissionDenied(ENetPeer* client); + + /** + * Sends a IdModNoSuchUser message telling the client that the given user could not be found. + */ + void SendModNoSuchUser(ENetPeer* client); + + /** + * Sends the ban list in response to a client's request for getting ban list. + */ + void SendModBanListResponse(ENetPeer* client); + + /** + * Notifies the members that the room is closed, + */ + void SendCloseMessage(); + + /** + * Sends a system message to all the connected clients. + */ + void SendStatusMessage(StatusMessageTypes type, const std::string& nickname, + const std::string& username, const std::string& ip); + + /** + * Sends the information about the room, along with the list of members + * to every connected client in the room. + * The packet has the structure: + * <MessageID>ID_ROOM_INFORMATION + * <String> room_name + * <String> room_description + * <u32> member_slots: The max number of clients allowed in this room + * <String> uid + * <u16> port + * <u32> num_members: the number of currently joined clients + * This is followed by the following three values for each member: + * <String> nickname of that member + * <MacAddress> mac_address of that member + * <String> game_name of that member + */ + void BroadcastRoomInformation(); + + /** + * Generates a free MAC address to assign to a new client. + * The first 3 bytes are the NintendoOUI 0x00, 0x1F, 0x32 + */ + MacAddress GenerateMacAddress(); + + /** + * Broadcasts this packet to all members except the sender. + * @param event The ENet event containing the data + */ + void HandleWifiPacket(const ENetEvent* event); + + /** + * Extracts a chat entry from a received ENet packet and adds it to the chat queue. + * @param event The ENet event that was received. + */ + void HandleChatPacket(const ENetEvent* event); + + /** + * Extracts the game name from a received ENet packet and broadcasts it. + * @param event The ENet event that was received. + */ + void HandleGameNamePacket(const ENetEvent* event); + + /** + * Removes the client from the members list if it was in it and announces the change + * to all other clients. + */ + void HandleClientDisconnection(ENetPeer* client); +}; + +// RoomImpl +void Room::RoomImpl::ServerLoop() { + while (state != State::Closed) { + ENetEvent event; + if (enet_host_service(server, &event, 16) > 0) { + switch (event.type) { + case ENET_EVENT_TYPE_RECEIVE: + switch (event.packet->data[0]) { + case IdJoinRequest: + HandleJoinRequest(&event); + break; + case IdSetGameInfo: + HandleGameNamePacket(&event); + break; + case IdWifiPacket: + HandleWifiPacket(&event); + break; + case IdChatMessage: + HandleChatPacket(&event); + break; + // Moderation + case IdModKick: + HandleModKickPacket(&event); + break; + case IdModBan: + HandleModBanPacket(&event); + break; + case IdModUnban: + HandleModUnbanPacket(&event); + break; + case IdModGetBanList: + HandleModGetBanListPacket(&event); + break; + } + enet_packet_destroy(event.packet); + break; + case ENET_EVENT_TYPE_DISCONNECT: + HandleClientDisconnection(event.peer); + break; + case ENET_EVENT_TYPE_NONE: + case ENET_EVENT_TYPE_CONNECT: + break; + } + } + } + // Close the connection to all members: + SendCloseMessage(); +} + +void Room::RoomImpl::StartLoop() { + room_thread = std::make_unique<std::thread>(&Room::RoomImpl::ServerLoop, this); +} + +void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { + { + std::lock_guard lock(member_mutex); + if (members.size() >= room_information.member_slots) { + SendRoomIsFull(event->peer); + return; + } + } + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + std::string nickname; + packet.Read(nickname); + + std::string console_id_hash; + packet.Read(console_id_hash); + + MacAddress preferred_mac; + packet.Read(preferred_mac); + + u32 client_version; + packet.Read(client_version); + + std::string pass; + packet.Read(pass); + + std::string token; + packet.Read(token); + + if (pass != password) { + SendWrongPassword(event->peer); + return; + } + + if (!IsValidNickname(nickname)) { + SendNameCollision(event->peer); + return; + } + + if (preferred_mac != NoPreferredMac) { + // Verify if the preferred mac is available + if (!IsValidMacAddress(preferred_mac)) { + SendMacCollision(event->peer); + return; + } + } else { + // Assign a MAC address of this client automatically + preferred_mac = GenerateMacAddress(); + } + + if (!IsValidConsoleId(console_id_hash)) { + SendConsoleIdCollision(event->peer); + return; + } + + if (client_version != network_version) { + SendVersionMismatch(event->peer); + return; + } + + // At this point the client is ready to be added to the room. + Member member{}; + member.mac_address = preferred_mac; + member.console_id_hash = console_id_hash; + member.nickname = nickname; + member.peer = event->peer; + + std::string uid; + { + std::lock_guard lock(verify_uid_mutex); + uid = verify_uid; + } + member.user_data = verify_backend->LoadUserData(uid, token); + + std::string ip; + { + std::lock_guard lock(ban_list_mutex); + + // Check username ban + if (!member.user_data.username.empty() && + std::find(username_ban_list.begin(), username_ban_list.end(), + member.user_data.username) != username_ban_list.end()) { + + SendUserBanned(event->peer); + return; + } + + // Check IP ban + std::array<char, 256> ip_raw{}; + enet_address_get_host_ip(&event->peer->address, ip_raw.data(), sizeof(ip_raw) - 1); + ip = ip_raw.data(); + + if (std::find(ip_ban_list.begin(), ip_ban_list.end(), ip) != ip_ban_list.end()) { + SendUserBanned(event->peer); + return; + } + } + + // Notify everyone that the user has joined. + SendStatusMessage(IdMemberJoin, member.nickname, member.user_data.username, ip); + + { + std::lock_guard lock(member_mutex); + members.push_back(std::move(member)); + } + + // Notify everyone that the room information has changed. + BroadcastRoomInformation(); + if (HasModPermission(event->peer)) { + SendJoinSuccessAsMod(event->peer, preferred_mac); + } else { + SendJoinSuccess(event->peer, preferred_mac); + } +} + +void Room::RoomImpl::HandleModKickPacket(const ENetEvent* event) { + if (!HasModPermission(event->peer)) { + SendModPermissionDenied(event->peer); + return; + } + + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + + std::string nickname; + packet.Read(nickname); + + std::string username, ip; + { + std::lock_guard lock(member_mutex); + const auto target_member = + std::find_if(members.begin(), members.end(), + [&nickname](const auto& member) { return member.nickname == nickname; }); + if (target_member == members.end()) { + SendModNoSuchUser(event->peer); + return; + } + + // Notify the kicked member + SendUserKicked(target_member->peer); + + username = target_member->user_data.username; + + std::array<char, 256> ip_raw{}; + enet_address_get_host_ip(&target_member->peer->address, ip_raw.data(), sizeof(ip_raw) - 1); + ip = ip_raw.data(); + + enet_peer_disconnect(target_member->peer, 0); + members.erase(target_member); + } + + // Announce the change to all clients. + SendStatusMessage(IdMemberKicked, nickname, username, ip); + BroadcastRoomInformation(); +} + +void Room::RoomImpl::HandleModBanPacket(const ENetEvent* event) { + if (!HasModPermission(event->peer)) { + SendModPermissionDenied(event->peer); + return; + } + + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + + std::string nickname; + packet.Read(nickname); + + std::string username, ip; + { + std::lock_guard lock(member_mutex); + const auto target_member = + std::find_if(members.begin(), members.end(), + [&nickname](const auto& member) { return member.nickname == nickname; }); + if (target_member == members.end()) { + SendModNoSuchUser(event->peer); + return; + } + + // Notify the banned member + SendUserBanned(target_member->peer); + + nickname = target_member->nickname; + username = target_member->user_data.username; + + std::array<char, 256> ip_raw{}; + enet_address_get_host_ip(&target_member->peer->address, ip_raw.data(), sizeof(ip_raw) - 1); + ip = ip_raw.data(); + + enet_peer_disconnect(target_member->peer, 0); + members.erase(target_member); + } + + { + std::lock_guard lock(ban_list_mutex); + + if (!username.empty()) { + // Ban the forum username + if (std::find(username_ban_list.begin(), username_ban_list.end(), username) == + username_ban_list.end()) { + + username_ban_list.emplace_back(username); + } + } + + // Ban the member's IP as well + if (std::find(ip_ban_list.begin(), ip_ban_list.end(), ip) == ip_ban_list.end()) { + ip_ban_list.emplace_back(ip); + } + } + + // Announce the change to all clients. + SendStatusMessage(IdMemberBanned, nickname, username, ip); + BroadcastRoomInformation(); +} + +void Room::RoomImpl::HandleModUnbanPacket(const ENetEvent* event) { + if (!HasModPermission(event->peer)) { + SendModPermissionDenied(event->peer); + return; + } + + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + + std::string address; + packet.Read(address); + + bool unbanned = false; + { + std::lock_guard lock(ban_list_mutex); + + auto it = std::find(username_ban_list.begin(), username_ban_list.end(), address); + if (it != username_ban_list.end()) { + unbanned = true; + username_ban_list.erase(it); + } + + it = std::find(ip_ban_list.begin(), ip_ban_list.end(), address); + if (it != ip_ban_list.end()) { + unbanned = true; + ip_ban_list.erase(it); + } + } + + if (unbanned) { + SendStatusMessage(IdAddressUnbanned, address, "", ""); + } else { + SendModNoSuchUser(event->peer); + } +} + +void Room::RoomImpl::HandleModGetBanListPacket(const ENetEvent* event) { + if (!HasModPermission(event->peer)) { + SendModPermissionDenied(event->peer); + return; + } + + SendModBanListResponse(event->peer); +} + +bool Room::RoomImpl::IsValidNickname(const std::string& nickname) const { + // A nickname is valid if it matches the regex and is not already taken by anybody else in the + // room. + const std::regex nickname_regex("^[ a-zA-Z0-9._-]{4,20}$"); + if (!std::regex_match(nickname, nickname_regex)) + return false; + + std::lock_guard lock(member_mutex); + return std::all_of(members.begin(), members.end(), + [&nickname](const auto& member) { return member.nickname != nickname; }); +} + +bool Room::RoomImpl::IsValidMacAddress(const MacAddress& address) const { + // A MAC address is valid if it is not already taken by anybody else in the room. + std::lock_guard lock(member_mutex); + return std::all_of(members.begin(), members.end(), + [&address](const auto& member) { return member.mac_address != address; }); +} + +bool Room::RoomImpl::IsValidConsoleId(const std::string& console_id_hash) const { + // A Console ID is valid if it is not already taken by anybody else in the room. + std::lock_guard lock(member_mutex); + return std::all_of(members.begin(), members.end(), [&console_id_hash](const auto& member) { + return member.console_id_hash != console_id_hash; + }); +} + +bool Room::RoomImpl::HasModPermission(const ENetPeer* client) const { + std::lock_guard lock(member_mutex); + const auto sending_member = + std::find_if(members.begin(), members.end(), + [client](const auto& member) { return member.peer == client; }); + if (sending_member == members.end()) { + return false; + } + if (room_information.enable_yuzu_mods && + sending_member->user_data.moderator) { // Community moderator + + return true; + } + if (!room_information.host_username.empty() && + sending_member->user_data.username == room_information.host_username) { // Room host + + return true; + } + return false; +} + +void Room::RoomImpl::SendNameCollision(ENetPeer* client) { + Packet packet; + packet.Write(static_cast<u8>(IdNameCollision)); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendMacCollision(ENetPeer* client) { + Packet packet; + packet.Write(static_cast<u8>(IdMacCollision)); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendConsoleIdCollision(ENetPeer* client) { + Packet packet; + packet.Write(static_cast<u8>(IdConsoleIdCollision)); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendWrongPassword(ENetPeer* client) { + Packet packet; + packet.Write(static_cast<u8>(IdWrongPassword)); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendRoomIsFull(ENetPeer* client) { + Packet packet; + packet.Write(static_cast<u8>(IdRoomIsFull)); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendVersionMismatch(ENetPeer* client) { + Packet packet; + packet.Write(static_cast<u8>(IdVersionMismatch)); + packet.Write(network_version); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendJoinSuccess(ENetPeer* client, MacAddress mac_address) { + Packet packet; + packet.Write(static_cast<u8>(IdJoinSuccess)); + packet.Write(mac_address); + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendJoinSuccessAsMod(ENetPeer* client, MacAddress mac_address) { + Packet packet; + packet.Write(static_cast<u8>(IdJoinSuccessAsMod)); + packet.Write(mac_address); + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendUserKicked(ENetPeer* client) { + Packet packet; + packet.Write(static_cast<u8>(IdHostKicked)); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendUserBanned(ENetPeer* client) { + Packet packet; + packet.Write(static_cast<u8>(IdHostBanned)); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendModPermissionDenied(ENetPeer* client) { + Packet packet; + packet.Write(static_cast<u8>(IdModPermissionDenied)); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendModNoSuchUser(ENetPeer* client) { + Packet packet; + packet.Write(static_cast<u8>(IdModNoSuchUser)); + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendModBanListResponse(ENetPeer* client) { + Packet packet; + packet.Write(static_cast<u8>(IdModBanListResponse)); + { + std::lock_guard lock(ban_list_mutex); + packet.Write(username_ban_list); + packet.Write(ip_ban_list); + } + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(client, 0, enet_packet); + enet_host_flush(server); +} + +void Room::RoomImpl::SendCloseMessage() { + Packet packet; + packet.Write(static_cast<u8>(IdCloseRoom)); + std::lock_guard lock(member_mutex); + if (!members.empty()) { + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + for (auto& member : members) { + enet_peer_send(member.peer, 0, enet_packet); + } + } + enet_host_flush(server); + for (auto& member : members) { + enet_peer_disconnect(member.peer, 0); + } +} + +void Room::RoomImpl::SendStatusMessage(StatusMessageTypes type, const std::string& nickname, + const std::string& username, const std::string& ip) { + Packet packet; + packet.Write(static_cast<u8>(IdStatusMessage)); + packet.Write(static_cast<u8>(type)); + packet.Write(nickname); + packet.Write(username); + std::lock_guard lock(member_mutex); + if (!members.empty()) { + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + for (auto& member : members) { + enet_peer_send(member.peer, 0, enet_packet); + } + } + enet_host_flush(server); + + const std::string display_name = + username.empty() ? nickname : fmt::format("{} ({})", nickname, username); + + switch (type) { + case IdMemberJoin: + LOG_INFO(Network, "[{}] {} has joined.", ip, display_name); + break; + case IdMemberLeave: + LOG_INFO(Network, "[{}] {} has left.", ip, display_name); + break; + case IdMemberKicked: + LOG_INFO(Network, "[{}] {} has been kicked.", ip, display_name); + break; + case IdMemberBanned: + LOG_INFO(Network, "[{}] {} has been banned.", ip, display_name); + break; + case IdAddressUnbanned: + LOG_INFO(Network, "{} has been unbanned.", display_name); + break; + } +} + +void Room::RoomImpl::BroadcastRoomInformation() { + Packet packet; + packet.Write(static_cast<u8>(IdRoomInformation)); + packet.Write(room_information.name); + packet.Write(room_information.description); + packet.Write(room_information.member_slots); + packet.Write(room_information.port); + packet.Write(room_information.preferred_game.name); + packet.Write(room_information.host_username); + + packet.Write(static_cast<u32>(members.size())); + { + std::lock_guard lock(member_mutex); + for (const auto& member : members) { + packet.Write(member.nickname); + packet.Write(member.mac_address); + packet.Write(member.game_info.name); + packet.Write(member.game_info.id); + packet.Write(member.user_data.username); + packet.Write(member.user_data.display_name); + packet.Write(member.user_data.avatar_url); + } + } + + ENetPacket* enet_packet = + enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE); + enet_host_broadcast(server, 0, enet_packet); + enet_host_flush(server); +} + +MacAddress Room::RoomImpl::GenerateMacAddress() { + MacAddress result_mac = + NintendoOUI; // The first three bytes of each MAC address will be the NintendoOUI + std::uniform_int_distribution<> dis(0x00, 0xFF); // Random byte between 0 and 0xFF + do { + for (std::size_t i = 3; i < result_mac.size(); ++i) { + result_mac[i] = dis(random_gen); + } + } while (!IsValidMacAddress(result_mac)); + return result_mac; +} + +void Room::RoomImpl::HandleWifiPacket(const ENetEvent* event) { + Packet in_packet; + in_packet.Append(event->packet->data, event->packet->dataLength); + in_packet.IgnoreBytes(sizeof(u8)); // Message type + in_packet.IgnoreBytes(sizeof(u8)); // WifiPacket Type + in_packet.IgnoreBytes(sizeof(u8)); // WifiPacket Channel + in_packet.IgnoreBytes(sizeof(MacAddress)); // WifiPacket Transmitter Address + MacAddress destination_address; + in_packet.Read(destination_address); + + Packet out_packet; + out_packet.Append(event->packet->data, event->packet->dataLength); + ENetPacket* enet_packet = enet_packet_create(out_packet.GetData(), out_packet.GetDataSize(), + ENET_PACKET_FLAG_RELIABLE); + + if (destination_address == BroadcastMac) { // Send the data to everyone except the sender + std::lock_guard lock(member_mutex); + bool sent_packet = false; + for (const auto& member : members) { + if (member.peer != event->peer) { + sent_packet = true; + enet_peer_send(member.peer, 0, enet_packet); + } + } + + if (!sent_packet) { + enet_packet_destroy(enet_packet); + } + } else { // Send the data only to the destination client + std::lock_guard lock(member_mutex); + auto member = std::find_if(members.begin(), members.end(), + [destination_address](const Member& member_entry) -> bool { + return member_entry.mac_address == destination_address; + }); + if (member != members.end()) { + enet_peer_send(member->peer, 0, enet_packet); + } else { + LOG_ERROR(Network, + "Attempting to send to unknown MAC address: " + "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", + destination_address[0], destination_address[1], destination_address[2], + destination_address[3], destination_address[4], destination_address[5]); + enet_packet_destroy(enet_packet); + } + } + enet_host_flush(server); +} + +void Room::RoomImpl::HandleChatPacket(const ENetEvent* event) { + Packet in_packet; + in_packet.Append(event->packet->data, event->packet->dataLength); + + in_packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + std::string message; + in_packet.Read(message); + auto CompareNetworkAddress = [event](const Member member) -> bool { + return member.peer == event->peer; + }; + + std::lock_guard lock(member_mutex); + const auto sending_member = std::find_if(members.begin(), members.end(), CompareNetworkAddress); + if (sending_member == members.end()) { + return; // Received a chat message from a unknown sender + } + + // Limit the size of chat messages to MaxMessageSize + message.resize(std::min(static_cast<u32>(message.size()), MaxMessageSize)); + + Packet out_packet; + out_packet.Write(static_cast<u8>(IdChatMessage)); + out_packet.Write(sending_member->nickname); + out_packet.Write(sending_member->user_data.username); + out_packet.Write(message); + + ENetPacket* enet_packet = enet_packet_create(out_packet.GetData(), out_packet.GetDataSize(), + ENET_PACKET_FLAG_RELIABLE); + bool sent_packet = false; + for (const auto& member : members) { + if (member.peer != event->peer) { + sent_packet = true; + enet_peer_send(member.peer, 0, enet_packet); + } + } + + if (!sent_packet) { + enet_packet_destroy(enet_packet); + } + + enet_host_flush(server); + + if (sending_member->user_data.username.empty()) { + LOG_INFO(Network, "{}: {}", sending_member->nickname, message); + } else { + LOG_INFO(Network, "{} ({}): {}", sending_member->nickname, + sending_member->user_data.username, message); + } +} + +void Room::RoomImpl::HandleGameNamePacket(const ENetEvent* event) { + Packet in_packet; + in_packet.Append(event->packet->data, event->packet->dataLength); + + in_packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + GameInfo game_info; + in_packet.Read(game_info.name); + in_packet.Read(game_info.id); + + { + std::lock_guard lock(member_mutex); + auto member = std::find_if(members.begin(), members.end(), + [event](const Member& member_entry) -> bool { + return member_entry.peer == event->peer; + }); + if (member != members.end()) { + member->game_info = game_info; + + const std::string display_name = + member->user_data.username.empty() + ? member->nickname + : fmt::format("{} ({})", member->nickname, member->user_data.username); + + if (game_info.name.empty()) { + LOG_INFO(Network, "{} is not playing", display_name); + } else { + LOG_INFO(Network, "{} is playing {}", display_name, game_info.name); + } + } + } + BroadcastRoomInformation(); +} + +void Room::RoomImpl::HandleClientDisconnection(ENetPeer* client) { + // Remove the client from the members list. + std::string nickname, username, ip; + { + std::lock_guard lock(member_mutex); + auto member = + std::find_if(members.begin(), members.end(), [client](const Member& member_entry) { + return member_entry.peer == client; + }); + if (member != members.end()) { + nickname = member->nickname; + username = member->user_data.username; + + std::array<char, 256> ip_raw{}; + enet_address_get_host_ip(&member->peer->address, ip_raw.data(), sizeof(ip_raw) - 1); + ip = ip_raw.data(); + + members.erase(member); + } + } + + // Announce the change to all clients. + enet_peer_disconnect(client, 0); + if (!nickname.empty()) + SendStatusMessage(IdMemberLeave, nickname, username, ip); + BroadcastRoomInformation(); +} + +// Room +Room::Room() : room_impl{std::make_unique<RoomImpl>()} {} + +Room::~Room() = default; + +bool Room::Create(const std::string& name, const std::string& description, + const std::string& server_address, u16 server_port, const std::string& password, + const u32 max_connections, const std::string& host_username, + const GameInfo preferred_game, + std::unique_ptr<VerifyUser::Backend> verify_backend, + const Room::BanList& ban_list, bool enable_yuzu_mods) { + ENetAddress address; + address.host = ENET_HOST_ANY; + if (!server_address.empty()) { + enet_address_set_host(&address, server_address.c_str()); + } + address.port = server_port; + + // In order to send the room is full message to the connecting client, we need to leave one + // slot open so enet won't reject the incoming connection without telling us + room_impl->server = enet_host_create(&address, max_connections + 1, NumChannels, 0, 0); + if (!room_impl->server) { + return false; + } + room_impl->state = State::Open; + + room_impl->room_information.name = name; + room_impl->room_information.description = description; + room_impl->room_information.member_slots = max_connections; + room_impl->room_information.port = server_port; + room_impl->room_information.preferred_game = preferred_game; + room_impl->room_information.host_username = host_username; + room_impl->room_information.enable_yuzu_mods = enable_yuzu_mods; + room_impl->password = password; + room_impl->verify_backend = std::move(verify_backend); + room_impl->username_ban_list = ban_list.first; + room_impl->ip_ban_list = ban_list.second; + + room_impl->StartLoop(); + return true; +} + +Room::State Room::GetState() const { + return room_impl->state; +} + +const RoomInformation& Room::GetRoomInformation() const { + return room_impl->room_information; +} + +std::string Room::GetVerifyUID() const { + std::lock_guard lock(room_impl->verify_uid_mutex); + return room_impl->verify_uid; +} + +Room::BanList Room::GetBanList() const { + std::lock_guard lock(room_impl->ban_list_mutex); + return {room_impl->username_ban_list, room_impl->ip_ban_list}; +} + +std::vector<Member> Room::GetRoomMemberList() const { + std::vector<Member> member_list; + std::lock_guard lock(room_impl->member_mutex); + for (const auto& member_impl : room_impl->members) { + Member member; + member.nickname = member_impl.nickname; + member.username = member_impl.user_data.username; + member.display_name = member_impl.user_data.display_name; + member.avatar_url = member_impl.user_data.avatar_url; + member.mac_address = member_impl.mac_address; + member.game = member_impl.game_info; + member_list.push_back(member); + } + return member_list; +} + +bool Room::HasPassword() const { + return !room_impl->password.empty(); +} + +void Room::SetVerifyUID(const std::string& uid) { + std::lock_guard lock(room_impl->verify_uid_mutex); + room_impl->verify_uid = uid; +} + +void Room::Destroy() { + room_impl->state = State::Closed; + room_impl->room_thread->join(); + room_impl->room_thread.reset(); + + if (room_impl->server) { + enet_host_destroy(room_impl->server); + } + room_impl->room_information = {}; + room_impl->server = nullptr; + { + std::lock_guard lock(room_impl->member_mutex); + room_impl->members.clear(); + } + room_impl->room_information.member_slots = 0; + room_impl->room_information.name.clear(); +} + +} // namespace Network diff --git a/src/network/room.h b/src/network/room.h new file mode 100644 index 000000000..6f7e3b5b5 --- /dev/null +++ b/src/network/room.h @@ -0,0 +1,151 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <array> +#include <memory> +#include <string> +#include <vector> +#include "common/announce_multiplayer_room.h" +#include "common/common_types.h" +#include "network/verify_user.h" + +namespace Network { + +using AnnounceMultiplayerRoom::GameInfo; +using AnnounceMultiplayerRoom::MacAddress; +using AnnounceMultiplayerRoom::Member; +using AnnounceMultiplayerRoom::RoomInformation; + +constexpr u32 network_version = 1; ///< The version of this Room and RoomMember + +constexpr u16 DefaultRoomPort = 24872; + +constexpr u32 MaxMessageSize = 500; + +/// Maximum number of concurrent connections allowed to this room. +static constexpr u32 MaxConcurrentConnections = 254; + +constexpr std::size_t NumChannels = 1; // Number of channels used for the connection + +/// A special MAC address that tells the room we're joining to assign us a MAC address +/// automatically. +constexpr MacAddress NoPreferredMac = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + +// 802.11 broadcast MAC address +constexpr MacAddress BroadcastMac = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + +// The different types of messages that can be sent. The first byte of each packet defines the type +enum RoomMessageTypes : u8 { + IdJoinRequest = 1, + IdJoinSuccess, + IdRoomInformation, + IdSetGameInfo, + IdWifiPacket, + IdChatMessage, + IdNameCollision, + IdMacCollision, + IdVersionMismatch, + IdWrongPassword, + IdCloseRoom, + IdRoomIsFull, + IdConsoleIdCollision, + IdStatusMessage, + IdHostKicked, + IdHostBanned, + /// Moderation requests + IdModKick, + IdModBan, + IdModUnban, + IdModGetBanList, + // Moderation responses + IdModBanListResponse, + IdModPermissionDenied, + IdModNoSuchUser, + IdJoinSuccessAsMod, +}; + +/// Types of system status messages +enum StatusMessageTypes : u8 { + IdMemberJoin = 1, ///< Member joining + IdMemberLeave, ///< Member leaving + IdMemberKicked, ///< A member is kicked from the room + IdMemberBanned, ///< A member is banned from the room + IdAddressUnbanned, ///< A username / ip address is unbanned from the room +}; + +/// This is what a server [person creating a server] would use. +class Room final { +public: + enum class State : u8 { + Open, ///< The room is open and ready to accept connections. + Closed, ///< The room is not opened and can not accept connections. + }; + + Room(); + ~Room(); + + /** + * Gets the current state of the room. + */ + State GetState() const; + + /** + * Gets the room information of the room. + */ + const RoomInformation& GetRoomInformation() const; + + /** + * Gets the verify UID of this room. + */ + std::string GetVerifyUID() const; + + /** + * Gets a list of the mbmers connected to the room. + */ + std::vector<Member> GetRoomMemberList() const; + + /** + * Checks if the room is password protected + */ + bool HasPassword() const; + + using UsernameBanList = std::vector<std::string>; + using IPBanList = std::vector<std::string>; + + using BanList = std::pair<UsernameBanList, IPBanList>; + + /** + * Creates the socket for this room. Will bind to default address if + * server is empty string. + */ + bool Create(const std::string& name, const std::string& description = "", + const std::string& server = "", u16 server_port = DefaultRoomPort, + const std::string& password = "", + const u32 max_connections = MaxConcurrentConnections, + const std::string& host_username = "", const GameInfo = {}, + std::unique_ptr<VerifyUser::Backend> verify_backend = nullptr, + const BanList& ban_list = {}, bool enable_yuzu_mods = false); + + /** + * Sets the verification GUID of the room. + */ + void SetVerifyUID(const std::string& uid); + + /** + * Gets the ban list (including banned forum usernames and IPs) of the room. + */ + BanList GetBanList() const; + + /** + * Destroys the socket + */ + void Destroy(); + +private: + class RoomImpl; + std::unique_ptr<RoomImpl> room_impl; +}; + +} // namespace Network diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp new file mode 100644 index 000000000..e4f823e98 --- /dev/null +++ b/src/network/room_member.cpp @@ -0,0 +1,696 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <atomic> +#include <list> +#include <mutex> +#include <set> +#include <thread> +#include "common/assert.h" +#include "enet/enet.h" +#include "network/packet.h" +#include "network/room_member.h" + +namespace Network { + +constexpr u32 ConnectionTimeoutMs = 5000; + +class RoomMember::RoomMemberImpl { +public: + ENetHost* client = nullptr; ///< ENet network interface. + ENetPeer* server = nullptr; ///< The server peer the client is connected to + + /// Information about the clients connected to the same room as us. + MemberList member_information; + /// Information about the room we're connected to. + RoomInformation room_information; + + /// The current game name, id and version + GameInfo current_game_info; + + std::atomic<State> state{State::Idle}; ///< Current state of the RoomMember. + void SetState(const State new_state); + void SetError(const Error new_error); + bool IsConnected() const; + + std::string nickname; ///< The nickname of this member. + + std::string username; ///< The username of this member. + mutable std::mutex username_mutex; ///< Mutex for locking username. + + MacAddress mac_address; ///< The mac_address of this member. + + std::mutex network_mutex; ///< Mutex that controls access to the `client` variable. + /// Thread that receives and dispatches network packets + std::unique_ptr<std::thread> loop_thread; + std::mutex send_list_mutex; ///< Mutex that controls access to the `send_list` variable. + std::list<Packet> send_list; ///< A list that stores all packets to send the async + + template <typename T> + using CallbackSet = std::set<CallbackHandle<T>>; + std::mutex callback_mutex; ///< The mutex used for handling callbacks + + class Callbacks { + public: + template <typename T> + CallbackSet<T>& Get(); + + private: + CallbackSet<WifiPacket> callback_set_wifi_packet; + CallbackSet<ChatEntry> callback_set_chat_messages; + CallbackSet<StatusMessageEntry> callback_set_status_messages; + CallbackSet<RoomInformation> callback_set_room_information; + CallbackSet<State> callback_set_state; + CallbackSet<Error> callback_set_error; + CallbackSet<Room::BanList> callback_set_ban_list; + }; + Callbacks callbacks; ///< All CallbackSets to all events + + void MemberLoop(); + + void StartLoop(); + + /** + * Sends data to the room. It will be send on channel 0 with flag RELIABLE + * @param packet The data to send + */ + void Send(Packet&& packet); + + /** + * Sends a request to the server, asking for permission to join a room with the specified + * nickname and preferred mac. + * @params nickname The desired nickname. + * @params console_id_hash A hash of the Console ID. + * @params preferred_mac The preferred MAC address to use in the room, the NoPreferredMac tells + * @params password The password for the room + * the server to assign one for us. + */ + void SendJoinRequest(const std::string& nickname_, const std::string& console_id_hash, + const MacAddress& preferred_mac = NoPreferredMac, + const std::string& password = "", const std::string& token = ""); + + /** + * Extracts a MAC Address from a received ENet packet. + * @param event The ENet event that was received. + */ + void HandleJoinPacket(const ENetEvent* event); + /** + * Extracts RoomInformation and MemberInformation from a received ENet packet. + * @param event The ENet event that was received. + */ + void HandleRoomInformationPacket(const ENetEvent* event); + + /** + * Extracts a WifiPacket from a received ENet packet. + * @param event The ENet event that was received. + */ + void HandleWifiPackets(const ENetEvent* event); + + /** + * Extracts a chat entry from a received ENet packet and adds it to the chat queue. + * @param event The ENet event that was received. + */ + void HandleChatPacket(const ENetEvent* event); + + /** + * Extracts a system message entry from a received ENet packet and adds it to the system message + * queue. + * @param event The ENet event that was received. + */ + void HandleStatusMessagePacket(const ENetEvent* event); + + /** + * Extracts a ban list request response from a received ENet packet. + * @param event The ENet event that was received. + */ + void HandleModBanListResponsePacket(const ENetEvent* event); + + /** + * Disconnects the RoomMember from the Room + */ + void Disconnect(); + + template <typename T> + void Invoke(const T& data); + + template <typename T> + CallbackHandle<T> Bind(std::function<void(const T&)> callback); +}; + +// RoomMemberImpl +void RoomMember::RoomMemberImpl::SetState(const State new_state) { + if (state != new_state) { + state = new_state; + Invoke<State>(state); + } +} + +void RoomMember::RoomMemberImpl::SetError(const Error new_error) { + Invoke<Error>(new_error); +} + +bool RoomMember::RoomMemberImpl::IsConnected() const { + return state == State::Joining || state == State::Joined || state == State::Moderator; +} + +void RoomMember::RoomMemberImpl::MemberLoop() { + // Receive packets while the connection is open + while (IsConnected()) { + std::lock_guard lock(network_mutex); + ENetEvent event; + if (enet_host_service(client, &event, 16) > 0) { + switch (event.type) { + case ENET_EVENT_TYPE_RECEIVE: + switch (event.packet->data[0]) { + case IdWifiPacket: + HandleWifiPackets(&event); + break; + case IdChatMessage: + HandleChatPacket(&event); + break; + case IdStatusMessage: + HandleStatusMessagePacket(&event); + break; + case IdRoomInformation: + HandleRoomInformationPacket(&event); + break; + case IdJoinSuccess: + case IdJoinSuccessAsMod: + // The join request was successful, we are now in the room. + // If we joined successfully, there must be at least one client in the room: us. + ASSERT_MSG(member_information.size() > 0, + "We have not yet received member information."); + HandleJoinPacket(&event); // Get the MAC Address for the client + if (event.packet->data[0] == IdJoinSuccessAsMod) { + SetState(State::Moderator); + } else { + SetState(State::Joined); + } + break; + case IdModBanListResponse: + HandleModBanListResponsePacket(&event); + break; + case IdRoomIsFull: + SetState(State::Idle); + SetError(Error::RoomIsFull); + break; + case IdNameCollision: + SetState(State::Idle); + SetError(Error::NameCollision); + break; + case IdMacCollision: + SetState(State::Idle); + SetError(Error::MacCollision); + break; + case IdConsoleIdCollision: + SetState(State::Idle); + SetError(Error::ConsoleIdCollision); + break; + case IdVersionMismatch: + SetState(State::Idle); + SetError(Error::WrongVersion); + break; + case IdWrongPassword: + SetState(State::Idle); + SetError(Error::WrongPassword); + break; + case IdCloseRoom: + SetState(State::Idle); + SetError(Error::LostConnection); + break; + case IdHostKicked: + SetState(State::Idle); + SetError(Error::HostKicked); + break; + case IdHostBanned: + SetState(State::Idle); + SetError(Error::HostBanned); + break; + case IdModPermissionDenied: + SetError(Error::PermissionDenied); + break; + case IdModNoSuchUser: + SetError(Error::NoSuchUser); + break; + } + enet_packet_destroy(event.packet); + break; + case ENET_EVENT_TYPE_DISCONNECT: + if (state == State::Joined || state == State::Moderator) { + SetState(State::Idle); + SetError(Error::LostConnection); + } + break; + case ENET_EVENT_TYPE_NONE: + break; + case ENET_EVENT_TYPE_CONNECT: + // The ENET_EVENT_TYPE_CONNECT event can not possibly happen here because we're + // already connected + ASSERT_MSG(false, "Received unexpected connect event while already connected"); + break; + } + } + std::list<Packet> packets; + { + std::lock_guard send_lock(send_list_mutex); + packets.swap(send_list); + } + for (const auto& packet : packets) { + ENetPacket* enetPacket = enet_packet_create(packet.GetData(), packet.GetDataSize(), + ENET_PACKET_FLAG_RELIABLE); + enet_peer_send(server, 0, enetPacket); + } + enet_host_flush(client); + } + Disconnect(); +}; + +void RoomMember::RoomMemberImpl::StartLoop() { + loop_thread = std::make_unique<std::thread>(&RoomMember::RoomMemberImpl::MemberLoop, this); +} + +void RoomMember::RoomMemberImpl::Send(Packet&& packet) { + std::lock_guard lock(send_list_mutex); + send_list.push_back(std::move(packet)); +} + +void RoomMember::RoomMemberImpl::SendJoinRequest(const std::string& nickname_, + const std::string& console_id_hash, + const MacAddress& preferred_mac, + const std::string& password, + const std::string& token) { + Packet packet; + packet.Write(static_cast<u8>(IdJoinRequest)); + packet.Write(nickname_); + packet.Write(console_id_hash); + packet.Write(preferred_mac); + packet.Write(network_version); + packet.Write(password); + packet.Write(token); + Send(std::move(packet)); +} + +void RoomMember::RoomMemberImpl::HandleRoomInformationPacket(const ENetEvent* event) { + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + + // Ignore the first byte, which is the message id. + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + + RoomInformation info{}; + packet.Read(info.name); + packet.Read(info.description); + packet.Read(info.member_slots); + packet.Read(info.port); + packet.Read(info.preferred_game.name); + packet.Read(info.host_username); + room_information.name = info.name; + room_information.description = info.description; + room_information.member_slots = info.member_slots; + room_information.port = info.port; + room_information.preferred_game = info.preferred_game; + room_information.host_username = info.host_username; + + u32 num_members; + packet.Read(num_members); + member_information.resize(num_members); + + for (auto& member : member_information) { + packet.Read(member.nickname); + packet.Read(member.mac_address); + packet.Read(member.game_info.name); + packet.Read(member.game_info.id); + packet.Read(member.username); + packet.Read(member.display_name); + packet.Read(member.avatar_url); + + { + std::lock_guard lock(username_mutex); + if (member.nickname == nickname) { + username = member.username; + } + } + } + Invoke(room_information); +} + +void RoomMember::RoomMemberImpl::HandleJoinPacket(const ENetEvent* event) { + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + + // Ignore the first byte, which is the message id. + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + + // Parse the MAC Address from the packet + packet.Read(mac_address); +} + +void RoomMember::RoomMemberImpl::HandleWifiPackets(const ENetEvent* event) { + WifiPacket wifi_packet{}; + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + + // Ignore the first byte, which is the message id. + packet.IgnoreBytes(sizeof(u8)); // Ignore the message type + + // Parse the WifiPacket from the packet + u8 frame_type; + packet.Read(frame_type); + WifiPacket::PacketType type = static_cast<WifiPacket::PacketType>(frame_type); + + wifi_packet.type = type; + packet.Read(wifi_packet.channel); + packet.Read(wifi_packet.transmitter_address); + packet.Read(wifi_packet.destination_address); + packet.Read(wifi_packet.data); + + Invoke<WifiPacket>(wifi_packet); +} + +void RoomMember::RoomMemberImpl::HandleChatPacket(const ENetEvent* event) { + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + + // Ignore the first byte, which is the message id. + packet.IgnoreBytes(sizeof(u8)); + + ChatEntry chat_entry{}; + packet.Read(chat_entry.nickname); + packet.Read(chat_entry.username); + packet.Read(chat_entry.message); + Invoke<ChatEntry>(chat_entry); +} + +void RoomMember::RoomMemberImpl::HandleStatusMessagePacket(const ENetEvent* event) { + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + + // Ignore the first byte, which is the message id. + packet.IgnoreBytes(sizeof(u8)); + + StatusMessageEntry status_message_entry{}; + u8 type{}; + packet.Read(type); + status_message_entry.type = static_cast<StatusMessageTypes>(type); + packet.Read(status_message_entry.nickname); + packet.Read(status_message_entry.username); + Invoke<StatusMessageEntry>(status_message_entry); +} + +void RoomMember::RoomMemberImpl::HandleModBanListResponsePacket(const ENetEvent* event) { + Packet packet; + packet.Append(event->packet->data, event->packet->dataLength); + + // Ignore the first byte, which is the message id. + packet.IgnoreBytes(sizeof(u8)); + + Room::BanList ban_list = {}; + packet.Read(ban_list.first); + packet.Read(ban_list.second); + Invoke<Room::BanList>(ban_list); +} + +void RoomMember::RoomMemberImpl::Disconnect() { + member_information.clear(); + room_information.member_slots = 0; + room_information.name.clear(); + + if (!server) { + return; + } + enet_peer_disconnect(server, 0); + + ENetEvent event; + while (enet_host_service(client, &event, ConnectionTimeoutMs) > 0) { + switch (event.type) { + case ENET_EVENT_TYPE_RECEIVE: + enet_packet_destroy(event.packet); // Ignore all incoming data + break; + case ENET_EVENT_TYPE_DISCONNECT: + server = nullptr; + return; + case ENET_EVENT_TYPE_NONE: + case ENET_EVENT_TYPE_CONNECT: + break; + } + } + // didn't disconnect gracefully force disconnect + enet_peer_reset(server); + server = nullptr; +} + +template <> +RoomMember::RoomMemberImpl::CallbackSet<WifiPacket>& RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_wifi_packet; +} + +template <> +RoomMember::RoomMemberImpl::CallbackSet<RoomMember::State>& +RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_state; +} + +template <> +RoomMember::RoomMemberImpl::CallbackSet<RoomMember::Error>& +RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_error; +} + +template <> +RoomMember::RoomMemberImpl::CallbackSet<RoomInformation>& +RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_room_information; +} + +template <> +RoomMember::RoomMemberImpl::CallbackSet<ChatEntry>& RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_chat_messages; +} + +template <> +RoomMember::RoomMemberImpl::CallbackSet<StatusMessageEntry>& +RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_status_messages; +} + +template <> +RoomMember::RoomMemberImpl::CallbackSet<Room::BanList>& +RoomMember::RoomMemberImpl::Callbacks::Get() { + return callback_set_ban_list; +} + +template <typename T> +void RoomMember::RoomMemberImpl::Invoke(const T& data) { + std::lock_guard lock(callback_mutex); + CallbackSet<T> callback_set = callbacks.Get<T>(); + for (auto const& callback : callback_set) { + (*callback)(data); + } +} + +template <typename T> +RoomMember::CallbackHandle<T> RoomMember::RoomMemberImpl::Bind( + std::function<void(const T&)> callback) { + std::lock_guard lock(callback_mutex); + CallbackHandle<T> handle; + handle = std::make_shared<std::function<void(const T&)>>(callback); + callbacks.Get<T>().insert(handle); + return handle; +} + +// RoomMember +RoomMember::RoomMember() : room_member_impl{std::make_unique<RoomMemberImpl>()} {} + +RoomMember::~RoomMember() { + ASSERT_MSG(!IsConnected(), "RoomMember is being destroyed while connected"); + if (room_member_impl->loop_thread) { + Leave(); + } +} + +RoomMember::State RoomMember::GetState() const { + return room_member_impl->state; +} + +const RoomMember::MemberList& RoomMember::GetMemberInformation() const { + return room_member_impl->member_information; +} + +const std::string& RoomMember::GetNickname() const { + return room_member_impl->nickname; +} + +const std::string& RoomMember::GetUsername() const { + std::lock_guard lock(room_member_impl->username_mutex); + return room_member_impl->username; +} + +const MacAddress& RoomMember::GetMacAddress() const { + ASSERT_MSG(IsConnected(), "Tried to get MAC address while not connected"); + return room_member_impl->mac_address; +} + +RoomInformation RoomMember::GetRoomInformation() const { + return room_member_impl->room_information; +} + +void RoomMember::Join(const std::string& nick, const std::string& console_id_hash, + const char* server_addr, u16 server_port, u16 client_port, + const MacAddress& preferred_mac, const std::string& password, + const std::string& token) { + // If the member is connected, kill the connection first + if (room_member_impl->loop_thread && room_member_impl->loop_thread->joinable()) { + Leave(); + } + // If the thread isn't running but the ptr still exists, reset it + else if (room_member_impl->loop_thread) { + room_member_impl->loop_thread.reset(); + } + + if (!room_member_impl->client) { + room_member_impl->client = enet_host_create(nullptr, 1, NumChannels, 0, 0); + ASSERT_MSG(room_member_impl->client != nullptr, "Could not create client"); + } + + room_member_impl->SetState(State::Joining); + + ENetAddress address{}; + enet_address_set_host(&address, server_addr); + address.port = server_port; + room_member_impl->server = + enet_host_connect(room_member_impl->client, &address, NumChannels, 0); + + if (!room_member_impl->server) { + room_member_impl->SetState(State::Idle); + room_member_impl->SetError(Error::UnknownError); + return; + } + + ENetEvent event{}; + int net = enet_host_service(room_member_impl->client, &event, ConnectionTimeoutMs); + if (net > 0 && event.type == ENET_EVENT_TYPE_CONNECT) { + room_member_impl->nickname = nick; + room_member_impl->StartLoop(); + room_member_impl->SendJoinRequest(nick, console_id_hash, preferred_mac, password, token); + SendGameInfo(room_member_impl->current_game_info); + } else { + enet_peer_disconnect(room_member_impl->server, 0); + room_member_impl->SetState(State::Idle); + room_member_impl->SetError(Error::CouldNotConnect); + } +} + +bool RoomMember::IsConnected() const { + return room_member_impl->IsConnected(); +} + +void RoomMember::SendWifiPacket(const WifiPacket& wifi_packet) { + Packet packet; + packet.Write(static_cast<u8>(IdWifiPacket)); + packet.Write(static_cast<u8>(wifi_packet.type)); + packet.Write(wifi_packet.channel); + packet.Write(wifi_packet.transmitter_address); + packet.Write(wifi_packet.destination_address); + packet.Write(wifi_packet.data); + room_member_impl->Send(std::move(packet)); +} + +void RoomMember::SendChatMessage(const std::string& message) { + Packet packet; + packet.Write(static_cast<u8>(IdChatMessage)); + packet.Write(message); + room_member_impl->Send(std::move(packet)); +} + +void RoomMember::SendGameInfo(const GameInfo& game_info) { + room_member_impl->current_game_info = game_info; + if (!IsConnected()) + return; + + Packet packet; + packet.Write(static_cast<u8>(IdSetGameInfo)); + packet.Write(game_info.name); + packet.Write(game_info.id); + room_member_impl->Send(std::move(packet)); +} + +void RoomMember::SendModerationRequest(RoomMessageTypes type, const std::string& nickname) { + ASSERT_MSG(type == IdModKick || type == IdModBan || type == IdModUnban, + "type is not a moderation request"); + if (!IsConnected()) + return; + + Packet packet; + packet.Write(static_cast<u8>(type)); + packet.Write(nickname); + room_member_impl->Send(std::move(packet)); +} + +void RoomMember::RequestBanList() { + if (!IsConnected()) + return; + + Packet packet; + packet.Write(static_cast<u8>(IdModGetBanList)); + room_member_impl->Send(std::move(packet)); +} + +RoomMember::CallbackHandle<RoomMember::State> RoomMember::BindOnStateChanged( + std::function<void(const RoomMember::State&)> callback) { + return room_member_impl->Bind(callback); +} + +RoomMember::CallbackHandle<RoomMember::Error> RoomMember::BindOnError( + std::function<void(const RoomMember::Error&)> callback) { + return room_member_impl->Bind(callback); +} + +RoomMember::CallbackHandle<WifiPacket> RoomMember::BindOnWifiPacketReceived( + std::function<void(const WifiPacket&)> callback) { + return room_member_impl->Bind(callback); +} + +RoomMember::CallbackHandle<RoomInformation> RoomMember::BindOnRoomInformationChanged( + std::function<void(const RoomInformation&)> callback) { + return room_member_impl->Bind(callback); +} + +RoomMember::CallbackHandle<ChatEntry> RoomMember::BindOnChatMessageRecieved( + std::function<void(const ChatEntry&)> callback) { + return room_member_impl->Bind(callback); +} + +RoomMember::CallbackHandle<StatusMessageEntry> RoomMember::BindOnStatusMessageReceived( + std::function<void(const StatusMessageEntry&)> callback) { + return room_member_impl->Bind(callback); +} + +RoomMember::CallbackHandle<Room::BanList> RoomMember::BindOnBanListReceived( + std::function<void(const Room::BanList&)> callback) { + return room_member_impl->Bind(callback); +} + +template <typename T> +void RoomMember::Unbind(CallbackHandle<T> handle) { + std::lock_guard lock(room_member_impl->callback_mutex); + room_member_impl->callbacks.Get<T>().erase(handle); +} + +void RoomMember::Leave() { + room_member_impl->SetState(State::Idle); + room_member_impl->loop_thread->join(); + room_member_impl->loop_thread.reset(); + + enet_host_destroy(room_member_impl->client); + room_member_impl->client = nullptr; +} + +template void RoomMember::Unbind(CallbackHandle<WifiPacket>); +template void RoomMember::Unbind(CallbackHandle<RoomMember::State>); +template void RoomMember::Unbind(CallbackHandle<RoomMember::Error>); +template void RoomMember::Unbind(CallbackHandle<RoomInformation>); +template void RoomMember::Unbind(CallbackHandle<ChatEntry>); +template void RoomMember::Unbind(CallbackHandle<StatusMessageEntry>); +template void RoomMember::Unbind(CallbackHandle<Room::BanList>); + +} // namespace Network diff --git a/src/network/room_member.h b/src/network/room_member.h new file mode 100644 index 000000000..bbb7d13d4 --- /dev/null +++ b/src/network/room_member.h @@ -0,0 +1,318 @@ +// SPDX-FileCopyrightText: Copyright 2017 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <functional> +#include <memory> +#include <string> +#include <vector> +#include "common/announce_multiplayer_room.h" +#include "common/common_types.h" +#include "network/room.h" + +namespace Network { + +using AnnounceMultiplayerRoom::GameInfo; +using AnnounceMultiplayerRoom::RoomInformation; + +/// Information about the received WiFi packets. +/// Acts as our own 802.11 header. +struct WifiPacket { + enum class PacketType : u8 { + Beacon, + Data, + Authentication, + AssociationResponse, + Deauthentication, + NodeMap + }; + PacketType type; ///< The type of 802.11 frame. + std::vector<u8> data; ///< Raw 802.11 frame data, starting at the management frame header + /// for management frames. + MacAddress transmitter_address; ///< Mac address of the transmitter. + MacAddress destination_address; ///< Mac address of the receiver. + u8 channel; ///< WiFi channel where this frame was transmitted. +}; + +/// Represents a chat message. +struct ChatEntry { + std::string nickname; ///< Nickname of the client who sent this message. + /// Web services username of the client who sent this message, can be empty. + std::string username; + std::string message; ///< Body of the message. +}; + +/// Represents a system status message. +struct StatusMessageEntry { + StatusMessageTypes type; ///< Type of the message + /// Subject of the message. i.e. the user who is joining/leaving/being banned, etc. + std::string nickname; + std::string username; +}; + +/** + * This is what a client [person joining a server] would use. + * It also has to be used if you host a game yourself (You'd create both, a Room and a + * RoomMembership for yourself) + */ +class RoomMember final { +public: + enum class State : u8 { + Uninitialized, ///< Not initialized + Idle, ///< Default state (i.e. not connected) + Joining, ///< The client is attempting to join a room. + Joined, ///< The client is connected to the room and is ready to send/receive packets. + Moderator, ///< The client is connnected to the room and is granted mod permissions. + }; + + enum class Error : u8 { + // Reasons why connection was closed + LostConnection, ///< Connection closed + HostKicked, ///< Kicked by the host + + // Reasons why connection was rejected + UnknownError, ///< Some error [permissions to network device missing or something] + NameCollision, ///< Somebody is already using this name + MacCollision, ///< Somebody is already using that mac-address + ConsoleIdCollision, ///< Somebody in the room has the same Console ID + WrongVersion, ///< The room version is not the same as for this RoomMember + WrongPassword, ///< The password doesn't match the one from the Room + CouldNotConnect, ///< The room is not responding to a connection attempt + RoomIsFull, ///< Room is already at the maximum number of players + HostBanned, ///< The user is banned by the host + + // Reasons why moderation request failed + PermissionDenied, ///< The user does not have mod permissions + NoSuchUser, ///< The nickname the user attempts to kick/ban does not exist + }; + + struct MemberInformation { + std::string nickname; ///< Nickname of the member. + std::string username; ///< The web services username of the member. Can be empty. + std::string display_name; ///< The web services display name of the member. Can be empty. + std::string avatar_url; ///< Url to the member's avatar. Can be empty. + GameInfo game_info; ///< Name of the game they're currently playing, or empty if they're + /// not playing anything. + MacAddress mac_address; ///< MAC address associated with this member. + }; + using MemberList = std::vector<MemberInformation>; + + // The handle for the callback functions + template <typename T> + using CallbackHandle = std::shared_ptr<std::function<void(const T&)>>; + + /** + * Unbinds a callback function from the events. + * @param handle The connection handle to disconnect + */ + template <typename T> + void Unbind(CallbackHandle<T> handle); + + RoomMember(); + ~RoomMember(); + + /** + * Returns the status of our connection to the room. + */ + State GetState() const; + + /** + * Returns information about the members in the room we're currently connected to. + */ + const MemberList& GetMemberInformation() const; + + /** + * Returns the nickname of the RoomMember. + */ + const std::string& GetNickname() const; + + /** + * Returns the username of the RoomMember. + */ + const std::string& GetUsername() const; + + /** + * Returns the MAC address of the RoomMember. + */ + const MacAddress& GetMacAddress() const; + + /** + * Returns information about the room we're currently connected to. + */ + RoomInformation GetRoomInformation() const; + + /** + * Returns whether we're connected to a server or not. + */ + bool IsConnected() const; + + /** + * Attempts to join a room at the specified address and port, using the specified nickname. + * A console ID hash is passed in to check console ID conflicts. + * This may fail if the username or console ID is already taken. + */ + void Join(const std::string& nickname, const std::string& console_id_hash, + const char* server_addr = "127.0.0.1", u16 server_port = DefaultRoomPort, + u16 client_port = 0, const MacAddress& preferred_mac = NoPreferredMac, + const std::string& password = "", const std::string& token = ""); + + /** + * Sends a WiFi packet to the room. + * @param packet The WiFi packet to send. + */ + void SendWifiPacket(const WifiPacket& packet); + + /** + * Sends a chat message to the room. + * @param message The contents of the message. + */ + void SendChatMessage(const std::string& message); + + /** + * Sends the current game info to the room. + * @param game_info The game information. + */ + void SendGameInfo(const GameInfo& game_info); + + /** + * Sends a moderation request to the room. + * @param type Moderation request type. + * @param nickname The subject of the request. (i.e. the user you want to kick/ban) + */ + void SendModerationRequest(RoomMessageTypes type, const std::string& nickname); + + /** + * Attempts to retrieve ban list from the room. + * If success, the ban list callback would be called. Otherwise an error would be emitted. + */ + void RequestBanList(); + + /** + * Binds a function to an event that will be triggered every time the State of the member + * changed. The function wil be called every time the event is triggered. The callback function + * must not bind or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle<State> BindOnStateChanged(std::function<void(const State&)> callback); + + /** + * Binds a function to an event that will be triggered every time an error happened. The + * function wil be called every time the event is triggered. The callback function must not bind + * or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle<Error> BindOnError(std::function<void(const Error&)> callback); + + /** + * Binds a function to an event that will be triggered every time a WifiPacket is received. + * The function wil be called everytime the event is triggered. + * The callback function must not bind or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle<WifiPacket> BindOnWifiPacketReceived( + std::function<void(const WifiPacket&)> callback); + + /** + * Binds a function to an event that will be triggered every time the RoomInformation changes. + * The function wil be called every time the event is triggered. + * The callback function must not bind or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle<RoomInformation> BindOnRoomInformationChanged( + std::function<void(const RoomInformation&)> callback); + + /** + * Binds a function to an event that will be triggered every time a ChatMessage is received. + * The function wil be called every time the event is triggered. + * The callback function must not bind or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle<ChatEntry> BindOnChatMessageRecieved( + std::function<void(const ChatEntry&)> callback); + + /** + * Binds a function to an event that will be triggered every time a StatusMessage is + * received. The function will be called every time the event is triggered. The callback + * function must not bind or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle<StatusMessageEntry> BindOnStatusMessageReceived( + std::function<void(const StatusMessageEntry&)> callback); + + /** + * Binds a function to an event that will be triggered every time a requested ban list + * received. The function will be called every time the event is triggered. The callback + * function must not bind or unbind a function. Doing so will cause a deadlock + * @param callback The function to call + * @return A handle used for removing the function from the registered list + */ + CallbackHandle<Room::BanList> BindOnBanListReceived( + std::function<void(const Room::BanList&)> callback); + + /** + * Leaves the current room. + */ + void Leave(); + +private: + class RoomMemberImpl; + std::unique_ptr<RoomMemberImpl> room_member_impl; +}; + +inline const char* GetStateStr(const RoomMember::State& s) { + switch (s) { + case RoomMember::State::Uninitialized: + return "Uninitialized"; + case RoomMember::State::Idle: + return "Idle"; + case RoomMember::State::Joining: + return "Joining"; + case RoomMember::State::Joined: + return "Joined"; + case RoomMember::State::Moderator: + return "Moderator"; + } + return "Unknown"; +} + +inline const char* GetErrorStr(const RoomMember::Error& e) { + switch (e) { + case RoomMember::Error::LostConnection: + return "LostConnection"; + case RoomMember::Error::HostKicked: + return "HostKicked"; + case RoomMember::Error::UnknownError: + return "UnknownError"; + case RoomMember::Error::NameCollision: + return "NameCollision"; + case RoomMember::Error::MacCollision: + return "MaxCollision"; + case RoomMember::Error::ConsoleIdCollision: + return "ConsoleIdCollision"; + case RoomMember::Error::WrongVersion: + return "WrongVersion"; + case RoomMember::Error::WrongPassword: + return "WrongPassword"; + case RoomMember::Error::CouldNotConnect: + return "CouldNotConnect"; + case RoomMember::Error::RoomIsFull: + return "RoomIsFull"; + case RoomMember::Error::HostBanned: + return "HostBanned"; + case RoomMember::Error::PermissionDenied: + return "PermissionDenied"; + case RoomMember::Error::NoSuchUser: + return "NoSuchUser"; + default: + return "Unknown"; + } +} + +} // namespace Network diff --git a/src/network/verify_user.cpp b/src/network/verify_user.cpp new file mode 100644 index 000000000..f84cfe59b --- /dev/null +++ b/src/network/verify_user.cpp @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "network/verify_user.h" + +namespace Network::VerifyUser { + +Backend::~Backend() = default; + +NullBackend::~NullBackend() = default; + +UserData NullBackend::LoadUserData([[maybe_unused]] const std::string& verify_uid, + [[maybe_unused]] const std::string& token) { + return {}; +} + +} // namespace Network::VerifyUser diff --git a/src/network/verify_user.h b/src/network/verify_user.h new file mode 100644 index 000000000..6fc64d8a3 --- /dev/null +++ b/src/network/verify_user.h @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: Copyright 2018 Citra Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include <string> +#include "common/logging/log.h" + +namespace Network::VerifyUser { + +struct UserData { + std::string username; + std::string display_name; + std::string avatar_url; + bool moderator = false; ///< Whether the user is a yuzu Moderator. +}; + +/** + * A backend used for verifying users and loading user data. + */ +class Backend { +public: + virtual ~Backend(); + + /** + * Verifies the given token and loads the information into a UserData struct. + * @param verify_uid A GUID that may be used for verification. + * @param token A token that contains user data and verification data. The format and content is + * decided by backends. + */ + virtual UserData LoadUserData(const std::string& verify_uid, const std::string& token) = 0; +}; + +/** + * A null backend where the token is ignored. + * No verification is performed here and the function returns an empty UserData. + */ +class NullBackend final : public Backend { +public: + ~NullBackend(); + + UserData LoadUserData(const std::string& verify_uid, const std::string& token) override; +}; + +} // namespace Network::VerifyUser |