Files
HexUnlocked/src/unlocker/spoofer.cpp
T

836 lines
29 KiB
C++

#include "spoofer.h"
#include "utils.h"
#include "log-sink-cout.h"
#include <regex>
#include <map>
#include <fstream>
#include <tinymitm/proxy.h>
#include <seallib/assert.h>
#include <seallib/log.h>
#include <ixwebsocket/IXWebSocketServer.h>
#include <glaze/glaze.hpp>
/*
misc helper functions
*/
std::unordered_set<std::string> slasherNames = {
"Chuckles", "Bob", "HillBilly", "Nurse", "Shape", "Witch", "Killer07", "Cannibal", "Bear",
"Nightmare", "Pig", "Clown", "Spirit", "Plague", "Ghostface", "Demogorgon", "Oni", "Gunslinger"};
bool isSlasher(std::string name)
{
if (slasherNames.contains(name) || (name.length() == 3 && name[0] == 'K')) return true;
return false;
}
/*
spoofer impl
*/
Spoofer::Spoofer()
{
_log = new seallib::Logger("Spoofer");
_log->addSink(std::make_shared<ConOutSink>());
_log->info("Spoofer init");
loadConfig();
_log->info("Starting WebSocket server");
initServer();
}
Spoofer::~Spoofer()
{
_log->info("Stopping WebSocket server");
stopServer();
delete _log;
}
void Spoofer::registerListeners(TinyMITMProxy* proxy)
{
proxy->onClientRequest.addListener(
[this](const std::string& url, std::string& body, std::string& headers, bool& blockOutgoing) {
this->clientRequestHandler(url, body, headers, blockOutgoing);
});
proxy->onServerResponse.addListener(
[this](const std::string& url, std::string& body, std::string& headers, bool wasBlocked) {
this->serverResponseHandler(url, body, headers, wasBlocked);
});
}
/*
config
*/
struct FileConfig
{
SpooferConfig profile;
WSMessages::Toggles toggles;
};
void Spoofer::loadConfig()
{
FileConfig conf;
std::string buffer;
std::string configPath = utils::getExePath() + "config.json";
auto errCtx = glz::read_file_json(conf, configPath, buffer);
if (errCtx.ec == glz::error_code::none)
{
_camperItems = conf.profile.camperItems;
_camperAddons = conf.profile.camperAddons;
_slasherAddons = conf.profile.slasherAddons;
_camperOfferings = conf.profile.camperOfferings;
_slasherOfferings = conf.profile.slasherOfferings;
_globalOfferings = conf.profile.globalOfferings;
_camperPerks = conf.profile.camperPerks;
_slasherPerks = conf.profile.slasherPerks;
_catalogItemIds = conf.profile.catalogItemIds;
_dlcListGRDK = conf.profile.dlcListGRDK;
_dlcListEGS = conf.profile.dlcListEGS;
_dlcListSteam = conf.profile.dlcListSteam;
_unlockedCharacters = conf.profile.unlockedCharacters;
_spoofItems = conf.toggles.spoofItems;
_spoofPerks = conf.toggles.spoofPerks;
_spoofCatalog = conf.toggles.spoofCatalog;
_spoofDLCs = conf.toggles.spoofDLCs;
_log->info("Config loaded successfully from {}", configPath);
}
else
{
_log->info("Config not found or invalid at {}, creating default.", configPath);
saveConfig();
}
}
void Spoofer::saveConfig()
{
std::string configPath = utils::getExePath() + "config.json";
FileConfig conf;
conf.profile.camperItems = _camperItems;
conf.profile.camperAddons = _camperAddons;
conf.profile.slasherAddons = _slasherAddons;
conf.profile.camperOfferings = _camperOfferings;
conf.profile.slasherOfferings = _slasherOfferings;
conf.profile.globalOfferings = _globalOfferings;
conf.profile.camperPerks = _camperPerks;
conf.profile.slasherPerks = _slasherPerks;
conf.profile.catalogItemIds = _catalogItemIds;
conf.profile.dlcListGRDK = _dlcListGRDK;
conf.profile.dlcListEGS = _dlcListEGS;
conf.profile.dlcListSteam = _dlcListSteam;
conf.profile.unlockedCharacters = _unlockedCharacters;
conf.toggles.spoofItems = _spoofItems;
conf.toggles.spoofPerks = _spoofPerks;
conf.toggles.spoofCatalog = _spoofCatalog;
conf.toggles.spoofDLCs = _spoofDLCs;
std::string buffer;
auto errCtx = glz::write_file_json(conf, configPath, buffer);
if (errCtx.ec != glz::error_code::none) _log->error("Failed to save config to {}", configPath);
}
/*
websocket
*/
void Spoofer::initServer()
{
if (_wsServer) PANIC("Attempted to init websocket server from invalid state");
_wsServer = new ix::WebSocketServer(WS_PORT, WS_ADDR);
_wsServer->setOnClientMessageCallback([this](std::shared_ptr<ix::ConnectionState> connectionState,
ix::WebSocket& webSocket,
const std::unique_ptr<ix::WebSocketMessage>& msg) {
this->wsMessageCallback(connectionState, webSocket, msg);
});
std::pair<bool, std::string> res = _wsServer->listen();
if (!res.first) PANIC("Websocket server failed to start");
_log->verbose("WebSocket server running @ {}:{}", WS_ADDR, WS_PORT);
_wsServer->start();
}
void Spoofer::stopServer()
{
if (_wsServer)
{
_wsServer->stop();
delete _wsServer;
_wsServer = nullptr;
}
}
void Spoofer::wsMessageCallback(std::shared_ptr<ix::ConnectionState> /*connectionState*/, ix::WebSocket& webSocket,
const std::unique_ptr<ix::WebSocketMessage>& msg)
{
std::lock_guard<std::mutex> lock(_mutex);
switch (msg->type)
{
case ix::WebSocketMessageType::Open:
{
_log->verbose("Websocket connection open, URI: {}", msg->openInfo.uri);
WSMessages::Init initMsg;
initMsg.profile.camperItems = _camperItems;
initMsg.profile.camperAddons = _camperAddons;
initMsg.profile.slasherAddons = _slasherAddons;
initMsg.profile.camperOfferings = _camperOfferings;
initMsg.profile.slasherOfferings = _slasherOfferings;
initMsg.profile.globalOfferings = _globalOfferings;
initMsg.profile.camperPerks = _camperPerks;
initMsg.profile.slasherPerks = _slasherPerks;
initMsg.profile.catalogItemIds = _catalogItemIds;
initMsg.profile.dlcListGRDK = _dlcListGRDK;
initMsg.profile.dlcListEGS = _dlcListEGS;
initMsg.profile.dlcListSteam = _dlcListSteam;
initMsg.profile.unlockedCharacters = _unlockedCharacters;
initMsg.toggles.spoofItems = _spoofItems;
initMsg.toggles.spoofPerks = _spoofPerks;
initMsg.toggles.spoofCatalog = _spoofCatalog;
initMsg.toggles.spoofDLCs = _spoofDLCs;
std::string out;
auto errCtx = glz::write_json(initMsg, out);
if (errCtx && errCtx.ec != glz::error_code::none)
_log->error("Error occurred while writing init msg");
else
webSocket.send(out);
break;
}
case ix::WebSocketMessageType::Close:
_log->verbose("Websocket connection close");
break;
case ix::WebSocketMessageType::Message:
{
WSMessages::Request req;
auto err = glz::read_json(req, msg->str);
if (err.ec != glz::error_code::none)
{
_log->error("Failed to parse websocket message");
break;
}
switch (req.action)
{
case WSMessages::SYNC_STATE:
{
_camperItems = std::move(req.profile.camperItems);
_camperAddons = std::move(req.profile.camperAddons);
_slasherAddons = std::move(req.profile.slasherAddons);
_camperOfferings = std::move(req.profile.camperOfferings);
_slasherOfferings = std::move(req.profile.slasherOfferings);
_globalOfferings = std::move(req.profile.globalOfferings);
_camperPerks = std::move(req.profile.camperPerks);
_slasherPerks = std::move(req.profile.slasherPerks);
_catalogItemIds = std::move(req.profile.catalogItemIds);
_dlcListGRDK = std::move(req.profile.dlcListGRDK);
_dlcListEGS = std::move(req.profile.dlcListEGS);
_dlcListSteam = std::move(req.profile.dlcListSteam);
_unlockedCharacters = std::move(req.profile.unlockedCharacters);
saveConfig();
_log->verbose("Profile state synchronized.");
break;
}
case WSMessages::SYNC_TOGGLES:
{
_spoofItems = req.toggles.spoofItems;
_spoofPerks = req.toggles.spoofPerks;
_spoofCatalog = req.toggles.spoofCatalog;
_spoofDLCs = req.toggles.spoofDLCs;
saveConfig();
_log->verbose("Toggles synchronized.");
break;
}
default:
_log->warning("Unknown action: {}", static_cast<int>(req.action));
break;
}
break;
}
default:
break;
}
}
/*
endpoint related helper functions
*/
void Spoofer::modifyCharacterInventory(glz::generic& js)
{
bool slasher = isSlasher(js["characterName"].get<std::string>());
bool hasItems = js.is_object() && js.get_object().contains("characterItems") && js["characterItems"].is_array();
if (!hasItems) return;
std::unordered_set<std::string> existingItemIds;
std::unordered_map<std::string, int> stackableQty;
stackableQty.insert(_camperItems.begin(), _camperItems.end());
stackableQty.insert(_camperOfferings.begin(), _camperOfferings.end());
stackableQty.insert(_globalOfferings.begin(), _globalOfferings.end());
stackableQty.insert(_camperAddons.begin(), _camperAddons.end());
stackableQty.insert(_slasherAddons.begin(), _slasherAddons.end());
stackableQty.insert(_slasherOfferings.begin(), _slasherOfferings.end());
auto& itemsArr = js["characterItems"].get_array();
/*
existing items
*/
for (auto& item : itemsArr)
{
if (item.is_object() && item.get_object().contains("itemId") && item["itemId"].is_string())
{
std::string itemId = item["itemId"].get<std::string>();
existingItemIds.insert(itemId);
auto it = stackableQty.find(itemId);
if (it != stackableQty.end()) item["quantity"] = it->second;
}
}
/*
item injection
*/
auto appendItems = [&](const std::unordered_map<std::string, int>& idMap) {
for (const auto& [itemId, qty] : idMap)
if (existingItemIds.find(itemId) == existingItemIds.end())
{
glz::json_t::object_t newItem;
newItem["itemId"] = itemId;
newItem["quantity"] = qty;
itemsArr.push_back(newItem);
}
};
auto appendPerks = [&](const std::unordered_set<std::string>& idSet) {
for (const auto& itemId : idSet)
if (existingItemIds.find(itemId) == existingItemIds.end())
{
glz::json_t::object_t newItem;
newItem["itemId"] = itemId;
newItem["quantity"] = 3;
itemsArr.push_back(newItem);
}
};
if (!slasher)
{
appendItems(_camperItems);
appendItems(_camperAddons);
appendItems(_camperOfferings);
appendPerks(_camperPerks);
}
else
{
appendItems(_slasherAddons);
appendItems(_slasherOfferings);
appendPerks(_slasherPerks);
}
appendItems(_globalOfferings);
}
void Spoofer::modifyCharacterData(glz::generic& js)
{
if (!js.contains("characterName") || !js["characterName"].is_string())
{
_log->verbose("attempted to modify invalid char");
return;
}
std::string name = js["characterName"].get_string();
if (_spoofCharacters)
{
bool needsLvlSpoofing = false;
if (js.contains("isEntitled") && !js["isEntitled"].get_boolean() && js.contains("characterName") &&
_unlockedCharacters.contains(js["characterName"].get_string()))
{
js["isEntitled"] = true;
js["purchaseInfo"] = glz::json_t::object_t{{"quantity", 1},
{"origin", "PlayerInventory"},
{"reason", "Item(s) added via Purchase"},
{"lastUpdateAt", static_cast<uint64_t>(std::time(nullptr))},
{"objectId", name}};
needsLvlSpoofing = true;
}
if (needsLvlSpoofing)
{
if (js.contains("bloodWebLevel") && js["bloodWebLevel"].is_number() &&
js["bloodWebLevel"].get_number() <= 15)
if (!js.contains("prestigeLevel") ||
(js["prestigeLevel"].is_number() && js["prestigeLevel"].get_number() <= 0))
js["bloodWebLevel"] = 16;
if (js.contains("bloodWebData")) generateBloodweb(js["bloodWebData"]);
}
}
if (_spoofItems || _spoofPerks) modifyCharacterInventory(js);
}
void Spoofer::generateBloodweb(glz::generic& js)
{
if (!js.is_object()) js = glz::json_t::object_t{};
std::vector<std::string> paths;
glz::json_t::array_t ringDataArray;
glz::json_t::object_t rootRing;
glz::json_t::array_t rootNodeData;
glz::json_t::object_t rootNode;
rootNode["nodeId"] = "0";
rootNode["state"] = "Collected";
rootNodeData.push_back(rootNode);
rootRing["nodeData"] = rootNodeData;
ringDataArray.push_back(rootRing);
int nodesPerRing[] = {6, 12, 12};
std::vector<std::string> prevRingNodes = {"0"};
for (int ring = 1; ring <= 3; ++ring)
{
glz::json_t::array_t nodeDataArray;
std::vector<std::string> currentRingNodes;
int numNodes = nodesPerRing[ring - 1];
for (int i = 1; i <= numNodes; ++i)
{
std::string childId = std::to_string((ring * 100) + i);
currentRingNodes.push_back(childId);
int parentIndex = (i - 1) / (numNodes / static_cast<int>(prevRingNodes.size()));
std::string parentId = prevRingNodes[(std::min)(parentIndex, (int)prevRingNodes.size() - 1)];
paths.push_back(parentId + "_" + childId);
std::string item = PLACEHOLDER_ITEM_ID;
glz::json_t::object_t node;
node["nodeId"] = childId;
node["state"] = "Collected";
node["contentId"] = item;
nodeDataArray.push_back(node);
}
glz::json_t::object_t ringEntry;
ringEntry["nodeData"] = nodeDataArray;
ringDataArray.push_back(ringEntry);
prevRingNodes = std::move(currentRingNodes);
}
glz::json_t::array_t pathsArr;
for (auto& p : paths)
pathsArr.push_back(p);
js["paths"] = pathsArr;
js["ringData"] = ringDataArray;
}
/*
api handlers
*/
void Spoofer::onServerGetAll(std::string& body)
{
glz::generic doc{};
auto ec = glz::read_json(doc, body);
if (ec) return _log->error("JSON parse error for dbd-character-data/get-all");
if (!doc.is_object() || !doc.get_object().contains("list") || !doc["list"].is_array())
return _log->error("Invalid json for dbd-character-data/get-all");
auto& list = doc["list"].get_array();
for (auto& charData : list)
modifyCharacterData(charData);
auto written = glz::write_json(doc);
if (written)
body = written.value();
else
_log->error("JSON dump error for dbd-character-data/get-all");
}
void Spoofer::onServerInventoryAll(std::string& body)
{
glz::generic doc{};
auto ec = glz::read_json(doc, body);
if (ec) return _log->error("JSON parse error for dbd-inventories/all");
if (!doc.is_object() || !doc.get_object().contains("inventoryItems") || !doc["inventoryItems"].is_array())
return _log->error("Invalid json for JSON parse error for dbd-inventories/all");
auto& itemsArr = doc["inventoryItems"].get_array();
std::unordered_set<std::string> existingIds;
int64_t now = std::time(nullptr);
std::unordered_map<std::string, int> spoofMap;
if (_spoofPerks)
{
for (const auto& id : _camperPerks)
spoofMap[id] = 3;
for (const auto& id : _slasherPerks)
spoofMap[id] = 3;
}
if (_spoofItems)
{
for (const auto& [id, qty] : _camperOfferings)
spoofMap[id] = qty;
for (const auto& [id, qty] : _slasherOfferings)
spoofMap[id] = qty;
for (const auto& [id, qty] : _globalOfferings)
spoofMap[id] = qty;
for (const auto& [id, qty] : _camperItems)
spoofMap[id] = qty;
for (const auto& [id, qty] : _camperAddons)
spoofMap[id] = qty;
for (const auto& [id, qty] : _slasherAddons)
spoofMap[id] = qty;
}
if (_spoofCatalog)
{
//for (const auto& id : _catalogOutfitIds)
//spoofMap[id] = 1;
for (const auto& id : _catalogItemIds)
spoofMap[id] = 1;
}
/*
item updates
*/
for (auto& item : itemsArr)
{
std::string id;
if (item.is_object() && item.get_object().contains("objectId") && item["objectId"].is_string())
id = item["objectId"].get<std::string>();
if (id.empty()) continue;
existingIds.insert(id);
auto it = spoofMap.find(id);
if (it != spoofMap.end()) item["quantity"] = it->second;
}
/*
item inserts
*/
for (const auto& [id, qty] : spoofMap)
{
if (!existingIds.contains(id))
{
glz::json_t::object_t newItem;
newItem["objectId"] = id;
newItem["quantity"] = qty;
newItem["lastUpdateAt"] = now;
itemsArr.push_back(newItem);
}
}
auto written = glz::write_json(doc);
if (written)
body = written.value();
else
_log->error("JSON dump error for dbd-inventories/all");
_log->verbose("Inventory spoofed");
}
void Spoofer::onServerMessageList(std::string& body)
{
glz::generic doc;
auto ec = glz::read_json(doc, body);
if (ec)
{
_log->error("JSON parse error for onServerMessageList");
return;
}
auto now = std::chrono::system_clock::now();
auto now_seconds = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
auto now_milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
/*
msg construction
*/
glz::generic bodyContent = glz::generic::object_t{
{"sections",
glz::generic::array_t{
glz::generic::object_t{
{"type", "text"},
{"text", "Japan is turning footsteps into electricity!<br><br>Using piezoelectric tiles, every step "
"you take generates a small amount of energy. Millions of steps together can power LED "
"lights and displays in busy places like Shibuya Station.<br><br>A brilliant way to create a "
"sustainable and smart city - turning movement into clean, renewable energy."}},
glz::generic::object_t{
{"type", "itemshowcase"},
{"rewards", glz::generic::array_t{glz::generic::object_t{
{"type", "inventory"}, {"id", "ADDON_flashlight_oddbulb"}, {"amount", 1.0}}}}}}},
{"image", glz::generic::object_t{
{"packagedPath", "/Game/UI/UMGAssets/Icons/ItemAddons/iconAddon_powerBulb.iconAddon_powerBulb"},
{"contentVersion", "ccc3f02b0a671fe19a0017d6a69293876a465fd9"},
{"uri", ""}}}};
std::string bodyContentStr;
ec = glz::write_json(bodyContent, bodyContentStr);
if (ec)
{
_log->error("JSON write error for onServerMessageList");
return;
}
glz::generic msg = glz::generic::object_t{
{"allowedPlatforms", glz::generic::array_t{"egs", "grdk", "ps4", "ps5", "steam", "xbox", "xsx"}},
{"flag", "READ"},
{"gameSpecificData", glz::generic::object_t{}},
{"read", false},
{"tag", glz::generic::array_t{"inbox"}},
{"expireAt", static_cast<double>(now_seconds + (1337 * 24 * 60 * 60))},
{"received", static_cast<double>(now_milliseconds)},
{"recipientId", "system"},
{"message",
glz::generic::object_t{{"title", "Japan is turning footsteps into electricity"}, {"body", bodyContentStr}}}};
doc["messages"].get_array().push_back(msg);
ec = glz::write_json(doc, body);
if (ec)
{
_log->error("JSON write error for onServerMessageList");
return;
}
_log->verbose("Spoofed message list");
}
void Spoofer::onServerBloodweb(std::string& body, std::string& respHeaders)
{
glz::generic doc;
auto ec = glz::read_json(doc, body);
if (ec)
{
_log->error("JSON parse error for onServerBloodweb");
return;
}
/*
false char response
*/
if (body.find("NotAllowedException") != std::string::npos && body.find("not owned") != std::string::npos)
{
glz::generic mock = glz::generic::object_t{};
mock["bloodWebLevelChanged"] = false;
mock["updatedWallets"] = glz::generic::array_t{};
mock["bloodWebLevel"] = 16;
mock["prestigeLevel"] = 0;
mock["bloodWebData"] = glz::generic::object_t{};
mock["characterItems"] = glz::generic::array_t{};
mock["characterName"] = this->_lastBwCharacter;
mock["isEntitled"] = true;
glz::generic::object_t purchaseInfo;
purchaseInfo["quantity"] = 1;
purchaseInfo["origin"] = "PlayerInventory";
purchaseInfo["reason"] = "Item(s) added via Purchase";
purchaseInfo["lastUpdateAt"] = static_cast<uint64_t>(std::time(nullptr));
purchaseInfo["objectId"] = this->_lastBwCharacter;
mock["purchaseInfo"] = purchaseInfo;
if (_spoofCharacters) generateBloodweb(mock["bloodWebData"]);
if (_spoofItems || _spoofPerks) modifyCharacterInventory(mock);
std::string new_body;
ec = glz::write_json(mock, new_body);
if (ec)
{
_log->error("JSON write error for onServerBloodweb");
return;
}
body = new_body;
std::regex statusRegex(R"(HTTP\/\d\.\d\s+403)");
respHeaders = std::regex_replace(respHeaders, statusRegex, "HTTP/1.1 200");
_log->verbose("Spoofed bloodweb request for unowned character.");
}
/*
bloodweb fixup for already owned perks
*/
if (_spoofPerks)
{
if (doc.contains("bloodWebData") && doc["bloodWebData"].is_object())
{
auto& bloodWebData = doc["bloodWebData"];
if (!bloodWebData.contains("paths") || !bloodWebData.contains("ringData")) return;
for (auto& ring : bloodWebData["ringData"].get_array())
{
if (!ring.contains("nodeData") || !ring["nodeData"].is_array()) continue;
for (auto& node : ring["nodeData"].get_array())
{
if (!node.contains("contentId") || !node["contentId"].is_string()) continue;
std::string contentId = node["contentId"].get_string();
if (_camperPerks.contains(contentId) || _slasherPerks.contains(contentId))
node["contentId"] = PLACEHOLDER_ITEM_ID;
}
}
_log->verbose("Fixed bloodweb request");
}
}
modifyCharacterData(doc);
auto written = glz::write_json(doc);
if (written)
body = written.value();
else
_log->error("JSON write error for onServerBloodweb");
_log->verbose("Spoofed bloodweb items for owned character");
}
void Spoofer::onServerUpdateEntitlements(const std::string& url, std::string& body)
{
if (!_spoofDLCs) return;
glz::generic doc;
auto ec = glz::read_json(doc, body);
if (ec)
{
_log->error("JSON parse error for onClientUpdateEntitlements");
return;
}
if (doc.contains("entitlements") && doc["entitlements"].is_array())
{
auto& entitlements = doc["entitlements"].get_array();
std::unordered_set<std::string>* dlcList = nullptr;
if (url.starts_with("https://grdk.live.bhvrdbd.com/"))
dlcList = &_dlcListGRDK;
else if (url.starts_with("https://egs.live.bhvrdbd.com/"))
dlcList = &_dlcListEGS;
else
return _log->error("Entitlement spoofing only works with Epic / Xbox");
for (const std::string& dlcId : *dlcList)
{
auto it = std::ranges::find_if(
entitlements, [&](const auto& item) { return item.is_string() && item.get_string() == dlcId; });
if (it == entitlements.end()) entitlements.emplace_back(dlcId);
}
}
ec = glz::write_json(doc, body);
if (ec)
{
_log->error("JSON write error for onClientUpdateEntitlements");
return;
}
_log->verbose("Spoofed entitlements (response)");
}
void Spoofer::onClientBloodweb(std::string& body)
{
glz::generic doc;
auto ec = glz::read_json(doc, body);
if (ec)
{
_log->error("JSON parse error for onClientBloodweb");
return;
}
if (doc.contains("characterName")) _lastBwCharacter = doc["characterName"].get_string();
}
void Spoofer::onClientUpdateEntitlements(const std::string& url, std::string& body)
{
if (!_spoofDLCs) return;
glz::generic doc;
auto ec = glz::read_json(doc, body);
if (ec)
{
_log->error("JSON parse error for onClientUpdateEntitlements");
return;
}
if (doc.contains("clientEntitlementIds") && doc["clientEntitlementIds"].is_array())
{
auto& entitlements = doc["clientEntitlementIds"].get_array();
std::unordered_set<std::string>* dlcList = nullptr;
if (url.starts_with("https://grdk.live.bhvrdbd.com/"))
dlcList = &_dlcListGRDK;
else if (url.starts_with("https://egs.live.bhvrdbd.com/"))
dlcList = &_dlcListEGS;
else
return _log->error("Entitlement spoofing only works with Epic / Xbox");
for (const std::string& dlcId : *dlcList)
{
auto it = std::ranges::find_if(
entitlements, [&](const auto& item) { return item.is_string() && item.get_string() == dlcId; });
if (it == entitlements.end()) entitlements.emplace_back(dlcId);
}
}
ec = glz::write_json(doc, body);
if (ec)
{
_log->error("JSON write error for onClientUpdateEntitlements");
return;
}
_log->verbose("Spoofed entitlements (client)");
}
/*
proxy handlers
*/
void Spoofer::serverResponseHandler(const std::string& url, std::string& body, std::string& headers,
bool& /*blockOutgoing*/)
{
if (url.find("bhvrdbd.com") == std::string::npos) return;
std::lock_guard<std::mutex> lock(_mutex);
if (url.find("/api/v1/messages/listV2") != std::string::npos) return onServerMessageList(body);
if (url.find("/api/v1/dbd-inventories/all") != std::string::npos) return onServerInventoryAll(body);
if (url.find("/api/v1/dbd-character-data/get-all") != std::string::npos) return onServerGetAll(body);
if (url.find("/api/v1/owned-products/get-update-entitlements") != std::string::npos)
return onServerUpdateEntitlements(url, body);
if (url.find("/api/v1/dbd-character-data/bloodweb") != std::string::npos ||
url.find("/api/v1/dbd-character-data/bloodweb/v2") != std::string::npos ||
url.find("/api/v1/dbd-character-data/bulk-spending-bloodweb") != std::string::npos)
return onServerBloodweb(body, headers);
}
void Spoofer::clientRequestHandler(const std::string& url, std::string& body, std::string& /*headers*/,
bool /*wasBlocked*/)
{
if (url.find("bhvrdbd.com") == std::string::npos) return;
std::lock_guard<std::mutex> lock(_mutex);
if (url.find("/api/v1/owned-products/get-update-entitlements") != std::string::npos)
return onClientUpdateEntitlements(url, body);
if (url.find("/api/v1/dbd-character-data/bloodweb") != std::string::npos ||
url.find("/api/v1/dbd-character-data/bloodweb/v2") != std::string::npos ||
url.find("/api/v1/dbd-character-data/bulk-spending-bloodweb") != std::string::npos)
return onClientBloodweb(body);
}