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:
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!
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 += "&"; break;
case '<': out += "<"; break;
case '>': out += ">"; break;
case '"': out += """; break;
case '\'': out += "'"; 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";