diff --git a/src/unlocker/spoofer.cpp b/src/unlocker/spoofer.cpp index 2f076c2..4c4d5c5 100644 --- a/src/unlocker/spoofer.cpp +++ b/src/unlocker/spoofer.cpp @@ -2,6 +2,8 @@ #include "utils.h" #include "log-sink.h" +#include +#include #include #include @@ -13,8 +15,22 @@ #include -#include +/* + misc helper functions +*/ +std::unordered_set 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"); @@ -239,6 +255,290 @@ void Spoofer::wsMessageCallback(std::shared_ptr /*connectio break; } } + +/* + endpoint related helper functions +*/ +void Spoofer::modifyCharacterInventory(glz::generic& js) +{ + bool slasher = isSlasher(js["characterName"].get()); + + bool hasItems = js.is_object() && js.get_object().contains("characterItems") && js["characterItems"].is_array(); + if (!hasItems) return; + + std::unordered_set existingItemIds; + std::unordered_map 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(); + existingItemIds.insert(itemId); + auto it = stackableQty.find(itemId); + if (it != stackableQty.end()) item["quantity"] = it->second; + } + } + + /* + item injection + */ + auto appendItems = [&](const std::unordered_map& 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& 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(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 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 prevRingNodes = {"0"}; + + for (int ring = 1; ring <= 3; ++ring) + { + glz::json_t::array_t nodeDataArray; + std::vector 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(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 existingIds; + int64_t now = std::time(nullptr); + + std::unordered_map 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(); + + 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; @@ -302,10 +602,101 @@ void Spoofer::onServerMessageList(std::string& body) return; } -#ifdef _DEBUG _log->verbose("Spoofed message list"); -#endif } + +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(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; @@ -346,10 +737,22 @@ void Spoofer::onServerUpdateEntitlements(const std::string& url, std::string& bo return; } -#ifdef _DEBUG - _log->verbose("Spoofed entitlements (client)"); -#endif + _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; @@ -390,9 +793,7 @@ void Spoofer::onClientUpdateEntitlements(const std::string& url, std::string& bo return; } -#ifdef _DEBUG _log->verbose("Spoofed entitlements (client)"); -#endif } /* @@ -406,11 +807,19 @@ void Spoofer::serverResponseHandler(const std::string& url, std::string& body, s std::lock_guard 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*/) +void Spoofer::clientRequestHandler(const std::string& url, std::string& body, std::string& /*headers*/, + bool /*wasBlocked*/) { if (url.find("bhvrdbd.com") == std::string::npos) return; @@ -418,4 +827,9 @@ void Spoofer::clientRequestHandler(const std::string& url, std::string& body, st 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); } diff --git a/src/unlocker/spoofer.h b/src/unlocker/spoofer.h index 0da2c11..1ca1fa8 100644 --- a/src/unlocker/spoofer.h +++ b/src/unlocker/spoofer.h @@ -9,9 +9,13 @@ #include +#include + #define WS_ADDR "0.0.0.0" #define WS_PORT 4444 +#define PLACEHOLDER_ITEM_ID "Anniversary2025Offering" + class TinyMITMProxy; namespace ix @@ -97,6 +101,27 @@ class Spoofer void wsMessageCallback(std::shared_ptr connectionState, ix::WebSocket& webSocket, const std::unique_ptr& msg); + + /* + helpers + */ + void modifyCharacterInventory(glz::generic& js); + void modifyCharacterData(glz::generic& js); + void generateBloodweb(glz::generic& js); + + /* + api handlers + */ + void onServerGetAll(std::string& body); + void onServerInventoryAll(std::string& body); + void onServerMessageList(std::string& body); + void onServerBloodweb(std::string& body, std::string& respHeaders); + void onServerUpdateEntitlements(const std::string& url, std::string& body); + + void onClientGetAll(std::string& body); + void onClientBloodweb(std::string& body); + void onClientUpdateEntitlements(const std::string& url, std::string& body); + /* proxy handlers */ @@ -111,6 +136,7 @@ class Spoofer bool _spoofPerks = false; bool _spoofCatalog = false; bool _spoofDLCs = false; + bool _spoofCharacters = false; std::unordered_map _camperItems; std::unordered_map _camperAddons; @@ -136,4 +162,5 @@ class Spoofer ix::WebSocketServer* _wsServer = nullptr; seallib::Logger* _log = nullptr; std::mutex _mutex; + std::string _lastBwCharacter; };