Füge Unterstützung für Würfelspiele hinzu und verbessere Debugging-Optionen
- Implementiere neue Funktionen in der ChatRoom-Klasse für das Starten, Rollen und Beenden von Würfelspielen. - Füge eine Option zur Aktivierung von Debug-Logging in CMake hinzu, um die Entwicklung zu erleichtern. - Aktualisiere die ChatUser-Klasse, um die Interaktion mit dem Würfelspiel zu ermöglichen. - Verbessere die Socket-Verwaltung im Server, um WebSocket-Verbindungen zu unterstützen und die Handhabung von Anfragen zu optimieren. - Aktualisiere die Konfiguration, um die neue Funktionalität zu unterstützen und die Benutzererfahrung zu verbessern.
This commit is contained in:
91
.vscode/tasks.json
vendored
91
.vscode/tasks.json
vendored
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
111
README.md
Normal file
111
README.md
Normal file
@@ -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.
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include <thread>
|
||||
#include <unordered_set>
|
||||
|
||||
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<std::string> 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<ChatUser>(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<ChatUser> 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<ChatUser> 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<ChatUser> 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<DiceResult>();
|
||||
_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<ChatUser> 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<ChatUser> 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
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include <future>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <chrono>
|
||||
|
||||
#include "lib/base.h"
|
||||
#include "chat_user.h"
|
||||
@@ -53,11 +55,23 @@ namespace Yc
|
||||
unsigned int addDice(std::shared_ptr<ChatUser> user, int diceValue);
|
||||
bool accessAllowed(std::string userName, std::string password);
|
||||
bool userIsInRoom(std::string userName);
|
||||
std::shared_ptr<ChatUser> findUserByName(std::string userName);
|
||||
void addUserWhenQueueEmpty(std::shared_ptr<ChatUser> user);
|
||||
bool userToNewRoom(std::shared_ptr<ChatUser> user, std::string newRoom, std::string password);
|
||||
unsigned int flags();
|
||||
Json::Value userList();
|
||||
|
||||
// Neue Würfel-Funktionen
|
||||
bool startDiceGame(int rounds, std::shared_ptr<ChatUser> admin);
|
||||
bool rollDice(std::shared_ptr<ChatUser> 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<Server> _parent;
|
||||
std::string _name;
|
||||
std::string _password;
|
||||
@@ -83,11 +105,30 @@ namespace Yc
|
||||
int _roundLength;
|
||||
std::vector<std::pair<std::shared_ptr<ChatUser>, 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<std::string, std::vector<DiceResult>> _gameResults; // userName -> Runden-Ergebnisse
|
||||
std::map<std::string, bool> _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<ChatUser> user) const;
|
||||
};
|
||||
|
||||
} // namespace Lib
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "chat_user.h"
|
||||
#include "server.h"
|
||||
#include "lib/tools.h"
|
||||
#include "lib/base.h"
|
||||
#include <json/json.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
@@ -12,12 +13,22 @@
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <sstream>
|
||||
#include <cctype>
|
||||
|
||||
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<unsigned char>(ch))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ChatUser::ChatUser(std::shared_ptr<ChatRoom> 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<std::thread>(&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: <hidden>)" << 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"] = "<hidden>";
|
||||
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 <runden>
|
||||
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 <würfelwert>
|
||||
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
|
||||
|
||||
@@ -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<ChatRoom> parent);
|
||||
@@ -54,7 +55,7 @@ namespace Yc
|
||||
int _socket;
|
||||
std::string _token;
|
||||
bool _stop;
|
||||
std::unique_ptr<std::thread> thread;
|
||||
std::thread thread;
|
||||
|
||||
void send(std::string out);
|
||||
void send(Json::Value out);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
#include <json/json.h>
|
||||
#include <iostream>
|
||||
#include <netinet/tcp.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <sstream>
|
||||
#include <regex>
|
||||
|
||||
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<int> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<ChatUser> user, std::string newRoom, std::string password) {
|
||||
bool Server::changeRoom(std::shared_ptr<ChatUser> 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<int>();
|
||||
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<int>();
|
||||
room["is_public"] = row["is_public"].as<bool>();
|
||||
room["owner_id"] = row["owner_id"].is_null() ? 0 : row["owner_id"].as<int>();
|
||||
room["min_age"] = row["min_age"].is_null() ? 0 : row["min_age"].as<int>();
|
||||
room["max_age"] = row["max_age"].is_null() ? 0 : row["max_age"].as<int>();
|
||||
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<ChatRoom>(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<int>();
|
||||
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<int>();
|
||||
room["is_public"] = row["is_public"].as<bool>();
|
||||
room["owner_id"] = row["owner_id"].is_null() ? 0 : row["owner_id"].as<int>();
|
||||
room["min_age"] = row["min_age"].is_null() ? 0 : row["min_age"].as<int>();
|
||||
room["max_age"] = row["max_age"].is_null() ? 0 : row["max_age"].as<int>();
|
||||
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<ChatRoom>(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<ChatRoom>(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<ChatRoom>(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<std::mutex> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<int> activeSockets;
|
||||
std::mutex socketMutex;
|
||||
};
|
||||
|
||||
} // namespace Lib
|
||||
|
||||
213
src/lib/base.cpp
213
src/lib/base.cpp
@@ -6,41 +6,234 @@
|
||||
#include <sys/socket.h>
|
||||
#include <memory>
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
#include <errno.h>
|
||||
#include <openssl/sha.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/bio.h>
|
||||
#include <openssl/buffer.h>
|
||||
#include <set>
|
||||
#include <mutex>
|
||||
|
||||
namespace Yc {
|
||||
namespace Lib {
|
||||
|
||||
std::string Base::getJsonString(Json::Value json) {
|
||||
std::string outString;
|
||||
static std::set<int> 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<size_t>(received));
|
||||
if (received < static_cast<ssize_t>(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<const unsigned char*>(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<std::mutex> lock(g_wsMutex);
|
||||
g_wsSockets.insert(socket);
|
||||
}
|
||||
void Base::unmarkWebSocket(int socket) {
|
||||
std::lock_guard<std::mutex> lock(g_wsMutex);
|
||||
g_wsSockets.erase(socket);
|
||||
}
|
||||
bool Base::isWebSocket(int socket) {
|
||||
std::lock_guard<std::mutex> 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<len;++i) payload[i] ^= mask[i % 4];
|
||||
}
|
||||
}
|
||||
if (opcode == 0x8) {
|
||||
// Close frame
|
||||
return std::string();
|
||||
} else if (opcode == 0x9) {
|
||||
// Ping -> 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<char*>(hdr), 2);
|
||||
} else if (len <= 0xFFFF) {
|
||||
hdr[1] = 126;
|
||||
frame.append(reinterpret_cast<char*>(hdr), 2);
|
||||
unsigned char ext[2] = { (unsigned char)((len>>8)&0xFF), (unsigned char)(len & 0xFF) };
|
||||
frame.append(reinterpret_cast<char*>(ext), 2);
|
||||
} else {
|
||||
hdr[1] = 127;
|
||||
frame.append(reinterpret_cast<char*>(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<char*>(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<Json::CharReader> const reader(rbuilder.newCharReader());
|
||||
|
||||
@@ -4,18 +4,32 @@
|
||||
#include <json/json.h>
|
||||
|
||||
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
|
||||
|
||||
@@ -33,5 +33,7 @@ namespace Yc
|
||||
const std::string& User::updated_at() const { return _updated_at; }
|
||||
const std::vector<std::string>& User::rights() const { return _rights; }
|
||||
|
||||
void User::set_color(const std::string& c) { _color = c; }
|
||||
|
||||
} // namespace Object
|
||||
} // namespace Yc
|
||||
|
||||
@@ -23,6 +23,9 @@ namespace Yc
|
||||
const std::string& updated_at() const;
|
||||
const std::vector<std::string>& rights() const;
|
||||
|
||||
// Mutators
|
||||
void set_color(const std::string& c);
|
||||
|
||||
private:
|
||||
int _id = 0;
|
||||
int _falukant_user_id = 0;
|
||||
|
||||
157
tools/ws_probe.cpp
Normal file
157
tools/ws_probe.cpp
Normal file
@@ -0,0 +1,157 @@
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<unsigned char> 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<int> 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<len;++i) frame.push_back((unsigned char)(payload[i] ^ mask[i%4]));
|
||||
return send_all(sock, frame.data(), frame.size());
|
||||
}
|
||||
|
||||
static std::string recv_ws_text(int sock) {
|
||||
unsigned char hdr[2];
|
||||
if (::recv(sock, hdr, 2, MSG_WAITALL) != 2) return {};
|
||||
bool fin = (hdr[0] & 0x80) != 0; (void)fin;
|
||||
unsigned char opcode = hdr[0] & 0x0F; (void)opcode;
|
||||
bool masked = (hdr[1] & 0x80) != 0;
|
||||
uint64_t len = hdr[1] & 0x7F;
|
||||
if (len == 126) {
|
||||
unsigned char ext[2];
|
||||
if (::recv(sock, ext, 2, MSG_WAITALL) != 2) return {};
|
||||
len = (ext[0] << 8) | ext[1];
|
||||
} else if (len == 127) {
|
||||
unsigned char ext[8];
|
||||
if (::recv(sock, 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(sock, mask, 4, MSG_WAITALL) != 4) return {};
|
||||
}
|
||||
std::string payload; payload.resize((size_t)len);
|
||||
if (len > 0) {
|
||||
if (::recv(sock, payload.data(), len, MSG_WAITALL) != (ssize_t)len) return {};
|
||||
if (masked) {
|
||||
for (size_t i=0;i<len;++i) payload[i] ^= mask[i%4];
|
||||
}
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
int main() {
|
||||
int sock = connect_tcp("127.0.0.1", 1235);
|
||||
if (sock < 0) {
|
||||
std::cerr << "connect failed" << std::endl; return 1;
|
||||
}
|
||||
unsigned char keyraw[16];
|
||||
std::random_device rd; for (int i=0;i<16;++i) keyraw[i] = (unsigned char)rd();
|
||||
std::string key = base64(keyraw, 16);
|
||||
std::string req;
|
||||
req += "GET / HTTP/1.1\r\n";
|
||||
req += "Host: localhost:1235\r\n";
|
||||
req += "Upgrade: websocket\r\n";
|
||||
req += "Connection: Upgrade\r\n";
|
||||
req += "Sec-WebSocket-Key: " + key + "\r\n";
|
||||
req += "Sec-WebSocket-Version: 13\r\n\r\n";
|
||||
if (!send_all(sock, req.c_str(), req.size())) { std::cerr << "send handshake failed" << std::endl; return 2; }
|
||||
std::string resp = recv_headers(sock);
|
||||
std::cout << "Handshake response:\n" << resp << std::endl;
|
||||
std::string init = R"({"type":"init","name":"Probe","room":"Lobby","color":"#000000","password":""})";
|
||||
if (!send_ws_text(sock, init)) { std::cerr << "send frame failed" << std::endl; return 3; }
|
||||
std::string msg = recv_ws_text(sock);
|
||||
std::cout << "First WS message from server: " << msg << std::endl;
|
||||
::close(sock);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user