Revamp CNShared logic

* Use a specialized connection object
* Copy the Player object less frequently
* Use a randomly generated serial key for shard auth
* Refuse invalid shard connection attempts
* Clean up connection metadata when a Player joins the shard
* Prune abandoned connections when they time out
This commit is contained in:
dongresource 2022-07-24 00:16:04 +02:00
parent c5dd745aa1
commit 741bfb675b
6 changed files with 78 additions and 48 deletions

View File

@ -14,9 +14,7 @@
struct Player : public Entity {
int accountId = 0;
int accountLevel = 0; // permission level (see CN_ACCOUNT_LEVEL enums)
int64_t SerialKey = 0;
int32_t iID = 0;
uint64_t FEKey = 0;
int level = 0;
int HP = 0;

View File

@ -28,7 +28,7 @@ using namespace PlayerManager;
std::map<CNSocket*, Player*> PlayerManager::players;
static void addPlayer(CNSocket* key, Player plr) {
static void addPlayer(CNSocket* key, Player& plr) {
Player *p = new Player();
// copy object into heap memory
@ -209,20 +209,22 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
auto enter = (sP_CL2FE_REQ_PC_ENTER*)data->buf;
INITSTRUCT(sP_FE2CL_REP_PC_ENTER_SUCC, response);
// TODO: check if serialkey exists, if it doesn't send sP_FE2CL_REP_PC_ENTER_FAIL
Player plr = CNShared::getPlayer(enter->iEnterSerialKey);
LoginMetadata *lm = CNShared::getLoginMetadata(enter->iEnterSerialKey);
if (lm == nullptr) {
delete lm;
std::cout << "[WARN] Refusing invalid REQ_PC_ENTER" << std::endl;
INITSTRUCT(sP_FE2CL_REP_PC_ENTER_FAIL, fail);
sock->sendPacket(fail, P_FE2CL_REP_PC_ENTER_FAIL);
return;
}
// for convenience
Player& plr = lm->plr;
plr.groupCnt = 1;
plr.iIDGroup = plr.groupIDs[0] = plr.iID;
DEBUGLOG(
std::cout << "P_CL2FE_REQ_PC_ENTER:" << std::endl;
std::cout << "\tID: " << AUTOU16TOU8(enter->szID) << std::endl;
std::cout << "\tSerial: " << enter->iEnterSerialKey << std::endl;
std::cout << "\tTemp: " << enter->iTempValue << std::endl;
std::cout << "\tPC_UID: " << plr.PCStyle.iPC_UID << std::endl;
)
// check if account is already in use
if (isAccountInUse(plr.accountId)) {
// kick the other player
@ -267,7 +269,6 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
// nanos
for (int i = 1; i < SIZEOF_NANO_BANK_SLOT; i++) {
response.PCLoadData2CL.aNanoBank[i] = plr.Nanos[i];
//response.PCLoadData2CL.aNanoBank[i] = plr.Nanos[i] = {0};
}
for (int i = 0; i < 3; i++) {
response.PCLoadData2CL.aNanoSlots[i] = plr.equippedNanos[i];
@ -308,11 +309,10 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
response.PCLoadData2CL.iFirstUseFlag2 = plr.iFirstUseFlag[1];
}
plr.SerialKey = enter->iEnterSerialKey;
plr.instanceID = INSTANCE_OVERWORLD; // the player should never be in an instance on enter
sock->setEKey(CNSocketEncryption::createNewKey(response.uiSvrTime, response.iID + 1, response.PCLoadData2CL.iFusionMatter + 1));
sock->setFEKey(plr.FEKey);
sock->setFEKey(lm->FEKey);
sock->setActiveKey(SOCKETKEY_FE); // send all packets using the FE key from now on
sock->sendPacket(response, P_FE2CL_REP_PC_ENTER_SUCC);
@ -320,7 +320,9 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
// transmit MOTD after entering the game, so the client hopefully changes modes on time
Chat::sendServerMessage(sock, settings::MOTDSTRING);
// copy Player object into the shard
addPlayer(sock, plr);
// check if there is an expiring vehicle
Items::checkItemExpire(sock, getPlayer(sock));
@ -337,6 +339,9 @@ static void enterPlayer(CNSocket* sock, CNPacketData* data) {
for (auto& pair : players)
if (pair.second->notify)
Chat::sendServerMessage(pair.first, "[ADMIN]" + getPlayerName(&plr) + " has joined.");
// deallocate lm (and therefore the plr object)
delete lm;
}
void PlayerManager::sendToViewable(CNSocket* sock, void* buf, uint32_t type, size_t size) {
@ -705,4 +710,6 @@ void PlayerManager::init() {
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_VEHICLE_OFF, exitPlayerVehicle);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_CHANGE_MENTOR, changePlayerGuide);
REGISTER_SHARD_PACKET(P_CL2FE_REQ_PC_FIRST_USE_FLAG_SET, setFirstUseFlag);
REGISTER_SHARD_TIMER(CNShared::pruneLoginMetadata, CNSHARED_TIMEOUT);
}

View File

@ -5,23 +5,47 @@
#else
#include <mutex>
#endif
std::map<int64_t, Player> CNShared::players;
std::mutex playerCrit;
void CNShared::setPlayer(int64_t sk, Player& plr) {
std::lock_guard<std::mutex> lock(playerCrit); // the lock will be removed when the function ends
static std::unordered_map<int64_t, LoginMetadata*> login;
static std::mutex mtx;
players[sk] = plr;
void CNShared::storeLoginMetadata(int64_t sk, LoginMetadata *lm) {
std::lock_guard<std::mutex> lock(mtx);
// take ownership of connection data
login[sk] = lm;
}
Player CNShared::getPlayer(int64_t sk) {
std::lock_guard<std::mutex> lock(playerCrit); // the lock will be removed when the function ends
LoginMetadata* CNShared::getLoginMetadata(int64_t sk) {
std::lock_guard<std::mutex> lock(mtx);
return players[sk];
// fail if the key isn't found
if (login.find(sk) == login.end())
return nullptr;
// transfer ownership of connection data to shard
LoginMetadata *lm = login[sk];
login.erase(sk);
return lm;
}
void CNShared::erasePlayer(int64_t sk) {
std::lock_guard<std::mutex> lock(playerCrit); // the lock will be removed when the function ends
void CNShared::pruneLoginMetadata(CNServer *serv, time_t currTime) {
std::lock_guard<std::mutex> lock(mtx);
players.erase(sk);
auto it = login.begin();
while (it != login.end()) {
auto& sk = it->first;
auto& lm = it->second;
if (lm->timestamp + CNSHARED_TIMEOUT > currTime) {
std::cout << "[WARN] Pruning hung connection attempt" << std::endl;
// deallocate object and remove map entry
delete login[sk];
it = login.erase(it); // skip the invalidated iterator
} else {
it++;
}
}
}

View File

@ -10,11 +10,16 @@
#include "Player.hpp"
namespace CNShared {
// serialkey corresponds to player data
extern std::map<int64_t, Player> players;
#define CNSHARED_TIMEOUT 30000
void setPlayer(int64_t sk, Player& plr);
Player getPlayer(int64_t sk);
void erasePlayer(int64_t sk);
struct LoginMetadata {
uint64_t FEKey;
Player plr;
time_t timestamp;
};
namespace CNShared {
void storeLoginMetadata(int64_t sk, LoginMetadata *lm);
LoginMetadata* getLoginMetadata(int64_t sk);
void pruneLoginMetadata(CNServer *serv, time_t currTime);
}

View File

@ -468,21 +468,24 @@ void CNLoginServer::characterSelect(CNSocket* sock, CNPacketData* data) {
resp.g_FE_ServerIP[strlen(shard_ip)] = '\0';
resp.g_FE_ServerPort = settings::SHARDPORT;
// pass player to CNShared
Player passPlayer = {};
Database::getPlayer(&passPlayer, selection->iPC_UID);
LoginMetadata *lm = new LoginMetadata();
lm->FEKey = sock->getFEKey();
lm->timestamp = getTime();
Database::getPlayer(&lm->plr, selection->iPC_UID);
// this should never happen but for extra safety
if (passPlayer.iID == 0)
if (lm->plr.iID == 0)
return invalidCharacter(sock);
passPlayer.FEKey = sock->getFEKey();
resp.iEnterSerialKey = passPlayer.iID;
CNShared::setPlayer(resp.iEnterSerialKey, passPlayer);
resp.iEnterSerialKey = Rand::rand(); // TODO: cryptographic RNG
// transfer ownership of connection data to CNShared
CNShared::storeLoginMetadata(resp.iEnterSerialKey, lm);
sock->sendPacket(resp, P_LS2CL_REP_SHARD_SELECT_SUCC);
// update current slot in DB
Database::updateSelected(loginSessions[sock].userID, passPlayer.slot);
Database::updateSelected(loginSessions[sock].userID, lm->plr.slot);
}
void CNLoginServer::finishTutorial(CNSocket* sock, CNPacketData* data) {

View File

@ -99,14 +99,7 @@ void CNShardServer::_killConnection(CNSocket* cns) {
if (PlayerManager::players.find(cns) == PlayerManager::players.end())
return;
Player* plr = PlayerManager::getPlayer(cns);
int64_t key = plr->SerialKey;
PlayerManager::removePlayer(cns); // removes the player from the list and saves it to DB
// remove from CNShared
CNShared::erasePlayer(key);
}
void CNShardServer::killConnection(CNSocket *cns) {