fixes lol
All checks were successful
Build and Push / build-all (push) Successful in 10m54s

This commit is contained in:
doomtube 2026-01-09 16:38:24 -05:00
parent a0e6d40679
commit 954755fbc3
19 changed files with 356 additions and 321 deletions

View file

@ -0,0 +1,24 @@
#pragma once
#include <string>
#include <sstream>
#include <iomanip>
#include <array>
namespace crypto_utils {
// Convert raw bytes to lowercase hex string
inline std::string bytesToHex(const unsigned char* data, size_t length) {
std::stringstream ss;
for (size_t i = 0; i < length; ++i) {
ss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(data[i]);
}
return ss.str();
}
// Overload for std::array
template<size_t N>
inline std::string bytesToHex(const std::array<unsigned char, N>& data) {
return bytesToHex(data.data(), N);
}
} // namespace crypto_utils

View file

@ -3,6 +3,7 @@
#include "../services/AuthService.h"
#include "../services/ModerationService.h"
#include "../services/RedisMessageStore.h"
#include "../common/CryptoUtils.h"
#include <drogon/HttpClient.h>
#include <json/json.h>
#include <iostream>
@ -117,6 +118,28 @@ void ChatWebSocketController::broadcastParticipantLeft(const std::string& realmI
}
}
// Generate browser fingerprint from request headers
std::string ChatWebSocketController::generateFingerprint(const HttpRequestPtr& req,
const WebSocketConnectionPtr& wsConnPtr) {
std::string fingerprint = req->getHeader("X-Server-Fingerprint");
if (!fingerprint.empty()) {
return fingerprint;
}
std::string clientIp = req->getHeader("X-Real-IP");
if (clientIp.empty()) {
clientIp = wsConnPtr->peerAddr().toIp();
}
std::string userAgent = req->getHeader("User-Agent");
std::string acceptLang = req->getHeader("Accept-Language");
std::string toHash = clientIp + "|" + userAgent + "|" + acceptLang;
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256(reinterpret_cast<const unsigned char*>(toHash.c_str()), toHash.length(), hash);
return crypto_utils::bytesToHex(hash, SHA256_DIGEST_LENGTH);
}
void ChatWebSocketController::handleNewConnection(const HttpRequestPtr& req,
const WebSocketConnectionPtr& wsConnPtr) {
LOG_INFO << "New WebSocket connection from " << wsConnPtr->peerAddr().toIpPort();
@ -188,26 +211,7 @@ void ChatWebSocketController::handleNewConnection(const HttpRequestPtr& req,
LOG_WARN << "User " << info.username << " has pending uberban - capturing fingerprint and applying ban";
// Generate fingerprint NOW (only for pending_uberban users)
std::string fingerprint = req->getHeader("X-Server-Fingerprint");
if (fingerprint.empty()) {
std::string clientIp = req->getHeader("X-Real-IP");
if (clientIp.empty()) {
clientIp = wsConnPtr->peerAddr().toIp();
}
std::string userAgent = req->getHeader("User-Agent");
std::string acceptLang = req->getHeader("Accept-Language");
std::string toHash = clientIp + "|" + userAgent + "|" + acceptLang;
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256(reinterpret_cast<const unsigned char*>(toHash.c_str()), toHash.length(), hash);
std::stringstream ss;
for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
ss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(hash[i]);
}
fingerprint = ss.str();
}
std::string fingerprint = generateFingerprint(req, wsConnPtr);
// Add fingerprint to banned set
redis.addFingerprintBan(fingerprint);
@ -248,32 +252,12 @@ void ChatWebSocketController::handleNewConnection(const HttpRequestPtr& req,
}
// Generate fingerprint for guests (always needed for guest moderation)
std::string fingerprint = req->getHeader("X-Server-Fingerprint");
if (fingerprint.empty()) {
std::string clientIp = req->getHeader("X-Real-IP");
if (clientIp.empty()) {
clientIp = wsConnPtr->peerAddr().toIp();
}
std::string userAgent = req->getHeader("User-Agent");
std::string acceptLang = req->getHeader("Accept-Language");
std::string toHash = clientIp + "|" + userAgent + "|" + acceptLang;
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256(reinterpret_cast<const unsigned char*>(toHash.c_str()), toHash.length(), hash);
std::stringstream ss;
for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
ss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(hash[i]);
}
fingerprint = ss.str();
}
info.fingerprint = fingerprint;
info.fingerprint = generateFingerprint(req, wsConnPtr);
// Check fingerprint ban for guests
auto& redis = services::RedisMessageStore::getInstance();
if (redis.isFingerprintBanned(fingerprint)) {
LOG_WARN << "Guest connection rejected - fingerprint banned: " << fingerprint.substr(0, 8) << "...";
if (redis.isFingerprintBanned(info.fingerprint)) {
LOG_WARN << "Guest connection rejected - fingerprint banned: " << info.fingerprint.substr(0, 8) << "...";
Json::Value error;
error["type"] = "error";
error["error"] = "You have been banned from chat.";
@ -550,7 +534,8 @@ void ChatWebSocketController::handleChatMessage(const WebSocketConnectionPtr& ws
auto result = chatService.sendMessage(
currentInfo.realmId, currentInfo.userId, currentInfo.username, userColor, avatarUrl, content,
currentInfo.isGuest, currentInfo.isModerator, currentInfo.isStreamer, message, selfDestructSeconds,
currentInfo.isApiKeyConnection // SECURITY FIX: Pass bot flag for rate limiting
currentInfo.isApiKeyConnection, // SECURITY FIX: Pass bot flag for rate limiting
currentInfo.apiKeyId // SECURITY FIX: Rate limit per API key, not per user
);
if (result == services::SendMessageResult::SUCCESS) {
@ -1408,6 +1393,42 @@ void ChatWebSocketController::checkGuestTimeouts() {
}
}
void ChatWebSocketController::checkPendingConnectionTimeouts() {
constexpr int PENDING_TIMEOUT_SECONDS = 15;
auto now = std::chrono::steady_clock::now();
std::vector<WebSocketConnectionPtr> toDisconnect;
{
std::lock_guard<std::mutex> lock(connectionsMutex_);
for (const auto& [conn, startTime] : pendingConnections_) {
auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
now - startTime
).count();
if (elapsed >= PENDING_TIMEOUT_SECONDS) {
toDisconnect.push_back(conn);
}
}
// Remove timed-out connections from pending map
for (const auto& conn : toDisconnect) {
pendingConnections_.erase(conn);
}
}
// Disconnect outside the lock to avoid holding it while sending messages
for (const auto& conn : toDisconnect) {
Json::Value msg;
msg["type"] = "error";
msg["error"] = "API key validation timed out. Please reconnect and try again.";
try {
conn->send(Json::writeString(Json::StreamWriterBuilder(), msg));
} catch (...) {}
conn->shutdown(CloseCode::kViolation);
LOG_WARN << "Pending bot connection timed out after " << PENDING_TIMEOUT_SECONDS << " seconds - disconnecting";
}
}
// Debug: Static initializer to verify this file is loaded
static struct ChatWebSocketControllerLoader {
ChatWebSocketControllerLoader() {

View file

@ -99,6 +99,9 @@ private:
static void broadcastParticipantJoined(const std::string& realmId, const ConnectionInfo& joinedUser);
static void broadcastParticipantLeft(const std::string& realmId, const std::string& userId, const std::string& username);
// Generate browser fingerprint from request headers
static std::string generateFingerprint(const HttpRequestPtr& req, const WebSocketConnectionPtr& wsConnPtr);
public:
// Internal API: Try to uberban a user by ID (used by backend admin endpoint)
// Returns: fingerprint if user was connected and banned, empty string if not connected

View file

@ -121,6 +121,12 @@ int main() {
});
LOG_INFO << "Guest session timeout checker registered (45-123 minute random timeout)";
// Register pending bot connection timeout checker (runs every 5 seconds)
app().getLoop()->runEvery(5.0, []() {
ChatWebSocketController::checkPendingConnectionTimeouts();
});
LOG_INFO << "Pending bot connection timeout checker registered (15 second timeout)";
// Schedule sticker fetch (must be done here, after event loop is set up)
stickerService.scheduleFetch();

View file

@ -31,13 +31,14 @@ SendMessageResult ChatService::sendMessage(const std::string& realmId,
bool isStreamer,
models::ChatMessage& outMessage,
int selfDestructSeconds,
bool isBot) {
bool isBot,
int64_t botApiKeyId) {
auto& redis = RedisMessageStore::getInstance();
auto& modService = ModerationService::getInstance();
// SECURITY FIX: Bot rate limiting (1 message per second)
if (isBot) {
if (!canBotSendMessage(userId)) {
// SECURITY FIX: Bot rate limiting (1 message per second per API key)
if (isBot && botApiKeyId > 0) {
if (!canBotSendMessage(botApiKeyId)) {
return SendMessageResult::BOT_RATE_LIMITED;
}
}
@ -276,22 +277,22 @@ void ChatService::cleanupMessages() {
}
}
// SECURITY FIX: Bot rate limiting implementation
bool ChatService::canBotSendMessage(const std::string& botUserId) {
// SECURITY FIX: Bot rate limiting implementation (per API key, not per user)
bool ChatService::canBotSendMessage(int64_t apiKeyId) {
std::lock_guard<std::mutex> lock(botRateLimitMutex_);
auto now = std::chrono::steady_clock::now();
auto it = botLastMessage_.find(botUserId);
auto it = botLastMessage_.find(apiKeyId);
if (it != botLastMessage_.end()) {
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - it->second).count();
if (elapsed < BOT_RATE_LIMIT_MS) {
LOG_DEBUG << "Bot " << botUserId << " rate limited (only " << elapsed << "ms since last message)";
LOG_DEBUG << "Bot API key " << apiKeyId << " rate limited (only " << elapsed << "ms since last message)";
return false;
}
}
botLastMessage_[botUserId] = now;
botLastMessage_[apiKeyId] = now;
return true;
}

View file

@ -39,7 +39,8 @@ public:
bool isStreamer,
models::ChatMessage& outMessage,
int selfDestructSeconds = 0,
bool isBot = false); // SECURITY FIX: Bot rate limiting
bool isBot = false,
int64_t botApiKeyId = 0); // SECURITY FIX: Bot rate limiting per API key
// Schedule a message for self-destruction
void scheduleSelfDestruct(const std::string& realmId, const std::string& messageId, int delaySeconds);
@ -73,11 +74,11 @@ private:
bool isContentValid(const std::string& content);
void cleanupMessages();
// SECURITY FIX: Bot rate limiting (1 message per second per bot)
// SECURITY FIX: Bot rate limiting (1 message per second per API key)
static constexpr int BOT_RATE_LIMIT_MS = 1000; // 1 second between messages
std::unordered_map<std::string, std::chrono::steady_clock::time_point> botLastMessage_;
std::unordered_map<int64_t, std::chrono::steady_clock::time_point> botLastMessage_;
std::mutex botRateLimitMutex_;
bool canBotSendMessage(const std::string& botUserId);
bool canBotSendMessage(int64_t apiKeyId);
};
} // namespace services