#include "RealmController.h" #include "../services/DatabaseService.h" #include "../services/StatsService.h" #include "../services/RedisHelper.h" #include "../services/OmeClient.h" #include "../services/CensorService.h" #include "../common/HttpHelpers.h" #include "../common/AuthHelpers.h" #include #include #include #include #include #include #include #include #include #include // SECURITY FIX #4: Use cryptographic RNG using namespace drogon::orm; namespace { // SECURITY FIX #4: Use cryptographically secure random number generation // Replaces mt19937 (Mersenne Twister) which is NOT cryptographically secure std::string generateStreamKey() { unsigned char bytes[32]; // 32 bytes = 64 hex characters if (RAND_bytes(bytes, sizeof(bytes)) != 1) { LOG_ERROR << "Failed to generate cryptographically secure random bytes"; throw std::runtime_error("Failed to generate secure stream key"); } std::stringstream ss; for (size_t i = 0; i < sizeof(bytes); ++i) { ss << std::hex << std::setw(2) << std::setfill('0') << static_cast(bytes[i]); } return ss.str(); } bool validateRealmName(const std::string& name) { if (name.length() < 3 || name.length() > 30) { return false; } return std::regex_match(name, std::regex("^[a-z0-9-]+$")); } // SECURITY FIX #12: Safe file deletion with path traversal protection bool safeDeleteFile(const std::string& urlPath, const std::string& allowedDir) { try { if (urlPath.empty()) return false; // Construct full path std::filesystem::path fullPath = std::filesystem::weakly_canonical("." + urlPath); std::filesystem::path basePath = std::filesystem::canonical(allowedDir); // Verify path is within allowed directory auto [baseEnd, fullEnd] = std::mismatch(basePath.begin(), basePath.end(), fullPath.begin()); if (baseEnd != basePath.end()) { LOG_WARN << "Path traversal attempt blocked: " << urlPath << " is not within " << allowedDir; return false; } // Path is safe, delete the file if (std::filesystem::exists(fullPath)) { std::filesystem::remove(fullPath); LOG_DEBUG << "Safely deleted file: " << fullPath.string(); return true; } return false; } catch (const std::exception& e) { LOG_ERROR << "Error in safeDeleteFile: " << e.what(); return false; } } // Sync chat settings to Redis db 1 (chat-service database) void syncChatSettingToRedis(const std::string& realmName, const std::string& field, const std::string& value) { try { sw::redis::ConnectionOptions opts; const char* envHost = std::getenv("REDIS_HOST"); opts.host = envHost ? std::string(envHost) : "redis"; const char* envPort = std::getenv("REDIS_PORT"); opts.port = envPort ? std::stoi(envPort) : 6379; opts.db = 1; // Chat-service uses db 1 opts.socket_timeout = std::chrono::milliseconds(1000); auto redis = sw::redis::Redis(opts); std::string key = "chat:settings:realm:" + realmName; redis.hset(key, field, value); LOG_DEBUG << "Synced chat setting " << field << "=" << value << " for realm " << realmName; } catch (const sw::redis::Error& e) { LOG_ERROR << "Failed to sync chat setting to Redis: " << e.what(); } } // Helper to create a new viewer token void createNewViewerToken(std::function callback, const std::string& streamKey) { auto bytes = drogon::utils::genRandomString(32); std::string token = drogon::utils::base64Encode( (const unsigned char*)bytes.data(), bytes.length() ); RedisHelper::storeKeyAsync("viewer_token:" + token, streamKey, 300, [callback, token](bool stored) { if (!stored) { auto resp = HttpResponse::newHttpResponse(); resp->setStatusCode(k500InternalServerError); callback(resp); return; } auto resp = HttpResponse::newHttpResponse(); Cookie cookie("viewer_token", token); cookie.setPath("/"); cookie.setHttpOnly(true); cookie.setSecure(false); cookie.setMaxAge(300); resp->addCookie(cookie); Json::Value body; body["success"] = true; body["viewer_token"] = token; body["expires_in"] = 300; resp->setContentTypeCode(CT_APPLICATION_JSON); resp->setBody(Json::FastWriter().write(body)); callback(resp); } ); } void invalidateKeyInRedis(const std::string& oldKey) { RedisHelper::addToSet("streams_to_disconnect", oldKey); RedisHelper::deleteKey("stream_key:" + oldKey); services::RedisHelper::instance().keysAsync("viewer_token:*", [oldKey](const std::vector& keys) { for (const auto& tokenKey : keys) { services::RedisHelper::instance().getAsync(tokenKey, [tokenKey, oldKey](sw::redis::OptionalString streamKey) { if (streamKey.has_value() && streamKey.value() == oldKey) { RedisHelper::deleteKey(tokenKey); } } ); } } ); } } void RealmController::getUserRealms(const HttpRequestPtr &req, std::function &&callback) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } auto dbClient = app().getDbClient(); *dbClient << "SELECT r.id, r.name, r.description, r.stream_key, r.realm_type, r.is_active, r.is_live, " "r.viewer_count, r.chat_enabled, r.chat_guests_allowed, r.chat_slow_mode_seconds, " "r.chat_retention_hours, r.offline_image_url, r.title_color, r.created_at, " "(SELECT COUNT(*) FROM videos v WHERE v.realm_id = r.id AND v.status = 'ready') as video_count, " "(SELECT COUNT(*) FROM audio_files a WHERE a.realm_id = r.id AND a.status = 'ready') as audio_count, " "(SELECT COUNT(*) FROM ebooks e WHERE e.realm_id = r.id AND e.status = 'ready') as ebook_count " "FROM realms r WHERE r.user_id = $1 ORDER BY r.created_at DESC" << user.id >> [callback](const Result& r) { Json::Value resp; resp["success"] = true; Json::Value realms(Json::arrayValue); for (const auto& row : r) { Json::Value realm; realm["id"] = static_cast(row["id"].as()); realm["name"] = row["name"].as(); realm["description"] = row["description"].isNull() ? "" : row["description"].as(); realm["type"] = row["realm_type"].isNull() ? "stream" : row["realm_type"].as(); realm["streamKey"] = row["stream_key"].isNull() ? "" : row["stream_key"].as(); realm["isActive"] = row["is_active"].as(); realm["isLive"] = row["is_live"].as(); realm["viewerCount"] = static_cast(row["viewer_count"].as()); realm["videoCount"] = static_cast(row["video_count"].as()); realm["audioCount"] = static_cast(row["audio_count"].as()); realm["ebookCount"] = static_cast(row["ebook_count"].as()); realm["chatEnabled"] = row["chat_enabled"].as(); realm["chatGuestsAllowed"] = row["chat_guests_allowed"].as(); realm["chatSlowModeSeconds"] = row["chat_slow_mode_seconds"].as(); realm["chatRetentionHours"] = row["chat_retention_hours"].as(); realm["offlineImageUrl"] = row["offline_image_url"].isNull() ? "" : row["offline_image_url"].as(); realm["titleColor"] = row["title_color"].isNull() ? "#ffffff" : row["title_color"].as(); realm["createdAt"] = row["created_at"].as(); realms.append(realm); } resp["realms"] = realms; callback(jsonResp(resp)); } >> DB_ERROR_MSG(callback, "get realms", "Failed to get realms"); } void RealmController::createRealm(const HttpRequestPtr &req, std::function &&callback) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } auto json = req->getJsonObject(); if (!json) { callback(jsonError("Invalid JSON")); return; } std::string name = (*json)["name"].asString(); std::string realmType = (*json).isMember("type") ? (*json)["type"].asString() : "stream"; // Check for censored words in realm name if (CensorService::getInstance().containsCensoredWords(name)) { callback(jsonError("Realm name contains prohibited content")); return; } // Validate realm type if (realmType != "stream" && realmType != "video" && realmType != "audio" && realmType != "ebook" && realmType != "watch") { callback(jsonError("Invalid realm type. Must be 'stream', 'video', 'audio', 'ebook', or 'watch'")); return; } if (!validateRealmName(name)) { callback(jsonError("Realm name must be 3-30 characters, lowercase letters, numbers, and hyphens only")); return; } // Check permission based on realm type auto dbClient = app().getDbClient(); *dbClient << "SELECT is_streamer, is_uploader, is_watch_creator FROM users WHERE id = $1" << user.id >> [req, callback, user, dbClient, name, realmType](const Result& r) { if (r.empty()) { callback(jsonError("User not found", k404NotFound)); return; } bool isStreamer = r[0]["is_streamer"].as(); bool isUploader = r[0]["is_uploader"].as(); bool isWatchCreator = r[0]["is_watch_creator"].isNull() ? false : r[0]["is_watch_creator"].as(); // Check permission based on realm type if (realmType == "stream" && !isStreamer) { callback(jsonError("You must be a streamer to create stream realms", k403Forbidden)); return; } if (realmType == "video" && !isUploader) { callback(jsonError("You must be an uploader to create video realms", k403Forbidden)); return; } if (realmType == "audio" && !isUploader) { callback(jsonError("You must be an uploader to create audio realms", k403Forbidden)); return; } if (realmType == "ebook" && !isUploader) { callback(jsonError("You must be an uploader to create ebook realms", k403Forbidden)); return; } if (realmType == "watch" && !isWatchCreator) { callback(jsonError("You must be a watch creator to create watch rooms", k403Forbidden)); return; } // Check if realm name already exists *dbClient << "SELECT id FROM realms WHERE name = $1" << name >> [dbClient, user, name, realmType, callback](const Result& r2) { if (!r2.empty()) { callback(jsonError("Realm name already taken")); return; } // Check user's realm limit (e.g., 5 realms per user) *dbClient << "SELECT COUNT(*) as count FROM realms WHERE user_id = $1" << user.id >> [dbClient, user, name, realmType, callback](const Result& r3) { if (!r3.empty() && r3[0]["count"].as() >= 5) { callback(jsonError("You have reached the maximum number of realms (5)")); return; } if (realmType == "stream") { // Stream realm - generate stream key std::string streamKey = generateStreamKey(); *dbClient << "INSERT INTO realms (user_id, name, stream_key, realm_type) " "VALUES ($1, $2, $3, 'stream') RETURNING id" << user.id << name << streamKey >> [callback, name, streamKey](const Result& r4) { if (r4.empty()) { callback(jsonError("Failed to create realm")); return; } // Store stream key in Redis RedisHelper::storeKey("stream_key:" + streamKey, "1", 86400); Json::Value resp; resp["success"] = true; resp["realm"]["id"] = static_cast(r4[0]["id"].as()); resp["realm"]["name"] = name; resp["realm"]["streamKey"] = streamKey; resp["realm"]["type"] = "stream"; callback(jsonResp(resp)); } >> DB_ERROR_MSG(callback, "create realm", "Failed to create realm"); } else if (realmType == "watch") { // Watch room - no stream key, initialize watch_room_state *dbClient << "INSERT INTO realms (user_id, name, realm_type) " "VALUES ($1, $2, 'watch') RETURNING id" << user.id << name >> [callback, dbClient, name](const Result& r4) { if (r4.empty()) { callback(jsonError("Failed to create realm")); return; } int64_t realmId = r4[0]["id"].as(); // Initialize watch_room_state *dbClient << "INSERT INTO watch_room_state (realm_id) VALUES ($1)" << realmId >> [callback, name, realmId](const Result&) { Json::Value resp; resp["success"] = true; resp["realm"]["id"] = static_cast(realmId); resp["realm"]["name"] = name; resp["realm"]["type"] = "watch"; callback(jsonResp(resp)); } >> DB_ERROR_MSG(callback, "init watch state", "Failed to create watch room"); } >> DB_ERROR_MSG(callback, "create realm", "Failed to create realm"); } else { // Video, Audio, or Ebook realm - no stream key needed *dbClient << "INSERT INTO realms (user_id, name, realm_type) " "VALUES ($1, $2, $3) RETURNING id" << user.id << name << realmType >> [callback, name, realmType](const Result& r4) { if (r4.empty()) { callback(jsonError("Failed to create realm")); return; } Json::Value resp; resp["success"] = true; resp["realm"]["id"] = static_cast(r4[0]["id"].as()); resp["realm"]["name"] = name; resp["realm"]["type"] = realmType; callback(jsonResp(resp)); } >> DB_ERROR_MSG(callback, "create realm", "Failed to create realm"); } } >> DB_ERROR(callback, "count realms"); } >> DB_ERROR(callback, "check realm name"); } >> DB_ERROR(callback, "check user permissions"); } void RealmController::issueRealmViewerToken(const HttpRequestPtr &req, std::function &&callback, const std::string &realmId) { int64_t id = std::stoll(realmId); // Check for existing viewer token to avoid creating duplicates on page refresh auto existingToken = req->getCookie("viewer_token"); auto dbClient = app().getDbClient(); *dbClient << "SELECT stream_key FROM realms WHERE id = $1 AND is_active = true" << id >> [callback, existingToken](const Result& r) { if (r.empty()) { callback(jsonResp({}, k404NotFound)); return; } std::string streamKey = r[0]["stream_key"].as(); // If user has existing token, check if it's still valid for this stream if (!existingToken.empty()) { RedisHelper::getKeyAsync("viewer_token:" + existingToken, [callback, existingToken, streamKey](const std::string& storedKey) { if (storedKey == streamKey) { // Token is still valid for this stream - just refresh TTL and return it RedisHelper::storeKeyAsync("viewer_token:" + existingToken, streamKey, 300, [callback, existingToken](bool stored) { auto resp = HttpResponse::newHttpResponse(); // Refresh cookie Cookie cookie("viewer_token", existingToken); cookie.setPath("/"); cookie.setHttpOnly(true); cookie.setSecure(false); cookie.setMaxAge(300); resp->addCookie(cookie); Json::Value body; body["success"] = true; body["viewer_token"] = existingToken; body["expires_in"] = 300; body["reused"] = true; // Indicate token was reused resp->setContentTypeCode(CT_APPLICATION_JSON); resp->setBody(Json::FastWriter().write(body)); callback(resp); } ); } else { // Token invalid or for different stream - delete old and create new if (!storedKey.empty()) { RedisHelper::deleteKeyAsync("viewer_token:" + existingToken, [](bool){}); } createNewViewerToken(callback, streamKey); } } ); } else { // No existing token, create new one createNewViewerToken(callback, streamKey); } } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to issue viewer token: " << e.base().what(); callback(jsonResp({}, k500InternalServerError)); }; } void RealmController::getRealmStreamKey(const HttpRequestPtr &req, std::function &&callback, const std::string &realmId) { // Check for viewer token auto token = req->getCookie("viewer_token"); if (token.empty()) { callback(jsonError("No viewer token", k403Forbidden)); return; } int64_t id = std::stoll(realmId); // First get the stream key for this realm auto dbClient = app().getDbClient(); *dbClient << "SELECT stream_key FROM realms WHERE id = $1 AND is_active = true" << id >> [token, callback](const Result& r) { if (r.empty()) { callback(jsonError("Realm not found", k404NotFound)); return; } std::string streamKey = r[0]["stream_key"].as(); // Verify the token is valid for this stream RedisHelper::getKeyAsync("viewer_token:" + token, [callback, streamKey](const std::string& storedStreamKey) { if (storedStreamKey != streamKey) { callback(jsonError("Invalid token for this realm", k403Forbidden)); return; } // Token is valid, return the stream key Json::Value resp; resp["success"] = true; resp["streamKey"] = streamKey; callback(jsonResp(resp)); } ); } >> DB_ERROR(callback, "get realm stream key"); } void RealmController::getRealm(const HttpRequestPtr &, // Remove parameter name since it's unused std::function &&callback, const std::string &realmId) { // Remove authentication requirement for public viewing int64_t id = std::stoll(realmId); auto dbClient = app().getDbClient(); *dbClient << "SELECT r.*, u.username FROM realms r " "JOIN users u ON r.user_id = u.id " "WHERE r.id = $1 AND r.is_active = true" << id >> [callback](const Result& r) { if (r.empty()) { callback(jsonError("Realm not found", k404NotFound)); return; } Json::Value resp; resp["success"] = true; auto& realm = resp["realm"]; realm["id"] = static_cast(r[0]["id"].as()); realm["name"] = r[0]["name"].as(); realm["type"] = r[0]["realm_type"].isNull() ? "stream" : r[0]["realm_type"].as(); // Don't expose stream key in public endpoint // realm["streamKey"] = r[0]["stream_key"].as(); realm["isActive"] = r[0]["is_active"].as(); realm["isLive"] = r[0]["is_live"].as(); realm["viewerCount"] = static_cast(r[0]["viewer_count"].as()); realm["createdAt"] = r[0]["created_at"].as(); realm["username"] = r[0]["username"].as(); callback(jsonResp(resp)); } >> DB_ERROR(callback, "get realm"); } void RealmController::updateRealm(const HttpRequestPtr &req, std::function &&callback, const std::string &realmId) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } // Parse realm ID int64_t id; try { id = std::stoll(realmId); } catch (...) { callback(jsonError("Invalid realm ID", k400BadRequest)); return; } // Parse JSON body auto json = req->getJsonObject(); if (!json) { callback(jsonError("Invalid JSON")); return; } // Verify the realm exists and belongs to the user auto dbClient = app().getDbClient(); *dbClient << "SELECT id, name FROM realms WHERE id = $1 AND user_id = $2" << id << user.id >> [callback, json, dbClient, id, user](const Result& r) { if (r.empty()) { callback(jsonError("Realm not found or access denied", k404NotFound)); return; } std::string realmName = r[0]["name"].as(); // Update chat_enabled if provided if (json->isMember("chatEnabled")) { bool chatEnabled = (*json)["chatEnabled"].asBool(); *dbClient << "UPDATE realms SET chat_enabled = $1 WHERE id = $2 AND user_id = $3" << chatEnabled << id << user.id >> [callback](const Result&) { Json::Value resp; resp["success"] = true; resp["message"] = "Realm updated successfully"; callback(jsonResp(resp)); } >> DB_ERROR_MSG(callback, "update realm", "Failed to update realm"); } else if (json->isMember("chatGuestsAllowed")) { // Update chat_guests_allowed if provided bool chatGuestsAllowed = (*json)["chatGuestsAllowed"].asBool(); *dbClient << "UPDATE realms SET chat_guests_allowed = $1 WHERE id = $2 AND user_id = $3" << chatGuestsAllowed << id << user.id >> [callback, realmName, chatGuestsAllowed](const Result&) { // Sync to Redis (chat-service database) syncChatSettingToRedis(realmName, "chatGuestsAllowed", chatGuestsAllowed ? "1" : "0"); Json::Value resp; resp["success"] = true; resp["message"] = "Realm updated successfully"; callback(jsonResp(resp)); } >> DB_ERROR_MSG(callback, "update realm", "Failed to update realm"); } else if (json->isMember("chatSlowModeSeconds")) { // Update chat slow mode seconds int slowModeSeconds = (*json)["chatSlowModeSeconds"].asInt(); // Clamp to valid range (0-300) if (slowModeSeconds < 0) slowModeSeconds = 0; if (slowModeSeconds > 300) slowModeSeconds = 300; *dbClient << "UPDATE realms SET chat_slow_mode_seconds = $1 WHERE id = $2 AND user_id = $3" << slowModeSeconds << id << user.id >> [callback](const Result&) { Json::Value resp; resp["success"] = true; resp["message"] = "Slow mode updated successfully"; callback(jsonResp(resp)); } >> DB_ERROR_MSG(callback, "update slow mode", "Failed to update slow mode"); } else if (json->isMember("chatRetentionHours")) { // Update chat retention hours int retentionHours = (*json)["chatRetentionHours"].asInt(); // Clamp to valid range (1-168) if (retentionHours < 1) retentionHours = 1; if (retentionHours > 168) retentionHours = 168; *dbClient << "UPDATE realms SET chat_retention_hours = $1 WHERE id = $2 AND user_id = $3" << retentionHours << id << user.id >> [callback](const Result&) { Json::Value resp; resp["success"] = true; resp["message"] = "Message retention updated successfully"; callback(jsonResp(resp)); } >> DB_ERROR_MSG(callback, "update retention", "Failed to update retention"); } else if (json->isMember("name")) { // Update realm name std::string newName = (*json)["name"].asString(); // Validate name format if (!validateRealmName(newName)) { callback(jsonError("Invalid realm name. Use 3-30 lowercase letters, numbers, and hyphens only.")); return; } // Check if name is already taken by another realm *dbClient << "SELECT id FROM realms WHERE name = $1 AND id != $2" << newName << id >> [callback, dbClient, id, user, newName](const Result& nameCheck) { if (!nameCheck.empty()) { callback(jsonError("Realm name is already taken")); return; } *dbClient << "UPDATE realms SET name = $1 WHERE id = $2 AND user_id = $3" << newName << id << user.id >> [callback, newName](const Result&) { Json::Value resp; resp["success"] = true; resp["message"] = "Realm renamed successfully"; resp["name"] = newName; callback(jsonResp(resp)); } >> DB_ERROR_MSG(callback, "rename realm", "Failed to rename realm"); } >> DB_ERROR(callback, "check realm name"); } else if (json->isMember("description")) { // Update realm description std::string description = (*json)["description"].asString(); // Limit description length if (description.length() > 500) { description = description.substr(0, 500); } *dbClient << "UPDATE realms SET description = $1 WHERE id = $2 AND user_id = $3" << description << id << user.id >> [callback](const Result&) { Json::Value resp; resp["success"] = true; resp["message"] = "Description updated successfully"; callback(jsonResp(resp)); } >> DB_ERROR_MSG(callback, "update description", "Failed to update description"); } else { Json::Value resp; resp["success"] = true; resp["message"] = "No changes to apply"; callback(jsonResp(resp)); } } >> DB_ERROR(callback, "verify realm ownership"); } void RealmController::deleteRealm(const HttpRequestPtr &req, std::function &&callback, const std::string &realmId) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } int64_t id = std::stoll(realmId); auto dbClient = app().getDbClient(); // First get the stream key to invalidate it (if it's a stream realm) *dbClient << "SELECT stream_key, realm_type FROM realms WHERE id = $1 AND user_id = $2" << id << user.id >> [dbClient, id, user, callback](const Result& r) { if (r.empty()) { callback(jsonError("Realm not found", k404NotFound)); return; } // Only invalidate stream key for stream realms if (!r[0]["stream_key"].isNull()) { std::string streamKey = r[0]["stream_key"].as(); invalidateKeyInRedis(streamKey); } // Delete the realm (videos will be cascade deleted due to FK) *dbClient << "DELETE FROM realms WHERE id = $1 AND user_id = $2" << id << user.id >> [callback](const Result&) { Json::Value resp; resp["success"] = true; callback(jsonResp(resp)); } >> DB_ERROR_MSG(callback, "delete realm", "Failed to delete realm"); } >> DB_ERROR(callback, "get realm for delete"); } void RealmController::regenerateRealmKey(const HttpRequestPtr &req, std::function &&callback, const std::string &realmId) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } int64_t id = std::stoll(realmId); auto dbClient = app().getDbClient(); // Get old key *dbClient << "SELECT stream_key, realm_type FROM realms WHERE id = $1 AND user_id = $2" << id << user.id >> [dbClient, id, user, callback](const Result& r) { if (r.empty()) { callback(jsonError("Realm not found", k404NotFound)); return; } // Check if this is a stream realm std::string realmType = r[0]["realm_type"].isNull() ? "stream" : r[0]["realm_type"].as(); if (realmType != "stream") { callback(jsonError("Cannot regenerate key for video realms", k400BadRequest)); return; } std::string oldKey = r[0]["stream_key"].as(); invalidateKeyInRedis(oldKey); std::string newKey = generateStreamKey(); *dbClient << "UPDATE realms SET stream_key = $1 WHERE id = $2 AND user_id = $3" << newKey << id << user.id >> [callback, newKey](const Result&) { // Store new key in Redis RedisHelper::storeKey("stream_key:" + newKey, "1", 86400); Json::Value resp; resp["success"] = true; resp["streamKey"] = newKey; callback(jsonResp(resp)); } >> DB_ERROR_MSG(callback, "update stream key", "Failed to regenerate key"); } >> DB_ERROR(callback, "get realm for key regen"); } void RealmController::getRealmByName(const HttpRequestPtr &, std::function &&callback, const std::string &realmName) { auto dbClient = app().getDbClient(); *dbClient << "SELECT r.id, r.name, r.realm_type, r.is_live, r.viewer_count, r.viewer_multiplier, " "r.chat_enabled, r.chat_guests_allowed, " "r.chat_slow_mode_seconds, r.chat_retention_hours, " "r.offline_image_url, r.description, r.user_id, r.playlist_control_mode, r.playlist_whitelist, r.title_color, " "u.username, u.avatar_url, u.user_color FROM realms r " "JOIN users u ON r.user_id = u.id " "WHERE r.name = $1 AND r.is_active = true" << realmName >> [callback](const Result& r) { if (r.empty()) { callback(jsonError("Realm not found", k404NotFound)); return; } Json::Value resp; resp["success"] = true; auto& realm = resp["realm"]; realm["id"] = static_cast(r[0]["id"].as()); realm["name"] = r[0]["name"].as(); realm["type"] = r[0]["realm_type"].isNull() ? "stream" : r[0]["realm_type"].as(); realm["isLive"] = r[0]["is_live"].as(); // Apply viewer multiplier for visual "viewbotting" effect int64_t viewerCount = r[0]["viewer_count"].as(); int multiplier = r[0]["viewer_multiplier"].isNull() ? 1 : r[0]["viewer_multiplier"].as(); int64_t displayCount = viewerCount * multiplier; // Add random 0-99 bonus when multiplier is active if (multiplier > 1) { static thread_local std::mt19937 rng(std::random_device{}()); std::uniform_int_distribution dist(0, 99); displayCount += dist(rng); } realm["viewerCount"] = static_cast(displayCount); realm["chatEnabled"] = r[0]["chat_enabled"].as(); realm["chatGuestsAllowed"] = r[0]["chat_guests_allowed"].as(); realm["chatSlowModeSeconds"] = r[0]["chat_slow_mode_seconds"].as(); realm["chatRetentionHours"] = r[0]["chat_retention_hours"].as(); realm["offlineImageUrl"] = r[0]["offline_image_url"].isNull() ? "" : r[0]["offline_image_url"].as(); realm["description"] = r[0]["description"].isNull() ? "" : r[0]["description"].as(); realm["ownerId"] = static_cast(r[0]["user_id"].as()); realm["playlistControlMode"] = r[0]["playlist_control_mode"].isNull() ? "owner" : r[0]["playlist_control_mode"].as(); realm["playlistWhitelist"] = r[0]["playlist_whitelist"].isNull() ? "[]" : r[0]["playlist_whitelist"].as(); realm["username"] = r[0]["username"].as(); realm["avatarUrl"] = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as(); realm["colorCode"] = r[0]["user_color"].isNull() ? "" : r[0]["user_color"].as(); realm["titleColor"] = r[0]["title_color"].isNull() ? "#ffffff" : r[0]["title_color"].as(); callback(jsonResp(resp)); } >> DB_ERROR(callback, "get realm by name"); } void RealmController::getLiveRealms(const HttpRequestPtr &, std::function &&callback) { auto dbClient = app().getDbClient(); // SECURITY: Do NOT expose stream_key in public API - it allows stream hijacking *dbClient << "SELECT r.name, r.viewer_count, r.viewer_multiplier, u.username, u.avatar_url " "FROM realms r JOIN users u ON r.user_id = u.id " "WHERE r.is_live = true AND r.is_active = true " "ORDER BY (r.viewer_count * COALESCE(r.viewer_multiplier, 1)) DESC" >> [callback](const Result& r) { Json::Value resp(Json::arrayValue); for (const auto& row : r) { Json::Value realm; realm["name"] = row["name"].as(); // stream_key intentionally omitted - security fix // Apply viewer multiplier for visual "viewbotting" effect int64_t viewerCount = row["viewer_count"].as(); int multiplier = row["viewer_multiplier"].isNull() ? 1 : row["viewer_multiplier"].as(); int64_t displayCount = viewerCount * multiplier; // Add random 0-99 bonus when multiplier is active if (multiplier > 1) { static thread_local std::mt19937 rng(std::random_device{}()); std::uniform_int_distribution dist(0, 99); displayCount += dist(rng); } realm["viewerCount"] = static_cast(displayCount); realm["username"] = row["username"].as(); realm["avatarUrl"] = row["avatar_url"].isNull() ? "" : row["avatar_url"].as(); resp.append(realm); } callback(jsonResp(resp)); } >> DB_ERROR(callback, "get live realms"); } void RealmController::getAllRealms(const HttpRequestPtr &, std::function &&callback) { auto dbClient = app().getDbClient(); // Only show stream realms in the public list (video realms are accessed via /videos) // SECURITY: Do NOT expose stream_key in public API - it allows stream hijacking *dbClient << "SELECT r.id, r.name, r.realm_type, r.is_live, r.viewer_count, r.viewer_multiplier, " "r.chat_enabled, r.chat_guests_allowed, " "r.offline_image_url, r.title_color, u.username, u.avatar_url, u.user_color " "FROM realms r JOIN users u ON r.user_id = u.id " "WHERE r.is_active = true AND (r.realm_type = 'stream' OR r.realm_type IS NULL) " "ORDER BY r.is_live DESC, (r.viewer_count * COALESCE(r.viewer_multiplier, 1)) DESC, r.name ASC" >> [callback](const Result& r) { Json::Value resp; Json::Value realms(Json::arrayValue); for (const auto& row : r) { Json::Value realm; realm["id"] = static_cast(row["id"].as()); realm["name"] = row["name"].as(); realm["type"] = row["realm_type"].isNull() ? "stream" : row["realm_type"].as(); // stream_key intentionally omitted - security fix realm["isLive"] = row["is_live"].as(); // Apply viewer multiplier for visual "viewbotting" effect int64_t viewerCount = row["viewer_count"].as(); int multiplier = row["viewer_multiplier"].isNull() ? 1 : row["viewer_multiplier"].as(); int64_t displayCount = viewerCount * multiplier; // Add random 0-99 bonus when multiplier is active if (multiplier > 1) { static thread_local std::mt19937 rng(std::random_device{}()); std::uniform_int_distribution dist(0, 99); displayCount += dist(rng); } realm["viewerCount"] = static_cast(displayCount); realm["chatEnabled"] = row["chat_enabled"].as(); realm["chatGuestsAllowed"] = row["chat_guests_allowed"].as(); realm["offlineImageUrl"] = row["offline_image_url"].isNull() ? "" : row["offline_image_url"].as(); realm["titleColor"] = row["title_color"].isNull() ? "#ffffff" : row["title_color"].as(); realm["username"] = row["username"].as(); realm["avatarUrl"] = row["avatar_url"].isNull() ? "" : row["avatar_url"].as(); realm["colorCode"] = row["user_color"].isNull() ? "#561D5E" : row["user_color"].as(); realms.append(realm); } resp["success"] = true; resp["realms"] = realms; callback(jsonResp(resp)); } >> DB_ERROR(callback, "get all realms"); } void RealmController::validateRealmKey(const HttpRequestPtr &, std::function &&callback, const std::string &key) { auto dbClient = app().getDbClient(); *dbClient << "SELECT id FROM realms WHERE stream_key = $1 AND is_active = true" << key >> [callback, key](const Result& r) { bool valid = !r.empty(); if (valid) { // Store in Redis RedisHelper::storeKey("stream_key:" + key, "1", 86400); } Json::Value resp; resp["valid"] = valid; callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); Json::Value resp; resp["valid"] = false; callback(jsonResp(resp)); }; } void RealmController::getRealmStats(const HttpRequestPtr &, std::function &&callback, const std::string &realmId) { // Public endpoint - no authentication required int64_t id = std::stoll(realmId); auto dbClient = app().getDbClient(); *dbClient << "SELECT stream_key, viewer_multiplier FROM realms WHERE id = $1" << id >> [callback](const Result& r) { if (r.empty()) { callback(jsonError("Realm not found", k404NotFound)); return; } std::string streamKey = r[0]["stream_key"].as(); int multiplier = r[0]["viewer_multiplier"].isNull() ? 1 : r[0]["viewer_multiplier"].as(); StatsService::getInstance().getStreamStats(streamKey, [callback, multiplier](bool success, const StreamStats& stats) { if (success) { Json::Value json; json["success"] = true; // Calculate random bonus when multiplier is active int randomBonus = 0; if (multiplier > 1) { static thread_local std::mt19937 rng(std::random_device{}()); std::uniform_int_distribution dist(0, 99); randomBonus = dist(rng); } auto& s = json["stats"]; // Apply viewer multiplier for visual "viewbotting" effect s["connections"] = static_cast(stats.uniqueViewers * multiplier + randomBonus); s["total_connections"] = static_cast(stats.totalConnections * multiplier + randomBonus); s["bytes_in"] = static_cast(stats.totalBytesIn); s["bytes_out"] = static_cast(stats.totalBytesOut); s["bitrate"] = stats.bitrate; s["codec"] = stats.codec; s["resolution"] = stats.resolution; s["fps"] = stats.fps; s["is_live"] = stats.isLive; // Protocol breakdown (also multiplied for consistency) auto& pc = s["protocol_connections"]; pc["webrtc"] = static_cast(stats.protocolConnections.webrtc * multiplier); pc["hls"] = static_cast(stats.protocolConnections.hls * multiplier); pc["llhls"] = static_cast(stats.protocolConnections.llhls * multiplier); pc["dash"] = static_cast(stats.protocolConnections.dash * multiplier); callback(jsonResp(json)); } else { callback(jsonError("Failed to retrieve stats")); } }); } >> DB_ERROR(callback, "get realm stats"); } void RealmController::getPublicUserRealms(const HttpRequestPtr &, std::function &&callback, const std::string &username) { auto dbClient = app().getDbClient(); *dbClient << "SELECT r.id, r.name, r.realm_type, r.is_live, r.viewer_count, r.viewer_multiplier, " "r.title_color, r.created_at, " "(SELECT COUNT(*) FROM videos v WHERE v.realm_id = r.id AND v.status = 'ready') as video_count " "FROM realms r " "JOIN users u ON r.user_id = u.id " "WHERE u.username = $1 AND r.is_active = true " "ORDER BY r.realm_type ASC, r.is_live DESC, r.created_at DESC" << username >> [callback](const Result& r) { Json::Value resp; resp["success"] = true; Json::Value realms(Json::arrayValue); for (const auto& row : r) { Json::Value realm; realm["id"] = static_cast(row["id"].as()); realm["name"] = row["name"].as(); realm["type"] = row["realm_type"].isNull() ? "stream" : row["realm_type"].as(); realm["isLive"] = row["is_live"].as(); // Apply viewer multiplier for visual "viewbotting" effect int64_t viewerCount = row["viewer_count"].as(); int multiplier = row["viewer_multiplier"].isNull() ? 1 : row["viewer_multiplier"].as(); int64_t displayCount = viewerCount * multiplier; // Add random 0-99 bonus when multiplier is active if (multiplier > 1) { static thread_local std::mt19937 rng(std::random_device{}()); std::uniform_int_distribution dist(0, 99); displayCount += dist(rng); } realm["viewerCount"] = static_cast(displayCount); realm["titleColor"] = row["title_color"].isNull() ? "#ffffff" : row["title_color"].as(); realm["videoCount"] = static_cast(row["video_count"].as()); realm["createdAt"] = row["created_at"].as(); realms.append(realm); } resp["realms"] = realms; callback(jsonResp(resp)); } >> DB_ERROR(callback, "get user realms"); } void RealmController::uploadOfflineImage(const HttpRequestPtr &req, std::function &&callback, const std::string &realmId) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } int64_t id; try { id = std::stoll(realmId); } catch (...) { callback(jsonError("Invalid realm ID", k400BadRequest)); return; } // Get file from multipart form MultiPartParser fileParser; if (fileParser.parse(req) != 0 || fileParser.getFiles().empty()) { callback(jsonError("No file uploaded")); return; } auto& file = fileParser.getFiles()[0]; // Check file extension for allowed formats (supports animated GIF/WebP) std::string filename = file.getFileName(); std::string ext; auto dotPos = filename.find_last_of('.'); if (dotPos != std::string::npos) { ext = filename.substr(dotPos); std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); } if (ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".gif" && ext != ".webp") { callback(jsonError("Invalid file type. Only JPEG, PNG, GIF, and WebP are allowed.")); return; } // Check file size (max 5MB) if (file.fileLength() > 5 * 1024 * 1024) { callback(jsonError("File too large. Maximum size is 5MB.")); return; } auto dbClient = app().getDbClient(); // Verify realm ownership and get old image path *dbClient << "SELECT offline_image_url, realm_type FROM realms WHERE id = $1 AND user_id = $2" << id << user.id >> [callback, dbClient, id, user, file = std::move(file)](const Result& r) mutable { if (r.empty()) { callback(jsonError("Realm not found or access denied", k404NotFound)); return; } // Only allow for stream and watch realms std::string realmType = r[0]["realm_type"].isNull() ? "stream" : r[0]["realm_type"].as(); if (realmType != "stream" && realmType != "watch") { callback(jsonError("Offline images are only supported for stream and watch realms", k400BadRequest)); return; } // Delete old image if exists (SECURITY FIX #12: Use safe delete with path validation) if (!r[0]["offline_image_url"].isNull()) { std::string oldPath = r[0]["offline_image_url"].as(); safeDeleteFile(oldPath, "./uploads/offline"); } // Save new file std::string uploadDir = "./uploads/offline"; std::filesystem::create_directories(uploadDir); // Generate unique filename std::string ext = file.getFileName().substr(file.getFileName().find_last_of(".")); std::string filename = "realm_" + std::to_string(id) + "_" + std::to_string(std::chrono::system_clock::now().time_since_epoch().count()) + ext; std::string savePath = uploadDir + "/" + filename; std::string urlPath = "/uploads/offline/" + filename; // Save file file.saveAs(savePath); // Update database *dbClient << "UPDATE realms SET offline_image_url = $1 WHERE id = $2 AND user_id = $3" << urlPath << id << user.id >> [callback, urlPath](const Result&) { Json::Value resp; resp["success"] = true; resp["offlineImageUrl"] = urlPath; resp["message"] = "Offline image uploaded successfully"; callback(jsonResp(resp)); } >> DB_ERROR_MSG(callback, "update offline image", "Failed to update offline image"); } >> DB_ERROR(callback, "verify realm ownership"); } void RealmController::deleteOfflineImage(const HttpRequestPtr &req, std::function &&callback, const std::string &realmId) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } int64_t id; try { id = std::stoll(realmId); } catch (...) { callback(jsonError("Invalid realm ID", k400BadRequest)); return; } auto dbClient = app().getDbClient(); // Get old image path and verify ownership *dbClient << "SELECT offline_image_url FROM realms WHERE id = $1 AND user_id = $2" << id << user.id >> [callback, dbClient, id, user](const Result& r) { if (r.empty()) { callback(jsonError("Realm not found or access denied", k404NotFound)); return; } // Delete file if exists (SECURITY FIX #12: Use safe delete with path validation) if (!r[0]["offline_image_url"].isNull()) { std::string oldPath = r[0]["offline_image_url"].as(); safeDeleteFile(oldPath, "./uploads/offline"); } // Clear database field *dbClient << "UPDATE realms SET offline_image_url = NULL WHERE id = $1 AND user_id = $2" << id << user.id >> [callback](const Result&) { Json::Value resp; resp["success"] = true; resp["message"] = "Offline image deleted successfully"; callback(jsonResp(resp)); } >> DB_ERROR_MSG(callback, "delete offline image", "Failed to delete offline image"); } >> DB_ERROR(callback, "get realm for delete image"); } void RealmController::getRealmModerators(const HttpRequestPtr &req, std::function &&callback, const std::string &realmId) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } int64_t id; try { id = std::stoll(realmId); } catch (...) { callback(jsonError("Invalid realm ID", k400BadRequest)); return; } auto dbClient = app().getDbClient(); // Verify user is realm owner or admin *dbClient << "SELECT user_id FROM realms WHERE id = $1" << id >> [callback, dbClient, id, user](const Result& r) { if (r.empty()) { callback(jsonError("Realm not found", k404NotFound)); return; } int64_t ownerId = r[0]["user_id"].as(); if (ownerId != user.id && !user.isAdmin) { callback(jsonError("Access denied", k403Forbidden)); return; } // Get moderators for this realm *dbClient << "SELECT u.id, u.username, u.avatar_url, u.user_color, cm.granted_at " "FROM chat_moderators cm " "JOIN users u ON cm.user_id = u.id " "WHERE cm.realm_id = $1 " "ORDER BY cm.granted_at DESC" << id >> [callback](const Result& r2) { Json::Value resp; resp["success"] = true; Json::Value moderators(Json::arrayValue); for (const auto& row : r2) { Json::Value mod; mod["id"] = static_cast(row["id"].as()); mod["username"] = row["username"].as(); mod["avatarUrl"] = row["avatar_url"].isNull() ? "" : row["avatar_url"].as(); mod["colorCode"] = row["user_color"].isNull() ? "#561D5E" : row["user_color"].as(); mod["grantedAt"] = row["granted_at"].as(); moderators.append(mod); } resp["moderators"] = moderators; callback(jsonResp(resp)); } >> DB_ERROR_MSG(callback, "get moderators", "Failed to get moderators"); } >> DB_ERROR(callback, "check realm ownership"); } void RealmController::addRealmModerator(const HttpRequestPtr &req, std::function &&callback, const std::string &realmId) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } int64_t id; try { id = std::stoll(realmId); } catch (...) { callback(jsonError("Invalid realm ID", k400BadRequest)); return; } auto json = req->getJsonObject(); if (!json || !(*json)["userId"].isInt64()) { callback(jsonError("userId is required", k400BadRequest)); return; } int64_t targetUserId = (*json)["userId"].asInt64(); auto dbClient = app().getDbClient(); // Verify user is realm owner or admin *dbClient << "SELECT user_id FROM realms WHERE id = $1" << id >> [callback, dbClient, id, user, targetUserId](const Result& r) { if (r.empty()) { callback(jsonError("Realm not found", k404NotFound)); return; } int64_t ownerId = r[0]["user_id"].as(); if (ownerId != user.id && !user.isAdmin) { callback(jsonError("Only realm owner can add moderators", k403Forbidden)); return; } // Check if target user exists *dbClient << "SELECT id, username FROM users WHERE id = $1" << targetUserId >> [callback, dbClient, id, targetUserId](const Result& r2) { if (r2.empty()) { callback(jsonError("User not found", k404NotFound)); return; } std::string username = r2[0]["username"].as(); // Insert moderator (ignore if already exists) *dbClient << "INSERT INTO chat_moderators (realm_id, user_id) VALUES ($1, $2) " "ON CONFLICT (realm_id, user_id) DO NOTHING" << id << targetUserId >> [callback, username](const Result&) { Json::Value resp; resp["success"] = true; resp["message"] = username + " added as moderator"; callback(jsonResp(resp)); } >> DB_ERROR_MSG(callback, "add moderator", "Failed to add moderator"); } >> DB_ERROR(callback, "check user exists"); } >> DB_ERROR(callback, "check realm ownership"); } void RealmController::removeRealmModerator(const HttpRequestPtr &req, std::function &&callback, const std::string &realmId, const std::string &moderatorId) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } int64_t id, modId; try { id = std::stoll(realmId); modId = std::stoll(moderatorId); } catch (...) { callback(jsonError("Invalid ID", k400BadRequest)); return; } auto dbClient = app().getDbClient(); // Verify user is realm owner or admin *dbClient << "SELECT user_id FROM realms WHERE id = $1" << id >> [callback, dbClient, id, modId, user](const Result& r) { if (r.empty()) { callback(jsonError("Realm not found", k404NotFound)); return; } int64_t ownerId = r[0]["user_id"].as(); if (ownerId != user.id && !user.isAdmin) { callback(jsonError("Only realm owner can remove moderators", k403Forbidden)); return; } // Remove moderator *dbClient << "DELETE FROM chat_moderators WHERE realm_id = $1 AND user_id = $2" << id << modId >> [callback](const Result&) { Json::Value resp; resp["success"] = true; resp["message"] = "Moderator removed"; callback(jsonResp(resp)); } >> DB_ERROR_MSG(callback, "remove moderator", "Failed to remove moderator"); } >> DB_ERROR(callback, "check realm ownership"); } void RealmController::updateTitleColor(const HttpRequestPtr &req, std::function &&callback, const std::string &realmId) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } int64_t id; try { id = std::stoll(realmId); } catch (...) { callback(jsonError("Invalid realm ID", k400BadRequest)); return; } auto json = req->getJsonObject(); if (!json) { callback(jsonError("Invalid JSON")); return; } if (!(*json).isMember("titleColor")) { callback(jsonError("Title color is required")); return; } std::string titleColor = (*json)["titleColor"].asString(); // Validate title color format (hex color) if (titleColor.length() != 7 || titleColor[0] != '#') { callback(jsonError("Invalid color format. Use hex format like #ffffff")); return; } // Validate hex characters for (size_t i = 1; i < titleColor.length(); ++i) { char c = titleColor[i]; if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { callback(jsonError("Invalid color format. Use hex format like #ffffff")); return; } } auto dbClient = app().getDbClient(); // Verify realm ownership *dbClient << "SELECT user_id FROM realms WHERE id = $1" << id >> [callback, dbClient, id, user, titleColor](const Result& r) { if (r.empty()) { callback(jsonError("Realm not found", k404NotFound)); return; } int64_t ownerId = r[0]["user_id"].as(); if (ownerId != user.id && !user.isAdmin) { callback(jsonError("You don't have permission to modify this realm", k403Forbidden)); return; } // Update title color *dbClient << "UPDATE realms SET title_color = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2" << titleColor << id >> [callback, titleColor](const Result&) { Json::Value resp; resp["success"] = true; resp["titleColor"] = titleColor; callback(jsonResp(resp)); } >> DB_ERROR(callback, "update title color"); } >> DB_ERROR(callback, "check realm ownership"); }