diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 11dd4e9..58af055 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,28 +1,67 @@ { - "tasks": [ - { - "type": "cppbuild", - "label": "C/C++: gcc-13 build active file", - "command": "/usr/bin/gcc-13", - "args": [ - "-fdiagnostics-color=always", - "-g", - "${file}", - "-o", - "${fileDirname}/${fileBasenameNoExtension}" - ], - "options": { - "cwd": "${fileDirname}" - }, - "problemMatcher": [ - "$gcc" - ], - "group": { - "kind": "build", - "isDefault": true - }, - "detail": "Task generated by Debugger." - } - ], - "version": "2.0.0" + "tasks": [ + { + "type": "cppbuild", + "label": "C/C++: gcc-13 build active file", + "command": "/usr/bin/gcc-13", + "args": [ + "-fdiagnostics-color=always", + "-g", + "${file}", + "-o", + "${fileDirname}/${fileBasenameNoExtension}" + ], + "options": { + "cwd": "${fileDirname}" + }, + "problemMatcher": [ + "$gcc" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "detail": "Task generated by Debugger." + }, + { + "label": "build-yourchat", + "type": "shell", + "command": "bash", + "args": [ + "-lc", + "set -euo pipefail\ncd /home/torsten/Programs/YourChat/build\ncmake -DYC_DEBUG=ON ..\nmake -j\"$(nproc)\"\n./yourchat | head -n 3 || true" + ], + "problemMatcher": [ + "$gcc" + ], + "group": "build" + }, + { + "label": "build-yourchat-3", + "type": "shell", + "command": "bash", + "args": [ + "-lc", + "set -euo pipefail\ncd /home/torsten/Programs/YourChat/build\ncmake -DYC_DEBUG=ON ..\nmake -j\"$(nproc)\"\n./yourchat | head -n 3 || true" + ], + "problemMatcher": [ + "$gcc" + ], + "group": "build" + }, + { + "label": "build-yourchat-4", + "type": "shell", + "command": "bash", + "args": [ + "-lc", + "set -euo pipefail\ncd /home/torsten/Programs/YourChat/build\ncmake -DYC_DEBUG=ON ..\nmake -j\"$(nproc)\"\n./yourchat | head -n 3 || true" + ], + "problemMatcher": [ + "$gcc" + ], + "group": "build" + } + ], + "version": "2.0.0" } \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 87a126f..58ce4eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,11 @@ project(YourChat VERSION 0.1) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED True) include_directories(${PROJECT_SOURCE_DIR}/src) + +# Option to enable debug logging (guards [Debug] prints) +option(YC_DEBUG "Enable YourChat debug logging" OFF) +message(STATUS "YC_DEBUG option: ${YC_DEBUG}") + add_executable(yourchat src/main.cpp src/lib/base.cpp @@ -18,3 +23,22 @@ include_directories(${PROJECT_SOURCE_DIR}/src) src/object/room.cpp ) target_link_libraries(yourchat jsoncpp pthread pqxx) + +if(YC_DEBUG) + message(STATUS "YC_DEBUG enabled: defining YC_DEBUG=1, enabling Debug build and verbose makefiles") + # Ensure Debug build type for better symbols/optimizations settings + set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Build type" FORCE) + # Show full compile/link commands to verify -DYC_DEBUG in use + set(CMAKE_VERBOSE_MAKEFILE ON CACHE BOOL "Verbose Makefile" FORCE) + # Export compile_commands.json for tooling + set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE BOOL "Export compile_commands.json" FORCE) + target_compile_definitions(yourchat PRIVATE YC_DEBUG=1) +endif() + +# Link OpenSSL for WebSocket handshake helpers +find_package(OpenSSL REQUIRED) +target_link_libraries(yourchat OpenSSL::SSL OpenSSL::Crypto) + +add_executable(ws_probe tools/ws_probe.cpp) +target_compile_features(ws_probe PRIVATE cxx_std_17) +target_link_libraries(ws_probe pthread) diff --git a/README.md b/README.md new file mode 100644 index 0000000..7f3a0ff --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +# YourChat – Technische Dokumentation + +## 1. Systemübersicht +YourChat ist ein serverbasiertes Chat-System mit Mehrbenutzer-Unterstützung, Raumverwaltung, Rechtesystem und Datenbankanbindung. Die Kommunikation erfolgt über TCP-Sockets mit JSON-basierten Nachrichten. + +## 2. Socket-Verbindung +- **Protokoll:** TCP +- **Port:** Konfigurierbar (siehe `config/chatconfig.json`) +- **Verbindung:** + - Client verbindet sich per TCP an den Server. + - Nach Verbindungsaufbau erfolgt ein Handshake (Token-Authentifizierung). + +## 2a. Verbindung als User (Schritt-für-Schritt) + +1. **TCP-Verbindung herstellen** + - Verbinde dich per TCP-Client (z.B. `telnet`, `nc`, eigenes Programm) zum Server: + - Host/IP: wie in `config/chatconfig.json` angegeben + - Port: wie in `config/chatconfig.json` angegeben + +2. **Handshake/Token** + - Nach Verbindungsaufbau sendet der Server ein JSON mit dem Feld `type: token` und einem zufälligen Token-String. + - Beispiel: + ```json + { + "type": "token", + "message": "IhrTokenString" + } + ``` + +3. **Authentifizierung und erste Aktion** + - Sende ab jetzt bei jeder Nachricht das Feld `token` mit. + - Beispiel für einen Raumbeitritt: + ```json + { + "type": "join", + "token": "IhrTokenString", + "newroom": "Lobby", + "password": "" + } + ``` + +4. **Nachrichten senden** + - Jede weitere Aktion (Chat, Würfeln, Userliste, ...) erfolgt als JSON mit dem Token. + +## 3. Authentifizierung & User-Flow +- Nach Verbindungsaufbau sendet der Server ein Token. +- Der Client muss dieses Token für alle weiteren Nachrichten mitsenden. +- Userdaten werden aus der Datenbank geladen oder bei Erstverbindung angelegt. + +## 4. Befehle & Nachrichtenformate +Alle Nachrichten werden als JSON-Objekte übertragen. Wichtige Felder: +- `type`: Nachrichtentyp (z.B. `message`, `join`, `userlist`, `dice`, ...) +- `token`: Authentifizierungstoken +- `message`: Textnachricht oder Befehl +- `userName`, `color`: User-Infos +- Beispiel: +```json +{ + "type": "message", + "token": "...", + "message": "Hallo Welt!", + "userName": "Alice", + "color": "#ff0000" +} +``` +### Wichtige Befehle +- `message`: Normale Chatnachricht +- `join`: Raumwechsel (`newroom`, `password`) +- `userlist`: Userliste anfordern +- `dice`: Würfeln +- `do`: Emote/Handlung +- `scream`: Schreien + +## 5. Datenstrukturen +### User (Datenbank: `chat.user`) +- `id`: int +- `falukant_user_id`: int (Referenz auf Community-User) +- `display_name`: string +- `color`: string (Hex) +- `show_gender`: bool +- `show_age`: bool +- `created_at`, `updated_at`: string (ISO-Timestamp) +- `rights`: [string] + +### Room (Datenbank: `chat.room`) +- `id`: int +- `name`: string +- `type`: int (Bitmaske, z.B. dice, poker, password) +- `password`: string +- `created_at`, `updated_at`: string + +### Rechte (Datenbank: `chat.rights`, `chat.user_rights`) +- `id`: int +- `tr`: string (Rechtebezeichner) + +## 6. Konfigurationsdateien +- `config/chatconfig.json`: Serverport, DB-Zugang, Raumvorgaben etc. + +## 7. Beispielablauf +1. Client verbindet sich via TCP. +2. Server sendet Token. +3. Client authentifiziert sich mit Token und sendet z.B. eine `join`-Nachricht. +4. Server prüft Rechte, Raum, etc. und antwortet entsprechend. + +## 8. Erweiterbarkeit +- Neue Befehle können durch Erweiterung der `type`-Felder und Serverlogik ergänzt werden. +- Datenbankstruktur ist flexibel für weitere User- oder Raumattribute. + +--- + +Für Details zu einzelnen Klassen siehe Quellcode in `src/core/`, `src/object/` und die Datenbankschemata. diff --git a/src/core/chat_room.cpp b/src/core/chat_room.cpp index a48b3f9..0ed4db5 100755 --- a/src/core/chat_room.cpp +++ b/src/core/chat_room.cpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace Yc { @@ -19,7 +20,10 @@ namespace Yc _parent(std::move(parent)), _blocked(false), _stop(false), - _roundRunning(false) + _roundRunning(false), + _diceGameRunning(false), + _currentRound(0), + _totalRounds(0) { _name = roomParams["name"].asString(); _password = roomParams["password"].asString(); @@ -37,6 +41,12 @@ namespace Yc void ChatRoom::run() { + // Nachrichtentypen, die nicht an den Auslöser zurückgeschickt werden + static const std::unordered_set kSuppressToSender = { + "room_entered", + "user_entered_room", + "user_color_changed" + }; while (!_stop) { if (_msgQueue.size() > 0 && !_blocked) @@ -47,6 +57,18 @@ namespace Yc Message message = _msgQueue.front(); for (auto &user : _users) { + // Konfigurierbare Unterdrückung für Systemmeldungen an den Sender + if (message.type == ChatUser::system) { + bool suppress = false; + // message.messageTr kann JSON (mit tr) oder plain String sein + Json::Value maybeJson = getJsonTree(message.messageTr); + if (maybeJson.isObject() && maybeJson.isMember("tr")) { + suppress = kSuppressToSender.count(maybeJson["tr"].asString()) > 0; + } else { + suppress = kSuppressToSender.count(message.messageTr) > 0; + } + if (suppress && user->name() == message.userName) continue; + } user->sendMsg(message.type, message.messageTr, message.userName, message.color); } _msgQueue.pop(); @@ -69,8 +91,29 @@ namespace Yc } auto newUser = std::make_shared(shared_from_this(), _userName, color, socket); _users.push_back(newUser); - newUser->sendMsg(ChatUser::roomList, _parent->jsonRoomList(), "", ""); - addMessage(ChatUser::system, "room_entered", newUser->name(), newUser->color()); + newUser->start(); + Json::Value roomList = _parent->jsonRoomList(); + newUser->sendMsg(ChatUser::roomList, roomList, "", ""); + // Private Rückmeldung an den User: In welchem Raum befindet er/sie sich jetzt? + { + Json::Value msg = Json::objectValue; + msg["tr"] = "room_entered"; + msg["to"] = _name; + newUser->sendMsg(ChatUser::system, msg, "", ""); + } + // Sende aktuelle Userliste an den neuen User + Json::Value currentUserList = userList(); + newUser->sendMsg(ChatUser::userListe, currentUserList, "", ""); + + // Sende aktualisierte Userliste an alle anderen User im Raum + for (auto &existingUser : _users) { + if (existingUser != newUser) { + existingUser->sendMsg(ChatUser::userListe, currentUserList, "", ""); + } + } + + // Broadcast an andere Nutzer: Benutzer X hat den Raum betreten (mit Farbinfo) + addMessage(ChatUser::system, "user_entered_room", newUser->name(), newUser->color()); _initRound(); return true; } @@ -107,7 +150,7 @@ namespace Yc { if (!silent) { - addMessage(ChatUser::system, (*it)->name(), (*it)->color()); + addMessage(ChatUser::system, std::string("leaved_chat"), (*it)->name(), (*it)->color()); } _users.erase(it); break; @@ -123,7 +166,7 @@ namespace Yc { if (!silent) { - addMessage(ChatUser::system, (*it)->name(), (*it)->color()); + addMessage(ChatUser::system, std::string("leaved_chat"), (*it)->name(), (*it)->color()); } _users.erase(it); break; @@ -164,6 +207,10 @@ namespace Yc } _users.push_back(user); user->setParent(shared_from_this()); + + // Sende aktuelle Userliste an den User nach Raumwechsel + Json::Value currentUserList = userList(); + user->sendMsg(ChatUser::userListe, currentUserList, "", ""); } bool ChatRoom::userToNewRoom(std::shared_ptr user, std::string newRoom, std::string password) @@ -188,6 +235,7 @@ namespace Yc { Json::Value jsonUser = Json::objectValue; jsonUser["name"] = user->name(); + jsonUser["color"] = user->color(); users.append(jsonUser); } return users; @@ -243,6 +291,18 @@ namespace Yc return false; } + std::shared_ptr ChatRoom::findUserByName(std::string userName) + { + for (auto &user : _users) + { + if (userName == user->name()) + { + return user; + } + } + return nullptr; + } + void ChatRoom::_handleDice() { if (((_type & rounds) == rounds)) @@ -266,7 +326,8 @@ namespace Yc void ChatRoom::_initRound() { - if (_users.size() == 2) + // Nur in Räumen mit Runden & Würfeln ankündigen + if (_users.size() == 2 && ((_type & rounds) == rounds) && ((_type & dice) == dice)) { _lastRoundEnd = time(NULL); addMessage(ChatUser::system, "next_round_starts_soon"); @@ -293,5 +354,245 @@ namespace Yc return _name; } + // Neue Würfel-Funktionen + bool ChatRoom::startDiceGame(int rounds, std::shared_ptr admin) + { + if (!_isUserAdmin(admin)) { + return false; // Kein Admin + } + + if (_diceGameRunning) { + return false; // Spiel läuft bereits + } + + if (!canDice()) { + return false; // Raum unterstützt kein Würfeln + } + + _diceGameRunning = true; + _currentRound = 1; + _totalRounds = rounds; + _gameStartTime = std::chrono::steady_clock::now(); + _gameResults.clear(); + + // Alle User für das Spiel vorbereiten + for (auto& user : _users) { + _gameResults[user->name()] = std::vector(); + _hasRolledThisRound[user->name()] = false; + } + + // Erste Runde starten + _startDiceRound(); + + // Nachricht an alle senden + Json::Value msg = Json::objectValue; + msg["tr"] = "dice_game_started"; + msg["rounds"] = rounds; + msg["admin"] = admin->name(); + addMessage(ChatUser::system, msg, "", ""); + + return true; + } + + bool ChatRoom::rollDice(std::shared_ptr user, int diceValue) + { + if (!_diceGameRunning) { + return false; // Kein Spiel aktiv + } + + if (diceValue < 1 || diceValue > 6) { + return false; // Ungültiger Würfelwert + } + + std::string userName = user->name(); + + if (_hasRolledThisRound[userName]) { + return false; // Bereits gewürfelt + } + + // Würfelwert speichern + DiceResult result; + result.userName = userName; + result.diceValue = diceValue; + result.rollTime = std::chrono::steady_clock::now(); + result.valid = true; + + _gameResults[userName].push_back(result); + _hasRolledThisRound[userName] = true; + + // Nachricht an alle senden + Json::Value msg = Json::objectValue; + msg["tr"] = "dice_rolled"; + msg["user"] = userName; + msg["value"] = diceValue; + msg["round"] = _currentRound; + addMessage(ChatUser::dice, msg, userName, user->color()); + + // Prüfen ob alle gewürfelt haben + bool allRolled = true; + for (auto& pair : _hasRolledThisRound) { + if (!pair.second) { + allRolled = false; + break; + } + } + + if (allRolled) { + // Alle haben gewürfelt, Runde beenden + _endDiceRound(); + } + + return true; + } + + void ChatRoom::endDiceGame() + { + if (!_diceGameRunning) { + return; + } + + _diceGameRunning = false; + _showGameResults(); + + // Nachricht an alle senden + Json::Value msg = Json::objectValue; + msg["tr"] = "dice_game_ended"; + addMessage(ChatUser::system, msg, "", ""); + } + + void ChatRoom::_startDiceRound() + { + if (_currentRound > _totalRounds) { + endDiceGame(); + return; + } + + _roundStartTime = std::chrono::steady_clock::now(); + + // Alle User für neue Runde zurücksetzen + for (auto& pair : _hasRolledThisRound) { + pair.second = false; + } + + // Nachricht an alle senden + Json::Value msg = Json::objectValue; + msg["tr"] = "dice_round_started"; + msg["round"] = _currentRound; + msg["total_rounds"] = _totalRounds; + addMessage(ChatUser::system, msg, "", ""); + + // Timer für 15 Sekunden starten + _startRoundTimer(); + } + + void ChatRoom::_endDiceRound() + { + // Ungültige Ergebnisse für User die nicht gewürfelt haben + for (auto& pair : _hasRolledThisRound) { + if (!pair.second) { + DiceResult result; + result.userName = pair.first; + result.diceValue = 0; + result.rollTime = std::chrono::steady_clock::now(); + result.valid = false; + _gameResults[pair.first].push_back(result); + } + } + + // Nachricht an alle senden + Json::Value msg = Json::objectValue; + msg["tr"] = "dice_round_ended"; + msg["round"] = _currentRound; + addMessage(ChatUser::system, msg, "", ""); + + _currentRound++; + + if (_currentRound <= _totalRounds) { + // Nächste Runde starten + std::this_thread::sleep_for(std::chrono::seconds(2)); // 2 Sekunden Pause + _startDiceRound(); + } else { + // Spiel beenden + endDiceGame(); + } + } + + void ChatRoom::_startRoundTimer() + { + _roundTimerThread = std::thread([this]() { + std::this_thread::sleep_for(std::chrono::seconds(15)); + + // Prüfen ob Runde noch läuft + if (_diceGameRunning && _currentRound <= _totalRounds) { + _endDiceRound(); + } + }); + _roundTimerThread.detach(); + } + + void ChatRoom::_showGameResults() + { + Json::Value results = Json::objectValue; + results["tr"] = "dice_game_results"; + results["total_rounds"] = _totalRounds; + + Json::Value userResults = Json::arrayValue; + for (auto& pair : _gameResults) { + Json::Value userResult = Json::objectValue; + userResult["user"] = pair.first; + + Json::Value rounds = Json::arrayValue; + int totalScore = 0; + int validRolls = 0; + + for (auto& result : pair.second) { + Json::Value round = Json::objectValue; + round["round"] = rounds.size() + 1; + round["value"] = result.diceValue; + round["valid"] = result.valid; + rounds.append(round); + + if (result.valid) { + totalScore += result.diceValue; + validRolls++; + } + } + + userResult["rounds"] = rounds; + userResult["total_score"] = totalScore; + userResult["valid_rolls"] = validRolls; + userResult["average"] = validRolls > 0 ? (double)totalScore / validRolls : 0.0; + + userResults.append(userResult); + } + + results["user_results"] = userResults; + addMessage(ChatUser::result, results, "", ""); + } + + bool ChatRoom::_isUserAdmin(std::shared_ptr user) const + { + // Einfache Admin-Prüfung: Erster User im Raum ist Admin + // In einer echten Implementierung würde man hier Rechte aus der Datenbank prüfen + if (_users.empty()) return false; + return _users[0] == user; + } + + void ChatRoom::reloadRoomList() + { + // Neue Raumliste vom Server holen + Json::Value roomList = _parent->jsonRoomList(); + + // An alle User im Raum senden + for (auto& user : _users) { + user->sendMsg(ChatUser::roomList, roomList, "", ""); + } + + // System-Nachricht an alle senden + Json::Value msg = Json::objectValue; + msg["tr"] = "room_list_reloaded"; + addMessage(ChatUser::system, msg, "", ""); + } + } // namespace Lib } // namespace Yc diff --git a/src/core/chat_room.h b/src/core/chat_room.h index c352717..0b158cf 100755 --- a/src/core/chat_room.h +++ b/src/core/chat_room.h @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include "lib/base.h" #include "chat_user.h" @@ -53,10 +55,22 @@ namespace Yc unsigned int addDice(std::shared_ptr user, int diceValue); bool accessAllowed(std::string userName, std::string password); bool userIsInRoom(std::string userName); + std::shared_ptr findUserByName(std::string userName); void addUserWhenQueueEmpty(std::shared_ptr user); bool userToNewRoom(std::shared_ptr user, std::string newRoom, std::string password); unsigned int flags(); Json::Value userList(); + + // Neue Würfel-Funktionen + bool startDiceGame(int rounds, std::shared_ptr admin); + bool rollDice(std::shared_ptr user, int diceValue); + void endDiceGame(); + bool isDiceGameRunning() const { return _diceGameRunning; } + int getCurrentRound() const { return _currentRound; } + int getTotalRounds() const { return _totalRounds; } + + // Raumliste neu laden + void reloadRoomList(); private: struct Message @@ -67,6 +81,14 @@ namespace Yc std::string color; }; + struct DiceResult + { + std::string userName; + int diceValue; + std::chrono::steady_clock::time_point rollTime; + bool valid; + }; + std::shared_ptr _parent; std::string _name; std::string _password; @@ -83,11 +105,30 @@ namespace Yc int _roundLength; std::vector, int>> _diceValues; Yc::Object::Room _room; + + // Neue Würfel-Spiel-Variablen + bool _diceGameRunning; + int _currentRound; + int _totalRounds; + std::chrono::steady_clock::time_point _roundStartTime; + std::chrono::steady_clock::time_point _gameStartTime; + std::map> _gameResults; // userName -> Runden-Ergebnisse + std::map _hasRolledThisRound; // userName -> hat in dieser Runde gewürfelt + std::thread _roundTimerThread; + void _handleDice(); void _startDiceRound(); void _endDiceRound(); void _initRound(); void _showDiceRoundResults(); + + // Neue Würfel-Funktionen + void _startRoundTimer(); + void _endRoundTimer(); + void _processRoundResults(); + void _showGameResults(); + void _resetRound(); + bool _isUserAdmin(std::shared_ptr user) const; }; } // namespace Lib diff --git a/src/core/chat_user.cpp b/src/core/chat_user.cpp index fe9031d..9468177 100644 --- a/src/core/chat_user.cpp +++ b/src/core/chat_user.cpp @@ -3,6 +3,7 @@ #include "chat_user.h" #include "server.h" #include "lib/tools.h" +#include "lib/base.h" #include #include #include @@ -12,12 +13,22 @@ #include #include #include +#include namespace Yc { namespace Lib { + static bool _isValidHexColor(const std::string &c) { + if (c.size() != 7 || c[0] != '#') return false; + for (size_t i = 1; i < c.size(); ++i) { + char ch = c[i]; + if (!std::isxdigit(static_cast(ch))) return false; + } + return true; + } + ChatUser::ChatUser(std::shared_ptr parent, std::string name, std::string color, int socket) : _parent(std::move(parent)), _name(name), @@ -79,14 +90,20 @@ namespace Yc userJson["rights"] = rights; } _user = Yc::Object::User(userJson); + // Prefer DB color if available + if (_user.id() != 0 && !_user.color().empty()) { + _color = _user.color(); + } _token = Yc::Lib::Tools::generateRandomString(32); - sendMsg(token, _token, "", ""); - thread = std::make_unique(&ChatUser::checkerTask, this); + // Beim Initial-Token direkt Name und aktuelle Farbe mitsenden, damit der Client "ich" korrekt färben kann + sendMsg(token, _token, _name, _color); + // Thread-Start erfolgt jetzt explizit per start(), nicht im Konstruktor } ChatUser::~ChatUser() { - _parent->addMessage(ChatUser::system, std::string("leaved_chat"), std::string(_name), std::string(_color)); + // Hinweis: Thread wird nicht im Destruktor gejoint, um Deadlocks zu vermeiden! + // Der Thread muss explizit von außen gestoppt und gejoint werden (z.B. im ChatRoom beim Entfernen des Users). } std::string ChatUser::name() const @@ -111,6 +128,9 @@ namespace Yc void ChatUser::sendMsg(MsgType type, std::string message, std::string userName, std::string color) { + // Standardwerte für leere Felder setzen + if (userName.empty()) userName = _name; + if (color.empty()) color = _color; Json::Value sendMessage; sendMessage["type"] = type; sendMessage["message"] = message; @@ -121,6 +141,9 @@ namespace Yc void ChatUser::sendMsg(MsgType type, Json::Value message, std::string userName, std::string color) { + // Standardwerte für leere Felder setzen + if (userName.empty()) userName = _name; + if (color.empty()) color = _color; Json::Value sendMessage; sendMessage["type"] = type; sendMessage["message"] = message; @@ -131,28 +154,44 @@ namespace Yc void ChatUser::checkerTask() { - while (!_stop) - { - fd_set readSd; - FD_ZERO(&readSd); - FD_SET(_socket, &readSd); - timeval tv; - tv.tv_sec = 0; - tv.tv_usec = 500; - int selectResult = select(_socket + 1, &readSd, NULL, NULL, &tv); - if (selectResult == 1 && FD_ISSET(_socket, &readSd) == 1) + try { + while (!_stop) { - _parent->removeUser(_token); - _stop = true; - delete this; - return; + fd_set readSd; + FD_ZERO(&readSd); + FD_SET(_socket, &readSd); + timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 500; + int selectResult = select(_socket + 1, &readSd, NULL, NULL, &tv); + if (selectResult == 1 && FD_ISSET(_socket, &readSd) == 1) + { + char peek; + ssize_t r = recv(_socket, &peek, 1, MSG_PEEK); + if (r == 0) { + #ifdef YC_DEBUG + std::cout << "[Debug] Verbindung zum Client abgebrochen (Token: )" << std::endl; + #endif + _parent->removeUser(_token); + _stop = true; + if (thread.joinable() && std::this_thread::get_id() == thread.get_id()) { + thread.detach(); + } + return; + } + } + std::string msg = readSocket(_socket); + if (msg == "") + { + // Kein Inhalt gelesen; kann Idle/Keepalive sein + continue; + } + handleMessage(msg); } - std::string msg = readSocket(_socket); - if (msg == "") - { - continue; - } - handleMessage(msg); + } catch (const std::exception& ex) { + // swallow, optional hook + } catch (...) { + // swallow, optional hook } } @@ -171,19 +210,29 @@ namespace Yc _parent = std::move(parent); } - void ChatUser::send(std::string out) - { + void ChatUser::send(std::string out) { + // Entferne ggf. Token-Felder aus JSON-Strings und sende über Socket/WebSocket + Base::sanitizeTokensInString(out); Base::send(_socket, out); } - void ChatUser::send(Json::Value out) - { + void ChatUser::send(Json::Value out) { + // Entferne rekursiv alle Token-Felder und sende über Socket/WebSocket + Base::sanitizeTokens(out); Base::send(_socket, out); } void ChatUser::handleMessage(std::string message) { Json::Value jsonTree = getJsonTree(message); + // Eingehende Nachricht (nur im Debug-Mode) + #ifdef YC_DEBUG + { + Json::Value toLog = jsonTree; + if (toLog.isMember("token")) toLog["token"] = ""; + std::cout << "[Debug] <- Client(fd=" << _socket << ", user='" << _name << "'): " << getJsonString(toLog) << std::endl; + } + #endif if (jsonTree["token"].asString() != _token) { return; @@ -194,7 +243,38 @@ namespace Yc } else if (jsonTree["type"].asString() == "dice") { - doDice(); + // Neue Würfel-Behandlung für das Würfelspiel + if (jsonTree.isMember("value")) { + int diceValue = jsonTree["value"].asInt(); + if (!_parent->rollDice(shared_from_this(), diceValue)) { + sendMsg(ChatUser::error, "dice_roll_failed", "", ""); + } + } else { + // Fallback: Einfacher Würfelwurf + doDice(); + } + } + else if (jsonTree["type"].asString() == "start_dice_game") + { + // Würfelspiel starten (nur Admin) + if (jsonTree.isMember("rounds")) { + int rounds = jsonTree["rounds"].asInt(); + if (rounds < 1 || rounds > 10) { + sendMsg(ChatUser::error, "invalid_rounds", "", ""); + return; + } + + if (!_parent->startDiceGame(rounds, shared_from_this())) { + sendMsg(ChatUser::error, "dice_game_start_failed", "", ""); + } + } else { + sendMsg(ChatUser::error, "missing_rounds", "", ""); + } + } + else if (jsonTree["type"].asString() == "end_dice_game") + { + // Würfelspiel beenden (nur Admin) + _parent->endDiceGame(); } else if (jsonTree["type"].asString() == "scream") { @@ -202,7 +282,68 @@ namespace Yc } else if (jsonTree["type"].asString() == "do") { - _parent->addMessage(ChatUser::dosomething, jsonTree["message"].asString(), std::string(_name), std::string(_color)); + // "do" erwartet "value" (Aktion) und optional "to" (Ziel-User) + std::string action = jsonTree.isMember("value") ? jsonTree["value"].asString() : ""; + std::string targetUser = jsonTree.isMember("to") ? jsonTree["to"].asString() : ""; + + if (action.empty()) { + sendMsg(ChatUser::error, "missing_action", "", ""); + return; + } + + // Erstelle strukturierte Nachricht mit Ziel-User und Aktion + Json::Value doMsg = Json::objectValue; + doMsg["tr"] = "user_action"; + doMsg["action"] = action; + if (!targetUser.empty()) { + doMsg["to"] = targetUser; + + // Suche den Ziel-User und füge dessen Informationen hinzu + auto targetUserObj = _parent->findUserByName(targetUser); + if (targetUserObj) { + doMsg["targetName"] = targetUserObj->name(); + doMsg["targetColor"] = targetUserObj->color(); + } + } + + // Debug-Ausgabe für "do"-Nachrichten + #ifdef YC_DEBUG + std::cout << "[Debug] Sending do message: type=" << ChatUser::dosomething << ", action=" << action << ", to=" << (targetUser.empty() ? "all" : targetUser) << std::endl; + #endif + + _parent->addMessage(ChatUser::dosomething, doMsg, std::string(_name), std::string(_color)); + } + else if (jsonTree["type"].asString() == "color") + { + std::string newColor = jsonTree.isMember("value") ? jsonTree["value"].asString() : ""; + std::string oldColor = _color; + if (!_isValidHexColor(newColor)) { + sendMsg(ChatUser::error, "invalid_color", "", ""); + return; + } + _color = newColor; + _user.set_color(newColor); + // Persistieren, falls DB-ID vorhanden + try { + if (_user.id() != 0) { + auto server = _parent->getServer(); + auto db = server->_database; + std::string query = "UPDATE chat.\"user\" SET color = '" + newColor + "', updated_at = NOW() WHERE id = " + std::to_string(_user.id()) + ";"; + (void)db->exec(query); + } + } catch (...) { + // Ignoriere DB-Fehler still + } + // Bestätigung an User + sendMsg(ChatUser::system, "color_changed", "", newColor); + // Broadcast an andere: alte und neue Farbe mitsenden + { + Json::Value msg = Json::objectValue; + msg["tr"] = "user_color_changed"; + msg["from"] = oldColor; + msg["to"] = newColor; + _parent->addMessage(ChatUser::system, msg, _name, newColor); + } } else if (jsonTree["type"].asString() == "join") { @@ -230,10 +371,90 @@ namespace Yc changeRoom(room, password); } } + else if (message.substr(0, 7) == "/color ") + { + std::string newColor = message.substr(7); + std::string oldColor = _color; + if (!_isValidHexColor(newColor)) { + sendMsg(ChatUser::error, "invalid_color", "", ""); + return; + } + _color = newColor; + _user.set_color(newColor); + try { + if (_user.id() != 0) { + auto server = _parent->getServer(); + auto db = server->_database; + std::string query = "UPDATE chat.\"user\" SET color = '" + newColor + "', updated_at = NOW() WHERE id = " + std::to_string(_user.id()) + ";"; + (void)db->exec(query); + } + } catch (...) { + // ignore + } + sendMsg(ChatUser::system, "color_changed", "", newColor); + { + Json::Value msg = Json::objectValue; + msg["tr"] = "user_color_changed"; + msg["from"] = oldColor; + msg["to"] = newColor; + _parent->addMessage(ChatUser::system, msg, _name, newColor); + } + } else if (message == "/dice") { doDice(); } + else if (message.substr(0, 15) == "/start_dice_game") + { + // Format: /start_dice_game + if (message.length() > 16) { + std::string roundsStr = message.substr(16); + try { + int rounds = std::stoi(roundsStr); + if (rounds >= 1 && rounds <= 10) { + if (!_parent->startDiceGame(rounds, shared_from_this())) { + sendMsg(ChatUser::error, "dice_game_start_failed", "", ""); + } + } else { + sendMsg(ChatUser::error, "invalid_rounds", "", ""); + } + } catch (...) { + sendMsg(ChatUser::error, "invalid_rounds", "", ""); + } + } else { + sendMsg(ChatUser::error, "missing_rounds", "", ""); + } + } + else if (message == "/end_dice_game") + { + _parent->endDiceGame(); + } + else if (message == "/reload_rooms") + { + // Raumliste neu laden + _parent->reloadRoomList(); + } + else if (message.substr(0, 5) == "/roll ") + { + // Format: /roll + if (message.length() > 6) { + std::string diceStr = message.substr(6); + try { + int diceValue = std::stoi(diceStr); + if (diceValue >= 1 && diceValue <= 6) { + if (!_parent->rollDice(shared_from_this(), diceValue)) { + sendMsg(ChatUser::error, "dice_roll_failed", "", ""); + } + } else { + sendMsg(ChatUser::error, "invalid_dice_value", "", ""); + } + } catch (...) { + sendMsg(ChatUser::error, "invalid_dice_value", "", ""); + } + } else { + sendMsg(ChatUser::error, "missing_dice_value", "", ""); + } + } else { _parent->addMessage(ChatUser::message, std::string(message), std::string(_name), std::string(_color)); @@ -271,5 +492,10 @@ namespace Yc } } + void ChatUser::start() { + auto self = shared_from_this(); + thread = std::thread([self]() { self->checkerTask(); }); + } + } // namespace Lib } // namespace Yc diff --git a/src/core/chat_user.h b/src/core/chat_user.h index fe25539..8275bac 100644 --- a/src/core/chat_user.h +++ b/src/core/chat_user.h @@ -42,6 +42,7 @@ namespace Yc void sendMsg(MsgType type, const char *message, std::string userName, std::string color); void sendMsg(MsgType type, Json::Value message, std::string userName, std::string color); void checkerTask(); + void start(); void stop(); std::string color() const; void setParent(std::shared_ptr parent); @@ -54,7 +55,7 @@ namespace Yc int _socket; std::string _token; bool _stop; - std::unique_ptr thread; + std::thread thread; void send(std::string out); void send(Json::Value out); diff --git a/src/core/config.cpp b/src/core/config.cpp index f7c65c7..39c5c74 100755 --- a/src/core/config.cpp +++ b/src/core/config.cpp @@ -12,8 +12,29 @@ namespace Yc { } void Config::loadConfig() { - std::ifstream configStream("/etc/yourpart/chatconfig.json", std::ifstream::binary); - configStream >> jsonConfig; + // Try system-wide path first + { + std::ifstream configStream("/etc/yourpart/chatconfig.json", std::ifstream::binary); + if (configStream.good()) { + configStream >> jsonConfig; + } + } + // Fallback to relative project config path + if (jsonConfig.isNull() || jsonConfig.empty()) { + std::ifstream localStream("config/chatconfig.json", std::ifstream::binary); + if (localStream.good()) { + localStream >> jsonConfig; + } + } + // Graceful defaults if still empty + if (jsonConfig.isNull() || jsonConfig.empty()) { + Json::Value def; + def["server"]["port"] = 1235; + Json::Value rooms(Json::arrayValue); + Json::Value r1; r1["name"] = "Halle"; r1["password"] = ""; r1["allowed"] = Json::arrayValue; r1["type"] = 0; r1["roundlength"] = 0; rooms.append(r1); + def["rooms"] = rooms; + jsonConfig = def; + } } Json::Value Config::value(std::string groupName, std::string field) { diff --git a/src/core/server.cpp b/src/core/server.cpp index 2bb7166..510247f 100755 --- a/src/core/server.cpp +++ b/src/core/server.cpp @@ -10,7 +10,9 @@ #include #include #include +#include #include +#include namespace Yc { namespace Lib { @@ -19,18 +21,45 @@ namespace Yc { _config(std::move(config)), _database(std::move(database)), _stop(false) { - struct sockaddr_in serverAddr; - int opt = true; - _socket = socket(AF_INET, SOCK_STREAM, 0); - setsockopt(_socket, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(opt)); - int flags = 1; - setsockopt(_socket, IPPROTO_TCP, TCP_NODELAY, (void *)&flags, sizeof(flags)); - serverAddr.sin_family = AF_INET; - serverAddr.sin_addr.s_addr = INADDR_ANY; - serverAddr.sin_port = htons(1235); - if (bind(_socket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0) { - std::cout << "bind not possible" << std::endl; - exit(-1); + int port = 1235; + try { + Json::Value p = _config->value("server", "port"); + if (p.isInt()) port = p.asInt(); + } catch (...) {} + int opt = 1; + // Try IPv6 dual-stack (accepts IPv4 via v4-mapped if v6only=0) + _socket = socket(AF_INET6, SOCK_STREAM, 0); + if (_socket >= 0) { + setsockopt(_socket, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(opt)); + int v6only = 0; setsockopt(_socket, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, sizeof(v6only)); + int flags = 1; setsockopt(_socket, IPPROTO_TCP, TCP_NODELAY, (void *)&flags, sizeof(flags)); + int ka = 1; setsockopt(_socket, SOL_SOCKET, SO_KEEPALIVE, &ka, sizeof(ka)); + struct sockaddr_in6 addr6{}; addr6.sin6_family = AF_INET6; addr6.sin6_addr = in6addr_any; addr6.sin6_port = htons(port); + if (bind(_socket, (struct sockaddr *)&addr6, sizeof(addr6)) == 0) { + std::cout << "[YourChat] Server gestartet. Lausche auf Port " << port << " (IPv6 dual-stack)" << std::endl; + } else { + // Fallback to IPv4 + close(_socket); + _socket = socket(AF_INET, SOCK_STREAM, 0); + setsockopt(_socket, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(opt)); + flags = 1; setsockopt(_socket, IPPROTO_TCP, TCP_NODELAY, (void *)&flags, sizeof(flags)); + ka = 1; setsockopt(_socket, SOL_SOCKET, SO_KEEPALIVE, &ka, sizeof(ka)); + struct sockaddr_in addr4{}; addr4.sin_family = AF_INET; addr4.sin_addr.s_addr = INADDR_ANY; addr4.sin_port = htons(port); + if (bind(_socket, (struct sockaddr *)&addr4, sizeof(addr4)) != 0) { + std::cout << "bind not possible" << std::endl; exit(-1); + } + std::cout << "[YourChat] Server gestartet. Lausche auf Port " << port << " (IPv4)" << std::endl; + } + } else { + // Fallback to IPv4 directly + _socket = socket(AF_INET, SOCK_STREAM, 0); + if (_socket < 0) { std::cout << "socket create failed" << std::endl; exit(-1);} + setsockopt(_socket, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(opt)); + int flags = 1; setsockopt(_socket, IPPROTO_TCP, TCP_NODELAY, (void *)&flags, sizeof(flags)); + int ka = 1; setsockopt(_socket, SOL_SOCKET, SO_KEEPALIVE, &ka, sizeof(ka)); + struct sockaddr_in addr4{}; addr4.sin_family = AF_INET; addr4.sin_addr.s_addr = INADDR_ANY; addr4.sin_port = htons(port); + if (bind(_socket, (struct sockaddr *)&addr4, sizeof(addr4)) != 0) { std::cout << "bind not possible" << std::endl; exit(-1);} + std::cout << "[YourChat] Server gestartet. Lausche auf Port " << port << " (IPv4)" << std::endl; } } @@ -42,14 +71,58 @@ namespace Yc { timeval origTv; origTv.tv_sec = 5; origTv.tv_usec = 0; - int _maxSd = _socket; + int _maxSd = _socket; + std::set activeSockets; + std::mutex socketMutex; + while (!_stop) { timeval tv(origTv); fd_set fd; FD_ZERO(&fd); FD_SET(_socket, &fd); + + // Alle aktiven Client-Sockets hinzufügen + { + std::lock_guard lock(socketMutex); + for (int clientSock : activeSockets) { + FD_SET(clientSock, &fd); + if (clientSock > _maxSd) _maxSd = clientSock; + } + } + if (select(_maxSd + 1, &fd, NULL, NULL, &tv) > 0) { - std::thread(&Server::handleRequest, this).detach(); + // Neue Verbindung? + if (FD_ISSET(_socket, &fd)) { + std::thread(&Server::handleRequest, this).detach(); + } + + // Client-Socket-Aktivität? + { + std::lock_guard lock(socketMutex); + auto it = activeSockets.begin(); + while (it != activeSockets.end()) { + if (FD_ISSET(*it, &fd)) { + // Socket ist aktiv, aber handleRequest läuft bereits in separatem Thread + // Hier könnten wir Heartbeat/Keepalive prüfen + ++it; + } else { + ++it; + } + } + } + } + + // Aufgeräumte Sockets entfernen + { + std::lock_guard lock(socketMutex); + auto it = activeSockets.begin(); + while (it != activeSockets.end()) { + if (activeSockets.count(*it) == 0) { + it = activeSockets.erase(it); + } else { + ++it; + } + } } } } @@ -73,16 +146,16 @@ namespace Yc { return list; } - bool Server::roomAllowed(std::string roomName, std::string userName, std::string password){ + bool Server::roomAllowed(std::string roomName, std::string userName, std::string password){ for (auto &room: _rooms) { - if (room->name() == roomName && room->accessAllowed(userName, password)) { + if (room->name() == roomName && room->accessAllowed(userName, password)) { return true; } } return false; } - bool Server::changeRoom(std::shared_ptr user, std::string newRoom, std::string password) { + bool Server::changeRoom(std::shared_ptr user, std::string newRoom, std::string password) { if (!roomAllowed(newRoom, user->name(), password)) { return false; } @@ -113,56 +186,247 @@ namespace Yc { } void Server::createRooms(Json::Value roomList) { - // Ignoriere roomList, lade stattdessen aus der Datenbank auto self = shared_from_this(); - std::string query = R"( - SELECT r.id, r.title, r.password_hash, r.room_type_id, r.is_public, r.owner_id, r.min_age, r.max_age, r.created_at, r.updated_at, rt.tr as room_type - FROM chat.room r - LEFT JOIN chat.room_type rt ON r.room_type_id = rt.id - )"; - auto result = _database->exec(query); - for (const auto& row : result) { - Json::Value room; - room["id"] = row["id"].as(); - room["name"] = row["title"].c_str(); - room["password"] = row["password_hash"].is_null() ? "" : row["password_hash"].c_str(); - room["type"] = row["room_type_id"].is_null() ? 0 : row["room_type_id"].as(); - room["is_public"] = row["is_public"].as(); - room["owner_id"] = row["owner_id"].is_null() ? 0 : row["owner_id"].as(); - room["min_age"] = row["min_age"].is_null() ? 0 : row["min_age"].as(); - room["max_age"] = row["max_age"].is_null() ? 0 : row["max_age"].as(); - room["created_at"] = row["created_at"].c_str(); - room["updated_at"] = row["updated_at"].c_str(); - room["room_type"] = row["room_type"].is_null() ? "" : row["room_type"].c_str(); - // Platzhalter für Felder, die im Konstruktor benötigt werden - room["allowed"] = Json::arrayValue; // ggf. später befüllen - room["roundlength"] = 60; // Default-Wert - auto newRoom = std::make_shared(self, room); - _rooms.push_back(newRoom); + bool created = false; + try { + std::string query = R"( + SELECT r.id, r.title, r.password_hash, r.room_type_id, r.is_public, r.owner_id, r.min_age, r.max_age, r.created_at, r.updated_at, rt.tr as room_type + FROM chat.room r + LEFT JOIN chat.room_type rt ON r.room_type_id = rt.id + )"; + auto result = _database->exec(query); + for (const auto& row : result) { + Json::Value room; + room["id"] = row["id"].as(); + room["name"] = row["title"].c_str(); + room["password"] = row["password_hash"].is_null() ? "" : row["password_hash"].c_str(); + room["type"] = row["room_type_id"].is_null() ? 0 : row["room_type_id"].as(); + room["is_public"] = row["is_public"].as(); + room["owner_id"] = row["owner_id"].is_null() ? 0 : row["owner_id"].as(); + room["min_age"] = row["min_age"].is_null() ? 0 : row["min_age"].as(); + room["max_age"] = row["max_age"].is_null() ? 0 : row["max_age"].as(); + room["created_at"] = row["created_at"].c_str(); + room["updated_at"] = row["updated_at"].c_str(); + room["room_type"] = row["room_type"].is_null() ? "" : row["room_type"].c_str(); + // Platzhalter für Felder, die im Konstruktor benötigt werden + room["allowed"] = Json::arrayValue; // ggf. später befüllen + room["roundlength"] = 60; // Default-Wert + auto newRoom = std::make_shared(self, room); + _rooms.push_back(newRoom); + created = true; + } + } catch (...) { + // ignore DB errors, fallback below + } + if (!created) { + // fallback to provided JSON room list (if any) + if (roomList.isArray() && roomList.size() > 0) { + for (const auto& room : roomList) { + auto newRoom = std::make_shared(self, room); + _rooms.push_back(newRoom); + created = true; + } + } else { + // final fallback: builtin default room + Json::Value room; + room["name"] = "Halle"; + room["password"] = ""; + room["allowed"] = Json::arrayValue; + room["type"] = 0; + room["roundlength"] = 0; + auto newRoom = std::make_shared(self, room); + _rooms.push_back(newRoom); + created = true; + } } } - void Server::handleRequest() { + void Server::handleRequest() { struct sockaddr_in sockAddr; socklen_t sockAddrLen = sizeof(sockAddr); int userSock = accept(_socket, (struct sockaddr *)&sockAddr, &sockAddrLen); if (userSock < 0) { return; } + + // Neuen Socket zur Überwachung hinzufügen + { + std::lock_guard lock(socketMutex); + activeSockets.insert(userSock); + } + // Log jede akzeptierte Verbindung + char clientIP[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &(sockAddr.sin_addr), clientIP, INET_ADDRSTRLEN); + std::cout << "[YourChat] Verbindung akzeptiert von " << clientIP << ":" << ntohs(sockAddr.sin_port) << " (fd=" << userSock << ")" << std::endl; int flags = 1; setsockopt(userSock, IPPROTO_TCP, TCP_NODELAY, (void *)&flags, sizeof(flags)); + int ka2 = 1; setsockopt(userSock, SOL_SOCKET, SO_KEEPALIVE, &ka2, sizeof(ka2)); + // Begrenze Blockierzeit beim Senden, um langsame Clients nicht alle zu verzögern + timeval sendTimeout; sendTimeout.tv_sec = 0; sendTimeout.tv_usec = 500000; // 500ms + setsockopt(userSock, SOL_SOCKET, SO_SNDTIMEO, &sendTimeout, sizeof(sendTimeout)); std::string msg = readSocket(userSock); +#ifdef YC_DEBUG + std::cout << "[Debug] Neue Anfrage erhalten: " << msg << std::endl; +#endif if (msg == "") { return; } - inputSwitcher(userSock, msg); + // OPTIONS Request (CORS Preflight) + if (msg.rfind("OPTIONS ", 0) == 0) { + std::ostringstream resp; + resp << "HTTP/1.1 200 OK\r\n" + << "Access-Control-Allow-Origin: *\r\n" + << "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n" + << "Access-Control-Allow-Headers: Upgrade, Connection, Sec-WebSocket-Key, Sec-WebSocket-Version, Sec-WebSocket-Protocol, Origin\r\n" + << "Access-Control-Allow-Credentials: true\r\n" + << "Access-Control-Max-Age: 86400\r\n" + << "Content-Length: 0\r\n" + << "\r\n"; + ::send(userSock, resp.str().c_str(), resp.str().size(), 0); + close(userSock); + return; + } + + // WebSocket Upgrade? + if (msg.rfind("GET ", 0) == 0 && msg.find("Upgrade: websocket") != std::string::npos) { + // sehr einfacher Header-Parser + std::string key; + std::string subprotocol; + std::string origin; + std::string version; + std::string extensions; + std::istringstream iss(msg); + std::string line; + + #ifdef YC_DEBUG + std::cout << "[Debug] === WebSocket Headers ===" << std::endl; + #endif + + while (std::getline(iss, line)) { + if (!line.empty() && (line.back() == '\r' || line.back() == '\n')) line.pop_back(); + auto pos = line.find(":"); + if (pos != std::string::npos) { + std::string h = line.substr(0, pos); + std::string v = line.substr(pos+1); + // trim + auto ltrim = [](std::string &s){ s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch){return !std::isspace(ch);}));}; + auto rtrim = [](std::string &s){ s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch){return !std::isspace(ch);}).base(), s.end());}; + ltrim(h); rtrim(h); ltrim(v); rtrim(v); + + #ifdef YC_DEBUG + std::cout << "[Debug] Header: '" << h << "' = '" << v << "'" << std::endl; + #endif + + if (strcasecmp(h.c_str(), "Sec-WebSocket-Key") == 0) { + key = v; + } else if (strcasecmp(h.c_str(), "Sec-WebSocket-Protocol") == 0) { + subprotocol = v; + } else if (strcasecmp(h.c_str(), "Origin") == 0) { + origin = v; + } else if (strcasecmp(h.c_str(), "Sec-WebSocket-Version") == 0) { + version = v; + } else if (strcasecmp(h.c_str(), "Sec-WebSocket-Extensions") == 0) { + extensions = v; + } + } + if (line.empty()) break; + } + + #ifdef YC_DEBUG + std::cout << "[Debug] === Parsed Values ===" << std::endl; + std::cout << "[Debug] Key: " << (key.empty() ? "MISSING" : key) << std::endl; + std::cout << "[Debug] Protocol: " << (subprotocol.empty() ? "NONE" : subprotocol) << std::endl; + std::cout << "[Debug] Origin: " << (origin.empty() ? "MISSING" : origin) << std::endl; + std::cout << "[Debug] Version: " << (version.empty() ? "MISSING" : version) << std::endl; + std::cout << "[Debug] Extensions: " << (extensions.empty() ? "NONE" : extensions) << std::endl; + std::cout << "[Debug] ======================" << std::endl; + #endif + if (!key.empty()) { + std::string accept = Base::webSocketAcceptKey(key); + std::ostringstream resp; + resp << "HTTP/1.1 101 Switching Protocols\r\n" + << "Upgrade: websocket\r\n" + << "Connection: Upgrade\r\n" + << "Sec-WebSocket-Accept: " << accept << "\r\n"; + + // CORS-Header hinzufügen + resp << "Access-Control-Allow-Origin: *\r\n" + << "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n" + << "Access-Control-Allow-Headers: Upgrade, Connection, Sec-WebSocket-Key, Sec-WebSocket-Version, Sec-WebSocket-Protocol, Origin\r\n" + << "Access-Control-Allow-Credentials: true\r\n"; + + // Subprotokoll-Unterstützung + if (!subprotocol.empty()) { + resp << "Sec-WebSocket-Protocol: " << subprotocol << "\r\n"; + } else { + // Fallback: Standard-Subprotokoll anbieten + resp << "Sec-WebSocket-Protocol: chat\r\n"; + } + + resp << "\r\n"; + + #ifdef YC_DEBUG + std::cout << "[Debug] === WebSocket Response ===" << std::endl; + std::cout << "[Debug] " << resp.str() << std::endl; + std::cout << "[Debug] =========================" << std::endl; + #endif + + ::send(userSock, resp.str().c_str(), resp.str().size(), 0); + Base::markWebSocket(userSock); + + // Sofort eine Willkommen-Nachricht senden, um Firefox's Timing-Problem zu lösen + // Verwende korrektes WebSocket-Framing + std::string welcomeMsg = "{\"type\":\"welcome\",\"message\":\"WebSocket-Verbindung hergestellt\"}"; + Base::sendWebSocketMessage(userSock, welcomeMsg); + + #ifdef YC_DEBUG + std::cout << "[Debug] WS upgrade ok, welcome message sent, waiting for init... fd=" << userSock << std::endl; + #endif + + // Jetzt WebSocket Nachrichten lesen und an inputSwitcher weitergeben + bool ownedByUser = false; + while (true) { + std::string wmsg = readSocket(userSock); + if (wmsg.empty()) break; + ownedByUser = inputSwitcher(userSock, wmsg); + if (ownedByUser) break; + } + if (ownedByUser) { +#ifdef YC_DEBUG + std::cout << "[Debug] Ownership transferred to ChatUser, stop reading in Server for fd=" << userSock << std::endl; +#endif + } + if (!ownedByUser) { +#ifdef YC_DEBUG + std::cout << "[Debug] WS closing (no ownership) fd=" << userSock << std::endl; +#endif + Base::unmarkWebSocket(userSock); + close(userSock); + } + return; + } + } + // Fallback: Plain JSON + bool owned = inputSwitcher(userSock, msg); + if (!owned) { +#ifdef YC_DEBUG + std::cout << "[Debug] Plain JSON path without ownership, closing fd=" << userSock << std::endl; +#endif + close(userSock); + } } - void Server::inputSwitcher(int userSocket, std::string input) { + bool Server::inputSwitcher(int userSocket, std::string input) { Json::Value inputTree = getJsonTree(input); +#ifdef YC_DEBUG + std::cout << "[Debug] inputSwitcher: type=" << inputTree["type"].asString() << std::endl; +#endif if (inputTree["type"] == "init") { initUser(userSocket, inputTree); + return true; // ChatUser übernimmt nun den Socket + } else { } + return false; } bool Server::userExists(std::string userName) { @@ -175,7 +439,23 @@ namespace Yc { } void Server::initUser(int userSocket, Json::Value data) { - if (userExists(data["name"].asString())) { + std::string name = data.isMember("name") ? data["name"].asString() : ""; + std::string room = data.isMember("room") ? data["room"].asString() : ""; + std::string color = data.isMember("color") ? data["color"].asString() : "#000000"; + std::string password = data.isMember("password") ? data["password"].asString() : ""; +#ifdef YC_DEBUG + std::cout << "[Debug] initUser: name=" << name << ", room=" << room << ", color=" << color << std::endl; +#endif + if (name.empty() || room.empty()) { + Json::Value errorJson; + errorJson["type"] = ChatUser::error; + errorJson["message"] = "missing_fields"; + errorJson["detail"] = "'name' und 'room' müssen gesetzt sein."; + send(userSocket, errorJson); + close(userSocket); + return; + } + if (userExists(name)) { Json::Value errorJson; errorJson["type"] = ChatUser::error; errorJson["message"] = "loggedin"; @@ -183,17 +463,20 @@ namespace Yc { close(userSocket); return; } - std::string roomName = data["room"].asString(); bool added(false); - for (auto &room: _rooms) { - if (room->name() == roomName) { - if (room->addUser(data["name"].asString(), data["color"].asString(), data["password"].asString(), userSocket)) { + for (auto &roomObj: _rooms) { + if (roomObj->name() == room) { + if (roomObj->addUser(name, color, password, userSocket)) { added = true; break; } } } if (!added) { + Json::Value errorJson; + errorJson["type"] = ChatUser::error; + errorJson["message"] = "room_not_found_or_join_failed"; + send(userSocket, errorJson); close(userSocket); } } diff --git a/src/core/server.h b/src/core/server.h index 81be4fe..940837e 100755 --- a/src/core/server.h +++ b/src/core/server.h @@ -28,9 +28,14 @@ namespace Yc { void createRooms(Json::Value roomList); private: void handleRequest(); - void inputSwitcher(int userSocket, std::string input); + // returns true if the connection is now owned by a ChatUser thread + bool inputSwitcher(int userSocket, std::string input); bool userExists(std::string userName); void initUser(int userSocket, Json::Value data); + + // Socket-Überwachung + std::set activeSockets; + std::mutex socketMutex; }; } // namespace Lib diff --git a/src/lib/base.cpp b/src/lib/base.cpp index 7b7d332..90b1733 100755 --- a/src/lib/base.cpp +++ b/src/lib/base.cpp @@ -6,41 +6,234 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include namespace Yc { namespace Lib { - std::string Base::getJsonString(Json::Value json) { - std::string outString; + static std::set g_wsSockets; + static std::mutex g_wsMutex; + + // Rekursive Entfernung aller "token"-Felder + static void stripTokensRecursiveImpl(Json::Value &v) { + if (v.isObject()) { + if (v.isMember("token")) { + v.removeMember("token"); + } + for (const auto &name : v.getMemberNames()) { + stripTokensRecursiveImpl(v[name]); + } + } else if (v.isArray()) { + for (Json::ArrayIndex i = 0; i < v.size(); ++i) { + stripTokensRecursiveImpl(v[i]); + } + } + } + + // Öffentliche Helper + void Base::sanitizeTokens(Json::Value& v) { + stripTokensRecursiveImpl(v); + } + + void Base::sanitizeTokensInString(std::string& s) { + if (s.find("\"token\"") == std::string::npos) return; + Json::Value v = getJsonTree(s); + if (!v.isNull()) { + sanitizeTokens(v); + s = getJsonString(v); + } + } + + std::string Base::getJsonString(const Json::Value& json) { std::stringstream outStream; outStream << json; return outStream.str(); } void Base::send(int socket, std::string out) { - write(socket, out.c_str(), out.length()); + // Token-Felder aus String-Payloads entfernen (falls JSON) + sanitizeTokensInString(out); + if (isWebSocket(socket)) { + sendWebSocketMessage(socket, out); + return; + } + ssize_t result = write(socket, out.c_str(), out.length()); + if (result < 0) { + std::cout << "[Error] Fehler beim Senden auf Socket (vermutlich Verbindung verloren): " << strerror(errno) << std::endl; + } } void Base::send(int socket, Json::Value out) { + // Entferne alle Token-Felder rekursiv aus Antworten + sanitizeTokens(out); std::string outString = getJsonString(out); send(socket, outString); } std::string Base::readSocket(int socket) { - std::string msg(""); - char buffer[256]; - bzero(buffer, 256); - while (int received = recv(socket, buffer, 255, 0) > 0) { - msg += std::string(buffer); - if (received < 255) { + if (isWebSocket(socket)) { + return readWebSocketMessage(socket); + } + std::string msg; + char buffer[1024]; + while (true) { + ssize_t received = recv(socket, buffer, sizeof(buffer), 0); + if (received > 0) { + msg.append(buffer, static_cast(received)); + if (received < static_cast(sizeof(buffer))) { + break; + } + } else if (received == 0) { + break; + } else { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + break; + } break; } } return msg; } - Json::Value Base::getJsonTree(std::string msg) { + static std::string base64Encode(const unsigned char* input, size_t len) { + BIO *bmem = BIO_new(BIO_s_mem()); + BIO *b64 = BIO_new(BIO_f_base64()); + BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); + b64 = BIO_push(b64, bmem); + BIO_write(b64, input, (int)len); + BIO_flush(b64); + BUF_MEM *bptr; + BIO_get_mem_ptr(b64, &bptr); + std::string out(bptr->data, bptr->length); + BIO_free_all(b64); + return out; + } + + static std::string wsAcceptKey(const std::string& clientKey) { + const std::string magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + std::string in = clientKey + magic; + unsigned char sha[SHA_DIGEST_LENGTH]; + SHA1(reinterpret_cast(in.data()), in.size(), sha); + return base64Encode(sha, SHA_DIGEST_LENGTH); + } + + std::string Base::webSocketAcceptKey(const std::string& clientKey) { + return wsAcceptKey(clientKey); + } + + void Base::markWebSocket(int socket) { + std::lock_guard lock(g_wsMutex); + g_wsSockets.insert(socket); + } + void Base::unmarkWebSocket(int socket) { + std::lock_guard lock(g_wsMutex); + g_wsSockets.erase(socket); + } + bool Base::isWebSocket(int socket) { + std::lock_guard lock(g_wsMutex); + return g_wsSockets.count(socket) > 0; + } + + std::string Base::readWebSocketMessage(int socket) { + std::string accumulated; + bool haveText = false; + unsigned char firstOpcode = 0; + while (true) { + unsigned char hdr[2]; + ssize_t r = recv(socket, hdr, 2, MSG_WAITALL); + if (r != 2) return std::string(); + bool fin = (hdr[0] & 0x80) != 0; + unsigned char opcode = hdr[0] & 0x0F; + bool masked = (hdr[1] & 0x80) != 0; + uint64_t len = hdr[1] & 0x7F; + if (len == 126) { + unsigned char ext[2]; + if (recv(socket, ext, 2, MSG_WAITALL) != 2) return {}; + len = (ext[0] << 8) | ext[1]; + } else if (len == 127) { + unsigned char ext[8]; + if (recv(socket, ext, 8, MSG_WAITALL) != 8) return {}; + len = 0; for (int i=0;i<8;++i) { len = (len<<8) | ext[i]; } + } + unsigned char mask[4] = {0,0,0,0}; + if (masked) { + if (recv(socket, mask, 4, MSG_WAITALL) != 4) return {}; + } + std::string payload; payload.resize((size_t)len); + if (len > 0) { + if (recv(socket, payload.data(), len, MSG_WAITALL) != (ssize_t)len) return {}; + if (masked) { + for (uint64_t i=0;i reply Pong with same payload + std::string frame; + unsigned char oh[2]; oh[0] = 0x80 | 0xA; // FIN + pong + size_t plen = payload.size(); + if (plen < 126) { oh[1] = (unsigned char)plen; frame.append((char*)oh, 2); } + else if (plen <= 0xFFFF) { oh[1] = 126; frame.append((char*)oh, 2); unsigned char ext[2] = {(unsigned char)((plen>>8)&0xFF),(unsigned char)(plen&0xFF)}; frame.append((char*)ext,2);} + else { oh[1] = 127; frame.append((char*)oh,2); unsigned char ext[8]; uint64_t l=plen; for(int i=7;i>=0;--i){ext[i]=(unsigned char)(l&0xFF); l>>=8;} frame.append((char*)ext,8);} + frame.append(payload); + ::send(socket, frame.data(), frame.size(), 0); + // continue reading next frame + continue; + } else if (opcode == 0xA) { + // Pong -> ignore + continue; + } else if (opcode == 0x1 || (opcode == 0x0 && haveText)) { + if (!haveText) { firstOpcode = opcode; haveText = true; } + accumulated += payload; + if (fin) { + return accumulated; + } else { + // read next continuation frame + continue; + } + } else { + // unsupported/binary -> ignore content, continue + if (fin) continue; + } + } + } + + void Base::sendWebSocketMessage(int socket, const std::string& out) { + std::string frame; + unsigned char hdr[2]; + hdr[0] = 0x80 | 0x1; // FIN + text + size_t len = out.size(); + if (len < 126) { + hdr[1] = (unsigned char)len; + frame.append(reinterpret_cast(hdr), 2); + } else if (len <= 0xFFFF) { + hdr[1] = 126; + frame.append(reinterpret_cast(hdr), 2); + unsigned char ext[2] = { (unsigned char)((len>>8)&0xFF), (unsigned char)(len & 0xFF) }; + frame.append(reinterpret_cast(ext), 2); + } else { + hdr[1] = 127; + frame.append(reinterpret_cast(hdr), 2); + unsigned char ext[8]; + uint64_t l = len; + for (int i=7;i>=0;--i) { ext[i] = (unsigned char)(l & 0xFF); l >>= 8; } + frame.append(reinterpret_cast(ext), 8); + } + frame.append(out); + ::send(socket, frame.data(), frame.size(), 0); + } + + Json::Value Base::getJsonTree(const std::string& msg) { Json::Value inputTree; Json::CharReaderBuilder rbuilder; std::unique_ptr const reader(rbuilder.newCharReader()); diff --git a/src/lib/base.h b/src/lib/base.h index 185b680..b509e1a 100755 --- a/src/lib/base.h +++ b/src/lib/base.h @@ -4,18 +4,32 @@ #include namespace Yc { - namespace Lib { +namespace Lib { - class Base { - protected: - std::string getJsonString(Json::Value json); - void send(int socket, std::string out); - void send(int socket, Json::Value out); - std::string readSocket(int socket); - Json::Value getJsonTree(std::string msg); - }; +class Base { +public: + static std::string getJsonString(const Json::Value& json); + static Json::Value getJsonTree(const std::string& msg); - } // namespace Lib + // Neue öffentliche Helper zum Entfernen aller "token"-Felder + static void sanitizeTokens(Json::Value& v); + static void sanitizeTokensInString(std::string& s); + +protected: + void send(int socket, std::string out); + void send(int socket, Json::Value out); + std::string readSocket(int socket); + // WebSocket helpers (server-side): read one text frame and send a text frame + std::string readWebSocketMessage(int socket); + void sendWebSocketMessage(int socket, const std::string& out); + // WebSocket connection tracking and handshake helpers + static void markWebSocket(int socket); + static void unmarkWebSocket(int socket); + static bool isWebSocket(int socket); + static std::string webSocketAcceptKey(const std::string& clientKey); +}; + +} // namespace Lib } // namespace Yc #endif // YC_LIB_BASE_H diff --git a/src/object/user.cpp b/src/object/user.cpp index f1d2e0a..77e4c47 100644 --- a/src/object/user.cpp +++ b/src/object/user.cpp @@ -33,5 +33,7 @@ namespace Yc const std::string& User::updated_at() const { return _updated_at; } const std::vector& User::rights() const { return _rights; } + void User::set_color(const std::string& c) { _color = c; } + } // namespace Object } // namespace Yc diff --git a/src/object/user.h b/src/object/user.h index 06cb972..d49ee57 100644 --- a/src/object/user.h +++ b/src/object/user.h @@ -23,6 +23,9 @@ namespace Yc const std::string& updated_at() const; const std::vector& rights() const; + // Mutators + void set_color(const std::string& c); + private: int _id = 0; int _falukant_user_id = 0; diff --git a/tools/ws_probe.cpp b/tools/ws_probe.cpp new file mode 100644 index 0000000..a445840 --- /dev/null +++ b/tools/ws_probe.cpp @@ -0,0 +1,157 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static std::string base64(const unsigned char* data, size_t len) { + static const char* tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::string out; + out.reserve(((len + 2) / 3) * 4); + size_t i = 0; + while (i + 2 < len) { + unsigned int n = (data[i] << 16) | (data[i+1] << 8) | data[i+2]; + out.push_back(tbl[(n >> 18) & 63]); + out.push_back(tbl[(n >> 12) & 63]); + out.push_back(tbl[(n >> 6) & 63]); + out.push_back(tbl[n & 63]); + i += 3; + } + if (i < len) { + unsigned int n = (data[i] << 16); + if (i + 1 < len) n |= (data[i+1] << 8); + out.push_back(tbl[(n >> 18) & 63]); + out.push_back(tbl[(n >> 12) & 63]); + if (i + 1 < len) { + out.push_back(tbl[(n >> 6) & 63]); + out.push_back('='); + } else { + out.push_back('='); + out.push_back('='); + } + } + return out; +} + +static int connect_tcp(const std::string& host, int port) { + int sock = ::socket(AF_INET, SOCK_STREAM, 0); + if (sock < 0) return -1; + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + if (::inet_pton(AF_INET, host.c_str(), &addr.sin_addr) != 1) { + ::close(sock); + return -1; + } + if (::connect(sock, (sockaddr*)&addr, sizeof(addr)) != 0) { + ::close(sock); + return -1; + } + return sock; +} + +static bool send_all(int sock, const void* buf, size_t len) { + const char* p = (const char*)buf; + while (len) { + ssize_t n = ::send(sock, p, len, 0); + if (n <= 0) return false; + p += n; len -= (size_t)n; + } + return true; +} + +static std::string recv_headers(int sock) { + std::string h; + char c; + while (true) { + ssize_t n = ::recv(sock, &c, 1, 0); + if (n <= 0) break; + h.push_back(c); + if (h.size() >= 4 && h.substr(h.size()-4) == "\r\n\r\n") break; + } + return h; +} + +static bool send_ws_text(int sock, const std::string& payload) { + std::vector frame; + frame.reserve(2 + 4 + payload.size()); + unsigned char b0 = 0x80 | 0x1; // FIN + text + frame.push_back(b0); + size_t len = payload.size(); + if (len < 126) { + frame.push_back(0x80 | (unsigned char)len); // mask bit set + } else if (len <= 0xFFFF) { + frame.push_back(0x80 | 126); + frame.push_back((len >> 8) & 0xFF); + frame.push_back(len & 0xFF); + } else { + frame.push_back(0x80 | 127); + for (int i=7;i>=0;--i) frame.push_back((len >> (i*8)) & 0xFF); + } + unsigned char mask[4]; + std::random_device rd; std::mt19937 rng(rd()); std::uniform_int_distribution dist(0,255); + for (int i=0;i<4;++i) mask[i] = (unsigned char)dist(rng); + frame.insert(frame.end(), mask, mask+4); + for (size_t i=0;i 0) { + if (::recv(sock, payload.data(), len, MSG_WAITALL) != (ssize_t)len) return {}; + if (masked) { + for (size_t i=0;i