Changeset View
Changeset View
Standalone View
Standalone View
source/network/NetServer.cpp
Show First 20 Lines • Show All 196 Lines • ▼ Show 20 Lines | |||||||||
void CNetServerWorker::SetPassword(const CStr& hashedPassword) | void CNetServerWorker::SetPassword(const CStr& hashedPassword) | ||||||||
{ | { | ||||||||
m_Password = hashedPassword; | m_Password = hashedPassword; | ||||||||
} | } | ||||||||
void CNetServerWorker::SetControllerSecret(const std::string& secret) | void CNetServerWorker::SetControllerSecret(const std::string& secret) | ||||||||
{ | { | ||||||||
m_ControllerSecret = secret; | m_ControllerSecret = secret; | ||||||||
phosit: I prefer `allowlist == "everyone"` | |||||||||
Not Done Inline ActionsI personally prefer enums over arbitrary strings. It is less error prone. Stan: I personally prefer enums over arbitrary strings. It is less error prone.
| |||||||||
} | } | ||||||||
bool CNetServerWorker::CheckPassword(const std::string& password, const std::string& salt) const | bool CNetServerWorker::CheckPassword(const std::string& password, const std::string& salt) const | ||||||||
{ | { | ||||||||
return HashCryptographically(m_Password, salt) == password; | return HashCryptographically(m_Password, salt) == password; | ||||||||
} | } | ||||||||
▲ Show 20 Lines • Show All 637 Lines • ▼ Show 20 Lines | void CNetServerWorker::KickPlayer(const CStrW& playerName, const bool ban) | ||||||||
// Send message notifying other clients | // Send message notifying other clients | ||||||||
CKickedMessage kickedMessage; | CKickedMessage kickedMessage; | ||||||||
kickedMessage.m_Name = playerName; | kickedMessage.m_Name = playerName; | ||||||||
kickedMessage.m_Ban = ban; | kickedMessage.m_Ban = ban; | ||||||||
Broadcast(&kickedMessage, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME }); | Broadcast(&kickedMessage, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME }); | ||||||||
} | } | ||||||||
CStrW CNetServerWorker::SimpleUsername(const CStrW& username, const bool lobbyAuth) | |||||||||
{ | |||||||||
return lobbyAuth ? CStrW(username.substr(0, username.find(L" ("))).LowerCase() : username.LowerCase(); | |||||||||
} | |||||||||
void CNetServerWorker::AssignPlayer(int playerID, const CStr& guid) | void CNetServerWorker::AssignPlayer(int playerID, const CStr& guid) | ||||||||
{ | { | ||||||||
// Remove anyone who's already assigned to this player | // Remove anyone who's already assigned to this player | ||||||||
for (std::pair<const CStr, PlayerAssignment>& p : m_PlayerAssignments) | for (std::pair<const CStr, PlayerAssignment>& p : m_PlayerAssignments) | ||||||||
{ | { | ||||||||
if (p.second.m_PlayerID == playerID) | if (p.second.m_PlayerID == playerID) | ||||||||
p.second.m_PlayerID = -1; | p.second.m_PlayerID = -1; | ||||||||
} | } | ||||||||
▲ Show 20 Lines • Show All 130 Lines • ▼ Show 20 Lines | bool CNetServerWorker::OnAuthenticate(void* context, CFsmEvent* event) | ||||||||
{ | { | ||||||||
LOGERROR("Net server: lobby auth: %s tried joining as %s", | LOGERROR("Net server: lobby auth: %s tried joining as %s", | ||||||||
session->GetUserName().ToUTF8(), | session->GetUserName().ToUTF8(), | ||||||||
usernameWithoutRating.ToUTF8()); | usernameWithoutRating.ToUTF8()); | ||||||||
session->Disconnect(NDR_LOBBY_AUTH_FAILED); | session->Disconnect(NDR_LOBBY_AUTH_FAILED); | ||||||||
return true; | return true; | ||||||||
} | } | ||||||||
// Check the password before anything else. | // Check the server join policy before anything else. Allow users with | ||||||||
// controller secret to join regardless of whether they are allowed by | |||||||||
// the ACLs or not | |||||||||
if (message->m_ControllerSecret != server.m_ControllerSecret && | |||||||||
!server.IsUsernameAllowedByACLs(message->m_Name, "join")) | |||||||||
{ | |||||||||
// LOGWARNING("Net server: user %s tried joining via direct connection but was not allowed due to ACL rules", | |||||||||
LOGWARNING("%s was denied direct join due to ACLs", | |||||||||
utf8_from_wstring(usernameWithoutRating.LowerCase())); | |||||||||
session->Disconnect(NDR_JOIN_PERMISSION_DENIED); | |||||||||
return true; | |||||||||
} | |||||||||
// Check the password | |||||||||
// NB: m_Name must match the client's salt, @see CNetClient::SetGamePassword | // NB: m_Name must match the client's salt, @see CNetClient::SetGamePassword | ||||||||
if (!server.CheckPassword(message->m_Password, message->m_Name.ToUTF8())) | if (!server.CheckPassword(message->m_Password, message->m_Name.ToUTF8())) | ||||||||
{ | { | ||||||||
// Noisy logerror because players are not supposed to be able to get the IP, | // Noisy logerror because players are not supposed to be able to get the IP, | ||||||||
// so this might be someone targeting the host for some reason | // so this might be someone targeting the host for some reason | ||||||||
// (or TODO a dedicated server and we do want to log anyways) | // (or TODO a dedicated server and we do want to log anyways) | ||||||||
LOGERROR("Net server: user %s tried joining with the wrong password", | LOGERROR("Net server: user %s tried joining with the wrong password", | ||||||||
session->GetUserName().ToUTF8()); | session->GetUserName().ToUTF8()); | ||||||||
session->Disconnect(NDR_SERVER_REFUSED); | session->Disconnect(NDR_INCORRECT_PASSWORD); | ||||||||
return true; | return true; | ||||||||
} | } | ||||||||
// Either deduplicate or prohibit join if name is in use | // Either deduplicate or prohibit join if name is in use | ||||||||
bool duplicatePlayernames = false; | bool duplicatePlayernames = false; | ||||||||
CFG_GET_VAL("network.duplicateplayernames", duplicatePlayernames); | CFG_GET_VAL("network.duplicateplayernames", duplicatePlayernames); | ||||||||
// If lobby authentication is enabled, the clients playername has already been registered. | // If lobby authentication is enabled, the clients playername has already been registered. | ||||||||
// There also can't be any duplicated names. | // There also can't be any duplicated names. | ||||||||
▲ Show 20 Lines • Show All 61 Lines • ▼ Show 20 Lines | if (!isRejoining) | ||||||||
CStr observerLateJoin; | CStr observerLateJoin; | ||||||||
CFG_GET_VAL("network.lateobservers", observerLateJoin); | CFG_GET_VAL("network.lateobservers", observerLateJoin); | ||||||||
if (observerLateJoin == "everyone") | if (observerLateJoin == "everyone") | ||||||||
{ | { | ||||||||
isRejoining = true; | isRejoining = true; | ||||||||
} | } | ||||||||
else if (observerLateJoin == "buddies") | else if (observerLateJoin == "buddies") | ||||||||
{ | isRejoining = IsUserInBuddyList(usernameWithoutRating); | ||||||||
CStr buddies; | else if (observerLateJoin == "acls") | ||||||||
CFG_GET_VAL("lobby.buddies", buddies); | isRejoining = server.IsUsernameAllowedByACLs(usernameWithoutRating, "observerlate"); | ||||||||
std::wstringstream buddiesStream(wstring_from_utf8(buddies)); | |||||||||
CStrW buddy; | |||||||||
while (std::getline(buddiesStream, buddy, L',')) | |||||||||
{ | |||||||||
if (buddy == usernameWithoutRating) | |||||||||
{ | |||||||||
isRejoining = true; | |||||||||
break; | |||||||||
} | |||||||||
} | |||||||||
} | |||||||||
} | } | ||||||||
if (!isRejoining) | if (!isRejoining) | ||||||||
{ | { | ||||||||
LOGMESSAGE("Refused connection after game start from not-previously-known user \"%s\"", utf8_from_wstring(username)); | LOGMESSAGE("Refused connection after game start from not-previously-known user \"%s\"", utf8_from_wstring(username)); | ||||||||
session->Disconnect(NDR_SERVER_ALREADY_IN_GAME); | session->Disconnect(NDR_SERVER_ALREADY_IN_GAME); | ||||||||
return true; | return true; | ||||||||
} | } | ||||||||
▲ Show 20 Lines • Show All 75 Lines • ▼ Show 20 Lines | if (Script::HasProperty(rq, settings, "CheatsEnabled")) | ||||||||
Script::GetProperty(rq, settings, "CheatsEnabled", cheatsEnabled); | Script::GetProperty(rq, settings, "CheatsEnabled", cheatsEnabled); | ||||||||
PlayerAssignmentMap::iterator it = server.m_PlayerAssignments.find(session->GetGUID()); | PlayerAssignmentMap::iterator it = server.m_PlayerAssignments.find(session->GetGUID()); | ||||||||
// When cheating is disabled, fail if the player the message claims to | // When cheating is disabled, fail if the player the message claims to | ||||||||
// represent does not exist or does not match the sender's player name | // represent does not exist or does not match the sender's player name | ||||||||
if (!cheatsEnabled && (it == server.m_PlayerAssignments.end() || it->second.m_PlayerID != message->m_Player)) | if (!cheatsEnabled && (it == server.m_PlayerAssignments.end() || it->second.m_PlayerID != message->m_Player)) | ||||||||
return true; | return true; | ||||||||
// Allow map flares only from users with permission from server ACLs | |||||||||
if (!message->m_Data.isNull() && | |||||||||
JS_TypeOfValue(rq.cx, message->m_Data) == JSTYPE_OBJECT && | |||||||||
Script::HasProperty(rq, message->m_Data, "type")) | |||||||||
{ | |||||||||
CStr simulationMessageDataType; | |||||||||
if (Script::GetProperty(rq, message->m_Data, "type", simulationMessageDataType)) | |||||||||
{ | |||||||||
if (simulationMessageDataType == "map-flare") | |||||||||
{ | |||||||||
CStrW usernameToFind = SimpleUsername(session->GetUserName(), server.m_LobbyAuth); | |||||||||
if (!server.IsUserAllowedByACLs(usernameToFind, session->GetGUID(), "mapflare")) | |||||||||
{ | |||||||||
LOGMESSAGE("CNetServerWorker::OnSimulationMessage: user %s denied map flare due to server ACLs", utf8_from_wstring(usernameToFind)); | |||||||||
CChatMessage message; | |||||||||
message.m_GUID = session->GetGUID(); | |||||||||
message.m_Message = wstring_from_utf8("(Notice: Your map flare was not broadcast due to server ACLs.)"); | |||||||||
session->SendMessage(&message); | |||||||||
return true; | |||||||||
} | |||||||||
} | |||||||||
} | |||||||||
} | |||||||||
// Send it back to all clients that have finished | // Send it back to all clients that have finished | ||||||||
// the loading screen (and the synchronization when rejoining) | // the loading screen (and the synchronization when rejoining) | ||||||||
server.Broadcast(message, { NSS_INGAME }); | server.Broadcast(message, { NSS_INGAME }); | ||||||||
// Save all the received commands | // Save all the received commands | ||||||||
if (server.m_SavedCommands.size() < message->m_Turn + 1) | if (server.m_SavedCommands.size() < message->m_Turn + 1) | ||||||||
server.m_SavedCommands.resize(message->m_Turn + 1); | server.m_SavedCommands.resize(message->m_Turn + 1); | ||||||||
server.m_SavedCommands[message->m_Turn].push_back(*message); | server.m_SavedCommands[message->m_Turn].push_back(*message); | ||||||||
Show All 31 Lines | |||||||||
bool CNetServerWorker::OnChat(void* context, CFsmEvent* event) | bool CNetServerWorker::OnChat(void* context, CFsmEvent* event) | ||||||||
{ | { | ||||||||
ENSURE(event->GetType() == (uint)NMT_CHAT); | ENSURE(event->GetType() == (uint)NMT_CHAT); | ||||||||
CNetServerSession* session = (CNetServerSession*)context; | CNetServerSession* session = (CNetServerSession*)context; | ||||||||
CNetServerWorker& server = session->GetServer(); | CNetServerWorker& server = session->GetServer(); | ||||||||
CStrW username = SimpleUsername(session->GetUserName(), server.m_LobbyAuth); | |||||||||
if (!server.IsUserAllowedByACLs(username, session->GetGUID(), "chat")) | |||||||||
{ | |||||||||
CChatMessage message; | |||||||||
message.m_GUID = session->GetGUID(); | |||||||||
message.m_Message = wstring_from_utf8("(Notice: Your message was not broadcast due to server ACLs.)"); | |||||||||
phositUnsubmitted Not Done Inline Actions
phosit: | |||||||||
session->SendMessage(&message); | |||||||||
return true; | |||||||||
} | |||||||||
// User is allowed by ACLs. Broadcast the message to the clients | |||||||||
CChatMessage* message = (CChatMessage*)event->GetParamRef(); | CChatMessage* message = (CChatMessage*)event->GetParamRef(); | ||||||||
message->m_GUID = session->GetGUID(); | message->m_GUID = session->GetGUID(); | ||||||||
server.Broadcast(message, { NSS_PREGAME, NSS_INGAME }); | server.Broadcast(message, { NSS_PREGAME, NSS_INGAME }); | ||||||||
return true; | return true; | ||||||||
} | } | ||||||||
▲ Show 20 Lines • Show All 359 Lines • ▼ Show 20 Lines | |||||||||
} | } | ||||||||
void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port) | void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port) | ||||||||
{ | { | ||||||||
if (m_Host) | if (m_Host) | ||||||||
StunClient::SendHolePunchingMessages(*m_Host, ipStr, port); | StunClient::SendHolePunchingMessages(*m_Host, ipStr, port); | ||||||||
} | } | ||||||||
bool CNetServerWorker::IsUserAllowedByACLs(const CStrW& username, const CStr& guid, const CStr& permissionName) | |||||||||
{ | |||||||||
if (guid == m_ControllerGUID) | |||||||||
return true; | |||||||||
return IsUsernameAllowedByACLs(username, permissionName); | |||||||||
} | |||||||||
bool CNetServerWorker::IsUsernameAllowedByACLs(const CStrW& username, const CStr& permissionName) | |||||||||
{ | |||||||||
// Check whether the checkbox is shown that enables ACLs | |||||||||
CStr showACLs; | |||||||||
CFG_GET_VAL("acls.showoption", showACLs); | |||||||||
if (!(showACLs == "true")) | |||||||||
// In this case all users are allowed. | |||||||||
return true; | |||||||||
// Check whether the ACLs are enabled | |||||||||
CStr enableACLs; | |||||||||
CFG_GET_VAL("acls.server.enable", enableACLs); | |||||||||
if (!(enableACLs == "true")) | |||||||||
// In this case all users are allowed. | |||||||||
return true; | |||||||||
CStrW usernameWithoutRating(username.LowerCase().substr(0, username.find(L" ("))); | |||||||||
// LOGMESSAGE("IsUsernameAllowedByACLs: Checking permission %s for user %s", permissionName, utf8_from_wstring(username)); | |||||||||
// Start with an implicit allow all unprivileged permissions to all users | |||||||||
bool userAllowed{true}; | |||||||||
for (uint32_t aclNum = 1; aclNum < 255; ++aclNum) | |||||||||
phositUnsubmitted Not Done Inline ActionsIt's weard the beginning is 1-indexed but the end is the 8bit-max when the min is 0. phosit: It's weard the beginning is 1-indexed but the end is the 8bit-max when the min is 0.
I prefere… | |||||||||
{ | |||||||||
CStr aclPermissionGroup; | |||||||||
CStr aclPermissionGroupCfgName("acls.server.acl" + CStr::FromUInt(aclNum) + ".group"); | |||||||||
CFG_GET_VAL(aclPermissionGroupCfgName, aclPermissionGroup); | |||||||||
// If the config entry is not found then stop looking for more | |||||||||
// TODO: Document this so that users know that rules must be numbered | |||||||||
// consecutively in order to be processed | |||||||||
if (aclPermissionGroup.length() == 0) | |||||||||
break; | |||||||||
CStr aclPermissionRuleCfgName("acls.server.acl" + CStr::FromUInt(aclNum) + ".rule"); | |||||||||
CStr aclPermissionRule; | |||||||||
CFG_GET_VAL(aclPermissionRuleCfgName, aclPermissionRule); | |||||||||
// LOGMESSAGE("IsUsernameAllowedByACLs: acl %d from cfg entry %s is %s", aclNum, aclPermissionRuleCfgName, aclPermissionRule); | |||||||||
if (userAllowed && aclPermissionRule.Left(5) == "deny " && | |||||||||
(aclPermissionRule.FindInsensitive("allunprivperms") != -1 || | |||||||||
aclPermissionRule.FindInsensitive(permissionName) != -1)) | |||||||||
{ | |||||||||
// LOGMESSAGE("IsUsernameAllowedByACLs: acl is deny"); | |||||||||
// Process the denied permissions. | |||||||||
if (aclPermissionGroup == "@all") | |||||||||
userAllowed = false; | |||||||||
else if (aclPermissionGroup == "@buddies" && IsUserInBuddyList(usernameWithoutRating)) | |||||||||
userAllowed = false; | |||||||||
else | |||||||||
{ | |||||||||
// Look for the user in the named ACL group member list, e.g. "group1" | |||||||||
CStr aclGroupMembers; | |||||||||
CStr aclGroupCfgName("acls.groups." + aclPermissionGroup + ".members"); | |||||||||
userAllowed = !IsUserInCfgUserList(usernameWithoutRating, aclGroupCfgName); | |||||||||
} | |||||||||
} | |||||||||
else if (!userAllowed && aclPermissionRule.Left(6) == "allow " && | |||||||||
(aclPermissionRule.FindInsensitive("allunprivperms") != -1 || | |||||||||
aclPermissionRule.FindInsensitive(permissionName) != -1)) | |||||||||
{ | |||||||||
// LOGMESSAGE("IsUsernameAllowedByACLs: acl is allow"); | |||||||||
// Process the allowed permissions. | |||||||||
if (aclPermissionGroup == "@all") | |||||||||
userAllowed = true; | |||||||||
else if (aclPermissionGroup == "@buddies") | |||||||||
userAllowed = IsUserInBuddyList(usernameWithoutRating); | |||||||||
else | |||||||||
{ | |||||||||
// Look for the user in the named ACL group member list, e.g. "group2" | |||||||||
CStr groupAllowMembers; | |||||||||
CStr groupAllowCfgName("acls.groups." + aclPermissionGroup + ".members"); | |||||||||
userAllowed = IsUserInCfgUserList(usernameWithoutRating, groupAllowCfgName); | |||||||||
} | |||||||||
} | |||||||||
} | |||||||||
phositUnsubmitted Not Done Inline ActionsI didn't read the complete loop but from a first glimpse... if (!RealyHasToRun(...)) return true; const CStrW usernameWithoutRating{...}; bool userAllowed{true}; for (...) { std::optional<bool> temp{AllowedForNthACL(aclNum, ...)}; if (temp.has_value()) userAllowed = *temp; } return userAllowed; phosit: I didn't read the complete loop but from a first glimpse...
The loop might mutate some… | |||||||||
// LOGMESSAGE("IsUsernameAllowedByACLs: acl result: userAllowed == %d", userAllowed); | |||||||||
return userAllowed; | |||||||||
} | |||||||||
bool CNetServerWorker::IsUserInBuddyList(const CStrW& usernameWithoutRating) | |||||||||
{ | |||||||||
return IsUserInCfgUserList(usernameWithoutRating, "lobby.buddies"); | |||||||||
} | |||||||||
bool CNetServerWorker::IsUserInCfgUserList(const CStrW& usernameWithoutRating, const CStr& userListCfgName) | |||||||||
{ | |||||||||
CStr userList; | |||||||||
CFG_GET_VAL(userListCfgName, userList); | |||||||||
// LOGMESSAGE("IsUserInCfgUserList: acl group %s memberlist: %s", userListCfgName.c_str(), userList.c_str()); | |||||||||
std::wstringstream groupStream(wstring_from_utf8(userList)); | |||||||||
CStrW username; | |||||||||
while (std::getline(groupStream, username, L',')) | |||||||||
{ | |||||||||
if (username.LowerCase() == usernameWithoutRating) | |||||||||
{ | |||||||||
// LOGMESSAGE("IsUserInCfgUserList: acl group %s, username %s matches usernameWithoutRating %s, returning true", userListCfgName.c_str(), utf8_from_wstring(username), utf8_from_wstring(usernameWithoutRating).c_str()); | |||||||||
return true; | |||||||||
} | |||||||||
} | |||||||||
// LOGMESSAGE("IsUserInCfgUserList: acl group %s, usernameWithoutRating %s not found, returning false", userListCfgName.c_str(), utf8_from_wstring(usernameWithoutRating).c_str()); | |||||||||
return false; | |||||||||
} | |||||||||
CNetServer::CNetServer(bool useLobbyAuth) : | CNetServer::CNetServer(bool useLobbyAuth) : | ||||||||
m_Worker(new CNetServerWorker(useLobbyAuth)), | m_Worker(new CNetServerWorker(useLobbyAuth)), | ||||||||
m_LobbyAuth(useLobbyAuth), m_UseSTUN(false), m_PublicIp(""), m_PublicPort(20595), m_Password() | m_LobbyAuth(useLobbyAuth), m_UseSTUN(false), m_PublicIp(""), m_PublicPort(20595), m_Password() | ||||||||
{ | { | ||||||||
} | } | ||||||||
CNetServer::~CNetServer() | CNetServer::~CNetServer() | ||||||||
▲ Show 20 Lines • Show All 67 Lines • ▼ Show 20 Lines | |||||||||
} | } | ||||||||
bool CNetServer::IsBanned(const std::string& username) const | bool CNetServer::IsBanned(const std::string& username) const | ||||||||
{ | { | ||||||||
std::unordered_map<std::string, int>::const_iterator it = m_FailedAttempts.find(username); | std::unordered_map<std::string, int>::const_iterator it = m_FailedAttempts.find(username); | ||||||||
return it != m_FailedAttempts.end() && it->second >= FAILED_PASSWORD_TRIES_BEFORE_BAN; | return it != m_FailedAttempts.end() && it->second >= FAILED_PASSWORD_TRIES_BEFORE_BAN; | ||||||||
} | } | ||||||||
bool CNetServer::IsUsernameAllowedByACLs(const std::string& username, const CStr& permissionName) | |||||||||
{ | |||||||||
CStrW theUsername(wstring_from_utf8(username)); | |||||||||
return m_Worker->IsUsernameAllowedByACLs(theUsername, permissionName); | |||||||||
} | |||||||||
void CNetServer::SetPassword(const CStr& password) | void CNetServer::SetPassword(const CStr& password) | ||||||||
{ | { | ||||||||
m_Password = password; | m_Password = password; | ||||||||
std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex); | std::lock_guard<std::mutex> lock(m_Worker->m_WorkerMutex); | ||||||||
m_Worker->SetPassword(password); | m_Worker->SetPassword(password); | ||||||||
} | } | ||||||||
void CNetServer::SetControllerSecret(const std::string& secret) | void CNetServer::SetControllerSecret(const std::string& secret) | ||||||||
Show All 37 Lines |
Wildfire Games · Phabricator
I prefer allowlist == "everyone"