diff --git a/src/network/room.cpp b/src/network/room.cpp index ee62df220..394e8644f 100644 --- a/src/network/room.cpp +++ b/src/network/room.cpp @@ -48,6 +48,10 @@ public: mutable std::mutex member_mutex; ///< Mutex for locking the members list /// This should be a std::shared_mutex as soon as C++17 is supported + 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() : random_gen(std::random_device()()), NintendoOUI{0x00, 0x1F, 0x32, 0x00, 0x00, 0x00} {} @@ -68,6 +72,30 @@ public: */ 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. */ @@ -85,6 +113,11 @@ public: */ 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. */ @@ -122,6 +155,32 @@ public: */ void SendJoinSuccess(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, */ @@ -202,6 +261,19 @@ void Room::RoomImpl::ServerLoop() { 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; @@ -296,6 +368,29 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { } member.user_data = verify_backend->LoadUserData(uid, token); + { + 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 + char ip_raw[256]; + enet_address_get_host_ip(&event->peer->address, ip_raw, sizeof(ip_raw) - 1); + std::string ip = ip_raw; + + 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); @@ -309,6 +404,153 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) { 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 >> nickname; + + std::string username; + { + 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; + + enet_peer_disconnect(target_member->peer, 0); + members.erase(target_member); + } + + // Announce the change to all clients. + SendStatusMessage(IdMemberKicked, nickname, username); + 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 >> nickname; + + std::string username; + std::string 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; + + char ip_raw[256]; + enet_address_get_host_ip(&target_member->peer->address, ip_raw, sizeof(ip_raw) - 1); + ip = ip_raw; + + 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); + 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 >> 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. @@ -336,6 +578,22 @@ bool Room::RoomImpl::IsValidConsoleId(const std::string& console_id_hash) const }); } +bool Room::RoomImpl::HasModPermission(const ENetPeer* client) const { + if (room_information.host_username.empty()) + return false; // This room does not support moderation + 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 (sending_member->user_data.username != room_information.host_username) { + return false; + } + return true; +} + void Room::RoomImpl::SendNameCollision(ENetPeer* client) { Packet packet; packet << static_cast(IdNameCollision); @@ -407,6 +665,61 @@ void Room::RoomImpl::SendJoinSuccess(ENetPeer* client, MacAddress mac_address) { enet_host_flush(server); } +void Room::RoomImpl::SendUserKicked(ENetPeer* client) { + Packet packet; + packet << static_cast(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 << static_cast(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 << static_cast(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 << static_cast(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 << static_cast(IdModBanListResponse); + { + std::lock_guard lock(ban_list_mutex); + packet << username_ban_list; + packet << 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 << static_cast(IdCloseRoom); @@ -450,6 +763,7 @@ void Room::RoomImpl::BroadcastRoomInformation() { packet << room_information.member_slots; packet << room_information.port; packet << room_information.preferred_game; + packet << room_information.host_username; packet << static_cast(members.size()); { @@ -625,8 +939,10 @@ 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& preferred_game, - u64 preferred_game_id, std::unique_ptr verify_backend) { + const u32 max_connections, const std::string& host_username, + const std::string& preferred_game, u64 preferred_game_id, + std::unique_ptr verify_backend, + const Room::BanList& ban_list) { ENetAddress address; address.host = ENET_HOST_ANY; if (!server_address.empty()) { @@ -648,8 +964,11 @@ bool Room::Create(const std::string& name, const std::string& description, room_impl->room_information.port = server_port; room_impl->room_information.preferred_game = preferred_game; room_impl->room_information.preferred_game_id = preferred_game_id; + room_impl->room_information.host_username = host_username; 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; @@ -668,6 +987,11 @@ std::string Room::GetVerifyUID() const { 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 Room::GetRoomMemberList() const { std::vector member_list; std::lock_guard lock(room_impl->member_mutex); diff --git a/src/network/room.h b/src/network/room.h index a3d93eea9..3181e84d7 100644 --- a/src/network/room.h +++ b/src/network/room.h @@ -31,6 +31,7 @@ struct RoomInformation { u16 port; ///< The port of this room std::string preferred_game; ///< Game to advertise that you want to play u64 preferred_game_id; ///< Title ID for the advertised game + std::string host_username; ///< Forum username of the host }; struct GameInfo { @@ -62,12 +63,26 @@ enum RoomMessageTypes : u8 { IdRoomIsFull, IdConsoleIdCollision, IdStatusMessage, + IdHostKicked, + IdHostBanned, + /// Moderation requests + IdModKick, + IdModBan, + IdModUnban, + IdModGetBanList, + // Moderation responses + IdModBanListResponse, + IdModPermissionDenied, + IdModNoSuchUser, }; /// Types of system status messages enum StatusMessageTypes : u8 { - IdMemberJoin = 1, ///< Member joining - IdMemberLeave, ///< Member leaving + 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. @@ -115,6 +130,11 @@ public: */ bool HasPassword() const; + using UsernameBanList = std::vector; + using IPBanList = std::vector; + + using BanList = std::pair; + /** * Creates the socket for this room. Will bind to default address if * server is empty string. @@ -123,14 +143,21 @@ public: const std::string& server = "", u16 server_port = DefaultRoomPort, const std::string& password = "", const u32 max_connections = MaxConcurrentConnections, - const std::string& preferred_game = "", u64 preferred_game_id = 0, - std::unique_ptr verify_backend = nullptr); + const std::string& host_username = "", const std::string& preferred_game = "", + u64 preferred_game_id = 0, + std::unique_ptr verify_backend = nullptr, + const BanList& ban_list = {}); /** * 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 */