Files
watrbx-game-engine/Network/ServerReplicator.cpp
T
2025-12-18 16:59:29 -05:00

2976 lines
100 KiB
C++

/* Copyright 2003-2006 ROBLOX Corporation, All Rights Reserved */
#include "ServerReplicator.h"
#include "Server.h"
#include "RakPeer.h"
#include "network/Players.h"
#include "network/API.h"
#include "NetworkProfiler.h"
#include "NetworkSettings.h"
#include "NetworkOwnerJob.h"
#include "Replicator.NewInstanceItem.h"
#include "Replicator.StreamJob.h"
#include "Replicator.TagItem.h"
#include "Util.h"
#include "TopNErrorsPhysicsSender.h"
#include "DirectPhysicsReceiver.h"
#include "v8datamodel/Stats.h"
#include "v8datamodel/hackdefines.h"
#include "v8datamodel/partinstance.h"
#include "v8datamodel/Workspace.h" // TODO - move distributed physics switch somewhere else
#include "v8datamodel/message.h"
#include "v8datamodel/DataModel.h"
#include "v8datamodel/Hopper.h"
#include "v8datamodel/Lighting.h"
#include "v8datamodel/LogService.h"
#include "v8datamodel/AdService.h"
#include "v8datamodel/Teams.h"
#include "v8datamodel/PointsService.h"
#include "v8datamodel/ReplicatedFirst.h"
#include "v8datamodel/HttpRbxApiService.h"
#include "v8datamodel/MegaCluster.h"
#include "V8World/Assembly.h"
#include "V8World/Mechanism.h"
#include "V8World/Primitive.h"
#include "v8world/DistributedPhysics.h"
#include "Script/ModuleScript.h"
#include "script/script.h"
#include "util/http.h"
#include "util/RobloxGoogleAnalytics.h"
#include "util/xxhash.h"
#include "rbx/Crypt.h"
#include "FastLog.h"
#include "Network/NetworkOwner.h"
#include "Util/Statistics.h"
#include "ConcurrentRakPeer.h"
#include "RobloxServicesTools.h"
#include "rbx/boost.hpp"
#include <boost/thread/xtime.hpp>
#include <boost/bind.hpp>
#include <sstream>
#include <boost/algorithm/string.hpp>
#include "Replicator.StatsItem.h"
#include "Replicator.RockyItem.h"
#include "script/LuaVM.h"
#include "NetPmc.h"
DYNAMIC_LOGGROUP(NetworkJoin)
LOGGROUP(JoinSendExtraItemCount)
FASTFLAG(DebugLocalRccServerConnection)
FASTFLAG(RemoveUnusedPhysicsSenders)
LOGGROUP(US14116)
DYNAMIC_FASTFLAGVARIABLE(UseProtocolCompatibilityCheck, false)
FASTFLAGVARIABLE(DebugForceRegenerateSchemaBitStream, false)
DYNAMIC_FASTFLAGVARIABLE(DebugLogProcessCharacterRequestTime, false)
DYNAMIC_FASTFLAGVARIABLE(DisablePlaceAuthenticationPoll, false)
DYNAMIC_FASTFLAGVARIABLE(FilterAllPlayerPropChanges, false)
DYNAMIC_FASTFLAGVARIABLE(LogAllPlayerPropChanges, false)
DYNAMIC_FASTFLAGVARIABLE(TeamCreateAcceptTerrainReplicatedUpdatesWhenFilteringEnabled, true)
DYNAMIC_FASTFLAG(LoadGuisWithoutChar)
DYNAMIC_FASTFLAG(RCCSupportCloudEdit)
DYNAMIC_FASTINT(JoinInfluxHundredthsPercentage)
// Security Configuration Flags that should not be removed:
#ifdef RBX_RCC_SECURITY
// Security Mask String (more json-friendly than using signed int)
static const char kKickChar = '.'; // kick+report for normal hash only
static const char kGoldKickChar = ':'; // kick+report for gold hash and
static const char kReportChar = 'r'; // report for normal hash only
static const char kGoldReportChar = 'g'; // kick for normal hash, report gold hash
// (any other char) ignore
// every 5th character is a number
// SendStats config
DYNAMIC_FASTSTRINGVARIABLE(US30605p1, "....7....6....5..rr4....3....2....1....0")
// HashStats config
// (see releasepatcher for the game client)
DYNAMIC_FASTSTRINGVARIABLE(US30605p2, "____7____6____5____4__..3....2....1....0")
// ApiStats config
DYNAMIC_FASTSTRINGVARIABLE(US30605p3, "....7....6....5....4....3....2....1._..0")
// MccStats config
DYNAMIC_FASTSTRINGVARIABLE(US30605p4, "____7____6____5____4____3____2____1____0")
// ExtraStats config
DYNAMIC_FASTSTRINGVARIABLE(US30605p5, "....7....6....5....4....3....2....1....0")
// VMProtect Extra Config
// US27664p3 = Enable
DYNAMIC_FASTFLAGVARIABLE(US27664p3, false)
// enable of fuzzy token reports
DYNAMIC_FASTFLAGVARIABLE(US26301, false)
// ApiStats Extra Config
// US28292p0 report fake skip
// US28292p1 kick for ^
// US28292p2 report tamper apiToken
// US28292p3 kick for ^
DYNAMIC_FASTFLAGVARIABLE(US28292p0, true)
DYNAMIC_FASTFLAGVARIABLE(US28292p1, false)
DYNAMIC_FASTFLAGVARIABLE(US28292p2, true)
DYNAMIC_FASTFLAGVARIABLE(US28292p3, false)
// kick for fuzzy token tamper.
DYNAMIC_FASTFLAGVARIABLE(US28814, false)
// p1: kick for rbxsize/base
// p2: report for rbxsize/base
DYNAMIC_FASTFLAGVARIABLE(US29001p1, false)
DYNAMIC_FASTFLAGVARIABLE(US29001p2, false)
// for tuning of some of these parameters
// hash
DYNAMIC_FASTINTVARIABLE(ReportTimeLimit1, 4000)
DYNAMIC_FASTINTVARIABLE(Rtl1InfluxHundredthsPercentage, 100)
// mcc
DYNAMIC_FASTINTVARIABLE(ReportTimeLimit2, 8000)
DYNAMIC_FASTINTVARIABLE(Rtl2InfluxHundredthsPercentage, 100)
// ping
DYNAMIC_FASTINTVARIABLE(ReportTimeLimit3, 1500)
DYNAMIC_FASTINTVARIABLE(Rtl3InfluxHundredthsPercentage, 100)
// This will report mcc time errors
DYNAMIC_FASTINTVARIABLE(Rtl5InfluxHundredthsPercentage, 1)
// For call info
DYNAMIC_FASTINTVARIABLE(Rtl6InfluxHundredthsPercentage, 100)
DYNAMIC_FASTFLAGVARIABLE(IgnoreInvalidTicket, true)
DYNAMIC_FASTFLAGVARIABLE(HashConfigP1, false)
DYNAMIC_FASTFLAGVARIABLE(HashConfigP2, false)
DYNAMIC_FASTFLAGVARIABLE(HashConfigP7, false)
DYNAMIC_FASTINTVARIABLE(HashConfigP9, 100)
namespace {
uint32_t getSecurityMask(const std::string& maskString, const char match, bool* configError = NULL)
{
uint32_t result = 0;
uint32_t mask = (1<<31);
const char* buffer = maskString.c_str();
for (size_t i = 0; i < (32*5)/4; ++i)
{
if (buffer[i] == 0) // too short
{
if (configError)
{
*configError = true;
}
return 0;
}
else if (i%5 != 4)
{
if (buffer[i] == match)
{
result |= mask;
}
mask >>= 1;
}
else
{
if (((unsigned char)(buffer[i]) - '0') >= 8) // align error
{
if (configError)
{
*configError = true;
}
return 0;
}
}
}
return result;
}
void reportConfigMaskError(const char* name)
{
static bool alreadyReported = false;
if (!alreadyReported)
{
std::stringstream msgStream;
msgStream << "SecurityConfigError: ";
msgStream << name;
std::string msg = msgStream.str();
RBX::Analytics::GoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "SecurityException", msg.c_str());
}
}
}
#endif
namespace RBX { namespace Network {
class ServerReplicator::ServerStatsItem : public Replicator::Stats
{
Item* itemCount;
Item* sendStreamData;
Item* avgStreamPacketSize;
Item* avgInstancesPerStreamPacket;
Item* numClientInstanceQuota;
public:
ServerStatsItem(const shared_ptr<const ServerReplicator>& replicator)
:Replicator::Stats(replicator)
{
Item* item = createChildItem("PropSync");
itemCount = item->createChildItem("ItemCount");
item->createBoundChildItem("RejectionCount", replicator->propSync.propertyRejectionCount);
createBoundChildItem("Num Parts Owned", replicator->numPartsOwned);
sendStreamData = createChildItem("Send Stream Data");
avgStreamPacketSize = sendStreamData->createChildItem("Avg Packet Size");
avgInstancesPerStreamPacket = sendStreamData->createChildItem("Avg Instance per step");
numClientInstanceQuota = sendStreamData->createChildItem("Client instance quota");
}
/*override*/ void update()
{
Replicator::Stats::update();
if(shared_ptr<const ServerReplicator> locked = shared_static_cast<const ServerReplicator>(replicator.lock()))
{
itemCount->formatValue(locked->propSync.itemCount());
if (locked->streamJob)
{
sendStreamData->formatRate(locked->streamJob->packetsSent);
avgStreamPacketSize->formatValue((int)locked->streamJob->avgPacketSize.value());
avgInstancesPerStreamPacket->formatValue((int)locked->streamJob->avgInstancePerStep.value());
numClientInstanceQuota->formatValue(locked->streamJob->getClientInstanceQuota());
}
}
}
};
class Replicator::SendStatsJob : public ReplicatorJob
{
int version;
public:
SendStatsJob(Replicator& replicator, int requestedVersion) : version(requestedVersion), ReplicatorJob("Replicator StatsSender", replicator, DataModelJob::DataOut) { }
private:
virtual Time::Interval sleepTime(const Stats& stats)
{
return computeStandardSleepTime(stats, 1);
}
virtual Error error(const Stats& stats)
{
return computeStandardErrorCyclicExecutiveSleeping(stats, 1);
}
virtual TaskScheduler::StepResult stepDataModelJob(const Stats& stats)
{
replicator->sendStats(version);
return TaskScheduler::Stepped;
}
};
}}
const char* const RBX::Network::sServerReplicator = "ServerReplicator";
using namespace RBX;
using namespace RBX::Network;
REFLECTION_BEGIN();
static Reflection::BoundCallbackDesc<FilterResult(shared_ptr<Instance>, shared_ptr<Instance>)> desc_filterNew("NewFilter", &ServerReplicator::filterNew, "newItem", "parent", Security::RobloxPlace);
static Reflection::BoundCallbackDesc<FilterResult(shared_ptr<Instance>)> desc_filterDelete("DeleteFilter", &ServerReplicator::filterDelete, "deletingItem", Security::RobloxPlace);
static Reflection::BoundCallbackDesc<FilterResult(shared_ptr<Instance>, std::string, Reflection::Variant)> desc_filterProperty("PropertyFilter", &ServerReplicator::filterProperty, "changingItem", "member", "value", Security::RobloxPlace);
static Reflection::BoundCallbackDesc<FilterResult(shared_ptr<Instance>, std::string)> desc_filterEvent("EventFilter", &ServerReplicator::filterEvent, "firingItem", "event", Security::RobloxPlace);
static Reflection::BoundFuncDesc<ServerReplicator, void(bool)> desc_SetBasicFilteringEnabled(&ServerReplicator::setBasicFilteringEnabled, "SetBasicFilteringEnabled", "value", Security::RobloxPlace);
static Reflection::BoundFuncDesc<ServerReplicator, void()> desc_PreventTerrainChanges(&ServerReplicator::preventTerrainChanges, "PreventTerrainChanges", Security::RobloxPlace);
static Reflection::EventDesc<ServerReplicator, void(int, bool, int)> event_TicketProcessed(&ServerReplicator::remoteTicketProcessedSignal, "TicketProcessed", "userId", "isAuthenticated", "protocolVersion");
REFLECTION_END();
shared_ptr<Replicator::Stats> ServerReplicator::createStatsItem()
{
return Creatable<Instance>::create<ServerStatsItem>(shared_from(this));
};
bool ServerReplicator::canUseProtocolVersion(int protocolVersion) const {
return remoteProtocolVersion == 0 ||
protocolVersion <= remoteProtocolVersion;
}
bool ServerReplicator::isProtocolCompatible() const
{
if (!DFFlag::UseProtocolCompatibilityCheck)
{
return true;
}
return remoteProtocolVersion == NETWORK_PROTOCOL_VERSION || remoteProtocolVersion == 0;
}
ServerReplicator::ServerReplicator(RakNet::SystemAddress systemAddress, Server* server, NetworkSettings* networkSettings)
: Super(systemAddress, server->rakPeer, networkSettings, /*ClusterDebounce*/DFFlag::RCCSupportCloudEdit && server->isCloudEdit())
, server(server)
, waitingForMarker(true)
, topReplicationContainersSent(false)
, remotePlayerInstalled(false)
, acceptsTerrainChanges(true)
, numPartsOwned(0)
, remoteProtocolVersion(NETWORK_PROTOCOL_VERSION)
, placeAuthenticationState(PlaceAuthenticationState_Init)
, pendingCharaterRequest(NULL)
, startTime(Time::nowFast())
{
generateSchema(this, FFlag::DebugForceRegenerateSchemaBitStream);
generateApiDictionary(this, FFlag::DebugForceRegenerateSchemaBitStream);
if (settings().distributedPhysicsEnabled) {
if(FFlag::RemoveInterpolationReciever)
{
physicsReceiver.reset(new DirectPhysicsReceiver(this, true));
physicsReceiver->start(physicsReceiver);
}
else
{
createPhysicsReceiver(NetworkSettings::Direct, true);
}
}
setName("ServerReplicator");
setBasicFilteringEnabled(true);
lightingService = ServiceProvider::find<Lighting>(server);
globalShadowsDescriptor = lightingService ? lightingService->getDescriptor().findPropertyDescriptor("GlobalShadows") : NULL;
outdoorAmbientDescriptor = lightingService ? lightingService->getDescriptor().findPropertyDescriptor("OutdoorAmbient") : NULL;
outlinesDescriptor = lightingService ? lightingService->getDescriptor().findPropertyDescriptor("Outlines") : NULL;
canTimeout = false;
}
ServerReplicator::~ServerReplicator()
{
}
void ServerReplicator::setBasicFilteringEnabled( bool value )
{
basicFilter.reset(value ? new NetworkFilter(this) : NULL);
}
void ServerReplicator::preventTerrainChanges()
{
acceptsTerrainChanges = false;
}
void ServerReplicator::receiveCluster( RakNet::BitStream& inBitstream, Instance* instance, bool usingOneQuarterIterator )
{
if ((DFFlag::TeamCreateAcceptTerrainReplicatedUpdatesWhenFilteringEnabled && isCloudEdit()) ||
(acceptsTerrainChanges && (strictFilter ? strictFilter->filterTerrainCellChange() == Accept : true)))
Super::receiveCluster(inBitstream, instance, usingOneQuarterIterator);
else
{
// bounce-back? Ideally, the client wouldn't be allowed to make terrain changes
}
}
const PartInstance* ServerReplicator::readPlayerSimulationRegion(Region2::WeightedPoint& weightedPoint)
{
if (const Player* proxyPlayer = findTargetPlayer())
{
CoordinateFrame temp;
if (const PartInstance* head = proxyPlayer->hasCharacterHead(temp))
{
weightedPoint.point = temp.translation.xz();
weightedPoint.radius = proxyPlayer->getSimulationRadius();
// TODO: FIX THIS
// Doing this will help with not assigning physics ownership to
// assemblies before they are totally streamed, but it does not
// prevent the client from doing local non-owner simulation of
// those assemblies.
if (streamJob) {
streamJob->adjustSimulationOwnershipRange(&weightedPoint);
}
return head;
}
}
return NULL;
}
void ServerReplicator::readPlayerSimulationRegion(const PartInstance* playerHead, Region2::WeightedPoint& weightedPoint)
{
Player* player = findTargetPlayer();
if (player && playerHead)
{
weightedPoint.point = playerHead->getCoordinateFrame().translation.xz();
weightedPoint.radius = player->getSimulationRadius();
}
}
PartInstance* getMechanismRootMovingPart(PartInstance* part)
{
Primitive* p = Mechanism::getRootMovingPrimitive(part->getPartPrimitive());
return PartInstance::fromPrimitive(p);
}
const PartInstance* getConstMechanismRootMovingPart(const PartInstance* part)
{
const Primitive* p = Mechanism::getConstRootMovingPrimitive(part->getConstPartPrimitive());
return PartInstance::fromConstPrimitive(p);
}
bool ServerReplicator::checkDistributedReceive(PartInstance* part)
{
PartInstance* rootMoving = getMechanismRootMovingPart(part);
bool isRootPart = (rootMoving == part);
bool fromOwner = (rootMoving->getNetworkOwner() == RakNetToRbxAddress(remotePlayerId));
return (isRootPart && fromOwner); // i.e. - allow receive if received from owner
}
bool ServerReplicator::checkDistributedSend(const PartInstance* part)
{
RBXASSERT(part);
const PartInstance* rootMoving = getConstMechanismRootMovingPart(part);
bool isRootPart = (rootMoving == part);
if (isRootPart)
{
return checkDistributedSendFast(rootMoving);
}
else
{
return false;
}
}
// assumes part is root
bool ServerReplicator::checkDistributedSendFast(const PartInstance* part)
{
RBXASSERT(part);
RBXASSERT(getConstMechanismRootMovingPart(part) == part);
bool toOwner = part->getNetworkOwner() == RakNetToRbxAddress(remotePlayerId);
// allow send only if NOT to owner
return (!toOwner);
}
void ServerReplicator::rebroadcastEvent(Reflection::EventInvocation& eventInvocation)
{
eventInvocation.replicateEvent();
}
bool ServerReplicator::prepareRemotePlayer(shared_ptr<Instance> instance)
{
shared_ptr<Player> player = Instance::fastSharedDynamicCast<Player>(instance);
if (!player)
return false;
if (remotePlayer)
throw std::runtime_error("remotePlayer already exists"); // we already have a player!
remotePlayer = player;
remotePlayer->setRemoteAddress(remotePlayerId);
if (streamJob) {
streamJob->setupListeners(remotePlayer.get());
// force early spawn location calculation if we are streaming
remotePlayer->setForceEarlySpawnLocationCalculation();
}
if (!isCloudEdit() && remotePlayer->calculatesSpawnLocationEarly()) {
remotePlayer->doFirstSpawnLocationCalculation(
ServiceProvider::findServiceProvider(this), initialSpawnName);
}
joinAnalytics.setUserIdOverride(player->getUserID());
joinAnalytics.addPoint("ReceivedPlayer", (Time::nowFast() - startTime).seconds());
// if we already sent all initial instances, just spawn the character
if (topReplicationContainersSent && !remotePlayerInstalled)
{
installRemotePlayer(initialSpawnName);
}
return server->getIsPlayerAuthenticationRequired();
}
void ServerReplicator::addTopReplicationContainers(ServiceProvider* newProvider)
{
Super::addTopReplicationContainers(newProvider);
// add tag item indicating end of initial instances
pendingItems.push_back(new TagItem(this, TOP_REPLICATION_CONTAINER_FINISHED_TAG,
boost::bind(&ServerReplicator::isInitialDataSent, this)));
}
void ServerReplicator::addTopReplicationContainer(Instance* instance, bool replicateProperties, bool replicateChildren,
boost::function<void (shared_ptr<Instance>)> replicationMethodFunc)
{
if (replicateProperties)
replicationMethodFunc(shared_from(instance));
Super::addTopReplicationContainer(instance, replicateProperties, replicateChildren, replicationMethodFunc);
}
bool ServerReplicator::isLegalDeleteInstance(Instance* instance)
{
if (isCloudEdit())
return true;
if (!Super::isLegalDeleteInstance(instance))
return false;
if (strictFilter)
{
if (strictFilter->filterDelete(instance) == Reject)
{
if (settings().printDataFilters)
StandardOut::singleton()->printf(MESSAGE_WARNING, "Filtering is enabled. Instance delete (%s) not be accepted.", instance->getName().c_str());
return false;
}
return true;
}
FilterResult result;
if (basicFilter && basicFilter->filterDelete(instance, result))
{
if (!server->dataBasicFilteredSignal.empty())
server->dataBasicFilteredSignal(shared_from(this), result, shared_from(instance), "~");
return result == Accept;
}
if (this->filterDelete)
{
result = filterDelete(shared_from(instance));
if (!server->dataCustomFilteredSignal.empty())
server->dataCustomFilteredSignal(shared_from(this), result, shared_from(instance), "~");
return result == Accept;
}
return true;
}
bool ServerReplicator::isLegalReceiveInstance(Instance* instance, Instance* parent)
{
if (isCloudEdit())
return true;
if (!Super::isLegalReceiveInstance(instance, parent))
return false;
if (!server->isLegalReceiveInstance(instance))
return false;
if (Instance::fastDynamicCast<Message>(instance))
return false;
if (remotePlayer)
{
if (Instance::fastDynamicCast<Player>(instance))
{
// After we've captured the remotePlayer, don't accept other player data from the client
throw std::runtime_error("remotePlayer already exists"); // we already have a player!
}
}
if (strictFilter)
return strictFilter->filterNew(instance, parent) == Accept;
FilterResult result;
if (basicFilter && basicFilter->filterNew(instance, parent, result))
{
if (!server->dataBasicFilteredSignal.empty())
server->dataBasicFilteredSignal(shared_from(this), result, shared_from(instance), instance->getClassName().str);
return result == Accept;
}
if (this->filterNew)
{
result = filterNew(shared_from(instance), shared_from(parent));
if (!server->dataCustomFilteredSignal.empty())
server->dataCustomFilteredSignal(shared_from(this), result, shared_from(instance), instance->getClassName().str);
return result == Accept;
}
return true;
}
bool ServerReplicator::isLegalReceiveEvent(Instance* instance, const Reflection::EventDescriptor& desc)
{
if(Player* player = Instance::fastDynamicCast<Player>(instance)){
//Player events are tied to the player, the server will otherwise ignore them
if (remotePlayer.get() != player)
return false;
}
if (strictFilter)
return strictFilter->filterEvent(instance, desc) == Accept;
FilterResult result;
if (basicFilter && basicFilter->filterEvent(instance, desc, result))
{
if (!server->dataBasicFilteredSignal.empty())
server->dataBasicFilteredSignal(shared_from(this), result, shared_from(instance), desc.name.str);
return result == Accept;
}
if (this->filterEvent)
{
result = filterEvent(shared_from(instance), desc.name.str);
if (!server->dataCustomFilteredSignal.empty())
server->dataCustomFilteredSignal(shared_from(this), result, shared_from(instance), desc.name.str);
return result == Accept;
}
return true;
}
bool ServerReplicator::isLegalReceiveProperty(Instance* instance, const Reflection::PropertyDescriptor& desc)
{
if (Instance::fastDynamicCast<Player>(instance))
{
if (DFFlag::LogAllPlayerPropChanges)
Analytics::GoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "PlayerPropChange", desc.name.c_str());
if (DFFlag::FilterAllPlayerPropChanges)
return false;
else
return (desc != Player::prop_userId) && (desc != Player::prop_userIdDeprecated) && (desc != Instance::desc_Name);
}
if (isCloudEdit())
return true;
if (desc == Script::prop_EmbeddedSourceCode)
return false; // TODO: Report hacker?
if (desc == BaseScript::prop_SourceCodeId)
return false; // TODO: Report hacker?
if (desc == ModuleScript::prop_Source)
return false;
return Super::isLegalReceiveProperty(instance, desc);
}
void ServerReplicator::onSentMarker(long id)
{
waitingForMarker = false;
}
void ServerReplicator::onSentTag(int id)
{
if (id == TOP_REPLICATION_CONTAINER_FINISHED_TAG)
{
joinAnalytics.addPoint("TopRepContSent", (Time::nowFast() - startTime).seconds());
topReplicationContainersSent = true;
if(FFlag::RemoveUnusedPhysicsSenders)
{
physicsSender.reset(new TopNErrorsPhysicsSender(*this));
PhysicsSender::start(physicsSender);
}
else
{
createPhysicsSender(settings().getPhysicsSendMethod());
}
if (streamJob)
{
// order matters!
streamJob->sendPackets(-1);
streamJob->setReady(true);
}
// install the player and spawn character
if (remotePlayer)
{
// submit a write task here, because we are in data out step
DataModel::get(this)->submitTask(boost::bind(&ServerReplicator::installRemotePlayerSafe, weak_from(this), initialSpawnName), DataModelJob::Write);
}
// if we have not received the player object, defer install until we do (see prepareRemotePlayer() )
// reset ping timers
canTimeout = true;
replicatorStats.resetSecurityTimes();
}
}
bool ServerReplicator::isLegalSendProperty(Instance* instance, const Reflection::PropertyDescriptor& desc)
{
return true;
}
bool ServerReplicator::isCloudEdit() const
{
return server->isCloudEdit();
}
void ServerReplicator::sendTop(RakNet::RakPeerInterface *peer)
{
#ifdef NETWORK_DEBUG
StandardOut::singleton()->printf(RBX::MESSAGE_INFO, "SendTop");
#endif
// First, set tags
FASTLOG(FLog::Network, "ServerReplicator:sendTop - begin");
RakNet::BitStream bitStream;
bitStream << (unsigned char) ID_SET_GLOBALS;
bitStream << settings().distributedPhysicsEnabled;
bitStream << (streamJob ? true : false);
Workspace* workspace = ServiceProvider::find<Workspace>(this);
if (!workspace)
throw std::runtime_error("ServerReplicator unable to find workspace.");
bitStream << (!isCloudEdit() && workspace->getNetworkFilteringEnabled());
if (canUseProtocolVersion(32))
{
bitStream << workspace->getAllowThirdPartySales();
}
if (canUseProtocolVersion(31))
{
// for analytics
RBXASSERT(players);
bitStream << players->getCharacterAutoSpawnProperty();
}
serverScope = Guid::getLocalScope();
std::string scopeName = serverScope.getName()->toString();
bitStream << scopeName;
if (!isCloudEdit() && LuaVM::useSecureReplication())
{
unsigned int xorKey = boost::hash_value(DataModel::get(this)->getPlaceID());
unsigned int scriptKey = LuaVM::getKey() ^ xorKey;
bitStream << scriptKey;
unsigned int coreScriptModKey = LuaVM::getModKeyCore() ^ xorKey;
bitStream << coreScriptModKey;
}
// push other replication data into stream
TopReplConts::iterator end = topReplicationContainers.end();
std::vector< TopReplConts::iterator > removeVector;
bool topRepContSync = true;
if (topRepContSync)
{
RBXASSERT(topReplicationContainers.size() < 0xFF);
bitStream << (uint8_t)topReplicationContainers.size();
}
for (TopReplConts::iterator iter = topReplicationContainers.begin(); iter!=end; ++iter)
{
RBXASSERT(*iter!=NULL);
bool canReplicate = topRepContSync || canReplicateInstance(*iter, remoteProtocolVersion);
if (topRepContSync)
{
DescriptorSender<RBX::Reflection::ClassDescriptor>::IdContainer idContainer = classDictionary.getId(&(*iter)->getDescriptor());
classDictionary.send(bitStream, idContainer.id);
}
// if remoteProtocolVersion is 0, this means we are running locally (don't worry about what can replicate)
if( (remoteProtocolVersion == 0) || canReplicate)
{
serializeId(bitStream, *iter);
if (!isCloudEdit())
{
// ReplicatedFirst needs to just send it's descendants once (so each client can get it)
if (RBX::ReplicatedFirst* replicatedFirst = RBX::Instance::fastDynamicCast<RBX::ReplicatedFirst>(*iter))
{
replicatedFirst->visitDescendants(boost::bind(&ServerReplicator::sendReplicatedFirstDescendants,this,_1));
// send a tag so client knows the replicatedFirst container is completely replicated when this is received
highPriorityPendingItems.push_back(new TagItem(this, REPLICATED_FIRST_FINISHED_TAG, NULL));
}
}
}
else
{
if (!topRepContSync)
{
FASTLOGS(FLog::Network,"ServerReplicator::sendTop Not replicating %s",(*iter)->getName().c_str());
removeVector.push_back(iter);
disconnectReplicationData( shared_from(*iter) );
}
}
}
if (topRepContSync)
{
topReplicationContainersMap.clear();
topReplicationContainers.clear();
}
else
{
// if we didn't send any top replication containers, remove them here
for (std::vector< TopReplConts::iterator >::iterator removeIter = removeVector.begin(); removeIter != removeVector.end(); ++removeIter)
topReplicationContainers.erase(*removeIter);
}
// Send ID_SET_GLOBALS
peer->Send(&bitStream, settings().getDataSendPriority(), DATAMODEL_RELIABILITY, DATA_CHANNEL, remotePlayerId, false);
}
void ServerReplicator::sendReplicatedFirstDescendants(shared_ptr<Instance> descendant)
{
highPriorityPendingItems.push_back(new (newInstancePool.get()) NewInstanceItem(this, descendant));
}
#if defined(RBX_RCC_SECURITY)
CheatHandlingServerReplicator::CheatHandlingServerReplicator(RakNet::SystemAddress systemAddress, Server* server, NetworkSettings* networkSettings)
:ServerReplicator(systemAddress, server, networkSettings)
, isAuthenticated(false)
, isBadTicket(false)
, userIdFromTicket(0)
, processedTicket(false)
, hashInitialized(false)
, ignoreHashFailureMask(0)
, ignoreGoldHashFailureMask(0)
, apiStatsMask(0)
, sendStatsMask(0)
, extraStatsMask(0)
, mccStatsMask(0)
, kickTimeSec(-1)
, securityToken(ServerFuzzySecurityToken(0, ~getSecurityMask(DFString::US30605p1, '.')))
, apiToken(ServerFuzzySecurityToken(0xFFFFFFFFFFFFFFFFULL, 0))
, reportedInvalid(false)
, reportedExploit(false)
, reportedSkip(false)
, reportedApiFail(false)
, reportedApiTamper(false)
, reportedRangeError(false)
, firstMccReportRxTime(0.0)
, reportedPingItemTime(false)
, reportedHashItemTime(false)
, reportedMccItemTime(false)
, reportedMccError(false)
, numHashItems(0)
, numMccItems(0)
, numPingItems(0)
, reportedNetPmcError(false)
, reportedNetPmcPending(false)
, reportedNetPmcSent(false)
{}
#endif
#if defined(RBX_RCC_SECURITY)
void CheatHandlingServerReplicator::preauthenticatePlayer(int userId)
{
try
{
if (server->getIsPlayerAuthenticationRequired())
{
if (ticket.empty()){
FASTLOG(FLog::Error, "ServerReplicator:preauthenticatePlayer - MissingTicket");
throw RBX::runtime_error("missing ticket");
}
if (server->preusedTickets.find(ticket) != server->preusedTickets.end()){
FASTLOG(FLog::Error, "ServerReplicator:preauthenticatePlayer - DuplicateTicket");
throw RBX::runtime_error("ticket has already been used: %s", ticket.c_str());
}
server->preusedTickets.insert(ticket);
// The ticket contains a timestamp and the signature
std::vector<std::string> s;
boost::split(s, ticket, boost::is_any_of(";"));
if (s.size()!=3){
FASTLOG(FLog::Error, "ServerReplicator:preauthenticatePlayer - BadTicket");
throw RBX::runtime_error("bad pre-ticket '%s'", ticket.c_str());
}
std::string timestamp = s[0];
std::string signature2 = s[2];
// re-create the signed message
std::string message = RBX::format(
"%d\n%s\n%s",
userId,
DataModel::get(this)->jobId.c_str(),
timestamp.c_str()
);
try
{
// verify now!
Crypt().verifySignatureBase64(message, signature2);
}
catch(RBX::base_exception&)
{
FASTLOG(FLog::Error, "ServerReplicator:preauthenticatePlayer - BadTicketSignature");
throw;
}
}
}
catch(RBX::base_exception& e)
{
isBadTicket = true;
if(ContentProvider* contentProvider = ServiceProvider::find<ContentProvider>(this)){
ReportStatisticWithMessage(contentProvider->getBaseUrl(),"Preauthenticate-TicketFail",e.what());
}
return;
}
userIdFromTicket = userId;
isAuthenticated = true;
}
#endif
void ServerReplicator::installRemotePlayerSafe(weak_ptr<ServerReplicator> weakThis, const std::string preferedSpawnName)
{
shared_ptr<ServerReplicator> strongThis = weakThis.lock();
if (!strongThis)
{
return;
}
try
{
strongThis->installRemotePlayer(preferedSpawnName);
}
catch (RBX::base_exception& e)
{
// catch the exception. Something has gone wrong in this process. This shouldn't crash RCC.
(void)(e);
strongThis->remotePlayer = RBX::Creatable<RBX::Instance>::create<RBX::Network::Player>();
strongThis->requestDisconnect(DisconnectReason_SendPacketError);
}
}
void ServerReplicator::installRemotePlayer(const std::string& preferedSpawnName)
{
if (remotePlayerInstalled)
return;
remotePlayer->setGameSessionID(gameSessionID);
remotePlayer->setParent(players);
FASTLOG(FLog::Network, "ServerReplicator:InstallRemotePlayer - LoadCharacter");
if (players->getShouldAutoSpawnCharacter())
{
remotePlayer->loadCharacter(true, preferedSpawnName);
}
else if(DFFlag::LoadGuisWithoutChar && !ServiceProvider::find<Workspace>(this)->getNetworkFilteringEnabled())
{
remotePlayer->rebuildGui();
}
remotePlayerInstalled = true;
#if !defined(RBX_STUDIO_BUILD)
joinAnalytics.addPoint("PlayerInstalled", (Time::nowFast() - startTime).seconds());
sendJoinStatsToInflux();
#endif
}
void ServerReplicator::sendDictionaryFormat()
{
RakNet::BitStream bitStream;
bitStream << (unsigned char) ID_DICTIONARY_FORMAT;
bitStream << true; // Protocol schema sync
bitStream << true; // ApiDictionaryCompression;
rakPeer->rawPeer()->Send(&bitStream, settings().getDataSendPriority(), DATAMODEL_RELIABILITY, DATA_CHANNEL, remotePlayerId, false);
}
PluginReceiveResult ServerReplicator::OnReceive(Packet *packet) {
if (packet->systemAddress!=remotePlayerId)
{
return RR_CONTINUE_PROCESSING;
}
switch ((unsigned char) packet->data[0])
{
case ID_NEW_INCOMING_CONNECTION:
{
sendDictionaryFormat();
sendDictionaries();
teachSchema();
// if we are not using a real game replicator (aka CheatHandlingServerReplicator), then send top containers right away
// Essentially we don't need to wait for the ticket to come in, as we are replicating in studio (guaranteed to have all the same top classes)
#if !defined(RBX_RCC_SECURITY)
sendTop(rakPeer->rawPeer());
#endif
}
return RR_CONTINUE_PROCESSING;
case ID_SPAWN_NAME:
{
RakNet::BitStream inBitstream(packet->data, packet->length, false);
inBitstream.IgnoreBits(8); // Ignore the packet id
deserializeStringCompressed(initialSpawnName, inBitstream);
FASTLOGS(DFLog::NetworkJoin, "initialSpawnName: %s", initialSpawnName);
}
return RR_STOP_PROCESSING_AND_DEALLOCATE;
case ID_PLACEID_VERIFICATION:
{
RakNet::BitStream inBitstream(packet->data, packet->length, false);
inBitstream.IgnoreBits(8); // Ignore the packet id
int previousPlaceId;
inBitstream >> previousPlaceId;
#ifdef NETWORK_DEBUG
StandardOut::singleton()->printf(MESSAGE_INFO, "Received previous PlaceID: %d", previousPlaceId);
#endif
if (isCloudEdit())
{
placeAuthenticationState = PlaceAuthenticationState_Authenticated;
}
else
{
boost::optional<int> placeAutenticationResult = server->getPlaceAuthenticationResultForOrigin(previousPlaceId);
if (placeAutenticationResult)
placeAuthenticationState = (PlaceAuthenticationState)placeAutenticationResult.get();
else
placeAuthenticationThread.reset(new boost::thread(RBX::thread_wrapper(boost::bind(&ServerReplicator::PlaceAuthenticationThread, shared_from(this), previousPlaceId, DataModel::get(this)->getPlaceID()), "PlaceAuthenticationThread")));
}
}
return RR_STOP_PROCESSING_AND_DEALLOCATE;
case ID_PROTOCOL_SYNC:
{
RakNet::BitStream inBitstream(packet->data, packet->length, false);
inBitstream.IgnoreBits(8); // Ignore the packet id
inBitstream >> remoteProtocolVersion;
#ifdef NETWORK_DEBUG
StandardOut::singleton()->printf(MESSAGE_INFO, "Client protocol version: %d", remoteProtocolVersion);
#endif
}
return RR_STOP_PROCESSING_AND_DEALLOCATE;
case ID_REQUEST_STATS:
{
bool req;
int version = 0;
RakNet::BitStream inBitstream(packet->data, packet->length, false);
inBitstream.IgnoreBits(8); // Ignore the packet id
inBitstream >> req;
if (req)
{
inBitstream >> version;
}
DataModel::get(this)->create<LogService>()->runCallbackIfPlayerHasConsoleAccess(
shared_from(getRemotePlayer()), boost::bind(&ServerReplicator::toggleSendStatsJob,
weak_from(this), req, version));
return RR_STOP_PROCESSING_AND_DEALLOCATE;
}
default:
return Replicator::OnReceive(packet);
}
}
#if defined(RBX_RCC_SECURITY)
bool CheatHandlingServerReplicator::checkRemotePlayer()
{
if (remotePlayer)
{
return true;
}
remotePlayer = RBX::Creatable<RBX::Instance>::create<RBX::Network::Player>();
requestDisconnect(DisconnectReason_SendPacketError);
return false;
}
#endif
#if defined(RBX_RCC_SECURITY)
void CheatHandlingServerReplicator::installRemotePlayer(const std::string& preferedSpawnName)
{
if (remotePlayerInstalled)
return;
bool shouldSetParent = !isBadTicket;
try
{
FASTLOG1(FLog::Network, "ServerReplicator:InstallRemotePlayer. UserID: %d", remotePlayer->getUserID());
remotePlayer->setGameSessionID(gameSessionID);
if (remotePlayer->getParent() != players)
{
if (server->getIsPlayerAuthenticationRequired())
{
if (players->getPlayerByID(remotePlayer->getUserID()))
{
FASTLOG(FLog::Error, "ServerReplicator:InstallRemotePlayer - DuplicatePlayer");
// uh-oh! This player is already in the world
StandardOut::singleton()->printf(MESSAGE_WARNING, "Player %d is already in the world", remotePlayer->getUserID());
requestDisconnect(DisconnectReason_DuplicatePlayer);
if(ContentProvider* contentProvider = ServiceProvider::find<ContentProvider>(this)){
ReportStatisticWithMessage(contentProvider->getBaseUrl(),"Authenticate-DupePlayer","");
}
return;
}
}
if (!DFFlag::IgnoreInvalidTicket)
{
remotePlayer->setParent(players);
remotePlayer->loadChatInfo();
}
}
else
{
shouldSetParent = false;
}
if (FFlag::DebugLocalRccServerConnection)
{
// skip the security check
}
else
{
if(userIdFromTicket != remotePlayer->getUserID())
throw RBX::runtime_error("userId has morphed");
}
if (server->getIsPlayerAuthenticationRequired())
{
if (ticket.empty()){
FASTLOG(FLog::Error, "ServerReplicator:InstallRemotePlayer - MissingTicket");
throw RBX::runtime_error("missing ticket");
}
if (server->usedTickets.find(ticket) != server->usedTickets.end()){
FASTLOG(FLog::Error, "ServerReplicator:InstallRemotePlayer - DuplicateTicket");
throw RBX::runtime_error("ticket has already been used: %s", ticket.c_str());
}
server->usedTickets.insert(ticket);
// The ticket contains a timestamp and the signature
std::vector<std::string> s;
boost::split(s, ticket, boost::is_any_of(";"));
if (s.size() < 2){
FASTLOG(FLog::Error, "ServerReplicator:InstallRemotePlayer - BadTicket");
throw RBX::runtime_error("bad ticket '%s'", ticket.c_str());
}
std::string timestamp = s[0];
std::string signature = s[1];
// re-create the signed message
std::string message = RBX::format(
"%d\n%s\n%s\n%s\n%s",
remotePlayer->getUserID(),
remotePlayer->getName().c_str(),
remotePlayer->getCharacterAppearance().c_str(),
DataModel::get(this)->jobId.c_str(),
timestamp.c_str()
);
try
{
// verify now!
Crypt().verifySignatureBase64(message, signature);
}
catch(RBX::base_exception&)
{
FASTLOG(FLog::Error, "ServerReplicator:InstallRemotePlayer - BadTicketSignature");
throw;
}
}
}
catch(RBX::base_exception& e)
{
isBadTicket = true;
if(ContentProvider* contentProvider = ServiceProvider::find<ContentProvider>(this)){
ReportStatisticWithMessage(contentProvider->getBaseUrl(),"Authenticate-TicketFail",e.what());
}
FASTLOGS(FLog::Network, "ServerReplicator:InstallRemotePlayer - Authenticate-TicketFail: %s", e.what());
throw;
}
if (DFFlag::IgnoreInvalidTicket && shouldSetParent)
{
remotePlayer->setParent(players);
remotePlayer->loadChatInfo();
}
FASTLOG(FLog::Network, "ServerReplicator:InstallRemotePlayer - LoadCharacter");
if (players->getShouldAutoSpawnCharacter()) {
remotePlayer->loadCharacter(true, preferedSpawnName);
}
remotePlayerInstalled = true;
#if !defined(RBX_STUDIO_BUILD)
joinAnalytics.addPoint("PlayerInstalled", (Time::nowFast() - startTime).seconds());
sendJoinStatsToInflux();
#endif
}
#endif
#if defined(RBX_RCC_SECURITY)
PluginReceiveResult CheatHandlingServerReplicator::OnReceive(Packet *packet)
{
if (packet->systemAddress!=remotePlayerId)
return ServerReplicator::OnReceive(packet);
switch ((unsigned char) packet->data[0])
{
case ID_SUBMIT_TICKET:
// need to process tickets right away before other packets such as DATA
processTicket(packet);
remoteTicketProcessedSignal(userIdFromTicket, isAuthenticated, remoteProtocolVersion);
processedTicket = true;
joinAnalytics.addPoint("ProcessedTicket", (Time::nowFast() - startTime).seconds());
FASTLOG1(FLog::Network, "Player authenticated = %d", isAuthenticated);
return RR_STOP_PROCESSING_AND_DEALLOCATE;
case ID_DATA:
if (!isAuthenticated)
{
FASTLOGS(FLog::Warning, "Player not authenticated %s", RakNetAddressToString(packet->systemAddress).c_str());
//Discard all data coming from non-authenticated clients
return RR_STOP_PROCESSING_AND_DEALLOCATE;
}
return ServerReplicator::OnReceive(packet);
default:
return ServerReplicator::OnReceive(packet);
}
}
#endif
bool ServerReplicator::sendItemsPacket()
{
if (!Super::sendItemsPacket())
return false;
int limit = settings().sendPacketBufferLimit;
// during loading of a level, we'll send multiple packets at a time
if (limit == -1 && !topReplicationContainersSent)
{
for (int i = 0; i < FLog::JoinSendExtraItemCount; i++)
if (!Super::sendItemsPacket())
return false;
}
return true;
}
#if !defined(RBX_STUDIO_BUILD)
namespace CryptStrings
{
bool cmpIosHash(const char* inString)
{
// compares string to ios,ios
const unsigned char cmpString[8] = {255, 16, 211, 41, 155, 172, 111, 17};
if (!inString) return false;
for (int i = 0; i < 8; ++i)
{
if ((unsigned char)((inString[i]+i)*39) != cmpString[i]) return false;
if (!inString[i]) return true;
};
return false;
}
}
#endif
#if defined(RBX_RCC_SECURITY)
void CheatHandlingServerReplicator::processTicket(Packet *packet)
{
try
{
FASTLOG(FLog::Network, "ServerReplicator:processTicket");
if (!ticket.empty())
{
// We already got a ticket. This must be a hack!
// TODO: report the hack
requestDisconnect(DisconnectReason_DuplicateTicket);
return;
}
RakNet::BitStream inBitstream(packet->data, packet->length, false);
inBitstream.IgnoreBits(8); // Ignore the packet id
Peer::decryptDataPart(inBitstream);
int userId;
inBitstream >> userId;
deserializeStringCompressed(ticket, inBitstream);
preauthenticatePlayer(userId);
std::string hash;
deserializeStringCompressed(hash, inBitstream);
inBitstream >> remoteProtocolVersion;
if (!isProtocolCompatible())
{
// invalidate caches
oneQuarterClusterPacketCache.reset();
clusterPacketCache.reset();
instancePacketCache.reset();
}
FASTLOG1(FLog::Network, "ServerReplicator:processTicket - remoteProtocolVersion = %i",remoteProtocolVersion);
if (!server->protocolVersionMatches(remoteProtocolVersion))
{
isAuthenticated = false;
// send disconnect message to client
RakNet::BitStream bitStream;
bitStream << (unsigned char) ID_PROTOCAL_MISMATCH;
rakPeer->rawPeer()->Send(&bitStream, IMMEDIATE_PRIORITY, DATAMODEL_RELIABILITY, DATA_CHANNEL, remotePlayerId, false);
}
std::string securityKey;
deserializeStringCompressed(securityKey, inBitstream);
std::string platform, product;
deserializeStringCompressed(platform, inBitstream);
deserializeStringCompressed(product, inBitstream);
deserializeStringCompressed(gameSessionID, inBitstream);
if (canUseProtocolVersion(29))
{
inBitstream >> reportedGoldHash;
}
if (!server->securityKeyMatches(securityKey))
{
isAuthenticated = false;
// log using StatsService
if (RBX::Stats::StatsService* stats = ServiceProvider::find<RBX::Stats::StatsService>(this))
{
shared_ptr<Reflection::ValueTable> entry(new Reflection::ValueTable());
(*entry)["PlayerId"] = userIdFromTicket;
stats->report("SecurityMismatch", entry);
}
FASTLOG(FLog::Network, "ServerReplicator:processTicket - Security key mismatch");
// send disconnect message to client
RakNet::BitStream bitStream;
bitStream << (unsigned char) ID_SECURITYKEY_MISMATCH;
rakPeer->rawPeer()->Send(&bitStream, IMMEDIATE_PRIORITY, DATAMODEL_RELIABILITY, DATA_CHANNEL, remotePlayerId, false);
requestDisconnect(DisconnectReason_SecurityKeyMismatch);
}
// Now make sure we have top replication containers, since we have remote network version to filter accordingly with
FASTLOG(FLog::Network, "ServerReplicator:processTicket - sendTop");
sendTop(rakPeer->rawPeer());
if (!players->hashMatches(hash))
{
isAuthenticated = false; // TODO: Add a new state field that indicates "blocked"
players->onRemoteSysStats(userIdFromTicket, "1920x1200", "baseball");
// send disconnect message to client
RakNet::BitStream bitStream;
bitStream << (unsigned char) ID_HASH_MISMATCH;
rakPeer->rawPeer()->Send(&bitStream, IMMEDIATE_PRIORITY, DATAMODEL_RELIABILITY, DATA_CHANNEL, remotePlayerId, false);
requestDisconnect(DisconnectReason_BadHash);
}
enableHashCheckBypass = true;
enableMccCheckBypass = true;
if (!CryptStrings::cmpIosHash(hash.c_str()) && !isCloudEdit())
{
if (platform != "Win32")
{
enableHashCheckBypass = true;
enableMccCheckBypass = true;
}
else
{
enableHashCheckBypass = false;
enableMccCheckBypass = false;
}
}
}
catch (std::exception e)
{
// This is here due to exploiters.
FASTLOG1(FLog::Network, "ServerReplicator::processTicket exception: %s", e.what() );
requestDisconnect(DisconnectReason_SendPacketError);
return;
}
}
#endif
void ServerReplicator::readHashItem(RakNet::BitStream& inBitstream)
{
using namespace RBX::Hasher;
// Reader
unsigned char numItems;
inBitstream >> numItems;
hashes.hash.resize(numItems);
inBitstream >> hashes.nonce;
for (size_t i = 0; i < numItems; ++i)
{
inBitstream >> hashes.hash[i];
}
size_t numTokens = 3;
for (size_t i = 0; i < numTokens; ++i)
{
inBitstream >> securityTokens[i];
}
decodeHashItem(hashes, securityTokens);
processHashValue(hashes);
processHashValuePost(securityTokens, hashes.nonce);
updateHashState(hashes, securityTokens);
}
void ServerReplicator::readRockyItem(RakNet::BitStream& inBitstream)
{
unsigned char subtype;
inBitstream >> subtype;
if (subtype == RockeyMccReportClient)
{
MccReport report;
inBitstream >> report.memcheckRunTime;
inBitstream >> report.memcheckDoneTime;
inBitstream >> report.mccRunTime;
inBitstream >> report.badAppRunTime;
inBitstream >> report.localChecksEncoded;
processRockyMccReport(report);
}
else if (subtype == RockeyNetPmcResponse)
{
processNetPmcResponseItem(inBitstream);
}
else if (subtype == RockeyCallInfo)
{
processRockyCallInfoItem(inBitstream);
}
}
#ifdef RBX_RCC_SECURITY
void CheatHandlingServerReplicator::processNetPmcResponseItem(RakNet::BitStream& inBitstream)
{
uint8_t idx;
uint32_t response;
uint64_t correct;
inBitstream >> idx;
inBitstream >> response;
inBitstream >> correct;
if (netPmc.removeFromList(idx))
{
if (!reportedNetPmcError && !netPmc.checkResult(idx, response, correct))
{
reportedNetPmcError = true;
if (DFFlag::HashConfigP2)
{
players->onRemoteSysStats(remotePlayer->getUserID(), "1920x1200", "WorldEdit");
}
RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "SecurityException", "NetPmc Error P2.");
}
}
}
# endif
#ifdef RBX_RCC_SECURITY
void CheatHandlingServerReplicator::processRockyCallInfoItem(RakNet::BitStream& inBitstream)
{
uint8_t size;
inBitstream >> size;
CallChainSetInfo info;
for (size_t i = 0; i < std::min(static_cast<size_t>(size), static_cast<size_t>(4)); ++i)
{
inBitstream >> info.handler[i];
inBitstream >> info.ret[i];
}
if (DFInt::Rtl6InfluxHundredthsPercentage > 0 && (reportedCallChains.size() < 100)
&& (std::find(reportedCallChains.begin(), reportedCallChains.end(), info) != reportedCallChains.end()))
{
Analytics::InfluxDb::Points callInfoAnalytics;
for (size_t i = 0; i < size; ++i)
{
std::string headerLabel("h");
std::string retLabel("r");
headerLabel += boost::lexical_cast<std::string>(i);
retLabel += boost::lexical_cast<std::string>(i);
callInfoAnalytics.addPoint(headerLabel, info.handler[i]);
callInfoAnalytics.addPoint(retLabel, info.ret[i]);
callInfoAnalytics.report("AddedDebug", DFInt::Rtl6InfluxHundredthsPercentage);
reportedCallChains.push_back(info);
}
}
}
#endif
void ServerReplicator::toggleSendStatsJob(
weak_ptr<ServerReplicator> weakServerReplicator, bool required, int version)
{
if (shared_ptr<ServerReplicator> serverReplicator = weakServerReplicator.lock())
{
shared_ptr<TaskScheduler::Job>& sendStatsJob = serverReplicator->sendStatsJob;
if (required)
{
if (!sendStatsJob)
{
sendStatsJob = shared_ptr<Replicator::SendStatsJob>(
new Replicator::SendStatsJob(*serverReplicator, version));
TaskScheduler::singleton().add(sendStatsJob);
}
}
else
{
TaskScheduler::singleton().remove(sendStatsJob);
sendStatsJob.reset();
}
}
}
#if defined(RBX_RCC_SECURITY)
void CheatHandlingServerReplicator::decodeHashItem(PmcHashContainer& netHashes, unsigned long long* securityTokens)
{
using namespace RBX::Hasher;
HashVector& hashes = netHashes.hash;
unsigned int& nonce = netHashes.nonce;
unsigned char numItems = hashes.size();
RakNet::Time delta = RakNet::GetTimeMS() - replicatorStats.lastReceivedHashTime;
if (!reportedHashItemTime && delta >= DFInt::ReportTimeLimit1)
{
RakNet::Time mccDelta = RakNet::GetTimeMS() - replicatorStats.lastReceivedMccTime;
RakNet::Time pingDelta = RakNet::GetTimeMS() - replicatorStats.lastReceivedPingTime;
Analytics::InfluxDb::Points hashItemAnalytics;
hashItemAnalytics.addPoint("delta", delta);
hashItemAnalytics.addPoint("num", numHashItems);
hashItemAnalytics.addPoint("pingDelta", pingDelta);
hashItemAnalytics.addPoint("pingNum", numPingItems);
hashItemAnalytics.addPoint("mccDelta", mccDelta);
hashItemAnalytics.addPoint("mccNum", numMccItems);
hashItemAnalytics.addPoint("kbSend", replicatorStats.kiloBytesReceivedPerSecond);
hashItemAnalytics.addPoint("kbRecv", replicatorStats.kiloBytesSentPerSecond);
hashItemAnalytics.addPoint("pingMean", replicatorStats.dataPing.value());
hashItemAnalytics.addPoint("pingVar", replicatorStats.dataPing.variance());
hashItemAnalytics.addPoint("queueMean", replicatorStats.dataTimeInQueue.value());
hashItemAnalytics.addPoint("queueVar", replicatorStats.dataTimeInQueue.variance());
hashItemAnalytics.report("ServerHashItem", DFInt::Rtl1InfluxHundredthsPercentage);
reportedHashItemTime = true;
}
replicatorStats.lastReceivedHashTime = RakNet::GetTimeMS();
++numHashItems;
if (DFFlag::US27664p3 && ((numItems < kNumberOfHashes) || (hashInitialized && (numItems != lastHashes.size()))))
{
players->onRemoteSysStats(remotePlayer->getUserID(), "1920x1200", "jesper");
RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "SecurityException", "invalid number of hashes");
return;
}
// Decoder
for (size_t i = numItems-2; i > 0; --i)
{
hashes[i] ^= hashes[i-1];
}
hashes[0] ^= nonce;
nonce ^= hashes[numItems-1];
}
#endif
#if defined(RBX_RCC_SECURITY)
void CheatHandlingServerReplicator::processHashValue(const PmcHashContainer& netHashes)
{
using namespace RBX::Hasher;
const HashVector& hashes = netHashes.hash;
const unsigned int& nonce = netHashes.nonce;
size_t numItems = netHashes.hash.size();
unsigned int checks = 0;
// Differential Checks (Can only be done after initialization)
// These can be safely duplicated so a fallback can exist if Golden property breaks.
// eg, if two recent versions hash to the same thing or whatnot.
if (hashInitialized)
{
checks |= kGoldHashStartFail*(hashes[kGoldHashStart] != lastHashes[kGoldHashStart]);
checks |= kGoldHashEndFail*(hashes[kGoldHashEnd] != lastHashes[kGoldHashEnd]);
checks |= kMiscHashFail*(hashes[kMiscHash] != lastHashes[kMiscHash]);
checks |= kRdataHashFail*(hashes[kRdataHash] != lastHashes[kRdataHash]);
checks |= kIatHashFail*(hashes[kIatHash] != lastHashes[kIatHash]);
checks |= kMsvcHashFail*(hashes[kMsvcHash] != lastHashes[kMsvcHash]);
checks |= kVmpPlainHashFail*(hashes[kVmpPlainHash] != lastHashes[kVmpPlainHash]);
checks |= kVmpMutantHashFail*(hashes[kVmpMutantHash] != lastHashes[kVmpMutantHash]);
checks |= kVmp0MiscHashFail*(hashes[kVmp0MiscHash] != lastHashes[kVmp0MiscHash]);
checks |= kVmp1MiscHashFail*(hashes[kVmp1MiscHash] != lastHashes[kVmp1MiscHash]);
checks |= kGoldHashStructFail*(hashes[kGoldHashStruct] != lastHashes[kGoldHashStruct]);
checks |= kAllHashStructFail*(hashes[kAllHashStruct] != lastHashes[kAllHashStruct]);
}
// Moving checks. These are based on sending a sequence of values.
// These are predictable values with the information in the packets.
// the reported nonce is updated before sending, so nonceDiff should be +1.
//
// The nonce now increments by kNonceGoodInc. To convert this delta back to something
// reasonable, I must multiply by kNonceGoodIncInv.
unsigned int nonceDiff = 0;
nonceDiff = nonce - XXH32_getRbxNonce(hashes[kGoldHashRot-1], hashes[kGoldHashRot]);
nonceDiff *= kPmcNonceGoodIncInv;
checks |= kGoldHashRotFail*((nonceDiff+3) > 7);
nonceDiff = nonce - XXH32_getRbxNonce(hashes[kNonGoldHashRot-1], hashes[kNonGoldHashRot]);
nonceDiff *= kPmcNonceGoodIncInv;
checks |= kNonGoldHashRotFail*((nonceDiff+3) > 7);
// And this prevents you from using the same nonce over and over!
if (hashInitialized)
{
nonceDiff = nonce - hashNonce;
nonceDiff *= kPmcNonceGoodIncInv;
checks |= kNonceFail*(nonceDiff > 3 || nonceDiff == 0);
}
// check against known values (This is set in gold hash struct)
checks |= kGoldHashFail * (hashes[kGoldHashStruct] != reportedGoldHash);
unsigned int goldChecks = RBX::Network::Players::checkGoldMemHashes(hashes);
// Two additional items are sent in the same packet. Check these items here.
static const size_t kRobloxTextBase = 0x401000;
static const size_t kRobloxSizeEstimate = 0x2000000;
if (hashes[numItems-1] != kRobloxTextBase || hashes[numItems-2] > kRobloxSizeEstimate)
{
if (DFFlag::US29001p1 && !reportedRangeError)
{
reportedRangeError = true;
RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "SecurityException", "Range has been modified");
}
if (DFFlag::US29001p2)
{
players->onRemoteSysStats(remotePlayer->getUserID(), "1920x1200", "Nyx");
}
}
// Decisions
processHashStats(checks & (~ignoreHashFailureMask));
processGoldHashStats(checks & (~ignoreGoldHashFailureMask));
ignoreHashFailureMask |= checks;
ignoreGoldHashFailureMask |= goldChecks;
}
void CheatHandlingServerReplicator::processHashValuePost(const unsigned long long* const tokens, unsigned int nonce)
{
const unsigned long long& clientToken = tokens[0];
const unsigned long long& rxApiToken = tokens[1];
const unsigned long long& rxPrevApiToken = tokens[2];
unsigned long long hackFlags = securityToken.decrypt(clientToken); // updates securityToken
if (hashInitialized)
{
unsigned int hackFlagsUpper = hackFlags >> 32;
unsigned int hackFlagsLower = hackFlags & 0xFFFFFFFF;
if (hackFlagsUpper)
{
if (!reportedInvalid)
{
reportedInvalid = true;
RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "SecurityException", "FuzzyToken Tamper, v3");
}
if (DFFlag::US28814)
{
players->onRemoteSysStats(remotePlayer->getUserID(), "1920x1200", "Zek");
}
}
else if ( (hackFlagsLower & sendStatsMask) && DFFlag::US26301)
{
processSendStats(static_cast<unsigned int>(hackFlags), kNoScornFlags);
}
else if ((hackFlagsLower & sendStatsMask)
&& !reportedExploit && !reportedInvalid)
{
reportedExploit = true;
std::stringstream msgStream;
msgStream << "FuzzyToken Exploit Detection v3: ";
msgStream << std::hex << hackFlagsLower;
std::string msg = msgStream.str();
RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "SecurityException", msg.c_str());
}
bool skippedPacket = (nonce - hashNonce == 2);
unsigned long long apiHackFlags;
if (!skippedPacket)
{
// the sent previous token and the stored previous token should match
if (!reportedApiFail && (prevApiToken != rxPrevApiToken))
{
if (DFFlag::US28292p0)
{
RBX::Analytics::GoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "SecurityException", "Api FakeSkip");
}
if (DFFlag::US28292p1)
{
players->onRemoteSysStats(remotePlayer->getUserID(), "1920x1200", "Zek");
}
reportedApiFail = true;
}
// decrypting the stored previous token should match the sent token
apiHackFlags = apiToken.decrypt(rxApiToken); // updates apiToken
}
else
{
// decrypting the stored previous token should match the sent previous token
apiHackFlags = apiToken.decrypt(rxPrevApiToken); // updates apiToken
// decrypting the sent previous token should match the sent token
apiHackFlags |= apiToken.decrypt(rxApiToken); // updates apiToken
}
// Call some function that acts on these values
processApiStats(apiHackFlags);
}
else
{
apiToken.setLastTag(rxApiToken);
}
}
#endif
#if defined(RBX_RCC_SECURITY)
// inner product in gf2.
// in gf2, * is &, + is ^. if x, decodeKey are treated as bit-vectors, then the
// bit-element multiply is handled by bitwise and. The summation is the parity of
// the result. this is the same as popcnt (number of 1's) being an odd number.
static unsigned int linearDecode(unsigned int x, unsigned int decodeKey)
{
return (1 & __popcnt(x & decodeKey));
}
void CheatHandlingServerReplicator::processRockyMccReport(const MccReport& report)
{
if (enableMccCheckBypass)
{
return;
}
RakNet::Time delta = RakNet::GetTimeMS() - replicatorStats.lastReceivedMccTime;
if (!reportedMccItemTime && delta >= DFInt::ReportTimeLimit2)
{
Analytics::InfluxDb::Points mccItemAnalytics;
mccItemAnalytics.addPoint("delta", delta);
mccItemAnalytics.addPoint("num", numMccItems);
mccItemAnalytics.report("ServerMccItem", DFInt::Rtl2InfluxHundredthsPercentage);
reportedMccItemTime = true;
}
replicatorStats.lastReceivedMccTime = RakNet::GetTimeMS();
++numMccItems;
const std::string& configString = DFString::US30605p4;
// add report checking here
int cpuid[4];
__cpuid(cpuid, 1);
if (cpuid[2] & (1<<23)) // advanced bit manipulation support (popcnt), bit 23 of ecx
{
// some of these are not sent from the client
unsigned int decodedStats =
(linearDecode(report.localChecksEncoded, kGf2DecodeLut[MCC_FAKE_FFLAG_IDX])<<MCC_FAKE_FFLAG_IDX)
|(linearDecode(report.localChecksEncoded, kGf2DecodeLut[MCC_FREECONSOLE_IDX])<<MCC_FREECONSOLE_IDX)
//(linearDecode(report.localChecksEncoded, kGf2DecodeLut[MCC_SPEED_IDX])<<MCC_SPEED_IDX)
//
//|(linearDecode(report.localChecksEncoded, kGf2DecodeLut[MCC_MCC_IDX])<<MCC_MCC_IDX)
//|(linearDecode(report.localChecksEncoded, kGf2DecodeLut[MCC_PMC_IDX])<<MCC_PMC_IDX)
//|(linearDecode(report.localChecksEncoded, kGf2DecodeLut[MCC_BAD_IDX])<<MCC_BAD_IDX)
|(linearDecode(report.localChecksEncoded, kGf2DecodeLut[MCC_INIT_IDX])<<MCC_INIT_IDX)
//
|(linearDecode(report.localChecksEncoded, kGf2DecodeLut[MCC_NULL1_IDX])<<MCC_NULL1_IDX)
|(linearDecode(report.localChecksEncoded, kGf2DecodeLut[MCC_NULL0_IDX])<<MCC_NULL0_IDX)
|(linearDecode(report.localChecksEncoded, kGf2DecodeLut[MCC_VEH_IDX])<<MCC_VEH_IDX)
|(linearDecode(report.localChecksEncoded, kGf2DecodeLut[MCC_GTX_IDX])<<MCC_GTX_IDX)
//
|(linearDecode(report.localChecksEncoded, kGf2DecodeLut[MCC_HWBP_IDX])<<MCC_HWBP_IDX)
|(linearDecode(report.localChecksEncoded, kGf2DecodeLut[MCC_RDATA_IDX])<<MCC_RDATA_IDX)
|(linearDecode(report.localChecksEncoded, kGf2DecodeLut[MCC_VMP_IDX])<<MCC_VMP_IDX)
|(linearDecode(report.localChecksEncoded, kGf2DecodeLut[MCC_TEXT_IDX])<<MCC_TEXT_IDX);
decodedStats &= ~mccStatsMask;
if (decodedStats)
{
bool configError = false;
auto checkKickOrReport = [this, decodedStats, configString](unsigned int mask, const char* msg){doRemoteSysStats(decodedStats, mask, "mcc", msg, configString);};
checkKickOrReport((1<<14), "mcc FakeFFlag, 14");
checkKickOrReport((1<<13), "mcc FreeConsole, 13");
checkKickOrReport((1<<5), "mcc VehUnhook, 5");
checkKickOrReport((1<<4), "mcc GtxHook, 4");
#if !defined(_NOOPT) && !defined(_DEBUG)
checkKickOrReport((1<<3), "mcc HwbpSet, 3");
checkKickOrReport((1<<2), "mcc BadRdataSection, 2");
checkKickOrReport((1<<1), "mcc BadVmpSection, 1");
checkKickOrReport((1<<0), "mcc BadTextSection, 0");
#endif
}
mccStatsMask |= decodedStats;
}
else
{
return;
}
double now = Time::nowFast().timestampSeconds();
if (firstMccReportRxTime == 0.0)
{
firstMccReportRxTime = now;
lastMccReportRxTime = now;
firstMccReport = report;
lastMccReport = report;
return;
}
auto validateTimes = [this, configString](unsigned int t1, unsigned int t0, unsigned int mask, const char* msg)
{
if (t1 - t0 + 10 >= 20) // this should be conservative. abs(t1-t0) >= 10
{
doRemoteSysStats(0xFFFFFFFF, mask, "mcc", msg, configString);
}
};
validateTimes(report.badAppRunTime, lastMccReport.badAppRunTime, (1<<MCC_BAD_IDX), "mcc too long between badApp, 9");
validateTimes(report.memcheckRunTime, lastMccReport.memcheckRunTime, (1<<MCC_PMC_IDX), "mcc too long between memcheckRun, 10");
validateTimes(report.mccRunTime, lastMccReport.mccRunTime, (1<<MCC_MCC_IDX), "mcc too long between mcc run, 11");
unsigned int diffRxTimes = now - firstMccReportRxTime;
unsigned int diffRunTimes = report.memcheckRunTime - firstMccReport.memcheckRunTime;
validateTimes(diffRxTimes, diffRunTimes, (1<<MCC_SPEED_IDX), "mcc time moving too fast, 12");
if (!reportedMccError && (diffRxTimes - diffRunTimes + 10 >= 20))
{
Analytics::InfluxDb::Points mccErrorAnalytics;
mccErrorAnalytics.addPoint("rxFirst", firstMccReportRxTime);
mccErrorAnalytics.addPoint("rxNow", now);
mccErrorAnalytics.addPoint("rptFirst", firstMccReport.memcheckRunTime);
mccErrorAnalytics.addPoint("rptNow", report.memcheckRunTime);
mccErrorAnalytics.report("mccTime", DFInt::Rtl5InfluxHundredthsPercentage);
reportedMccError = true;
}
lastMccReportRxTime = now;
lastMccReport = report;
}
#endif
#if defined(RBX_RCC_SECURITY)
void CheatHandlingServerReplicator::processApiStats(unsigned long long apiStats)
{
// split the msb's out from the lsb
unsigned int apiStatsUpper = apiStats >> 32;
unsigned int apiStatsLower = apiStats & ~apiStatsMask;
bool configError = false;
if (apiStatsUpper)
{
if (DFFlag::US28292p2 && !reportedApiTamper)
{
RBX::Analytics::GoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "SecurityException", "Api Token Tamper");
}
if (DFFlag::US28292p3)
{
players->onRemoteSysStats(remotePlayer->getUserID(), "1920x1200", "Zot");
}
reportedApiTamper = true;
}
else if (apiStatsLower)
{
uint32_t configMask;
configMask = getSecurityMask(DFString::US30605p3, kKickChar, &configError);
if (configMask & apiStatsLower)
{
std::stringstream msgStream;
msgStream << "ApiStats: ";
msgStream << std::hex << apiStatsLower;
std::string msg = msgStream.str();
RBX::Analytics::GoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "SecurityException", msg.c_str());
}
configMask |= getSecurityMask(DFString::US30605p3, kReportChar, &configError);
if (configMask & apiStatsLower)
{
players->onRemoteSysStats(remotePlayer->getUserID(), "1920x1200", "Zix");
}
apiStatsMask |= apiStatsLower;
}
if (configError)
{
reportConfigMaskError("US30605p3");
}
}
#endif
#if defined(RBX_RCC_SECURITY)
void CheatHandlingServerReplicator::updateHashState(PmcHashContainer& netHashes, unsigned long long* securityTokens)
{
size_t numItems = netHashes.hash.size();
if (!hashInitialized)
{
lastHashes.resize(numItems);
for (size_t i = 0; i < numItems; ++i)
{
lastHashes[i] = hashes.hash[i];
}
hashInitialized = true;
}
hashNonce = netHashes.nonce;
prevApiToken = securityTokens[1];
}
#endif
#if defined(RBX_RCC_SECURITY)
void CheatHandlingServerReplicator::checkPingItemTime()
{
RakNet::Time delta = RakNet::GetTimeMS() - replicatorStats.lastReceivedPingTime;
if (!reportedPingItemTime && delta >= DFInt::ReportTimeLimit3)
{
Analytics::InfluxDb::Points pingItemAnalytics;
pingItemAnalytics.addPoint("delta", delta);
pingItemAnalytics.addPoint("num", numPingItems);
pingItemAnalytics.report("ServerPingItem", DFInt::Rtl3InfluxHundredthsPercentage);
reportedPingItemTime = true;
}
++numPingItems;
}
#endif
#if defined(RBX_RCC_SECURITY)
void CheatHandlingServerReplicator::doRemoteSysStats(unsigned int sendStats, unsigned int mask, const char* codeName, const char* details, const std::string& configString)
{
bool configError = false;
if (sendStats & mask)
{
uint32_t configMask = getSecurityMask(configString, kKickChar, &configError);
if (configMask & mask)
{
players->onRemoteSysStats(remotePlayer->getUserID(), "1920x1200", codeName);
}
configMask |= getSecurityMask(configString, kReportChar, &configError);
if (configMask & mask)
{
RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "SecurityException", details);
}
}
if (configError)
{
reportConfigMaskError(configString.c_str());
}
}
void CheatHandlingServerReplicator::doDelayedSysStats(unsigned int sendStats, unsigned int mask, const char* codeName, const char* details)
{
bool configError = false;
if (sendStats & mask)
{
uint32_t configMask = getSecurityMask(DFString::US30605p1, kKickChar, &configError);
if (configMask & mask)
{
kickName = codeName;
kickTimeSec = Time::nowFastSec() + 60 + (rand() % 0x80);
}
configMask |= getSecurityMask(DFString::US30605p1, kReportChar, &configError);
if (configMask & mask)
{
RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "SecurityException", details);
}
}
if (configError)
{
reportConfigMaskError("US30605p1");
}
}
#endif
#if defined(RBX_RCC_SECURITY)
void CheatHandlingServerReplicator::processHashStats(unsigned int hashStats)
{
using namespace RBX::Hasher;
bool configError = false;
if ((!enableHashCheckBypass) && (hashStats))
{
unsigned int mask = getSecurityMask(DFString::US30605p2, kKickChar, &configError)
| getSecurityMask(DFString::US30605p2, kGoldKickChar, &configError)
| getSecurityMask(DFString::US30605p2, kGoldReportChar, &configError);
if (hashStats & mask)
{
//players->onRemoteSysStats(remotePlayer->getUserID(), "1920x1200", "hector");
}
mask |= getSecurityMask(DFString::US30605p2, kReportChar, &configError);
if (hashStats & mask)
{
std::stringstream msgStream;
msgStream << "MemHashError: ";
msgStream << std::hex << hashStats;
std::string msg = msgStream.str();
RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "SecurityException", msg.c_str());
}
}
if (configError)
{
reportConfigMaskError("US30605p2");
}
}
void CheatHandlingServerReplicator::processGoldHashStats(unsigned int hashStats)
{
using namespace RBX::Hasher;
bool configError = false;
if ((!enableHashCheckBypass) && (hashStats))
{
unsigned int mask = getSecurityMask(DFString::US30605p2, kGoldKickChar, &configError);
if (hashStats & mask)
{
players->onRemoteSysStats(remotePlayer->getUserID(), "1920x1200", "ghector");
}
mask |= getSecurityMask(DFString::US30605p2, kGoldReportChar, &configError);
if (hashStats & mask)
{
std::stringstream msgStream;
msgStream << "GoldMemHashError: ";
msgStream << std::hex << hashStats;
std::string msg = msgStream.str();
RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "SecurityException", msg.c_str());
}
}
if (configError)
{
reportConfigMaskError("US30605p2");
}
}
#endif
#if defined(RBX_RCC_SECURITY)
void CheatHandlingServerReplicator::processSendStats(unsigned int sendStats, unsigned int extraStats) {
unsigned int maskedSendStats = sendStats & ~sendStatsMask;
if (maskedSendStats)
{
#if defined(_DEBUG) || defined(_NOOPT)
StandardOut::singleton()->printf(MESSAGE_ERROR, "ServerReplicator::processSendStats sendStats=0x%08X, extraStats=0x%08X", sendStats, extraStats);
#endif
doRemoteSysStats(maskedSendStats, HATE_IMPOSSIBLE_ERROR, "impala", "Impossible Error (31)");
if (maskedSendStats && ((maskedSendStats & HATE_IMPOSSIBLE_ERROR) == 0))
{
//doRemoteSysStats(maskedSendStats, HATE_CE_ASM, "robert", "WriteCopy changed (30)");
//doRemoteSysStats(maskedSendStats, HATE_NEW_AV_CHECK, "moded", "Stealth Edit Revival (29)");
doRemoteSysStats(maskedSendStats, HATE_HASH_FUNCTION_CHANGED, "booing", "Tried to modify hash function (28)");
doRemoteSysStats(maskedSendStats, HATE_RETURN_CHECK, "bobby", "Function Return Check Failed (27)");
doRemoteSysStats(maskedSendStats, HATE_VERB_SNATCH, "vera", "Tried to get build tools (26)");
doRemoteSysStats(maskedSendStats, HATE_VEH_HOOK, "vegah", "VEH used (25)"); // this one is still weird for some reason
doRemoteSysStats(maskedSendStats, HATE_HSCE_HASH_CHANGED, "fisher", "HumanoidState::computeEvent changed (24)");
doRemoteSysStats(maskedSendStats, HATE_DLL_INJECTION, "dallas", "DLL Injection (23)");
doRemoteSysStats(maskedSendStats, HATE_INVALID_ENVIRONMENT, "tomy", "Sandbox or VM detected (22)");
doRemoteSysStats(maskedSendStats, HATE_SPEEDHACK, "usain", "Speedhack. (21)");
doRemoteSysStats(maskedSendStats, HATE_LUA_VM_HOOKED, "carol", "Lua vm hooked (20)");
doRemoteSysStats(maskedSendStats, HATE_OSX_MEMORY_HASH_CHANGED, "steven", "OSX hash changed (19)");
doRemoteSysStats(maskedSendStats, HATE_UNHOOKED_VEH, "larry", "Our VEH hook removed (18)");
doRemoteSysStats(maskedSendStats, HATE_CHEATENGINE_NEW, "mal", "Any New CE Method (17)");
doRemoteSysStats(maskedSendStats, HATE_HSCE_EBX, "ebx", " HumanoidState::computeEvent changed ebx (16)");
doRemoteSysStats(maskedSendStats, HATE_WEAK_DM_POINTER_BROKEN, "terrance", "Early null weak pointer (15)");
doRemoteSysStats(maskedSendStats, HATE_LUA_HASH_CHANGED, "ursula", "Lua hash changed (14)");
doRemoteSysStats(maskedSendStats, HATE_DESTROY_ALL, "bruger", "Speculative Call Check (13)");
doRemoteSysStats(maskedSendStats, HATE_SEH_CHECK, "seth", "SEH chain into dll (12)");
doRemoteSysStats(maskedSendStats, HATE_HOOKED_GTX, "curly", "Hooked API function (11)");
doRemoteSysStats(maskedSendStats, HATE_DEBUGGER, "olivia", "Debugger found (10)");
doRemoteSysStats(maskedSendStats, HATE_LUA_SCRIPT_HASH_CHANGED, "norman", "Lua script hash changed (9)");
doRemoteSysStats(maskedSendStats, HATE_CATCH_EXECUTABLE_ACCESS_VIOLATION, "mallory", "Catch executable acccess violation (8)");
doRemoteSysStats(maskedSendStats, HATE_CONST_CHANGED, "lance", "Const Changed (7)");
doRemoteSysStats(maskedSendStats, HATE_INVALID_BYTECODE, "jack", "Invalid bytecode (6)");
doRemoteSysStats(maskedSendStats, HATE_MEMORY_HASH_CHANGED, "imogen", "Memory hash changed (5)");
doRemoteSysStats(maskedSendStats, HATE_ILLEGAL_SCRIPTS, "ivan", "Illegal scripts (4)");
doRemoteSysStats(maskedSendStats, HATE_SIGNATURE, "omar", "Bad signature (3)");
doRemoteSysStats(maskedSendStats, HATE_NEW_HWBP, "moe", "detected HWBP (2)");
doRemoteSysStats(maskedSendStats, HATE_XXHASH_BROKEN, "lafayette", "xxhash broken (1)");
doRemoteSysStats(maskedSendStats, HATE_CHEATENGINE_OLD, "murdle", "Cheat Engine Stable Methods (0)");
}
sendStatsMask |= sendStats;
}
unsigned int maskedExtraStats = extraStats & ~extraStatsMask;
if (maskedExtraStats)
{
doRemoteSysStats(maskedExtraStats, SCORN_IMPOSSIBLE_ERROR, "impala", "Scorn Impossible Error (31:12)", ::DFString::US30605p5);
if (maskedExtraStats && ((maskedExtraStats & SCORN_IMPOSSIBLE_ERROR) == 0))
{
doRemoteSysStats(maskedExtraStats, SCORN_REPLICATE_PROP, "tochigi", "Scorn Replication (11:0)", ::DFString::US30605p5);
}
extraStatsMask |= extraStats;
}
if ((kickTimeSec > 0) && (Time::nowFastSec() > kickTimeSec))
{
players->onRemoteSysStats(remotePlayer->getUserID(), "1920x1200", kickName);
}
}
#endif
void ServerReplicator::readItem(RakNet::BitStream& inBitstream, RBX::Network::Item::ItemType itemType)
{
switch (itemType)
{
default:
Super::readItem(inBitstream, itemType);
break;
case Item::ItemTypeHash:
readHashItem(inBitstream);
break;
case Item::ItemTypeUpdateClientQuota:
readClientQuotaUpdate(inBitstream);
break;
case Item::ItemTypeRegionRemoval:
NETPROFILE_START("ReadItemTypeRegionRemoval", &inBitstream);
streamJob->readRegionRemoval(inBitstream);
NETPROFILE_END("ReadItemTypeRegionRemoval", &inBitstream);
break;
case Item::ItemTypeInstanceRemoval:
streamJob->readInstanceRemoval(inBitstream);
break;
case Item::ItemTypeRequestCharacter:
readRequestCharacter(inBitstream);
break;
case Item::ItemTypePropAcknowledgement:
readPropAcknowledgement(inBitstream);
break;
case Item::ItemTypeRocky:
readRockyItem(inBitstream);
break;
}
}
void ServerReplicator::readClientQuotaUpdate(RakNet::BitStream& bitStream)
{
int diff;
short maxRegionRadius;
bitStream >> diff;
bitStream >> maxRegionRadius;
if (streamJob)
streamJob->updateClientQuota(diff, maxRegionRadius);
}
void ServerReplicator::PlaceAuthenticationThread(int previousPlaceId, int requestedPlaceId)
{
PlaceAuthenticationThreadImpl(previousPlaceId, requestedPlaceId);
}
void ServerReplicator::PlaceAuthenticationThreadImpl(int previousPlaceId, int requestedPlaceId)
{
placeAuthenticationState = PlaceAuthenticationState_Authenticated;
}
void ServerReplicator::onPlaceAuthenticationComplete(PlaceAuthenticationState placeAuthenticationResult)
{
RBXASSERT(placeAuthenticationState == placeAuthenticationResult);
if (placeAuthenticationState == PlaceAuthenticationState_Authenticated)
placeAutenticatedSignal();
}
#if defined(RBX_RCC_SECURITY)
void CheatHandlingServerReplicator::PlaceAuthenticationThreadImpl(int previousPlaceId, int requestedPlaceId)
{
placeAuthenticationState = PlaceAuthenticationState_Requesting;
#ifdef NETWORK_DEBUG
if (requestedPlaceId == 0)
{
requestedPlaceId = 1818;
}
#endif
std::string baseUrl = ServiceProvider::create<ContentProvider>(this)->getApiBaseUrl();
char urlBuf[2048] = {0};
sprintf_s(urlBuf, 2048, "%suniverses/validate-place-join?originPlaceId=%d&destinationPlaceId=%d", baseUrl.c_str(), previousPlaceId, requestedPlaceId);
std::string url = urlBuf;
try
{
std::string response = "";
if (RBX::HttpRbxApiService* apiService = RBX::ServiceProvider::find<RBX::HttpRbxApiService>(this))
{
RBX::Http http(url);
apiService->get(http, true, RBX::PRIORITY_EXTREME, response);
}
else
{
RBX::Http(url).get(response);
}
#ifdef NETWORK_DEBUG
StandardOut::singleton()->printf(MESSAGE_INFO, "Place authentication requested (%d -> %d). Result: %s", previousPlaceId, requestedPlaceId, response.c_str());
#endif
if (response == "true")
{
placeAuthenticationState = PlaceAuthenticationState_Authenticated;
}
else
{
placeAuthenticationState = PlaceAuthenticationState_Denied;
}
}
catch (RBX::base_exception& e)
{
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_ERROR, "Exception in place validation: %s", e.what());
// we have a web service exception, just let the user play if they are not teleporting
if (previousPlaceId == 0)
{
placeAuthenticationState = PlaceAuthenticationState_Authenticated;
}
else
{
placeAuthenticationState = PlaceAuthenticationState_Denied;
}
}
if (DFFlag::DisablePlaceAuthenticationPoll)
{
// use a write task here to avoid threading issues
DataModel::get(this)->submitTask(boost::bind(&ServerReplicator::onPlaceAuthenticationComplete, this, placeAuthenticationState), DataModelJob::Write);
}
server->registerPlaceAuthenticationResult(previousPlaceId, placeAuthenticationState);
}
#endif
#if defined(RBX_RCC_SECURITY)
void CheatHandlingServerReplicator::sendNetPmcChallenge()
{
if (!reportedNetPmcPending && (!reportedNetPmcError || !DFFlag::HashConfigP2) && netPmc.tooManyPending())
{
reportedNetPmcPending = true;
if (DFFlag::HashConfigP7)
{
players->onRemoteSysStats(remotePlayer->getUserID(), "1920x1200", "Guardian");
}
RobloxGoogleAnalytics::trackEvent(GA_CATEGORY_GAME, "SecurityException", "NetPmc Pending P7.");
// additional reporting
if (!reportedNetPmcSent )
{
std::stringstream msg;
uint32_t sent;
uint32_t recv;
uint32_t pending;
unsigned int debugValue = netPmc.generateDebugInfo(this, sent, recv, pending);
debugValue |= (enableHashCheckBypass ? (1<<16) : 0);
debugValue |= (netPmc.tooManyPending() ? (1<<17) : 0);
debugValue |= (DFFlag::HashConfigP1 ? (1<<18) : 0);
debugValue |= (DFFlag::HashConfigP2 ? (1<<19) : 0);
debugValue |= (DFFlag::HashConfigP7 ? (1<<20) : 0);
msg << std::hex << debugValue;
double duration = (Time::nowFast() - startTime).msec();
Analytics::InfluxDb::Points netPmcAnalytics;
netPmcAnalytics.addPoint("flags", msg.str().c_str());
netPmcAnalytics.addPoint("v0", sent);
netPmcAnalytics.addPoint("v1", recv);
netPmcAnalytics.addPoint("v2", pending);
netPmcAnalytics.addPoint("v3", duration);
netPmcAnalytics.report("hashstats", DFInt::HashConfigP9);
reportedNetPmcSent = true;
}
}
else if (DFFlag::HashConfigP1 && !enableHashCheckBypass && netPmc.canSendChallenge(this))
{
uint8_t idx = netPmc.getRandomChallenge();
netPmc.sendChallenge(idx);
pendingItems.push_back(new NetPmcChallengeItem(this, idx));
}
}
#endif
void ServerReplicator::readRequestCharacter(RakNet::BitStream& inBitstream)
{
// #TODO: remove this function
joinAnalytics.addPoint("CharacterRequestReceived", (Time::nowFast() - startTime).seconds());
// Note, this function can be called multiple times due to multiple client requests
// Make sure all the core logic here are wrapped by canSpawnPlayer
#ifdef NETWORK_DEBUG
StandardOut::singleton()->printf(MESSAGE_INFO, "Handling character request...");
#endif
bool canSpawnPlayer = true;
if (!DFFlag::DisablePlaceAuthenticationPoll)
{
if (placeAuthenticationState == PlaceAuthenticationState_DisconnectingClient)
{
canSpawnPlayer = false;
}
else
{
if (placeAuthenticationState == PlaceAuthenticationState_Requesting)
{
// ask client to retry
RakNet::BitStream outBitStream;
bool retry = true;
outBitStream << (unsigned char) ID_PLACEID_VERIFICATION;
outBitStream << retry;
rakPeer->rawPeer()->Send(&outBitStream, settings().getDataSendPriority(), DATAMODEL_RELIABILITY, DATA_CHANNEL, remotePlayerId, false);
canSpawnPlayer = false;
}
else if (placeAuthenticationState == PlaceAuthenticationState_Authenticated)
{
canSpawnPlayer = true;
}
else
{
canSpawnPlayer = false;
}
}
}
unsigned int sendStats;
inBitstream >> sendStats;
std::string spawnName;
inBitstream >> spawnName;
shared_ptr<Instance> instance;
RBX::Guid::Data id;
if (deserializeInstanceRef(inBitstream, instance, id))
{
// ignore character request from client, it'll be automatically handled after server receives the player and sent all initial instances.
return;
}
else
{
FASTLOG(FLog::Error, "RequestCharacter - could not resolve RemotePlayer");
throw std::runtime_error(format("Couldn't resolve remotePlayer %s from %s", id.readableString().c_str(), RakNetAddressToString(remotePlayerId).c_str()));
}
}
void ServerReplicator::processRequestCharacter(Instance* instance, RBX::Guid::Data id, unsigned int sendStats, std::string preferedSpawnName)
{
if (settings().printInstances) {
StandardOut::singleton()->printf(MESSAGE_SENSITIVE,
"Received remotePlayer %s from %s",
id.readableString().c_str(),
RakNetAddressToString(remotePlayerId).c_str());
}
if (!remotePlayer)
{
FASTLOG(FLog::Error, "RequestCharacter - RemotePlayer is null");
StandardOut::singleton()->printf(MESSAGE_SENSITIVE, "RequestCharacter - RemotePlayer is null. %s", RakNetAddressToString(remotePlayerId).c_str());
throw std::runtime_error("remotePlayer is NULL");
}
if (remotePlayer.get() != instance)
{
FASTLOG(FLog::Error, "RequestCharacter - RemotePlayer is wrong");
// TODO: Should we report this, too (using onRemoteSysStats)?
throw std::runtime_error("remotePlayer is wrong");
}
processSendStats(sendStats, kNoScornFlags);
installRemotePlayer(preferedSpawnName);
}
FilterResult ServerReplicator::filterReceivedChangedProperty(Instance* instance, const Reflection::PropertyDescriptor& desc)
{
RBXASSERT(instance);
if (Super::filterReceivedChangedProperty(instance, desc) == Reject)
return Reject;
if (propSync.onReceivedPropertyChanged(Reflection::ConstProperty(desc, instance)) == Reject)
return Reject;
if (isCloudEdit())
{
return Accept;
}
if (strictFilter)
{
FilterResult result = strictFilter->filterChangedProperty(instance, desc);
if (result == Reject && settings().printDataFilters)
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_WARNING, "Filtering is enabled. Property %s change for instance %s will not be replicated.", desc.name.c_str(), instance->getFullName().c_str());
return result;
}
FilterResult result;
if (basicFilter && basicFilter->filterChangedProperty(instance, desc, result))
{
if (!server->dataBasicFilteredSignal.empty())
server->dataBasicFilteredSignal(shared_from(this), result, shared_from(instance), desc.name.str);
return result;
}
if (desc != Instance::propParent) // propParent is handled in filterReceivedParent
if (this->filterProperty)
{
result = filterProperty(shared_from(instance), desc.name.str, Reflection::Variant());
if (!server->dataCustomFilteredSignal.empty())
server->dataCustomFilteredSignal(shared_from(this), result, shared_from(instance), desc.name.str);
return result;
}
return Accept;
}
FilterResult ServerReplicator::filterReceivedParent( Instance* instance, Instance* parent )
{
RBXASSERT(instance);
if (isCloudEdit())
{
return Accept;
}
if (Super::filterReceivedParent(instance, parent) == Reject)
return Reject;
FilterResult result;
// Can't move illegal scripts
if (!server->isScriptLegal(instance))
return Reject;
if (strictFilter)
{
result = strictFilter->filterParent(instance, parent);
if (result == Reject)
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_WARNING, "Filtering is enabled. Parent %s change for instance %s will not be accepted.", parent->getName().c_str(), instance->getFullName().c_str());
return result;
}
if (basicFilter && basicFilter->filterParent(instance, parent, result))
{
if (!server->dataBasicFilteredSignal.empty())
server->dataBasicFilteredSignal(shared_from(this), result, shared_from(instance), Instance::propParent.name.str);
return result;
}
if (this->filterProperty)
{
result = filterProperty(shared_from(instance), Instance::propParent.name.str, shared_from(parent));
if (!server->dataCustomFilteredSignal.empty())
server->dataCustomFilteredSignal(shared_from(this), result, shared_from(instance), Instance::propParent.name.str);
return result;
}
return Accept;
}
FilterResult ServerReplicator::filterPhysics(PartInstance* instance)
{
if (Super::filterPhysics(instance) == Reject)
return Reject;
return propSync.onReceivedPropertyChanged(Reflection::ConstProperty(PartInstance::prop_CFrame, instance));
}
void ServerReplicator::dataOutStep()
{
if (placeAuthenticationState == PlaceAuthenticationState_Denied)
{
// place authentication failed, disconnect the client
placeAuthenticationState = PlaceAuthenticationState_DisconnectingClient;
RakNet::BitStream outBitStream;
bool retry = false;
outBitStream << (unsigned char) ID_PLACEID_VERIFICATION;
outBitStream << retry;
rakPeer->rawPeer()->Send(&outBitStream, IMMEDIATE_PRIORITY, DATAMODEL_RELIABILITY, DATA_CHANNEL, remotePlayerId, false);
setAuthenticated(false);
}
propSync.expireItems();
Super::dataOutStep();
}
void ServerReplicator::onPropertyChanged(Instance* instance, const Reflection::PropertyDescriptor* descriptor)
{
Super::onPropertyChanged(instance, descriptor);
propSync.onPropertyChanged(Reflection::ConstProperty(*descriptor, instance));
}
void ServerReplicator::writeChangedProperty(const Instance* instance, const Reflection::PropertyDescriptor& desc, RakNet::BitStream& outBitStream)
{
DescriptorSender<RBX::Reflection::PropertyDescriptor>::IdContainer idContainer = propDictionary.getId(&desc);
int byteStart = outBitStream.GetNumberOfBytesUsed();
Item::writeItemType(outBitStream, Item::ItemTypeChangeProperty);
// Write the GUID
serializeId(outBitStream, instance);
// Write property name
propDictionary.send(outBitStream, idContainer.id);
// This is the mirror image of ClientReplicator::readChangedProperty
bool versionReset = propSync.onPropertySend(Reflection::ConstProperty(desc, instance)) == PropSync::Master::SendVersionReset;
outBitStream << versionReset;
serializePropertyValue(Reflection::ConstProperty(desc, instance), outBitStream, true/*useDictionary*/);
if (settings().printProperties) {
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_SENSITIVE,
"Replication prop: %s:%s.%s >> %s, bytes: %d",
instance->getClassName().c_str(),
instance->getGuid().readableString().c_str(),
desc.name.c_str(),
RakNetAddressToString(remotePlayerId).c_str(),
outBitStream.GetNumberOfBytesUsed()-byteStart
);
}
if (settings().trackDataTypes) {
replicatorStats.incrementPacketsSent(desc.category.str);
replicatorStats.samplePacketsSent(desc.category.str, outBitStream.GetNumberOfBytesUsed()-byteStart);
}
}
void ServerReplicator::writeChangedRefProperty(const Instance* instance,
const Reflection::RefPropertyDescriptor& desc, const Guid::Data& newRefGuid,
RakNet::BitStream& outBitStream)
{
DescriptorSender<RBX::Reflection::PropertyDescriptor>::IdContainer idContainer = propDictionary.getId(&desc);
int byteStart = outBitStream.GetNumberOfBytesUsed();
Item::writeItemType(outBitStream, Item::ItemTypeChangeProperty);
// Write the GUID
serializeId(outBitStream, instance);
// Write property name
propDictionary.send(outBitStream, idContainer.id);
// This is the mirror image of ClientReplicator::readChangedProperty
bool versionReset = propSync.onPropertySend(Reflection::ConstProperty(desc, instance)) == PropSync::Master::SendVersionReset;
outBitStream << versionReset;
if (newRefGuid.scope.isNull())
{
scopeNames.sendEmptyItem(outBitStream);
}
else
{
serializeId(outBitStream, newRefGuid);
}
if (settings().printProperties) {
RBX::StandardOut::singleton()->printf(RBX::MESSAGE_SENSITIVE,
"Replication ref prop: %s:%s.%s >> %s, bytes: %d",
instance->getClassName().c_str(),
instance->getGuid().readableString().c_str(),
desc.name.c_str(),
RakNetAddressToString(remotePlayerId).c_str(),
outBitStream.GetNumberOfBytesUsed()-byteStart
);
}
if (settings().trackDataTypes) {
replicatorStats.incrementPacketsSent(desc.category.str);
replicatorStats.samplePacketsSent(desc.category.str, outBitStream.GetNumberOfBytesUsed()-byteStart);
}
if (DFFlag::DebugLogProcessCharacterRequestTime)
{
if (pendingCharaterRequest && (pendingCharaterRequest == instance) && (desc.name == "Character"))
{
RobloxGoogleAnalytics::trackUserTiming(GA_CATEGORY_GAME, "ProcessRequestCharacterTime", (Time::nowFast()-pendingCharacterRequestStartTime).msec(), "Replicate");
pendingCharaterRequest = NULL;
}
}
}
void ServerReplicator::readPropAcknowledgement(RakNet::BitStream& inBitstream)
{
int version;
inBitstream >> version;
const Reflection::PropertyDescriptor* propertyDescriptor;
propDictionary.receive(inBitstream, propertyDescriptor, false);
shared_ptr<Instance> instance;
RBX::Guid::Data id;
if (deserializeInstanceRef(inBitstream, instance, id))
propSync.onReceivedAcknowledgement(Reflection::ConstProperty(*propertyDescriptor, instance.get()), version);
}
static bool mightRunOnClient(const Instance* instance)
{
if (!instance)
return false;
if (Instance::fastDynamicCast<const LocalScript>(instance))
return true;
return false;
}
void ServerReplicator::onServiceProvider(ServiceProvider* oldProvider, ServiceProvider* newProvider)
{
if (streamJob)
{
TaskScheduler::singleton().remove(streamJob);
streamJob->unregisterCoarsePrimitiveCallback();
streamJob.reset();
}
if (sendStatsJob)
{
TaskScheduler::singleton().remove(sendStatsJob);
sendStatsJob.reset();
}
if (newProvider)
{
placeAuthenticationState = PlaceAuthenticationState_Init;
Workspace* workspace = newProvider->find<Workspace>();
if (!workspace)
throw std::runtime_error("ServerReplicator unable to find workspace.");
if (!isCloudEdit() && workspace->getNetworkStreamingEnabled())
{
streamingEnabled = true;
streamJob = shared_ptr<Replicator::StreamJob>(new Replicator::StreamJob(*this));
TaskScheduler::singleton().add(streamJob);
}
if (workspace->getNetworkFilteringEnabled())
strictFilter.reset(new StrictNetworkFilter(this));
}
// TODO: FIX THIS BEFORE INTEGRATING TO CI
// addTopReplicationContainers needs streamJob to be set.
// Ideally addTopReplicationContainers should only be called by server replicators.
// If addTopReplicationContainers was called here, then we could manage to initialize
// streamJob in the right order.
Super::onServiceProvider(oldProvider, newProvider);
joinAnalytics.addPoint("ReplicatorAdded", (Time::nowFast()-startTime).seconds());
}
static void serializeSFFlag(const std::string& name, const std::string& varValue, void* context)
{
RakNet::BitStream* bitStream = reinterpret_cast<RakNet::BitStream*>(context);
//RBX::StandardOut::singleton()->printf(RBX::MESSAGE_INFO,
// "Sent FFLag: %s: %s", name.c_str(), varValue.c_str());
RakNet::RakString rakName = name.c_str();
RakNet::RakString rakValue = varValue.c_str();
bitStream->Write(rakName);
bitStream->Write(rakValue);
FLog::SetValueFromServer(name, varValue);
}
void ServerReplicator::serializeSFFlags(RakNet::BitStream& outBitStream) const
{
outBitStream.Write(FLog::GetNumSynchronizedVariable());
FLog::ForEachVariable(&serializeSFFlag, &outBitStream, FASTVARTYPE_SYNC);
}
void ServerReplicator::sendDictionaries()
{
rakPeer->rawPeer()->Send(&getApiDictionaryBitStream(), settings().getDataSendPriority(),
DATAMODEL_RELIABILITY, DATA_CHANNEL, remotePlayerId, false);
}
using namespace RBX::Reflection;
void ServerReplicator::writeDescriptorSchema(const ClassDescriptor* classDesc, RakNet::BitStream& bitStream) const
{
unsigned int classId = classDictionary.getId(classDesc).id;
#ifdef NETWORK_DEBUG
//StandardOut::singleton()->printf(RBX::MESSAGE_INFO, "Class name: %s, id: %d", classDesc->name.toString().c_str(), classId);
#endif
RakNet::RakString className = classDesc->name.c_str();
bitStream << classId; // uint
bitStream.Write(className); // rakStr
bitStream << (unsigned char)classDesc->getReplicationLevel();
bitStream << (unsigned int)classDesc->Reflection::MemberDescriptorContainer<Reflection::PropertyDescriptor>::descriptor_size(); // uint
Reflection::MemberDescriptorContainer<Reflection::PropertyDescriptor>::Collection::const_iterator propIter =
classDesc->Reflection::MemberDescriptorContainer<Reflection::PropertyDescriptor>::descriptors_begin();
for (; propIter != classDesc->Reflection::MemberDescriptorContainer<Reflection::PropertyDescriptor>::descriptors_end(); propIter++)
{
const Reflection::PropertyDescriptor* propDesc = *propIter;
unsigned int propId = propDictionary.getId(propDesc).id;
RakNet::RakString propName = propDesc->name.c_str();
unsigned int typeId = typeDictionary.getId(&propDesc->type).id;
RakNet::RakString propType = propDesc->type.name.c_str();
bitStream << propId; // uint
bitStream.Write(propName); // rakStr
bitStream << typeId; // unit
bitStream.Write(propType); // rakStr
bitStream << propDesc->canReplicate(); // bool
bitStream << propDesc->bIsEnum; // bool
if (propDesc->bIsEnum)
{
const EnumPropertyDescriptor& enumDesc = static_cast<const EnumPropertyDescriptor&>(*propDesc);
bitStream << (unsigned int)enumDesc.enumDescriptor.getEnumCountMSB(); // uint
}
//StandardOut::singleton()->printf(RBX::MESSAGE_INFO, " Prop name: %s, type: %s, id: %d"
// , propDesc->name.c_str()
// , propDesc->type.name.c_str()
// , propertyId
// );
}
bitStream << (unsigned int)classDesc->Reflection::MemberDescriptorContainer<Reflection::EventDescriptor>::descriptor_size(); // uint
Reflection::MemberDescriptorContainer<Reflection::EventDescriptor>::Collection::const_iterator eventIter =
classDesc->Reflection::MemberDescriptorContainer<Reflection::EventDescriptor>::descriptors_begin();
for (; eventIter != classDesc->Reflection::MemberDescriptorContainer<Reflection::EventDescriptor>::descriptors_end(); eventIter++)
{
const Reflection::EventDescriptor* eventDesc = *eventIter;
const Reflection::SignatureDescriptor& signatureDescriptor = eventDesc->getSignature();
unsigned int eventId = eventDictionary.getId(eventDesc).id;
bitStream << eventId; // uint
RakNet::RakString eventName = eventDesc->name.c_str();
bitStream.Write(eventName); // rakStr
bitStream << (unsigned int)signatureDescriptor.arguments.size(); //unit
for(std::list<Reflection::SignatureDescriptor::Item>::const_iterator typeIter = signatureDescriptor.arguments.begin();
typeIter != signatureDescriptor.arguments.end();
++typeIter)
{
unsigned int typeId = typeDictionary.getId(&(*typeIter->type)).id;
bitStream << typeId;
}
//StandardOut::singleton()->printf(RBX::MESSAGE_INFO, " Event name: %s, id: %d, size: %d"
// , eventDesc->name.c_str()
// , eventId
// , signatureDescriptor.arguments.size()
// );
}
}
RakNet::BitStream ServerReplicator::apiSchemaBitStream;
void ServerReplicator::generateSchema(const ServerReplicator* serverRep, bool force)
{
if (force)
{
// for unit tests
// since we will be testing with different API sets in unit tests, we will have different schema
apiSchemaBitStream.Reset();
}
else if (apiSchemaBitStream.GetNumberOfBytesUsed() > 0)
{
// already generated
return;
}
apiSchemaBitStream << (unsigned char) ID_SCHEMA_SYNC;
RakNet::BitStream bitStream;
// first, teach all enums
bitStream << (unsigned int)EnumDescriptor::allEnumSize(); // uint
std::vector< const EnumDescriptor* >::const_iterator enumIter = EnumDescriptor::enumsBegin();
std::vector< const EnumDescriptor* >::const_iterator enumEnd = EnumDescriptor::enumsEnd();
while (enumIter != enumEnd)
{
const EnumDescriptor* e = *enumIter;
RakNet::RakString enumName = e->name.c_str();
bitStream.Write(enumName); // rakStr
bitStream << (unsigned int)e->getEnumCountMSB(); // uint
++enumIter;
}
// then teach all classes
bitStream << (unsigned int)ClassDescriptor::all_size(); // uint
ClassDescriptor::ClassDescriptors::const_iterator iter = ClassDescriptor::all_begin();
ClassDescriptor::ClassDescriptors::const_iterator end = ClassDescriptor::all_end();
while (iter!=end)
{
serverRep->writeDescriptorSchema(*iter, bitStream);
++iter;
}
compressBitStream(bitStream, apiSchemaBitStream, 9);
}
RakNet::BitStream ServerReplicator::apiDictionaryBitStream;
void ServerReplicator::generateApiDictionary(const ServerReplicator* serverRep, bool force)
{
bool teachSchema = true;
if (force)
{
apiDictionaryBitStream.Reset();
}
else if (apiDictionaryBitStream.GetNumberOfBytesUsed() > 0)
{
return;
}
apiDictionaryBitStream << (unsigned char) ID_TEACH_DESCRIPTOR_DICTIONARIES;
RakNet::BitStream bitStream;
teachDictionaries(serverRep, bitStream, teachSchema, true);
compressBitStream(bitStream, apiDictionaryBitStream, 9);
}
const RakNet::BitStream& ServerReplicator::getSchemaBitStream() const
{
RBXASSERT(apiSchemaBitStream.GetNumberOfBytesUsed() > 0);
return apiSchemaBitStream;
}
const RakNet::BitStream& ServerReplicator::getApiDictionaryBitStream() const
{
RBXASSERT(apiDictionaryBitStream.GetNumberOfBytesUsed() > 0);
return apiDictionaryBitStream;
}
void ServerReplicator::teachSchema()
{
#ifdef NETWORK_DEBUG
StandardOut::singleton()->printf(RBX::MESSAGE_INFO, "ServerReplicator::teachSchema()");
#endif
FASTLOG(FLog::Network, "ServerReplicator::teachSchema()");
rakPeer->rawPeer()->Send(&getSchemaBitStream(), settings().getDataSendPriority(), DATAMODEL_RELIABILITY, DATA_CHANNEL, remotePlayerId, false);
}
bool ServerReplicator::isProtectedStringEnabled()
{
return false;
}
std::string ServerReplicator::encodeProtectedString(const ProtectedString& value, const Instance* instance, const Reflection::PropertyDescriptor& desc)
{
if (!isCloudEdit() && LuaVM::useSecureReplication())
{
boost::optional<long> index = server->getScriptIndexForSource(value.getSource());
if (!index)
{
return std::string();
}
else if (desc == Script::prop_EmbeddedSourceCode && instance && !mightRunOnClient(instance))
{
return StringConverter<long>::convertToString(*index);
}
else if (canUseProtocolVersion(28))
{
boost::optional<std::string> bytecode = server->getScriptBytecodeForIndex(*index, canUseProtocolVersion(33));
RBXASSERT(bytecode);
return *bytecode;
}
else
{
return value.getSource();
}
}
else
{
return value.getSource();
}
}
boost::optional<ProtectedString> ServerReplicator::decodeProtectedString(const std::string& value, const Instance* instance, const Reflection::PropertyDescriptor& desc)
{
if (!isCloudEdit() && LuaVM::useSecureReplication())
{
if (desc == Script::prop_EmbeddedSourceCode && instance && !mightRunOnClient(instance))
{
long index;
if (StringConverter<long>::convertToValue(value, index))
return server->getScriptSourceForIndex(index);
else
return boost::optional<ProtectedString>();
}
else if (canUseProtocolVersion(28))
{
boost::optional<long> index = server->getScriptIndexForBytecode(value, canUseProtocolVersion(33));
if (index)
return server->getScriptSourceForIndex(*index);
else
return boost::optional<ProtectedString>();
}
else
return ProtectedString::fromTrustedSource(value);
}
else
{
return ProtectedString::fromTrustedSource(value);
}
}
void ServerReplicator::sendJoinStatsToInflux()
{
bool hasTerrain = (megaClusterInstance && megaClusterInstance->isAllocated());
joinAnalytics.addPoint("HasTerrain", hasTerrain);
joinAnalytics.addPoint("BytesSent", getRakNetStats()->runningTotal[ACTUAL_BYTES_SENT]);
joinAnalytics.report("ServerJoin", DFInt::JoinInfluxHundredthsPercentage);
}