This commit is contained in:
parent
a0e6d40679
commit
954755fbc3
19 changed files with 356 additions and 321 deletions
24
chat-service/src/common/CryptoUtils.h
Normal file
24
chat-service/src/common/CryptoUtils.h
Normal 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
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue