Mini Web Server in C++ (Part One)

Joined
Jun 12, 2020
Messages
60
Reaction score
3
Mini Web Server in C++


I’m excited to present a mini web server implemented in C++ that offers a variety of features, making it a great foundation for web application development. While this server is not secure for production use, it provides essential functionalities for managing messages and users.


Key Features:


  • Multi-User System: Users can register, log in, and manage their messages. Each user has specific rights, allowing for a customized experience.
  • Message Management: Users can easily create, edit, and delete messages, making it simple to maintain content.
  • Search Functionality: A built-in search feature enables users to find messages quickly and efficiently.
  • Client-Side JavaScript: The server includes JavaScript for enhanced user interaction on the client side, improving the overall experience.

The code is divided into two parts for easier sharing, as it is quite lengthy. This project serves as a learning tool and a base for anyone looking to understand web server concepts and expand upon them.


Feel free to explore the code and contribute to its development!


C++:
/*
MIT License
Copyright (c) 2025 CoTon_TiGe_MoUaRf

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

This project is divided into two parts for sharing purposes due to its length. Each part can be used independently while maintaining the same terms outlined in this license.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>

#include <string>
#include <vector>
#include <map>
#include <ctime>
#include <sstream>
#include <iostream>
#include <fstream>
#include <algorithm>
#include <iomanip>
#include <random>
#include <cctype>

#pragma comment(lib, "Ws2_32.lib")

using std::string;
using std::vector;
using std::map;
using std::stringstream;
using std::ifstream;
using std::ofstream;
using std::cerr;
using std::cout;
using std::endl;

/* ---------------------------
   Structures de base
   --------------------------- */

enum Role { ROLE_USER = 0, ROLE_ADMIN = 1 };

struct Message {
    int id;
    string username;
    string content;
    string publishedAt; // "YYYY-MM-DD HH:MM:SS"
    Message(int i = 0, const string& u = "", const string& c = "", const string& p = "") : id(i), username(u), content(c), publishedAt(p) {}
};

struct User {
    string username;
    string passwordHex; // password encoded in hex (simple encoding)
    Role role;
};

struct Session {
    string sessionId;
    string username;
    time_t lastActivity;
};

/* ---------------------------
   Données globales (simple)
   --------------------------- */

static vector<Message> g_messages;
static int g_nextMessageId = 1;

static map<string, User> g_users;           // username -> User
static map<string, Session> g_sessions;     // sessionId -> Session

// Configuration
static const int SESSION_TIMEOUT_SECONDS = 60 * 30; // 30 minutes

static const string USERS_FILENAME = "users.txt";
static const string MESSAGES_FILENAME = "database.txt";

static std::mt19937_64 rng((unsigned)std::time(nullptr));

/* ---------------------------
   Utilitaires : encodage / décodage URL & escaping simple
   --------------------------- */

static string urlDecode(const string& s) {
    string res;
    res.reserve(s.size());
    for (size_t i = 0; i < s.size(); ++i) {
        if (s[i] == '+') {
            res.push_back(' ');
        } else if (s[i] == '%' && i + 2 < s.size()) {
            char hex[3] = { s[i+1], s[i+2], 0 };
            int val = (int)std::strtol(hex, nullptr, 16);
            res.push_back(static_cast<char>(val));
            i += 2;
        } else {
            res.push_back(s[i]);
        }
    }
    return res;
}

static map<string, string> parseFormUrlEncoded(const string& body) {
    map<string, string> out;
    size_t start = 0;
    while (start < body.size()) {
        size_t eq = body.find('=', start);
        if (eq == string::npos) break;
        size_t amp = body.find('&', eq + 1);
        string key = body.substr(start, eq - start);
        string val = (amp == string::npos) ? body.substr(eq+1) : body.substr(eq+1, amp - (eq+1));
        out[key] = urlDecode(val);
        if (amp == string::npos) break;
        start = amp + 1;
    }
    return out;
}

static string htmlEscape(const string& s) {
    string out;
    out.reserve(s.size());
    for (char c : s) {
        switch (c) {
            case '&': out += "&amp;"; break;
            case '<': out += "&lt;"; break;
            case '>': out += "&gt;"; break;
            case '"': out += "&quot;"; break;
            case '\'': out += "&#39;"; break;
            default: out.push_back(c); break;
        }
    }
    return out;
}

/* ---------------------------
   Simple hex "encoding" for passwords (not a secure hash; basic obfuscation)
   - encode string to hex representation (lowercase)
   - compare by encoding input password and comparing hex strings
   --------------------------- */

static string toHex(const string& s) {
    static const char* hexChars = "0123456789abcdef";
    string out;
    out.reserve(s.size() * 2);
    for (unsigned char c : s) {
        out.push_back(hexChars[(c >> 4) & 0xF]);
        out.push_back(hexChars[c & 0xF]);
    }
    return out;
}

static string fromHex(const string& hex) {
    string out;
    if (hex.size() % 2 != 0) return out;
    out.reserve(hex.size()/2);
    for (size_t i = 0; i < hex.size(); i += 2) {
        auto hexVal = [](char c)->int {
            if (c >= '0' && c <= '9') return c - '0';
            if (c >= 'a' && c <= 'f') return c - 'a' + 10;
            if (c >= 'A' && c <= 'F') return c - 'A' + 10;
            return 0;
        };
        unsigned char high = (unsigned char)hexVal(hex[i]);
        unsigned char low = (unsigned char)hexVal(hex[i+1]);
        out.push_back(static_cast<char>((high << 4) | low));
    }
    return out;
}

/* ---------------------------
   Dates formatting
   --------------------------- */

static string currentDateTime() {
    std::time_t t = std::time(nullptr);
    std::tm tm;
    localtime_s(&tm, &t);
    char buf[20];
    std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
    return string(buf);
}

/* ---------------------------
   Sessions
   --------------------------- */

static string generateSessionId() {
    std::uniform_int_distribution<unsigned long long> dist;
    unsigned long long r = dist(rng);
    std::stringstream ss;
    ss << std::hex << r << std::dec << std::time(nullptr);
    return ss.str();
}

static void touchSession(const string& sessionId) {
    auto it = g_sessions.find(sessionId);
    if (it != g_sessions.end()) {
        it->second.lastActivity = std::time(nullptr);
    }
}

static void purgeExpiredSessions() {
    time_t now = std::time(nullptr);
    vector<string> toDelete;
    for (auto& kv : g_sessions) {
        if (now - kv.second.lastActivity > SESSION_TIMEOUT_SECONDS) {
            toDelete.push_back(kv.first);
        }
    }
    for (auto &k : toDelete) g_sessions.erase(k);
}

static void logoutSession(const string& sessionId) {
    auto it = g_sessions.find(sessionId);
    if (it != g_sessions.end()) g_sessions.erase(it);
}

static bool isUserLoggedIn(const string& username) {
    for (auto& kv : g_sessions) {
        if (kv.second.username == username) return true;
    }
    return false;
}

/* ---------------------------
   Gestion des utilisateurs dans un fichier
   Format simple: username<space>passwordHex<space>role\n
   role: 0=user,1=admin
   --------------------------- */

static void loadUsersFromFile(const string& filename) {
    g_users.clear();
    ifstream in(filename);
    if (!in) {
        cerr << "Erreur: Impossible d'ouvrir le fichier " << filename << "\n";
        return;
    }
   
    string line;
    while (std::getline(in, line)) {
        if (line.empty()) continue;

        std::istringstream iss(line);
        string uname, pwdhex;
        int roleInt = 0;

        // Vérification de la lecture correcte des données
        if (!(iss >> uname >> pwdhex >> roleInt)) {
            cerr << "Erreur: Ligne mal formée dans le fichier " << filename << ": " << line << "\n";
            continue; // Passer à la ligne suivante
        }

        Role r = (roleInt == 1) ? ROLE_ADMIN : ROLE_USER;
        g_users[uname] = User{uname, pwdhex, r};
    }
}

static void saveUsersToFile(const string& filename) {
    ofstream out(filename, std::ios::trunc);
    if (!out) return;
    for (const auto& kv : g_users) {
        out << kv.second.username << " " << kv.second.passwordHex << " " << (kv.second.role == ROLE_ADMIN ? 1 : 0) << "\n";
    }
}

/* ---------------------------
   Simple "base de données" messages (fichier)
   Format per line: id<space>username<space>publishedAt<space>content\n
   publishedAt has no spaces because we use underscore between date and time when storing,
   then we convert underscores back to space when loading.
   To keep content raw, we replace newlines with \\n when saving and reverse when loading.
   --------------------------- */

/* ---------------------------
Simple "base de données" messages (fichier)
Format per line: idusernamepublishedAtcontent\n
publishedAt has no spaces because we use underscore between date and time when storing,
then we convert underscores back to space when loading.
Messages are stored with content encoded in base64; when loading we decode.
Newlines are encoded inside content before base64 as literal '\n' chars (no extra escaping needed
because base64 is safe for file storage), but to preserve backward compatibility we still handle
files that may contain plain (non-base64) escaped content.
--------------------------- */

static string escapeContentForFile(const string& s) {
    string out;
    out.reserve(s.size());
    for (char c : s) {
        if (c == '\n') {
            out += "\\n";
        } else if (c == '\r') {
            // skip
        } else {
            out.push_back(c);
        }
    }
    return out;
}

static string unescapeContentFromFile(const string& s) {
    string out;
    out.reserve(s.size());
    for (size_t i = 0; i < s.size(); ++i) {
        if (s[i] == '\\' && i + 1 < s.size()) {
            if (s[i+1] == 'n') { out.push_back('\n'); ++i; continue; }
        }
        out.push_back(s[i]);
    }
    return out;
}

static string base64_encode(const string &in) {
static const string b64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
string out;
out.reserve(((in.size() + 2) / 3) * 4);
int val = 0, valb = -6;
for (unsigned char c : in) {
val = (val << 8) + c;
valb += 8;
while (valb >= 0) {
out.push_back(b64_chars[(val >> valb) & 0x3F]);
valb -= 6;
}
}
if (valb > -6) out.push_back(b64_chars[((val << 8) >> (valb + 8)) & 0x3F]);
while (out.size() % 4) out.push_back('=');
return out;
}

static string base64_decode(const string &in) {
static const string b64_chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
vector T(256, -1);
for (int i = 0; i < 64; i++) T[(unsigned char)b64_chars[i]] = i;
string out;
out.reserve((in.size() / 4) * 3);
int val = 0, valb = -8;
for (unsigned char c : in) {
if (std::isspace(c)) continue;
if (c == '=') break;
if (T[c] == -1) return string(); // invalid base64 -> return empty to signal failure
val = (val << 6) + T[c];
valb += 6;
if (valb >= 0) {
out.push_back(char((val >> valb) & 0xFF));
valb -= 8;
}
}
return out;
}

static void loadMessagesFromFile(const string& filename) {
g_messages.clear();
ifstream in(filename);
if (!in) return;
string line;
while (std::getline(in, line)) {
if (line.empty()) continue;
std::istringstream iss(line);
int id;
string user;
string publishedUnderscore;
if (!(iss >> id >> user >> publishedUnderscore)) continue;
string rest;
std::getline(iss, rest);
if (!rest.empty() && rest[0] == ' ') rest.erase(0,1);



    // First try to unescape legacy escaped content (contains literal backslash-n sequences),
    // then try base64 decode. If base64 decoding succeeds (non-empty), prefer decoded result.
    string candidate = rest;
    // If candidate looks like base64 (only base64 chars plus '=' and no spaces), try decode
    bool looksLikeBase64 = true;
    int validChars = 0;
    for (unsigned char ch : candidate) {
        if (std::isspace(ch)) { looksLikeBase64 = false; break; }
        if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') ||
            (ch >= '0' && ch <= '9') || ch == '+' || ch == '/' || ch == '=') {
            validChars++;
            continue;
        }
        looksLikeBase64 = false;
        break;
    }
    string decoded;
    if (looksLikeBase64 && validChars > 0) {
        decoded = base64_decode(candidate);
    }
    if (decoded.empty()) {
        // either not base64 or decoding failed -> treat as legacy escaped text
        decoded = unescapeContentFromFile(candidate);
    }
    string published = publishedUnderscore;
    std::replace(published.begin(), published.end(), '_', ' ');
    g_messages.emplace_back(id, user, decoded, published);
    g_nextMessageId = std::max(g_nextMessageId, id + 1);
}

}

static bool saveMessagesToFile(const string& filename) {
    ofstream out(filename, std::ios::trunc);
    if (!out) {
        cerr << "Erreur: Impossible d'ouvrir le fichier " << filename << " pour écriture.\n";
        return false; // Échec de la sauvegarde
    }
    for (const auto& m : g_messages) {
        string pubUnd = m.publishedAt;
        std::replace(pubUnd.begin(), pubUnd.end(), ' ', '_');
        string escaped = escapeContentForFile(m.content);
        string b64 = base64_encode(escaped);
        out << m.id << " " << m.username << " " << pubUnd << " " << b64 << "\n";
    }
    return true; // Succès de la sauvegarde
}
/* ---------------------------
   HTTP pages templates (dark theme, edit/delete buttons)
   --------------------------- */

const string indexHtml = R"HTML(
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="utf-8">
    <title>Accueil</title>
    <meta name="description" content="Page d'accueil de notre site de messagerie. Postez et consultez des messages facilement.">
    <meta name="keywords" content="messagerie, messages, forum, discussion">
    <style>
        body {
            background: #0b0f14;
            color: #dbe9f2;
            font-family: 'Segoe UI', Arial;
            padding: 20px;
            text-align: center;
        }
        a {
            color: #8be4c3;
            text-decoration: none;
            margin: 0 10px;
            padding: 8px 15px;
            border: 2px solid #8be4c3;
            border-radius: 4px;
            transition: background 0.3s, color 0.3s;
        }
        a:hover {
            background: #8be4c3;
            color: #0b0f14;
        }
        input, button, textarea {
            padding: 10px;
            margin: 6px 0;
            background: #0f1720;
            color: #dbe9f2;
            border: 2px solid #22303a;
            border-radius: 4px;
            transition: background 0.3s;
            width: 80%;
            max-width: 400px;
        }
        input:focus, textarea:focus {
            background: #1a1e24;
        }
        button {
            cursor: pointer;
            background: #8be4c3;
            color: #0b0f14;
            border: none;
            border-radius: 4px;
            padding: 10px 20px;
            transition: background 0.3s, color 0.3s;
        }
        button:hover {
            background: #dbe9f2;
            color: #0b0f14;
        }
        form {
            margin-top: 20px;
            display: inline-block;
            text-align: left;
        }
        .header {
            display: flex;
            flex-direction: column;
            align-items: center;
            margin-bottom: 20px;
        }
        textarea {
            height: 80px;
        }
        .small {
            font-size: 0.9em;
            color: #9fb6c3;
        }
        .error {
            color: #ff8080;
            margin-top: 10px;
        }
        h1 {
            font-size: 3em;
            margin-bottom: 10px;
        }
        h2 {
            font-size: 1.5em;
            color: #8be4c3;
            margin-bottom: 20px;
        }
    </style>
</head>
<body>
    <header class="header">
        <h1>Accueil</h1>
        <div>
            <a href="/messages">Voir les messages</a> |
            <a href="/login">Se connecter</a>
        </div>
    </header>
    <main>
        <h2>Bienvenue sur notre site de messagerie</h2>
        <h3>Poster un message</h3>
        <form action="/add" method="POST">
            <label for="username">Nom d'utilisateur (si non connecté):</label>
            <input type="text" id="username" name="username" placeholder="Nom d'utilisateur" aria-label="Nom d'utilisateur">
            <br>
            <label for="content">Message:</label>
            <textarea id="content" name="content" placeholder="Votre message" required aria-label="Message"></textarea>
            <br>
            <button type="submit">Envoyer</button>
        </form>
        <div class="error" id="error-message"></div>
    </main>
</body>
</html>
)HTML";

const string loginHtmlTemplate = R"HTML(
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="utf-8">
    <title>Connexion</title>
    <style>
        body {
            background: #0f1620;
            color: #e7eef6;
            font-family: Segoe UI, Arial;
            padding: 20px;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
            flex-direction: column;
        }
        form {
            background: #1a1e24;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25);
            width: 300px;
            text-align: center;
        }
        input {
            padding: 10px; /* Réduire légèrement le padding */
            margin: 10px 0;
            background: #12171d;
            color: #e7eef6;
            border: 1px solid #27313a;
            border-radius: 4px;
            width: calc(100% - 22px); /* Ajustement pour compenser les bordures */
        }
        button {
            padding: 10px; /* Réduire légèrement le padding */
            background: #80d0ff;
            color: #0b0f14;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            transition: background 0.3s;
            width: 100%; /* Assurez-vous que le bouton s'aligne */
        }
        button:hover {
            background: #64b3e0;
        }
        .error { color: #ff8080; }
        a {
            color: #80d0ff;
            display: block;
            margin-top: 10px;
        }
    </style>
</head>
<body>
    <h1>Connexion</h1>
    %s
    <form action="/do_login" method="POST">
        <input type="text" name="username" placeholder="Nom d'utilisateur" required><br>
        <input type="password" name="password" placeholder="Mot de passe" required><br>
        <button type="submit">Se connecter</button>
    </form>
    <p><a href="/">Retour</a></p>
</body>
</html>
)HTML";

const string messagesHtmlTemplate = R"HTML(
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="utf-8">
    <title>Messages</title>
    <style>
        body { background: #0b0f14; color: #dbe9f2; font-family: Segoe UI, Arial; padding: 20px; }
        a { color: #8be4c3; text-decoration: none; }
        a:hover { text-decoration: underline; }
        ul { list-style: none; padding: 0; }
        li { background: #0f1720; margin: 8px 0; padding: 10px; border-radius: 6px; }
        .header { display: flex; justify-content: space-between; align-items: center; }
        button, input, form { padding: 6px; }
        .meta { color: #9fb6c3; font-size: 0.9em; margin-bottom: 4px; }
        .message-content { color: #dbe9f2; font-size: 1em; }
        .action { margin-left: 12px; display: inline-block; }
        .error { color: #ff8080; }
    </style>
</head>
<body>
    <header class="header">
        <h1>Messages</h1>
        <div>
            %s
            <a href="/">Accueil</a>
        </div>
    </header>
    <main>
        <ul>
            %s
        </ul>
        <p><a href="/admin">Admin</a></p>
    </main>
</body>
</html>
)HTML";

const string adminHtmlTemplate = R"HTML(
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="utf-8">
    <title>Admin</title>
    <meta name="description" content="Page d'administration pour gérer les utilisateurs et les messages.">
    <meta name="keywords" content="administration, gestion, utilisateurs, messages">
    <style>
        body {
            background: #0b0f14;
            color: #dbe9f2;
            font-family: 'Segoe UI', Arial;
            padding: 20px;
            text-align: center;
        }
        a {
            color: #8be4c3;
            text-decoration: none;
            margin: 10px;
            padding: 8px 15px;
            border: 2px solid #8be4c3;
            border-radius: 4px;
            transition: background 0.3s, color 0.3s;
        }
        a:hover {
            background: #8be4c3;
            color: #0b0f14;
        }
        input, button {
            padding: 10px;
            margin: 6px 0;
            background: #0f1720;
            color: #dbe9f2;
            border: 2px solid #22303a;
            border-radius: 4px;
            transition: background 0.3s;
            width: 80%;
            max-width: 400px;
        }
        input:focus {
            background: #1a1e24;
        }
        button {
            cursor: pointer;
            background: #8be4c3;
            color: #0b0f14;
            border: none;
            padding: 10px 20px;
            transition: background 0.3s, color 0.3s;
        }
        button:hover {
            background: #dbe9f2;
            color: #0b0f14;
        }
        form {
            margin-top: 20px;
            display: inline-block;
            text-align: left;
        }
        h1 {
            font-size: 3em;
            margin-bottom: 20px;
        }
        h2, h3 {
            font-size: 1.5em;
            color: #8be4c3;
            margin-bottom: 15px;
        }
        label {
            display: block;
            margin: 10px 0 5px;
            text-align: left;
        }
        select {
            width: 84%; /* t'ajuste à la largeur */
        }
    </style>
</head>
<body>
    <h1>Administration</h1>
    <p>Utilisateur connecté: <strong>%s</strong> (role: %s)</p>
    <form action="/logout" method="POST" style="display:inline;">
        <button type="submit">Se déconnecter</button>
    </form>
    <h2>Actions</h2>
    <form action="/admin/delete" method="POST">
        <label for="delete-id">Message ID à supprimer:</label>
        <input type="number" id="delete-id" name="id" required />
        <button type="submit">Supprimer</button>
    </form>
    <h3>Gestion des utilisateurs</h3>
    <form action="/admin/add_user" method="POST">
        <label for="add-username">Ajouter utilisateur:</label>
        <input type="text" id="add-username" name="username" required />
        <label for="add-password">Mot de passe:</label>
        <input type="password" id="add-password" name="password" required />
        <label for="role">Role:</label>
        <select id="role" name="role">
            <option value="0">USER</option>
            <option value="1">ADMIN</option>
        </select>
        <button type="submit">Ajouter</button>
    </form>
    <form action="/admin/delete_user" method="POST">
        <label for="delete-username">Supprimer utilisateur:</label>
        <input type="text" id="delete-username" name="username" required />
        <button type="submit">Supprimer</button>
    </form>
    <p><a href="/">Retour à l'accueil</a></p>
</body>
</html>
)HTML";

const string editHtmlTemplate = R"HTML(
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Éditer le message</title>
<style>
body { background:#0b0f14; color:#dbe9f2; font-family:Segoe UI,Arial; padding:20px; }
textarea { width:60%; height:120px; background:#0f1720; color:#dbe9f2; border:1px solid #22303a; padding:6px; }
input { padding:6px; margin:6px 0; background:#0f1720; color:#dbe9f2; border:1px solid #22303a; }
a { color:#8be4c3; }
</style>
</head>
<body>
  <h1>Éditer le message (id=%d)</h1>
  <form action="/edit_confirm" method="POST">
    <input type="hidden" name="id" value="%d" />
    <textarea name="content" required>%s</textarea><br>
    <input type="submit" value="Enregistrer les modifications" />
  </form>
  <p><a href="/messages">Annuler</a></p>
</body>
</html>
)HTML";
 
Joined
Jun 12, 2020
Messages
60
Reaction score
3
C++:
/*
MIT License
Copyright (c) 2025 CoTon_TiGe_MoUaRf

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

This project is divided into two parts for sharing purposes due to its length. Each part can be used independently while maintaining the same terms outlined in this license.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/* ---------------------------
   HTTP parsing minimal
   --------------------------- */

static string getHeaderValue(const string& headers, const string& name) {
    string lname = name;
    std::transform(lname.begin(), lname.end(), lname.begin(), ::tolower);
    std::istringstream ss(headers);
    string line;
    while (std::getline(ss, line)) {
        if (line.empty()) continue;
        string tmp = line;
        std::transform(tmp.begin(), tmp.end(), tmp.begin(), ::tolower);
        if (tmp.rfind(lname + ":", 0) == 0) {
            size_t pos = line.find(':');
            if (pos != string::npos) {
                string val = line.substr(pos+1);
                size_t a = val.find_first_not_of(" \t\r\n");
                size_t b = val.find_last_not_of(" \t\r\n");
                if (a == string::npos) return "";
                return val.substr(a, b - a + 1);
            }
        }
    }
    return "";
}

static string extractCookie(const string& headers, const string& cookieName) {
    string cookieHeader = getHeaderValue(headers, "Cookie");
    if (cookieHeader.empty()) return "";
    size_t pos = cookieHeader.find(cookieName + "=");
    if (pos == string::npos) return "";
    size_t start = pos + cookieName.size() + 1;
    size_t end = cookieHeader.find(';', start);
    if (end == string::npos) end = cookieHeader.size();
    return cookieHeader.substr(start, end - start);
}

/* ---------------------------
   Rendering Helpers
   --------------------------- */

string renderMessagesListForSession(const string& sessionId) {
    string list;

    // Styles CSS unifiés (palette verte identique à indexHtml)
    string styles = R"HTML(
    <style>
        body {
            background: #0b0f14;
            color: #dbe9f2;
            font-family: 'Segoe UI', Arial;
            margin: 0;
            padding: 20px;
        }
        .message-item {
            background: #0f1720; /* Fond sombre pour les messages */
            margin: 8px 0;
            padding: 10px;
            border-radius: 6px;
            transition: background 0.3s;
            border: 1px solid rgba(139,228,195,0.06);
        }
        .message-item:hover {
            background: #1a1e24; /* Changement de couleur au survol */
        }
        .meta {
            color: #9fb6c3; /* Couleur pour les métadonnées */
            font-size: 0.9em;
            margin-top: 5px;
        }
        .action {
            margin-top: 5px;
        }
        .edit-button, .delete-button {
            padding: 4px 8px;
            margin-right: 5px;
            background: #8be4c3; /* Couleur verte principale */
            color: #0b0f14; /* Couleur du texte (contraste) */
            border: none;
            border-radius: 4px;
            cursor: pointer;
            text-decoration: none;
            display: inline-block;
        }
        .edit-button:hover, .delete-button:hover {
            background: #dbe9f2; /* Couleur au survol */
            color: #0b0f14;
        }
        .highlight {
            box-shadow: 0 0 10px rgba(139, 228, 195, 0.14);
            border-color: rgba(139,228,195,0.2);
        }
        /* Style du champ de recherche */
        #searchInput {
            padding: 10px;
            margin-bottom: 20px;
            background: #0f1720;
            color: #dbe9f2;
            border: 2px solid #22303a;
            border-radius: 4px;
            width: calc(100% - 22px);
        }
        a {
            color: #8be4c3;
            text-decoration: none;
        }
        /* Pas de styles supplémentaires pour les messages non trouvés */
    </style>
    )HTML";

    // Champ de recherche et ouverture de la liste
    list += R"HTML(
    <input type="text" id="searchInput" placeholder="Rechercher des messages..." oninput="searchMessages()" />
    <ul id="messagesList" style="padding:0; margin:0; list-style:none;">
    )HTML";

    // Ajout des styles au début de la liste
    list += styles;

    for (const auto& m : g_messages) {
        string contentEsc = htmlEscape(m.content);
        string userEsc = htmlEscape(m.username);
        string pubEsc = htmlEscape(m.publishedAt);

        // Amélioration du design avec classes CSS
        string item = "<li class=\"message-item\">";
        item += "<div class=\"meta\"><strong>" + userEsc + ":</strong></div>"; // Affichage de l'auteur
        item += "<div class=\"message-content\">" + contentEsc + "</div>"; // Contenu du message
        item += "<div class=\"meta\">Publié: " + pubEsc + " (id=" + std::to_string(m.id) + ")</div>";

        // Vérification des permissions d'édition
        bool canEdit = false;
        if (!sessionId.empty() && g_sessions.find(sessionId) != g_sessions.end()) {
            string uname = g_sessions[sessionId].username;
            if (uname == m.username) canEdit = true;
            else {
                // Vérification du rôle admin
                auto it = g_users.find(uname);
                if (it != g_users.end() && it->second.role == ROLE_ADMIN) canEdit = true;
            }
        }

        // Ajout des actions d'édition et de suppression
        if (canEdit) {
            item += "<div class=\"action\"><a href=\"/edit?id=" + std::to_string(m.id) + "\" class=\"edit-button\">Éditer</a>";
            item += "<form action=\"/delete_message\" method=\"POST\" style=\"display:inline;\">"
                    "<input type=\"hidden\" name=\"id\" value=\"" + std::to_string(m.id) + "\"/>"
                    "<input type=\"submit\" value=\"Supprimer\" class=\"delete-button\" onclick=\"return confirm('Confirmer la suppression du message id=" + std::to_string(m.id) + "?');\"/>"
                    "</form></div>";
        }

        item += "</li>\n";
        list += item;
    }

    list += "</ul>"; // Fermer la liste

    // Ajout du script JS pour la recherche
    list += R"HTML(
    <script>
        function searchMessages() {
            const input = document.getElementById("searchInput").value.toLowerCase();
            const messages = document.querySelectorAll(".message-item");
            let found = false;

            // Retirer ancien message "Aucun message trouvé."; si existant
            const existingNoResults = document.getElementById("noResultsItem");
            if (existingNoResults) {
                existingNoResults.remove();
            }

            messages.forEach(message => {
                const text = message.textContent.toLowerCase();
                if (text.includes(input)) {
                    message.style.display = ""; // Afficher le message
                    message.classList.add("highlight"); // Mettre en avant le message
                    found = true;
                } else {
                    message.style.display = "none"; // Masquer le message
                    message.classList.remove("highlight"); // Enlever la surbrillance
                }
            });

            // Si aucun message n'est trouvé, afficher un message approprié
            if (!found && input) {
                const noResults = document.createElement("li");
                noResults.id = "noResultsItem";
                noResults.textContent = "Aucun message trouvé.";
                noResults.className = "meta";  // Appliquer le style de meta
                noResults.style.color = "#ff8080"; // Couleur rouge pour le message
                noResults.style.listStyle = "none"; // Pas de style de liste
                noResults.style.marginTop = "8px";
                document.getElementById("messagesList").appendChild(noResults);
            }
        }
    </script>
    )HTML";

    return list; // Retourner la liste complète des messages
}

/* ---------------------------
   Authorization helpers
   --------------------------- */

static bool isSessionValid(const string& sessionId) {
    auto it = g_sessions.find(sessionId);
    if (it == g_sessions.end()) return false;
    time_t now = std::time(nullptr);
    if (now - it->second.lastActivity > SESSION_TIMEOUT_SECONDS) {
        g_sessions.erase(it);
        return false;
    }
    touchSession(sessionId);
    return true;
}

static Role getRoleForSession(const string& sessionId) {
    auto it = g_sessions.find(sessionId);
    if (it == g_sessions.end()) return ROLE_USER;
    const string& uname = it->second.username;
    auto uIt = g_users.find(uname);
    if (uIt == g_users.end()) return ROLE_USER;
    return uIt->second.role;
}

/* ---------------------------
   Fonctions métier: addMessage, editMessage, deleteMessage, logout, etc.
   --------------------------- */

static bool addMessage(const string& sessionId, const string& usernameFromForm, const string& content) {
    // Vérification que le contenu du message n'est pas vide
    if (content.empty()) {
        cerr << "Erreur: Le contenu du message ne peut pas être vide.\n";
        return false; // Échec de l'ajout
    }

    string username = usernameFromForm;
    if (isSessionValid(sessionId)) {
        username = g_sessions[sessionId].username; // Utiliser le nom d'utilisateur de la session
    } else if (username.empty()) {
        username = "anonyme"; // Utiliser "anonyme" si aucun nom d'utilisateur n'est fourni
    }

    string published = currentDateTime();
    Message m(g_nextMessageId++, username, content, published);
    g_messages.push_back(m);
   
    // Sauvegarder les messages dans le fichier
    saveMessagesToFile(MESSAGES_FILENAME); // Pas de vérification ici

    return true; // Succès de l'ajout
}

static bool editMessage(int id, const string& sessionId, const string& newContent) {
    // Vérification que le contenu du message n'est pas vide
    if (newContent.empty()) {
        cerr << "Erreur: Le nouveau contenu du message ne peut pas être vide.\n";
        return false; // Échec de l'édition
    }

    // Recherche du message par ID
    auto it = std::find_if(g_messages.begin(), g_messages.end(), [id](const Message& m) {
        return m.id == id;
    });

    if (it == g_messages.end()) {
        cerr << "Erreur: Message avec ID " << id << " introuvable.\n";
        return false; // Échec de l'édition
    }

    // Vérification des autorisations
    if (!isSessionValid(sessionId)) {
        cerr << "Erreur: Session invalide.\n";
        return false; // Échec de l'édition
    }

    string uname = g_sessions[sessionId].username;
    if (uname != it->username) {
        auto userIt = g_users.find(uname);
        if (userIt == g_users.end() || userIt->second.role != ROLE_ADMIN) {
            cerr << "Erreur: Autorisation refusée pour l'utilisateur " << uname << ".\n";
            return false; // Échec de l'édition
        }
    }

    // Modification du contenu du message
    it->content = newContent;
    saveMessagesToFile(MESSAGES_FILENAME);
    return true; // Succès de l'édition
}

static bool deleteMessageById(int id, const string& sessionId) {
    for (auto it = g_messages.begin(); it != g_messages.end(); ++it) {
        if (it->id == id) {
            // authorization: author or admin
            if (!isSessionValid(sessionId)) return false;
            string uname = g_sessions[sessionId].username;
            if (uname != it->username) {
                auto uit = g_users.find(uname);
                if (uit == g_users.end() || uit->second.role != ROLE_ADMIN) return false;
            }
            g_messages.erase(it);
            saveMessagesToFile(MESSAGES_FILENAME);
            return true;
        }
    }
    return false;
}

/* ---------------------------
   Request handler
   --------------------------- */

struct HttpResponse {
    int status = 200;
    string contentType = "text/html; charset=utf-8";
    string body;
    vector<std::pair<string,string>> extraHeaders;
    string cookieToSet; // simple "name=value; Path=/; HttpOnly"
    string toString() const {
        stringstream s;
        s << "HTTP/1.1 " << status << " OK\r\n";
        s << "Content-Type: " << contentType << "\r\n";
        s << "Content-Length: " << body.size() << "\r\n";
        if (!cookieToSet.empty()) s << "Set-Cookie: " << cookieToSet << "\r\n";
        for (const auto& h : extraHeaders) s << h.first << ": " << h.second << "\r\n";
        s << "\r\n";
        s << body;
        return s.str();
    }
};

static HttpResponse handleRequest(const string& requestRaw) {
    HttpResponse resp;
    size_t headerEnd = requestRaw.find("\r\n\r\n");
    string head = (headerEnd == string::npos) ? requestRaw : requestRaw.substr(0, headerEnd);
    string body = (headerEnd == string::npos) ? "" : requestRaw.substr(headerEnd + 4);

    stringstream hs(head);
    string requestLine;
    std::getline(hs, requestLine);
    if (requestLine.size() && requestLine.back() == '\r') requestLine.pop_back();
    string method, urlWithQuery, httpver;
    {
        stringstream rl(requestLine);
        rl >> method >> urlWithQuery >> httpver;
    }

    string headers;
    string line;
    while (std::getline(hs, line)) {
        if (line.size() && line.back() == '\r') line.pop_back();
        headers += line + "\n";
    }

    purgeExpiredSessions();

    string sessionId = extractCookie(headers, "SESSIONID");

    // parse URL path and query
    string path = urlWithQuery;
    string query;
    size_t qpos = urlWithQuery.find('?');
    if (qpos != string::npos) {
        path = urlWithQuery.substr(0, qpos);
        query = urlWithQuery.substr(qpos+1);
    }

    // Helper: build logged-in header for messages page
    auto buildLoggedHeader = [&](const string& sessionId)->string {
        if (isSessionValid(sessionId)) {
            string uname = g_sessions[sessionId].username;
            string s = "<form action=\"/logout\" method=\"POST\" style=\"display:inline;\"><input type=\"submit\" value=\"Se déconnecter\"/></form> ";
            s += "<span style=\"margin-left:8px\">Connecté: " + htmlEscape(uname) + "</span> ";
            return s;
        }
        return "<a href=\"/login\">Se connecter</a>";
    };

    // Handle GET
    if (method == "GET") {
        if (path == "/") {
            resp.body = indexHtml;
            return resp;
        } else if (path == "/login") {
            // If already logged in, show message
            string extra;
            if (isSessionValid(sessionId)) {
                string uname = g_sessions[sessionId].username;
                extra = "<p class=\"error\">Vous êtes déjà connecté en tant que " + htmlEscape(uname) + ".</p>";
            } else {
                extra = "";
            }
            string page = loginHtmlTemplate;
            size_t pos = page.find("%s");
            if (pos != string::npos) page.replace(pos, 2, extra);
            resp.body = page;
            return resp;
        } else if (path == "/messages") {
            string list = renderMessagesListForSession(sessionId);
            string loginPart = buildLoggedHeader(sessionId);
            string page = messagesHtmlTemplate;
            // first %s -> loginPart, second %s -> list
            size_t p1 = page.find("%s");
            if (p1 != string::npos) page.replace(p1, 2, loginPart);
            size_t p2 = page.find("%s", p1);
            if (p2 != string::npos) page.replace(p2, 2, list);
            resp.body = page;
            return resp;
        } else if (path == "/admin") {
            if (!isSessionValid(sessionId) || getRoleForSession(sessionId) != ROLE_ADMIN) {
                resp.status = 302;
                resp.extraHeaders.push_back({"Location", "/login"});
                resp.body = "<html><body>Redirecting...</body></html>";
                return resp;
            }
            string username = g_sessions[sessionId].username;
            string roleStr = (getRoleForSession(sessionId) == ROLE_ADMIN) ? "ADMIN" : "USER";
            string page = adminHtmlTemplate;
            size_t p1 = page.find("%s");
            if (p1 != string::npos) page.replace(p1, 2, htmlEscape(username));
            size_t p2 = page.find("%s", p1);
            if (p2 != string::npos) page.replace(p2, 2, roleStr);
            resp.body = page;
            return resp;
        } else if (path == "/edit") {
    // Vérification que la requête contient un ID
    int id = 0;
    size_t pos = query.find("id=");
    if (pos != string::npos) {
        try {
            id = std::stoi(query.substr(pos + 3));
        } catch(...) {
            id = 0;
        }
    }
    if (id == 0) {
        resp.status = 400;
        resp.body = "<h1>400 Bad Request</h1><p>ID manquant</p>";
        return resp;
    }

    // Recherche du message par ID
    auto it = std::find_if(g_messages.begin(), g_messages.end(), [id](const Message& m) {
        return m.id == id;
    });
    if (it == g_messages.end()) {
        resp.status = 404;
        resp.body = "<h1>404 Not Found</h1><p>Message introuvable</p>";
        return resp;
    }

    // Vérification des autorisations
    if (!isSessionValid(sessionId)) {
        resp.status = 403;
        resp.body = "<h1>403 Forbidden</h1><p>Non authentifié</p>";
        return resp;
    }

    string uname = g_sessions[sessionId].username;
    if (uname != it->username) {
        auto userIt = g_users.find(uname);
        if (userIt == g_users.end() || userIt->second.role != ROLE_ADMIN) {
            resp.status = 403;
            resp.body = "<h1>403 Forbidden</h1><p>Pas autorisé</p>";
            return resp;
        }
    }

    // Construction de la page d'édition avec le contenu pré-rempli (échappé)
    string contentEsc = htmlEscape(it->content);
    char pageBuf[16384];
    snprintf(pageBuf, sizeof(pageBuf), editHtmlTemplate.c_str(), id, id, contentEsc.c_str());
    resp.body = string(pageBuf);
    return resp;
} else {
            resp.status = 404;
            resp.body = "<h1>404 Not Found</h1>";
            return resp;
        }
    }
    // Handle POST
    else if (method == "POST") {
        string ct = getHeaderValue(headers, "Content-Type");
        if (path == "/do_login") {
            map<string,string> form = parseFormUrlEncoded(body);
            string username = form.count("username") ? form["username"] : "";
            string password = form.count("password") ? form["password"] : "";

            // Check if user already logged in elsewhere
            if (isUserLoggedIn(username)) {
                string page = "<p class=\"error\">Utilisateur déjà connecté ailleurs (ou session active).</p>";
                string pageFull = loginHtmlTemplate;
                size_t pos = pageFull.find("%s");
                if (pos != string::npos) pageFull.replace(pos, 2, page);
                resp.body = pageFull;
                return resp;
            }

            auto it = g_users.find(username);
            if (it == g_users.end()) {
                string extra = "<p class=\"error\">Nom d'utilisateur introuvable.</p>";
                string pageFull = loginHtmlTemplate;
                size_t pos = pageFull.find("%s");
                if (pos != string::npos) pageFull.replace(pos, 2, extra);
                resp.body = pageFull;
                return resp;
            }
            // compare hex-encoded password
            string passHex = toHex(password);
            if (it->second.passwordHex != passHex) {
                string extra = "<p class=\"error\">Mot de passe incorrect.</p>";
                string pageFull = loginHtmlTemplate;
                size_t pos = pageFull.find("%s");
                if (pos != string::npos) pageFull.replace(pos, 2, extra);
                resp.body = pageFull;
                return resp;
            }

            // create session
            string sid = generateSessionId();
            Session s; s.sessionId = sid; s.username = username; s.lastActivity = std::time(nullptr);
            g_sessions[sid] = s;
            resp.cookieToSet = "SESSIONID=" + sid + "; Path=/; HttpOnly";
            resp.status = 302;
            resp.extraHeaders.push_back({"Location", "/"});
            resp.body = "<html><body>Login success. Redirecting...</body></html>";
            return resp;
        } else if (path == "/logout") {
            if (!sessionId.empty()) {
                logoutSession(sessionId);
            }
            // Clear cookie by setting expired cookie
            resp.cookieToSet = "SESSIONID=deleted; Path=/; HttpOnly; Expires=Thu, 01 Jan 1970 00:00:00 GMT";
            resp.status = 302;
            resp.extraHeaders.push_back({"Location", "/"});
            resp.body = "<html><body>Logged out. Redirecting...</body></html>";
            return resp;
        } else if (path == "/add") {
            map<string,string> form;
            if (ct.find("application/x-www-form-urlencoded") != string::npos) {
                form = parseFormUrlEncoded(body);
            }
            string usernameField = form.count("username") ? form["username"] : "";
            string contentField = form.count("content") ? form["content"] : "";
            if (contentField.empty()) {
                resp.body = "<h1>Contenu vide</h1><p><a href=\"/\">Retour</a></p>";
                return resp;
            }
            addMessage(sessionId, usernameField, contentField);
            resp.status = 302;
            resp.extraHeaders.push_back({"Location", "/messages"});
            resp.body = "<html><body>Message ajouté. Redirecting...</body></html>";
            return resp;
        } else if (path == "/edit_confirm") {
    map<string,string> form = parseFormUrlEncoded(body);
    int id = 0;
    if (form.count("id")) {
        try {
            id = std::stoi(form["id"]);
        } catch(...) {
            id = 0;
        }
    }
    string newContent = form.count("content") ? form["content"] : "";
    if (id == 0 || newContent.empty()) {
        resp.status = 400;
        resp.body = "<h1>400 Bad Request</h1><p>Paramètres invalides</p>";
        return resp;
    }

    bool ok = editMessage(id, sessionId, newContent);
    if (!ok) {
        resp.status = 403;
        resp.body = "<h1>403 Forbidden</h1><p>Impossible d'éditer (droits manquants ou message introuvable)</p>";
        return resp;
    }

    resp.status = 302;
    resp.extraHeaders.push_back({"Location", "/messages"});
    resp.body = "<html><body>Modifié. Redirection...</body></html>";
    return resp;
} else if (path == "/delete_message") {
            map<string,string> form = parseFormUrlEncoded(body);
            int id = 0;
            if (form.count("id")) {
                try { id = std::stoi(form["id"]); } catch(...) { id = 0; }
            }
            if (id == 0) {
                resp.status = 400;
                resp.body = "<h1>400 Bad Request</h1><p>id manquant</p>";
                return resp;
            }
            bool ok = deleteMessageById(id, sessionId);
            if (!ok) {
                resp.status = 403;
                resp.body = "<h1>403 Forbidden</h1><p>Impossible de supprimer (droits manquants ou message introuvable)</p>";
                return resp;
            }
            resp.status = 302;
            resp.extraHeaders.push_back({"Location", "/messages"});
            resp.body = "<html><body>Supprimé. Redirecting...</body></html>";
            return resp;
        } else if (path == "/admin/delete") {
            if (!isSessionValid(sessionId) || getRoleForSession(sessionId) != ROLE_ADMIN) {
                resp.status = 403;
                resp.body = "<h1>403 Forbidden</h1>";
                return resp;
            }
            map<string,string> form = parseFormUrlEncoded(body);
            int idToDelete = 0;
            if (form.count("id")) {
                try { idToDelete = std::stoi(form["id"]); } catch(...) { idToDelete = 0; }
            }
            bool removed = false;
            for (auto it = g_messages.begin(); it != g_messages.end(); ++it) {
                if (it->id == idToDelete) { g_messages.erase(it); removed = true; break; }
            }
            if (removed) saveMessagesToFile(MESSAGES_FILENAME);
            resp.status = 302;
            resp.extraHeaders.push_back({"Location", "/admin"});
            resp.body = "<html><body>Done. Redirecting...</body></html>";
            return resp;
        } else if (path == "/admin/add_user") {
            if (!isSessionValid(sessionId) || getRoleForSession(sessionId) != ROLE_ADMIN) {
                resp.status = 403;
                resp.body = "<h1>403 Forbidden</h1>";
                return resp;
            }
            map<string,string> form = parseFormUrlEncoded(body);
            string newUser = form.count("username") ? form["username"] : "";
            string newPass = form.count("password") ? form["password"] : "";
            int roleInt = 0;
            if (form.count("role")) {
                try { roleInt = std::stoi(form["role"]); } catch(...) { roleInt = 0; }
            }
            if (newUser.empty() || newPass.empty()) {
                resp.body = "<h1>Champs vides</h1><p><a href=\"/admin\">Retour</a></p>";
                return resp;
            }
            if (g_users.find(newUser) != g_users.end()) {
                resp.body = "<h1>Utilisateur existe déjà</h1><p><a href=\"/admin\">Retour</a></p>";
                return resp;
            }
            Role r = (roleInt == 1) ? ROLE_ADMIN : ROLE_USER;
            string passHex = toHex(newPass);
            g_users[newUser] = User{newUser, passHex, r};
            saveUsersToFile(USERS_FILENAME);
            resp.status = 302;
            resp.extraHeaders.push_back({"Location", "/admin"});
            resp.body = "<html><body>Utilisateur ajouté. Redirecting...</body></html>";
            return resp;
        } else if (path == "/admin/delete_user") {
            if (!isSessionValid(sessionId) || getRoleForSession(sessionId) != ROLE_ADMIN) {
                resp.status = 403;
                resp.body = "<h1>403 Forbidden</h1>";
                return resp;
            }
            map<string,string> form = parseFormUrlEncoded(body);
            string remUser = form.count("username") ? form["username"] : "";
            if (remUser.empty()) {
                resp.body = "<h1>Nom utilisateur vide</h1><p><a href=\"/admin\">Retour</a></p>";
                return resp;
            }
            auto it = g_users.find(remUser);
            if (it == g_users.end()) {
                resp.body = "<h1>Utilisateur introuvable</h1><p><a href=\"/admin\">Retour</a></p>";
                return resp;
            }
            g_users.erase(it);
            saveUsersToFile(USERS_FILENAME);
            resp.status = 302;
            resp.extraHeaders.push_back({"Location", "/admin"});
            resp.body = "<html><body>Utilisateur supprimé. Redirecting...</body></html>";
            return resp;
        } else {
            resp.status = 404;
            resp.body = "<h1>404 Not Found</h1>";
            return resp;
        }
    } else {
        resp.status = 405;
        resp.body = "<h1>405 Method Not Allowed</h1>";
        return resp;
    }
}

/* ---------------------------
   Networking: serveur simple
   --------------------------- */

int main() {
    // init data
    // If users file missing, create default users (admin, alice, bob)
    loadUsersFromFile(USERS_FILENAME);
    if (g_users.empty()) {
        g_users["admin"] = User{"admin", toHex("adminpass"), ROLE_ADMIN};
        g_users["alice"] = User{"alice", toHex("alicepass"), ROLE_USER};
        g_users["bob"] = User{"bob", toHex("bobpass"), ROLE_USER};
        saveUsersToFile(USERS_FILENAME);
    }
    loadMessagesFromFile(MESSAGES_FILENAME);

    WSADATA wsaData;
    int r = WSAStartup(MAKEWORD(2,2), &wsaData);
    if (r != 0) {
        cerr << "WSAStartup failed: " << r << "\n";
        return 1;
    }

    struct addrinfo hints;
    ZeroMemory(&hints, sizeof(hints));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    hints.ai_flags = AI_PASSIVE;

    struct addrinfo* addr = nullptr;
    r = getaddrinfo("127.0.0.1", "8000", &hints, &addr);
    if (r != 0) {
        cerr << "getaddrinfo failed: " << r << "\n";
        WSACleanup();
        return 1;
    }

    SOCKET listenSocket = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol);
    if (listenSocket == INVALID_SOCKET) {
        cerr << "socket failed: " << WSAGetLastError() << "\n";
        freeaddrinfo(addr);
        WSACleanup();
        return 1;
    }

    r = bind(listenSocket, addr->ai_addr, (int)addr->ai_addrlen);
    if (r == SOCKET_ERROR) {
        cerr << "bind failed: " << WSAGetLastError() << "\n";
        closesocket(listenSocket);
        freeaddrinfo(addr);
        WSACleanup();
        return 1;
    }

    freeaddrinfo(addr);

    if (listen(listenSocket, SOMAXCONN) == SOCKET_ERROR) {
        cerr << "listen failed: " << WSAGetLastError() << "\n";
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }

    cout << "Serveur démarré sur http://127.0.0.1:8000/  (Ctrl-C pour quitter)\n";

    while (true) {
        SOCKET client = accept(listenSocket, NULL, NULL);
        if (client == INVALID_SOCKET) {
            cerr << "accept failed: " << WSAGetLastError() << "\n";
            break;
        }

        const int BUFSZ = 8192;
        char buffer[BUFSZ + 1];
        int bytes = recv(client, buffer, BUFSZ, 0);
        if (bytes > 0) {
            buffer[bytes] = 0;
            string request(buffer, bytes);

            string headersPart;
            size_t hdrEnd = request.find("\r\n\r\n");
            if (hdrEnd != string::npos) {
                headersPart = request.substr(0, hdrEnd+4);
                string contentLengthStr = getHeaderValue(headersPart, "Content-Length");
                if (!contentLengthStr.empty()) {
                    int contentLength = 0;
                    try { contentLength = std::stoi(contentLengthStr); } catch(...) { contentLength = 0; }
                    int have = (int)request.size() - (int)(hdrEnd + 4);
                    while (have < contentLength) {
                        int rcv = recv(client, buffer, BUFSZ, 0);
                        if (rcv <= 0) break;
                        request.append(buffer, rcv);
                        have += rcv;
                    }
                }
            }

            HttpResponse response = handleRequest(request);
            string out = response.toString();
            int sent = send(client, out.c_str(), (int)out.size(), 0);
            (void)sent;
        }
        closesocket(client);
    }

    closesocket(listenSocket);
    WSACleanup();
    saveMessagesToFile(MESSAGES_FILENAME);
    saveUsersToFile(USERS_FILENAME);
    return 0;
}
 
Joined
Jun 12, 2020
Messages
60
Reaction score
3
I'm sorry, I made a mistake in the thread title; the two parts of the code are above.
 
Joined
Jun 12, 2020
Messages
60
Reaction score
3
To improve search within messages, here is an update to the C++ message list function with a JavaScript implementation that allows searching using TF-IDF and changing the background color of the most relevant phrase selected for the user's query.

C++:
/*
MIT License
Copyright (c) 2025 CoTon_TiGe_MoUaRf

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
string renderMessagesListForSession(const string& sessionId) {
    string list;

    // Styles CSS unifiés (palette verte identique à indexHtml)
    string styles = R"HTML(
    <style>
        body {
            background: #0b0f14;
            color: #dbe9f2;
            font-family: 'Segoe UI', Arial;
            margin: 0;
            padding: 20px;
        }
        .message-item {
            background: #0f1720; /* Fond sombre pour les messages */
            margin: 8px 0;
            padding: 10px;
            border-radius: 6px;
            transition: background 0.3s;
            border: 1px solid rgba(139,228,195,0.06);
        }
        .message-item:hover {
            background: #1a1e24; /* Changement de couleur au survol */
        }
        .meta {
            color: #9fb6c3; /* Couleur pour les métadonnées */
            font-size: 0.9em;
            margin-top: 5px;
        }
        .action {
            margin-top: 5px;
        }
        .edit-button, .delete-button {
            padding: 4px 8px;
            margin-right: 5px;
            background: #8be4c3; /* Couleur verte principale */
            color: #0b0f14; /* Couleur du texte (contraste) */
            border: none;
            border-radius: 4px;
            cursor: pointer;
            text-decoration: none;
            display: inline-block;
        }
        .edit-button:hover, .delete-button:hover {
            background: #dbe9f2; /* Couleur au survol */
            color: #0b0f14;
        }
        .highlight {
            box-shadow: 0 0 10px rgba(139, 228, 195, 0.14);
            border-color: rgba(139,228,195,0.2);
        }
        /* Style du champ de recherche */
        #searchInput {
            padding: 10px;
            margin-bottom: 20px;
            background: #0f1720;
            color: #dbe9f2;
            border: 2px solid #22303a;
            border-radius: 4px;
            width: calc(100% - 22px);
        }
        a {
            color: #8be4c3;
            text-decoration: none;
        }
        .match {
            background-color: rgba(139, 228, 195, 0.35);
            font-weight: 600;
            color: #062019;
            padding: 0 2px;
            border-radius: 2px;
        }
    </style>
    )HTML";

    // Champ de recherche et ouverture de la liste
    list += R"HTML(
    <input type="text" id="searchInput" placeholder="Rechercher des messages..." oninput="searchMessages()" />
    <ul id="messagesList" style="padding:0; margin:0; list-style:none;">
    )HTML";

    // Ajout des styles au début de la liste
    list += styles;

    // Construire la liste d'items en incluant le contenu original en data-original
    for (const auto& m : g_messages) {
        string contentEsc = htmlEscape(m.content);
        string userEsc = htmlEscape(m.username);
        string pubEsc = htmlEscape(m.publishedAt);

        // Amélioration du design avec classes CSS
        string item = "<li class=\"message-item\">";
        item += "<div class=\"meta\"><strong>" + userEsc + ":</strong></div>"; // Affichage de l'auteur
        // stocker le contenu original en data-original pour que le JS puisse le récupérer proprement
        item += "<div class=\"message-content\" data-original=\"" + htmlEscape(m.content) + "\">" + contentEsc + "</div>"; // Contenu du message
        item += "<div class=\"meta\">Publié: " + pubEsc + " (id=" + std::to_string(m.id) + ")</div>";

        // Vérification des permissions d'édition
        bool canEdit = false;
        if (!sessionId.empty() && g_sessions.find(sessionId) != g_sessions.end()) {
            string uname = g_sessions.at(sessionId).username;
            if (uname == m.username) canEdit = true;
            else {
                // Vérification du rôle admin
                auto it = g_users.find(uname);
                if (it != g_users.end() && it->second.role == ROLE_ADMIN) canEdit = true;
            }
        }

        // Ajout des actions d'édition et de suppression
        if (canEdit) {
            item += "<div class=\"action\"><a href=\"/edit?id=" + std::to_string(m.id) + "\" class=\"edit-button\">Éditer</a>";
            item += "<form action=\"/delete_message\" method=\"POST\" style=\"display:inline;\">"
                    "<input type=\"hidden\" name=\"id\" value=\"" + std::to_string(m.id) + "\"/>"
                    "<input type=\"submit\" value=\"Supprimer\" class=\"delete-button\" onclick=\"return confirm('Confirmer la suppression du message id=" + std::to_string(m.id) + "?');\"/>"
                    "</form></div>";
        }

        item += "</li>\n";
        list += item;
    }

    list += "</ul>"; // Fermer la liste

    // Ajout du script JS pour la recherche et surbrillance de la meilleure phrase
    list += R"HTML(
    <script>
    (function(){
        const messagesList = document.getElementById('messagesList');
        const messageNodes = Array.from(messagesList.querySelectorAll('.message-item'));
        // docs contains node and original content (from data-original if present)
        const docs = messageNodes.map(node => {
            const contentNode = node.querySelector('.message-content');
            const original = contentNode.getAttribute('data-original') || contentNode.textContent || '';
            return { node, content: original };
        });

        function escapeHtml(str) {
            return String(str).replace(/[&<>"']/g, function(m) {
                return { '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[m];
            });
        }

        function tokenize(text) {
            return String(text).toLowerCase().split(/\W+/).filter(t => t.length > 0);
        }

        // Build document frequency (df)
        const df = {};
        docs.forEach(d => {
            const seen = new Set();
            const toks = tokenize(d.content);
            toks.forEach(t => {
                if (!seen.has(t)) {
                    df[t] = (df[t] || 0) + 1;
                    seen.add(t);
                }
            });
        });

        const N = docs.length;

        // Term counts per document
        const docTermMaps = docs.map(d => {
            const toks = tokenize(d.content);
            const termCount = {};
            toks.forEach(t => termCount[t] = (termCount[t] || 0) + 1);
            return { termCount, tokens: toks };
        });

        function idf(term) {
            const docFreq = df[term] || 0;
            return Math.log((N + 1) / (docFreq + 1)) + 1;
        }

        function dot(a,b) {
            let s = 0;
            for (const k in a) {
                if (b[k]) s += a[k] * b[k];
            }
            return s;
        }

        function cosineSim(a,b) {
            const na = Math.sqrt(Object.values(a).reduce((sum, val) => sum + val * val, 0));
            const nb = Math.sqrt(Object.values(b).reduce((sum, val) => sum + val * val, 0));
            return na === 0 || nb === 0 ? 0 : dot(a, b) / (na * nb);
        }

        function buildTfIdfVectorFromMap(map) {
            const vec = {};
            const total = Object.values(map).reduce((s,x) => s + x, 0) || 1;
            for (const t in map) {
                const tf = map[t] / total;
                vec[t] = tf * idf(t);
            }
            return vec;
        }

        const docVectors = docTermMaps.map(dtm => {
            const vec = {};
            const total = Object.values(dtm.termCount).reduce((s,x) => s + x, 0) || 1;
            for (const t in dtm.termCount) {
                const tf = dtm.termCount[t] / total;
                vec[t] = tf * idf(t);
            }
            return vec;
        });

        function queryVector(query) {
            const toks = tokenize(query);
            if (toks.length === 0) return {};
            const qc = {};
            toks.forEach(t => qc[t] = (qc[t] || 0) + 1);
            return buildTfIdfVectorFromMap(qc);
        }

        // Highlight the best matching sentence in a node (whole sentence highlighted, with matched terms emphasized)
function highlightBestPhraseInNode(node, content, query) {
    const contentNode = node.querySelector('.message-content');
    const original = content || '';
    // Diviser le contenu en phrases
    const sentences = original.split(/(?<=[.!?])\s+/).filter(s => s.trim().length > 0);
    const qTokens = tokenize(query);

    if (qTokens.length === 0) {
        contentNode.innerHTML = escapeHtml(original);
        return;
    }

    let bestSentenceIdx = -1;
    let bestScore = 0;

    sentences.forEach((sentence, idx) => {
        const sentenceTokens = tokenize(sentence);
        // Calculer le score de la phrase
        const score = qTokens.reduce((acc, token) => {
            return acc + ((sentenceTokens.includes(token) ? 1 : 0) * idf(token));
        }, 0);

        if (score > bestScore) {
            bestScore = score;
            bestSentenceIdx = idx;
        }
    });

    if (bestScore <= 0 || bestSentenceIdx < 0) {
        contentNode.innerHTML = escapeHtml(original);
        return;
    }

    // Surligner la phrase entière
    const chosenSentence = sentences[bestSentenceIdx];
    const highlightedSentence = `<span class="match">${escapeHtml(chosenSentence)}</span>`;

    // Remplacer la phrase choisie par la version surlignée
    sentences[bestSentenceIdx] = highlightedSentence;
    const safeParts = sentences.map((s, idx) => {
        return idx === bestSentenceIdx ? s : escapeHtml(s);
    });

    // Rejoindre les phrases pour produire le texte lisible
    contentNode.innerHTML = safeParts.join(' ');
}

        // Reset displayed content to original (escaped)
        function resetDocsDisplay() {
            docs.forEach(d => {
                const contentNode = d.node.querySelector('.message-content');
                contentNode.innerHTML = escapeHtml(d.content);
                d.node.classList.remove('highlight');
                d.node.style.display = '';
            });
            // remove any existing "no results" item
            const existingNo = document.getElementById('noResultsItem');
            if (existingNo) existingNo.remove();
        }

        window.searchMessages = function() {
            const raw = document.getElementById('searchInput').value.trim();
            const query = raw.toLowerCase();
            resetDocsDisplay();

            if (!query) {
                // show all
                docs.forEach(d => d.node.style.display = '');
                return;
            }

            const qvec = queryVector(query);
            const scores = docVectors.map((dv, i) => ({ idx: i, score: cosineSim(qvec, dv) }));
            scores.sort((a, b) => b.score - a.score);

            // Keep only docs with positive score (tunable threshold) and limit to e.g. top 50
            const visibleSet = new Set(scores.filter(s => s.score > 0).slice(0, 50).map(s => s.idx));

            docs.forEach((d, i) => {
                if (visibleSet.has(i)) {
                    d.node.style.display = '';
                    d.node.classList.add('highlight');
                    highlightBestPhraseInNode(d.node, d.content, query);
                } else {
                    d.node.style.display = 'none';
                }
            });

            // If no results, show a "no results" node
            if (!Array.from(visibleSet).length) {
                const noResults = document.createElement('li');
                noResults.id = 'noResultsItem';
                noResults.textContent = 'Aucun message trouvé.';
                noResults.className = 'meta';
                noResults.style.color = '#ff8080';
                noResults.style.marginTop = '8px';
                noResults.style.listStyle = 'none';
                messagesList.appendChild(noResults);
            }
        };

        // Initialize: ensure original content is preserved in data-original attribute (already set server-side)
        // and that message-content innerHTML is escaped
        docs.forEach(d => {
            const cn = d.node.querySelector('.message-content');
            cn.innerHTML = escapeHtml(d.content);
        });

    })();
    </script>
    )HTML";

    return list; // Retourner la liste complète des messages
}
 
Joined
Jun 12, 2020
Messages
60
Reaction score
3
Here’s a shortened version of the mini web server that integrates text clustering functionality in addition to tf-idf search. This server is not suitable for production use as is, but it can serve as a foundation for further development. The previous version was CRUD; this one only allows message addition and does not have a session system or user management. It’s just for demonstrating a 'portable' clustering system. If you notice any logical or mathematical errors, please feel free to let me know.

C++:
/*
MIT License
Copyright (c) 2025 CoTon_TiGe_MoUaRf

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>

#include <iostream>
#include <sstream>
#include <iomanip>
#include <string>
#include <vector>
#include <fstream>
#include <algorithm>
#include <cctype>
#include <mutex>

#pragma comment(lib, "Ws2_32.lib")

using std::string;
using std::vector;
using std::ifstream;
using std::ofstream;
using std::ostringstream;
using std::stringstream;
using std::cout;
using std::cerr;
using std::endl;

std::mutex g_file_mutex; // pour accès fichier simple (mono-thread here but future safe)

// ---------- utilitaires hex ----------
static inline std::string toHex(const std::string& input) {
    std::ostringstream oss;
    oss << std::hex << std::setfill('0');
    for (unsigned char c : input) {
        oss << std::setw(2) << static_cast<int>(c);
    }
    return oss.str();
}

static inline std::string fromHex(const std::string& hex) {
    std::string out;
    out.reserve(hex.size() / 2);
    auto hexval = [](char c)->int {
        if (c >= '0' && c <= '9') return c - '0';
        if (c >= 'a' && c <= 'f') return 10 + (c - 'a');
        if (c >= 'A' && c <= 'F') return 10 + (c - 'A');
        return -1;
    };
    size_t len = hex.size();
    for (size_t i = 0; i + 1 < len; i += 2) {
        int hi = hexval(hex[i]);
        int lo = hexval(hex[i+1]);
        if (hi < 0 || lo < 0) break;
        out.push_back(static_cast<char>((hi << 4) | lo));
    }
    return out;
}

// URL decoding minimal (handles %XX and +)
static inline std::string urlDecode(const std::string& s) {
    std::string out;
    out.reserve(s.size());
    for (size_t i = 0; i < s.size(); ++i) {
        char c = s[i];
        if (c == '+') out.push_back(' ');
        else if (c == '%' && i + 2 < s.size()) {
            auto hex = s.substr(i+1,2);
            char dec = static_cast<char>(std::strtol(hex.c_str(), nullptr, 16));
            out.push_back(dec);
            i += 2;
        } else out.push_back(c);
    }
    return out;
}

// ---------- modèle Message / Table ----------
struct Message {
    int id;
    std::string username;
    std::string content; // decoded content in memory
};

class MyTable {
private:
    vector<Message> messages;
    int nextId = 1;
    string dbFile;

public:
    MyTable(const string& filename = "database.txt") : dbFile(filename) { loadFromFile(); }

    void addMessage(const string& username, const string& content) {
        Message m;
        m.id = nextId++;
        m.username = username;
        m.content = content;
        messages.push_back(m);
        appendToFile(m);
    }

    const vector<Message>& getMessages() const { return messages; }

    void appendToFile(const Message& m) {
        std::lock_guard<std::mutex> lock(g_file_mutex);
        ofstream out(dbFile, std::ios::app | std::ios::binary);
        if (!out) {
            cerr << "Error opening DB file for append: " << dbFile << "\n";
            return;
        }
        // Format: id SP username SP hexcontent NEWLINE
        string hexc = toHex(m.content);
        out << m.id << ' ' << m.username << ' ' << hexc << '\n';
        out.close();
    }

    void loadFromFile() {
        std::lock_guard<std::mutex> lock(g_file_mutex);
        messages.clear();
        ifstream in(dbFile, std::ios::binary);
        if (!in) {
            // fichier absent = pas d'erreur grave
            return;
        }
        string line;
        int maxId = 0;
        while (std::getline(in, line)) {
            if (line.empty()) continue;
            std::istringstream iss(line);
            int id;
            string username;
            string hexcontent;
            if (!(iss >> id >> username >> hexcontent)) continue;
            Message m;
            m.id = id;
            m.username = username;
            m.content = fromHex(hexcontent);
            messages.push_back(m);
            if (id > maxId) maxId = id;
        }
        nextId = maxId + 1;
        in.close();
    }
};

// ---------- HTML templates (modern geek) ----------
static const char* CSS_STYLES = R"CSS(
:root{
  --bg:#0b0f1a; --card:#0f1724; --accent:#00f0ff; --accent2:#ff2bd8; --muted:#9aa7b2;
  --glass: rgba(255,255,255,0.04);
  font-synthesis: none;
}
*{box-sizing:border-box}
html,body{height:100%;margin:0;background:
 radial-gradient(1200px 600px at 10% 10%, rgba(0,255,255,0.04), transparent 6%),
 radial-gradient(1000px 500px at 90% 90%, rgba(255,43,216,0.03), transparent 8%),
 var(--bg);
 color:#dbeafe; font-family: Inter,Segoe UI,Roboto,Helvetica,Arial, sans-serif}
.container{max-width:980px;margin:36px auto;padding:28px}
.header{display:flex;align-items:center;gap:18px;margin-bottom:22px}
.logo{width:64px;height:64px;border-radius:12px;background:linear-gradient(135deg,var(--accent),var(--accent2));display:flex;align-items:center;justify-content:center;font-weight:700;color:#001012;box-shadow:0 6px 24px rgba(0,0,0,0.6)}
.title{font-size:20px;font-weight:700;letter-spacing:0.4px}
.sub{color:var(--muted);font-size:13px;margin-top:2px}
.card{background:linear-gradient(180deg, rgba(255,255,255,0.02), transparent);border:1px solid rgba(255,255,255,0.04);backdrop-filter: blur(6px);border-radius:12px;padding:18px;margin-bottom:18px;box-shadow:0 8px 30px rgba(2,6,23,0.6)}
.form-row{display:flex;gap:12px;flex-wrap:wrap}
.input,textarea{background:var(--glass);border:1px solid rgba(255,255,255,0.04);padding:10px 12px;border-radius:8px;color:#e6f7ff;min-width:0}
.input{height:40px;flex:0 1 220px}
textarea{min-height:84px;flex:1 1 420px;resize:vertical}
.btn{background:linear-gradient(90deg,var(--accent),var(--accent2));border:none;color:#001;padding:10px 14px;border-radius:10px;font-weight:700;cursor:pointer;box-shadow:0 8px 20px rgba(0,0,0,0.5)}
.btn:active{transform:translateY(1px)}
.msg-list{list-style:none;padding:0;margin:0}
.msg-item{display:flex;gap:12px;align-items:flex-start;padding:12px;border-radius:10px;border:1px solid rgba(255,255,255,0.02);margin-bottom:10px;background:linear-gradient(180deg, rgba(255,255,255,0.01), transparent)}
.avatar{width:44px;height:44px;border-radius:10px;background:linear-gradient(135deg,var(--accent2),var(--accent));display:flex;align-items:center;justify-content:center;font-weight:700;color:#001}
.meta{font-size:13px;color:var(--muted)}
.username{font-weight:800;color:#e6fffb}
.content{margin-top:6px;color:#d7f7ff;white-space:pre-wrap}
.footer{color:var(--muted);font-size:13px;margin-top:10px;text-align:center}
.small-hex{font-family:monospace;color:#9ff;opacity:0.9;font-size:12px;margin-top:6px}
.center{display:flex;justify-content:center}
)CSS";
static string htmlEscape(const string& s) {
    string out;
    out.reserve(s.size());
    for (char c : s) {
        switch (c) {
            case '&': out += "&amp;"; break;
            case '<': out += "&lt;"; break;
            case '>': out += "&gt;"; break;
            case '"': out += "&quot;"; break;
            case '\'': out += "&#39;"; break;
            default: out.push_back(c); break;
        }
    }
    return out;
}
// Page: index (form + link to messages)
static string buildIndexHtml() {
    ostringstream o;
    o << R"(<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">)";
    o << "<title>GeekBoard</title><style>" << CSS_STYLES << "</style></head><body>";
    o << R"(<div class="container"><div class="header"><div class="logo">GB</div><div><div class="title">GeekBoard — Neon Messages</div><div class="sub">Envoyez des messages — stockés en hex pour style et démonstration</div></div></div>)";
    o << R"(<div class="card"><form method="POST" action="/add" class="form-row">)";
    o << R"(<input class="input" name="username" placeholder="Nom d'utilisateur" required maxlength="32">)";
    o << R"(<textarea name="content" placeholder="Votre message..." required></textarea>)";
    o << R"(<div style="flex-basis:100%;display:flex;justify-content:flex-end"><button class="btn" type="submit">Envoyer</button></div>)";
    o << R"(</form></div>)";
    o << R"(<div class="card center"><a href="/messages" style="text-decoration:none"><button class="btn">Voir les messages</button></a></div>)";
    o << R"(<div class="footer">Serveur minimal — design modern geek • 04/10/2025</div>)";
    o << "</div></body></html>";
    return o.str();
}

// Page: messages list
static string buildMessagesHtml(const MyTable& table) {
    ostringstream o;
    o << "<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'>";
    o << "<title>GeekBoard — Messages</title><style>" << CSS_STYLES << "</style></head><body><div class='container'>";

    o << R"(<div class='header'><div class='logo'>GB</div><div><div class='title'>Messages</div><div class='sub'>Stockés en hex & affichés décodés</div></div></div>)";
    o << R"(<div class='card' style='display:flex;gap:10px;align-items:center;flex-wrap:wrap'>)";
    o << R"(<input id='searchInput' class='input' placeholder='Recherche par mot-clé...' style='flex:1' oninput='window.searchMessages && window.searchMessages()'>)";
    o << R"(<button class='btn' onclick='window.searchMessages && window.searchMessages()'>Rechercher</button>)";
    o << R"(</div>)";

    o << "<div class='card'><ul id='messagesList' class='msg-list' style='margin:0;padding:0'>";

    for (const auto& m : table.getMessages()) {
        string init = "U";
        if (!m.username.empty()) init = std::string(1, std::toupper((unsigned char)m.username[0]));
        string escapedUser = htmlEscape(m.username);
        string escapedContent = htmlEscape(m.content);
        string hex = toHex(m.content);
        string hexPreview = hex.substr(0, std::min<size_t>(hex.size(), 64));
        if (hex.size() > 64) hexPreview += "...";

        // prepare data-original attribute (escape single quote)
        string dataOriginal;
        dataOriginal.reserve(escapedContent.size());
        for (char c : escapedContent) {
            if (c == '\'') dataOriginal += "&#39;"; else dataOriginal.push_back(c);
        }

        o << "<li class='msg-item message-item' style='list-style:none;margin-bottom:10px;padding:12px;border-radius:10px;border:1px solid rgba(255,255,255,0.02);background:linear-gradient(180deg, rgba(255,255,255,0.01), transparent)'>";
        o << "<div style='display:flex;gap:12px;align-items:flex-start'>";
        o << "<div class='avatar'>" << init << "</div>";
        o << "<div style='flex:1'>";
        o << "<div class='username'>" << escapedUser << " <span class='meta'>#" << m.id << "</span></div>";
        o << "<div class='content message-content' data-original='" << dataOriginal << "'>" << escapedContent << "</div>";
        o << "<div class='small-hex'>hex: " << hexPreview << "</div>";
        o << "</div></div></li>";
    }

    o << "</ul></div>";

    o << "<div style='display:flex;gap:10px;justify-content:space-between;align-items:center'><a href='/'><button class='btn'>Nouvel</button></a><div class='sub'>Total: " << table.getMessages().size() << "</div></div>";
    o << "<div class='footer'>GeekBoard • messages stockés en hex</div>";
    o << "</div>";

    // Script : clustering + recherche + coloration phrase pertinente
    o << R"(<script>
(() => {
  const BASE_COLORS = ['#1f77b4','#ff7f0e','#2ca02c','#d62728','#9467bd','#8c564b','#e377c2','#7f7f7f','#bcbd22','#17becf'];
  const messagesList = document.getElementById('messagesList');
  if (!messagesList) return;
  const noResultsItem = document.createElement('li');
  noResultsItem.id = 'noResultsItem';
  noResultsItem.className = 'meta';
  noResultsItem.style.cssText = 'color:#ff8080;margin-top:8px;list-style:none;';

  const messageNodes = Array.from(messagesList.querySelectorAll('.message-item'));
  const docs = messageNodes.map(node => {
    const contentNode = node.querySelector('.message-content');
    const original = (contentNode && (contentNode.getAttribute('data-original') || contentNode.textContent)) || '';
    return { node, content: String(original) };
  });

  const escapeHtml = str => String(str).replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]));
  const tokenize = text => String(text||'').toLowerCase().split(/\W+/).filter(t=>t.length>0);
  const hexToRgba = (hex, alpha) => {
    const h = hex.replace('#','');
    const bigint = parseInt(h.length===3 ? h.split('').map(c=>c+c).join('') : h, 16);
    const r=(bigint>>16)&255, g=(bigint>>8)&255, b=bigint&255;
    return `rgba(${r}, ${g}, ${b}, ${alpha})`;
  };
  const clusterStyle = (hex, alphaBg, alphaBorder) => ({ backgroundColor: hexToRgba(hex, alphaBg), borderLeft: `6px solid ${hexToRgba(hex, alphaBorder)}` });
  const applyStyles = (node, styles) => Object.entries(styles).forEach(([k,v])=>node.style[k]=v);
  const clearNodeStyles = node => { node.classList.remove('highlight'); node.style.display=''; node.style.backgroundColor=''; node.style.borderLeft=''; node.style.boxShadow=''; node.querySelectorAll('.match').forEach(el => { el.style.background=''; el.style.padding=''; el.style.borderRadius=''; }); };

  const updateRelevanceScore = (node, score) => {
    let scoreEl = node.querySelector('.relevance-score');
    if (!scoreEl) {
      scoreEl = document.createElement('span');
      scoreEl.className = 'relevance-score';
      const meta = node.querySelector('.meta');
      if (meta) meta.appendChild(scoreEl);
    }
    scoreEl.textContent = ` Score: ${score.toFixed(2)}`;
    node.dataset.score = score.toFixed(2);
  };

  // df / term counts
  const df = Object.create(null);
  const docTermMaps = docs.map(d => {
    const toks = tokenize(d.content);
    const termCount = Object.create(null);
    const seen = new Set();
    toks.forEach(t => { termCount[t]=(termCount[t]||0)+1; if(!seen.has(t)){ df[t]=(df[t]||0)+1; seen.add(t); }});
    return { termCount, tokens: toks };
  });
  const N = docs.length;
  const idf = term => { const dfv = df[term]||0; return Math.log((N+1)/(dfv+1))+1; };

  const dot = (a,b) => { let s=0; for(const k in a) if(a[k] && b[k]) s+=a[k]*b[k]; return s; };
  const norm = v => { let s=0; for(const k in v) s += v[k]*v[k]; return Math.sqrt(s); };
  const cosineSim = (a,b) => { const na=norm(a), nb=norm(b); if(na===0||nb===0) return 0; return dot(a,b)/(na*nb); };

  const buildTfIdfFromTermCount = termCount => {
    const vec = Object.create(null);
    const total = Object.values(termCount).reduce((s,v)=>s+v,0)||1;
    for(const t in termCount){ const tf = termCount[t]/total; vec[t]=tf*idf(t); }
    return vec;
  };
  const docVectors = docTermMaps.map(dtm => buildTfIdfFromTermCount(dtm.termCount));
  const queryVector = query => {
    const toks = tokenize(query);
    if(!toks.length) return {};
    const qc = Object.create(null);
    toks.forEach(t=>qc[t]=(qc[t]||0)+1);
    return buildTfIdfFromTermCount(qc);
  };

  // kmeans
  const kMeansClustering = (vectors, K, opts={}) => {
    const maxIter = opts.maxIter||100, tol = opts.tol||1e-6, rng = opts.rng||Math.random;
    if(K<=0||vectors.length===0) return { assignments: [], centroids: [] };
    K = Math.min(K, vectors.length);
    const cloneVec = v => { const o = Object.create(null); for(const k in v) o[k]=v[k]; return o; };

    const centroids = [];
    centroids.push(cloneVec(vectors[Math.floor(rng()*vectors.length)]));
    while(centroids.length < K) {
      const distances = vectors.map(v => {
        let best = 0;
        for(const c of centroids){ const d = 1 - cosineSim(v,c); if(d>best) best=d; }
        return best;
      });
      const sum = distances.reduce((s,d)=>s+d,0);
      let pick = rng() * (sum||1);
      let idx=0;
      for(; idx<distances.length; idx++){ pick -= distances[idx]; if(pick<=0) break; }
      centroids.push(cloneVec(vectors[idx % vectors.length]));
    }

    const assignments = new Array(vectors.length).fill(-1);
    let iter=0, moved=true;
    while(iter < maxIter && moved) {
      moved = false;
      for(let i=0;i<vectors.length;i++){
        const v = vectors[i];
        let bestIdx=-1, bestSim=-Infinity;
        for(let j=0;j<centroids.length;j++){ const sim = cosineSim(v, centroids[j]); if(sim>bestSim){ bestSim=sim; bestIdx=j; } }
        if(assignments[i] !== bestIdx){ moved = true; assignments[i] = bestIdx; }
      }

      const newCentroids = [];
      for(let j=0;j<K;j++){
        const sumVec = Object.create(null);
        let count=0;
        for(let i=0;i<vectors.length;i++){
          if(assignments[i]===j){
            count++;
            const v=vectors[i];
            for(const t in v) sumVec[t]=(sumVec[t]||0)+v[t];
          }
        }
        if(count===0) newCentroids[j] = cloneVec(vectors[Math.floor(rng()*vectors.length)]);
        else { for(const t in sumVec) sumVec[t] /= count; newCentroids[j]=sumVec; }
      }

      let maxMove=0;
      for(let j=0;j<K;j++){ const sim = cosineSim(centroids[j], newCentroids[j]); const move = 1 - sim; if(move>maxMove) maxMove=move; }
      centroids.length=0;
      for(const c of newCentroids) centroids.push(c);
      if(maxMove <= tol) break;
      iter++;
    }
    return { assignments, centroids };
  };

  const highlightBestPhraseInNode = (node, content, query, highlightColor) => {
    const contentNode = node.querySelector('.message-content');
    if(!contentNode) return;
    const original = content || '';
    const sentences = original.split(/(?<=[.!?])\s+/).filter(s=>s.trim().length>0);
    const qTokens = tokenize(query);
    if(!qTokens.length){ contentNode.innerHTML = escapeHtml(original); return; }
    let bestIdx=-1, bestScore=0;
    sentences.forEach((s,i) => {
      const sTokens = tokenize(s);
      let score=0;
      qTokens.forEach(qt => { if(sTokens.includes(qt)) score += idf(qt); });
      if(sTokens.length) score /= sTokens.length;
      if(score > bestScore){ bestScore = score; bestIdx = i; }
    });
    if(bestIdx<0 || bestScore<=0){ contentNode.innerHTML = escapeHtml(original); return; }

    // wrap sentences; apply inline background to best sentence using highlightColor
    const highlighted = sentences.map((s,i) => {
      if(i===bestIdx) {
        return `<span class='match' style='background:${highlightColor};padding:3px;border-radius:6px;display:inline-block'>${escapeHtml(s)}</span>`;
      }
      return escapeHtml(s);
    }).join(' ');
    contentNode.innerHTML = highlighted;
    updateRelevanceScore(node, bestScore);
  };

  const resetDocsDisplay = () => {
    docs.forEach(d => {
      const cn = d.node.querySelector('.message-content');
      if(cn) cn.innerHTML = escapeHtml(d.content);
      clearNodeStyles(d.node);
      const scoreEl = d.node.querySelector('.relevance-score');
      if(scoreEl) scoreEl.remove();
      delete d.node.dataset.score;
      delete d.node.dataset.highlightColor;
    });
    const existingNoRes = document.getElementById('noResultsItem');
    if(existingNoRes) existingNoRes.remove();
  };

  const defaultK = Math.max(1, Math.min(6, Math.round(Math.sqrt(Math.max(1, docs.length)))));
  const K = defaultK;
  const { assignments } = kMeansClustering(docVectors, K, { maxIter: 200, tol: 1e-4 });

  const clusterColors = [];
  for(let i=0;i<K;i++) clusterColors.push(BASE_COLORS[i % BASE_COLORS.length]);

  docs.forEach((doc,i) => {
    const clusterId = assignments[i] != null && assignments[i] >= 0 ? assignments[i] : 0;
    const hex = clusterColors[clusterId];
    doc.node.dataset.cluster = String(clusterId);
    applyStyles(doc.node, clusterStyle(hex, 0.08, 0.22));
  });

  window.searchMessages = () => {
    const input = document.getElementById('searchInput');
    const query = (input?.value.trim() ?? '').toLowerCase();
    resetDocsDisplay();
    if(!query){
      docs.forEach(d => {
        d.node.style.display = '';
        const cid = Number(d.node.dataset.cluster) || 0;
        applyStyles(d.node, clusterStyle(BASE_COLORS[cid], 0.08, 0.22));
      });
      return;
    }

    const qVec = queryVector(query);
    const scores = docVectors.map((dv,i) => ({ i, score: cosineSim(qVec, dv) }));
    const visible = scores.filter(s => s.score > 0).sort((a,b)=>b.score-a.score).slice(0,50);
    const visibleSet = new Set(visible.map(v=>v.i));
    const clusterCount = {};
    visible.forEach(v => { const cid = docs[v.i].node.dataset.cluster || '0'; clusterCount[cid] = (clusterCount[cid]||0)+1; });
    const topCluster = Object.entries(clusterCount).reduce((best,[k,v]) => (!best||v>best[1] ? [k,v] : best), null)?.[0];

    // compute highlight colors per cluster (slightly lighter variants)
    const highlightColors = clusterColors.map(hex => {
      // semi-transparent rgba for inline highlight
      return hex.replace('#','') .length === 3 ? hex : hex; // keep hex; will convert in CSS below
    });

    docs.forEach((d, idx) => {
      if(visibleSet.has(idx)){
        d.node.style.display = '';
        d.node.classList.add('highlight');
        const cid = Number(d.node.dataset.cluster) || 0;
        const hex = BASE_COLORS[cid];
        const isTop = topCluster !== null && String(cid) === topCluster;
        applyStyles(d.node, clusterStyle(hex, isTop ? 0.18 : 0.10, isTop ? 0.36 : 0.22));
        d.node.style.boxShadow = isTop ? `0 6px 18px ${hexToRgba(hex,0.12)}` : '';

        // choose highlight color: use rgba of cluster color with alpha 0.18
        const highlightColor = hexToRgba(hex, 0.18);
        d.node.dataset.highlightColor = highlightColor;
        highlightBestPhraseInNode(d.node, d.content, query, highlightColor);
      } else {
        d.node.style.display = 'none';
      }
    });

    if(visible.length === 0){
      const fragment = document.createDocumentFragment();
      const li = noResultsItem.cloneNode(true);
      li.textContent = 'Aucun message trouvé.';
      fragment.appendChild(li);
      messagesList.appendChild(fragment);
    }
  };

  // initialize display
  docs.forEach(d => {
    const cn = d.node.querySelector('.message-content');
    if(cn) cn.innerHTML = escapeHtml(d.content);
  });
})();
</script>)";

    o << "</body></html>";
    return o.str();
}



// ---------- HTTP parsing & handling ----------
static string readRequestBody(const string& rawRequest) {
    // If POST, body after blank line (\r\n\r\n)
    size_t pos = rawRequest.find("\r\n\r\n");
    if (pos == string::npos) return "";
    return rawRequest.substr(pos + 4);
}

// Parse application/x-www-form-urlencoded key=val&...
static void parseFormUrlEncoded(const string& body, std::vector<std::pair<string,string>>& out) {
    size_t i = 0;
    while (i < body.size()) {
        size_t eq = body.find('=', i);
        if (eq == string::npos) break;
        string key = body.substr(i, eq - i);
        size_t amp = body.find('&', eq+1);
        string val;
        if (amp == string::npos) {
            val = body.substr(eq+1);
            i = body.size();
        } else {
            val = body.substr(eq+1, amp - (eq+1));
            i = amp + 1;
        }
        out.emplace_back(urlDecode(key), urlDecode(val));
    }
}

static string buildHttpResponse(const string& body, const string& contentType = "text/html; charset=utf-8", int code = 200, const string& extraHeaders = "") {
    std::ostringstream oss;
    oss << "HTTP/1.1 " << code << " OK\r\n";
    oss << "Content-Type: " << contentType << "\r\n";
    oss << "Content-Length: " << body.size() << "\r\n";
    if (!extraHeaders.empty()) oss << extraHeaders;
    oss << "Connection: close\r\n";
    oss << "\r\n";
    oss << body;
    return oss.str();
}

// Simplified request line parse (method and path)
struct RequestLine { string method; string path; string httpVer; };
static RequestLine parseRequestLine(const string& req) {
    RequestLine r;
    std::istringstream iss(req);
    iss >> r.method >> r.path >> r.httpVer;
    // strip query from path if present
    size_t q = r.path.find('?');
    if (q != string::npos) r.path = r.path.substr(0, q);
    return r;
}

static string getQueryPart(const string& rawRequest) {
    // extract full request line and find query part after GET /path?query HTTP/1.1
    std::istringstream iss(rawRequest);
    string method, url, ver;
    iss >> method >> url >> ver;
    size_t qm = url.find('?');
    if (qm == string::npos) return "";
    return url.substr(qm+1);
}

// Handler
static void handleRequest(const string& rawRequest, MyTable& table, string& outResponse) {
    if (rawRequest.empty()) { outResponse = buildHttpResponse("<h1>400 Bad Request</h1>", "text/html", 400); return; }
    RequestLine rl = parseRequestLine(rawRequest);
    if (rl.method == "GET") {
        string path;
        // extract path+query
        std::istringstream iss(rawRequest);
        iss >> rl.method >> path;
        size_t qm = path.find('?');
        if (path == "/" || path == "/index.html") {
            string body = buildIndexHtml();
            outResponse = buildHttpResponse(body);
            return;
        } else if (path == "/messages" || path == "/messages.html") {
            string body = buildMessagesHtml(table);
            outResponse = buildHttpResponse(body);
            return;
        } else {
            // 404
            string body = "<!doctype html><html><head><meta charset='utf-8'><title>404</title></head><body><h1>404 Not Found</h1><p>Resource not found.</p></body></html>";
            outResponse = buildHttpResponse(body, "text/html", 404);
            return;
        }
    } else if (rl.method == "POST") {
        // read body
        string body = readRequestBody(rawRequest);
        // we expect application/x-www-form-urlencoded
        vector<std::pair<string,string>> fields;
        parseFormUrlEncoded(body, fields);
        string username, content;
        for (auto& kv : fields) {
if (kv.first == "username") username = kv.second;
else if (kv.first == "content") content = kv.second;
}



    // Apply reasonable defaults / simple validation
    if (username.empty()) username = "Anonymous";
    if (content.empty()) {
        string body404 = "<!doctype html><html><head><meta charset='utf-8'><title>400</title></head><body><h1>400 Bad Request</h1><p>Content is empty.</p></body></html>";
        outResponse = buildHttpResponse(body404, "text/html", 400);
        return;
    }
    // Truncate to sane limits
    if (username.size() > 64) username = username.substr(0,64);
    if (content.size() > 4096) content = content.substr(0,4096);

    // Add to table (which appends hex to file)
    table.addMessage(username, content);

    // Redirect to messages page (POST-Redirect-GET)
    string redirectHeaders = "Location: /messages\r\n";
    outResponse = buildHttpResponse("", "text/html", 302, redirectHeaders);
    return;
} else {
    string body = "<!doctype html><html><head><meta charset='utf-8'><title>405</title></head><body><h1>405 Method Not Allowed</h1></body></html>";
    outResponse = buildHttpResponse(body, "text/html", 405);
    return;
}

}

// ---------- server main loop ----------
int main() {
WSADATA wsaData;
int res = WSAStartup(MAKEWORD(2,2), &wsaData);
if (res != 0) {
cerr << "WSAStartup failed: " << res << "\n";
return 1;
}



struct addrinfo hints;
struct addrinfo* addr = nullptr;
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
hints.ai_flags = AI_PASSIVE;

res = getaddrinfo(nullptr, "8000", &hints, &addr);
if (res != 0) {
    cerr << "getaddrinfo failed: " << res << "\n";
    WSACleanup();
    return 1;
}

SOCKET listen_socket = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol);
if (listen_socket == INVALID_SOCKET) {
    cerr << "socket failed: " << WSAGetLastError() << "\n";
    freeaddrinfo(addr);
    WSACleanup();
    return 1;
}

// Allow rapid restarts
int opt = 1;
setsockopt(listen_socket, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt));

res = bind(listen_socket, addr->ai_addr, (int)addr->ai_addrlen);
if (res == SOCKET_ERROR) {
    cerr << "bind failed: " << WSAGetLastError() << "\n";
    closesocket(listen_socket);
    freeaddrinfo(addr);
    WSACleanup();
    return 1;
}

freeaddrinfo(addr);

if (listen(listen_socket, SOMAXCONN) == SOCKET_ERROR) {
    cerr << "listen failed: " << WSAGetLastError() << "\n";
    closesocket(listen_socket);
    WSACleanup();
    return 1;
}

MyTable table("database.txt"); // charge existant

cout << "Server running on http://localhost:8000/  (CTRL-C to quit)\n";

while (true) {
    SOCKET client = accept(listen_socket, nullptr, nullptr);
    if (client == INVALID_SOCKET) {
        cerr << "accept failed: " << WSAGetLastError() << "\n";
        break;
    }

    // receive (simple, single read)
    const int BUF_SZ = 8192;
    std::vector<char> buffer(BUF_SZ + 1);
    int bytes = recv(client, buffer.data(), BUF_SZ, 0);
    if (bytes > 0) {
        buffer[bytes] = '\0';
        string rawRequest(buffer.data(), bytes);
        string response;
        handleRequest(rawRequest, table, response);

        // send response (may be empty for 302 with no body)
        int sent = 0;
        int total = (int)response.size();
        const char* data = response.c_str();
        while (sent < total) {
            int s = send(client, data + sent, total - sent, 0);
            if (s == SOCKET_ERROR) {
                cerr << "send failed: " << WSAGetLastError() << "\n";
                break;
            }
            sent += s;
        }
    }

    shutdown(client, SD_SEND);
    closesocket(client);
}

closesocket(listen_socket);
WSACleanup();
return 0;

}
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
474,432
Messages
2,571,680
Members
48,796
Latest member
Greg L.

Latest Threads

Top