network/room: Moderation implementation

Currently consist of 4 moderation commands (kick, ban, unban and get ban list).
This commit is contained in:
zhupengfei 2018-11-24 16:08:32 +08:00
parent 6c29d441f4
commit 38f86cce94
2 changed files with 357 additions and 6 deletions

View file

@ -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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<u8>(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<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 << 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 << 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 << 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 << static_cast<u8>(IdModBanListResponse);
{
std::lock_guard<std::mutex> 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<u8>(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<u32>(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<VerifyUser::Backend> verify_backend) {
const u32 max_connections, const std::string& host_username,
const std::string& preferred_game, u64 preferred_game_id,
std::unique_ptr<VerifyUser::Backend> 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<std::mutex> lock(room_impl->ban_list_mutex);
return {room_impl->username_ban_list, room_impl->ip_ban_list};
}
std::vector<Room::Member> Room::GetRoomMemberList() const {
std::vector<Room::Member> member_list;
std::lock_guard<std::mutex> lock(room_impl->member_mutex);

View file

@ -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<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.
@ -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<VerifyUser::Backend> verify_backend = nullptr);
const std::string& host_username = "", const std::string& preferred_game = "",
u64 preferred_game_id = 0,
std::unique_ptr<VerifyUser::Backend> 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
*/