// Ryzom - MMORPG Framework
// Copyright (C) 2010 Winch Gate Property Limited
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
#include "stdpch.h"
#include "game_share/tick_event_handler.h"
#include "game_share/ryzom_version.h"
#include "game_share/crypt.h"
#include "nel/misc/time_nl.h"
#include "client.h"
#include "mirrors.h"
#include "messages.h"
#ifdef NL_OS_WINDOWS
# ifndef NL_COMP_MINGW
# define NOMINMAX
# endif
# include
# include
typedef unsigned long ulong;
#endif // NL_OS_WINDOWS
#include
using namespace NLMISC;
using namespace NLNET;
using namespace std;
bool EGSHasMirrorReady = false;
bool IOSHasMirrorReady = false;
bool EmulateShard = false;
std::map StringMap;
std::set StringAsked;
// Must be a pointer to control when to start listening socket and when stop it
CCallbackServer *Server;
// Dates at which bad login msg should be sent
std::multimap > BadLoginClients;
const NLMISC::TTime LOGIN_RETRY_DELAY_IN_MILLISECONDS = 4000;
// Code taken from login service
/**
* Encapsulation of MySQL result
*/
class CMySQLResult
{
public:
/// Constructor
CMySQLResult(MYSQL_RES* res = NULL);
/// Constructor
CMySQLResult(MYSQL* database);
/// Destructor
~CMySQLResult();
/// Cast operator
operator MYSQL_RES*();
/// Affectation
CMySQLResult& operator = (MYSQL_RES* res);
/// Test success
bool success() const;
/// Test failure
bool failed() const;
/// Number of rows of result
uint numRows();
/// Number of fields of result
uint numFields();
/// Fetch row
MYSQL_ROW fetchRow();
private:
MYSQL_RES* _Result;
};
//
// Constructor
CMySQLResult::CMySQLResult(MYSQL_RES* res)
{
_Result = res;
}
/// Constructor
CMySQLResult::CMySQLResult(MYSQL* database)
{
_Result = mysql_store_result(database);
}
/// Destructor
CMySQLResult::~CMySQLResult()
{
if (_Result != NULL)
mysql_free_result(_Result);
}
/// Cast operator
CMySQLResult::operator MYSQL_RES*()
{
return _Result;
}
/// Affectation
CMySQLResult& CMySQLResult::operator = (MYSQL_RES* res)
{
if (res == _Result)
return *this;
if (_Result != NULL)
mysql_free_result(_Result);
_Result = res;
return *this;
}
/// Test success
bool CMySQLResult::success() const
{
return _Result != NULL;
}
/// Test failure
bool CMySQLResult::failed() const
{
return !success();
}
/// Number of rows of result
uint CMySQLResult::numRows()
{
return (uint)mysql_num_rows(_Result);
}
uint CMySQLResult::numFields()
{
return (uint)mysql_num_fields(_Result);
}
/// Fetch row
MYSQL_ROW CMySQLResult::fetchRow()
{
return mysql_fetch_row(_Result);
}
/* ***************************************************************************
Doc :
When an entity is added in the service mirror, the service checks if its name is in the name cache. If
the string ID is not known, the server ask the IOS for the string.
When the IOS sends back a string, the server broadcasts this string to the connected clients.
When a client connects the server, the server send it all the strings in the cache. The client sends a WINDOW message
to set the world window that is visible on the client.
A each tick, the server update some entities (UpdatePerTick) to the connected clients.
If the entity doesn't exist on the client, it send a ADD message with its entity ID and the string ID.
The server always sends a POS message for the entites it updates.
// ***************************************************************************/
// ***************************************************************************
// PASSWORD DB
// ***************************************************************************
MYSQL *DatabaseConnection = NULL;
void disconnectFromDatabase();
void connectToDatabase()
{
disconnectFromDatabase();
std::string DatabaseName;
std::string DatabaseHost;
std::string DatabaseLogin;
std::string DatabasePassword;
try
{
DatabaseName = IService::getInstance ()->ConfigFile.getVar("DatabaseName").asString ();
DatabaseHost = IService::getInstance ()->ConfigFile.getVar("DatabaseHost").asString ();
DatabaseLogin = IService::getInstance ()->ConfigFile.getVar("DatabaseLogin").asString ();
DatabasePassword = IService::getInstance ()->ConfigFile.getVar("DatabasePassword").asString ();
}
catch(const EConfigFile &e)
{
nlwarning(e.what());
return;
}
MYSQL *db = mysql_init(NULL);
if(db == NULL)
{
nlwarning ("mysql_init() failed");
return;
}
DatabaseConnection = mysql_real_connect(db, DatabaseHost.c_str(), DatabaseLogin.c_str(), DatabasePassword.c_str(), DatabaseName.c_str(),0,NULL,0);
if (DatabaseConnection == NULL || DatabaseConnection != db)
{
mysql_close(db);
nlerror ("mysql_real_connect() failed to '%s' with login '%s' and database name '%s'", DatabaseHost.c_str(), DatabaseLogin.c_str(), DatabaseName.c_str());
return;
}
}
void disconnectFromDatabase()
{
if (DatabaseConnection)
{
mysql_close(DatabaseConnection);
DatabaseConnection = NULL;
}
}
// ***************************************************************************
// SERVICE CLASS
// ***************************************************************************
class CMonitorService : public NLNET::IService
{
public:
bool LoginRequired;
public:
CMonitorService() : LoginRequired(false) {}
void init();
bool update();
void release();
};
// get instanceof the service
static CMonitorService &getMonitorService()
{
return *NLMISC::safe_cast(IService::getInstance());
}
// callback for the tick service 'tick' message
void cbTick();
// ***************************************************************************
void clientWantsToConnect ( TSockId from, void *arg )
{
// Called when a client wants to connect
nlinfo ("Add client %d", Clients.size());
Clients.push_back (new CMonitorClient(from));
CMonitorService &ms = getMonitorService();
// send params about this sever the client
CMessage msgout;
msgout.setType("SERVER_PARAMS");
uint32 version = 0;
msgout.serial(version);
msgout.serial(ms.LoginRequired);
Server->send(msgout, from);
Clients.back()->Authentificated = !ms.LoginRequired;
// Send all the string in cache to the client
uint i;
for (i=0; i::iterator ite = StringMap.begin ();
while (ite != StringMap.end ())
{
Clients[i]->Str.push_back (ite->first);
ite++;
}
}
}
// ***************************************************************************
void clientWantsToDisconnect ( TSockId from, void *arg )
{
// Called when a client wants to disconnect
for (uint i = 0; i < Clients.size(); ++i)
{
if (Clients[i]->getSock() == from)
{
nlinfo ("Remove client %d", i);
Clients.erase(Clients.begin()+i);
return;
}
}
nlwarning ("Client not found for remove");
}
// ***************************************************************************
void clientSetWindow (CMessage &msgin, TSockId from, CCallbackNetBase &netbase)
{
// Called when a client sent a WINDOW message
float xmin, ymin, xmax, ymax;
msgin.serial(xmin);
msgin.serial(ymin);
msgin.serial(xmax);
msgin.serial(ymax);
for (uint i = 0; i < Clients.size(); ++i)
{
if (Clients[i]->getSock() == from && Clients[i]->Authentificated)
{
nlinfo ("Client %d sets window (%.0f,%.0f) (%.0f,%.0f)", i, xmin, ymin, xmax, ymax);
Clients[i]->setWindow(xmin,ymin,xmax,ymax);
Clients[i]->resetVision();
return;
}
}
}
// ***************************************************************************
void clientSetBandwidth (CMessage &msgin, TSockId from, CCallbackNetBase &netbase)
{
// Called when a client sent a WINDOW message
uint32 bandw;
msgin.serial(bandw);
for (uint i = 0; i < Clients.size(); ++i)
{
if (Clients[i]->getSock() == from && Clients[i]->Authentificated)
{
nlinfo ("Client %d sets bandwidth to %.1f kB/s", i, bandw/1024.0);
Clients[i]->AllowedUploadBandwidth = bandw;
return;
}
}
}
// ***************************************************************************
void clientAuthentication(CMessage &msgin, TSockId from, CCallbackNetBase &netbase)
{
std::string login;
std::string password;
sint version = msgin.serialVersion(0);
msgin.serial(login);
msgin.serial(password);
for (uint i = 0; i < Clients.size(); ++i)
{
if (!Clients[i]->Authentificated && Clients[i]->getSock() == from)
{
if (!Clients[i]->BadLogin) // don't allow new login attempt while thisflag is set
{
// make a db request to to db to see if password is valid
std::string queryStr = toString("SELECT Password FROM user where Login='%s'", login.c_str());
int result = mysql_query(DatabaseConnection, queryStr.c_str());
if (result == 0)
{
CMySQLResult sqlResult(DatabaseConnection);
if (sqlResult.success() && sqlResult.numRows() == 1)
{
MYSQL_ROW row = sqlResult.fetchRow();
if (sqlResult.numFields() == 1)
{
if (strlen(row[0]) > 2)
{
std::string salt;
if (row[0][0] == '$')
{
salt = std::string(row[0], row[0] + 19);
}
else
{
salt = std::string(row[0], row[0] + 2);
}
std::string cryptedVersion = CCrypt::crypt(password, salt);
if (cryptedVersion == row[0])
{
Clients[i]->Authentificated = true;
// password is good
CMessage msgout;
msgout.setType("AUTHENT_VALID");
Server->send(msgout, from);
return;
}
}
}
}
}
// fail the authentication
// Do not send result immediatly to avoid a potential hacker
// to try a dictionnary or that dort of things
BadLoginClients.insert(std::pair >(
NLMISC::CTime::getLocalTime() + LOGIN_RETRY_DELAY_IN_MILLISECONDS,
(NLMISC::CRefPtr)Clients[i]));
Clients[i]->BadLogin =true;
return;
}
}
}
}
// ***************************************************************************
void cbReceiveString( CMessage& msgin, const string &serviceName, TServiceId serviceId )
{
uint32 nameIndex;
ucstring ucs;
msgin.serial( nameIndex );
msgin.serial( ucs );
// Add the string to the map
StringMap.insert (std::map::value_type (nameIndex, ucs.toString()));
StringAsked.erase (nameIndex);
// Add string a id to send to the clients
uint i;
for (i=0; iStr.push_back (nameIndex);
}
}
// ***************************************************************************
TCallbackItem CallbackArray[] =
{
{ "WINDOW", clientSetWindow },
{ "BANDW", clientSetBandwidth },
{ "AUTHENT", clientAuthentication }
};
TUnifiedCallbackItem CallbackArray5[] =
{
{ "RECV_STRING", cbReceiveString },
};
// ***************************************************************************
// SERVICE INIT & RELEASE
// ***************************************************************************
static void cbServiceMirrorUp( const std::string& serviceName, TServiceId serviceId, void * )
{
if ( serviceName == "IOS" )
IOSHasMirrorReady = true;
if ( serviceName == "EGS" )
EGSHasMirrorReady = true;
}
// ***************************************************************************
static void cbServiceDown( const std::string& serviceName, TServiceId serviceId, void * )
{
if ( serviceName == "IOS" )
IOSHasMirrorReady = false;
if ( serviceName == "EGS" )
EGSHasMirrorReady = false;
}
// ***************************************************************************
void CMonitorService::init ()
{
setVersion (RYZOM_VERSION);
// Init the server on port
Server = new CCallbackServer();
Server->init (48888);
Server->setConnectionCallback (clientWantsToConnect, NULL);
Server->setDisconnectionCallback (clientWantsToDisconnect, NULL);
Server->addCallbackArray (CallbackArray, sizeof(CallbackArray)/sizeof(CallbackArray[0]));
// setup the update systems
setUpdateTimeout(100);
// read sheet_id.bin and don't prune out unknown files
CSheetId::init(false);
// init sub systems
CMirrors::init(cbTick);
// CMessages::init();
// register the service up and service down callbacks
CMirrors::Mirror.setServiceMirrorUpCallback("*", cbServiceMirrorUp, 0);
CUnifiedNetwork::getInstance()->setServiceDownCallback( "*", cbServiceDown, 0);
LoginRequired = false;
// if password are required for that service, then connect to db
CConfigFile::CVar *loginRequiredVar = getMonitorService().ConfigFile.getVarPtr("LoginRequired");
LoginRequired = loginRequiredVar ? loginRequiredVar->asInt() != 0 : false;
if (LoginRequired)
{
connectToDatabase();
}
}
// ***************************************************************************
void CMonitorService::release ()
{
disconnectFromDatabase();
// release sub systems
// CMessages::release();
CMirrors::release();
if (Server)
{
delete Server;
Server = NULL;
}
}
// ***************************************************************************
// SERVICE UPDATES
// ***************************************************************************
///update called on each 'tick' message from tick service
void cbTick()
{
}
uint ForceTicks=0;
///update called every complete cycle of service loop
bool CMonitorService::update ()
{
Server->update();
uint iclient;
for (iclient=0; iclientfirst <= currentTime)
{
CMonitorClient *client = BadLoginClients.begin()->second;
if (client != NULL)
{
CMessage msgout;
msgout.setType("AUTHENT_INVALID");
Server->send(msgout, client->getSock());
client->BadLogin = false; // allow to accept login again for that client
}
BadLoginClients.erase(BadLoginClients.begin());
}
/*
if (!Clients.empty() && !Entites.empty())
{
// Update some primitive
static uint primitiveToUpdate = 0;
CConfigFile::CVar *var = ConfigFile.getVarPtr ("UpdatePerTick");
uint count = 10;
if (var && (var->Type == CConfigFile::CVar::T_INT))
count = var->asInt();
// Loop to the beginning
if (primitiveToUpdate >= Entites.size())
primitiveToUpdate = 0;
// Resize the client array
// For each client
uint i;
for (i=0; iEntites.size() <= Entites.size());
Clients[i]->Entites.resize (Entites.size());
}
// For each primitive
uint firstPrimitiveToUpdate = primitiveToUpdate;
while (count)
{
// Present ?
if (Entites[primitiveToUpdate].Flags & CEntityEntry::Present)
{
// One more
count--;
// Get the primitive position
TDataSetRow entityIndex = TDataSetRow::createFromRawIndex (primitiveToUpdate);
CMirrorPropValueRO valueX( TheDataset, entityIndex, DSPropertyPOSX );
CMirrorPropValueRO valueY( TheDataset, entityIndex, DSPropertyPOSY );
CMonitorClient::CPosData posData;
posData.X = (float)valueX / 1000.f;
posData.Y = (float)valueY / 1000.f;
// For each client
for (i=0; i=topLeft.x) && (posData.Y>=topLeft.y) && (posData.X<=bottomRight.x) && (posData.Y<=bottomRight.y))
{
// Inside
if (client.Entites[primitiveToUpdate].Flags & CMonitorClient::CEntityEntry::Present)
{
// Send a POS message
sendPos = true;
}
else
{
// Send a ADD and a POS message
CMirrorPropValueRO stringId( TheDataset, entityIndex, DSPropertyNAME_STRING_ID);
CMonitorClient::CAddData addData;
addData.Id = primitiveToUpdate;
addData.StringId = stringId;
addData.EntityId = TheDataset.getEntityId (entityIndex);
client.Add.push_back (addData);
sendPos = true;
}
}
else
{
// Outside
// Inside
if (client.Entites[primitiveToUpdate].Flags & CMonitorClient::CEntityEntry::Present)
{
// Send a RMV message
client.Rmv.push_back (primitiveToUpdate);
}
}
// Send position ?
if (sendPos)
{
CMirrorPropValueRO valueT( TheDataset, entityIndex, DSPropertyORIENTATION );
posData.Id = primitiveToUpdate;
posData.Tetha = valueT;
client.Pos.push_back (posData);
}
}
}
else
{
// Not present
for (i=0; iEntites[primitiveToUpdate].Flags & CMonitorClient::CEntityEntry::Present)
{
// One more
count--;
// Send a RMV message
Clients[i]->Rmv.push_back (primitiveToUpdate);
}
}
}
// Next primitive
primitiveToUpdate++;
// Loop to the beginning
if (primitiveToUpdate >= Entites.size())
primitiveToUpdate = 0;
// Return to the first ?
if (firstPrimitiveToUpdate == primitiveToUpdate)
break;
}
// For each client
for (i=0; i