first commit: echo server

This commit is contained in:
Antonio Cossari
2026-09-07 00:45:35 +02:00
commit b15ce130f9
47 changed files with 2614 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
/* ************************************************************************** */
/* */
/* ::: :::::::: */
/* Log.hpp :+: :+: :+: */
/* +:+ +:+ +:+ */
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */
/* Created: 2026/08/19 20:30:58 by acossari #+# #+# */
/* Updated: 2026/08/20 22:18:40 by acossari ### ########.fr */
/* */
/* ************************************************************************** */
#pragma once
#include <format>
#include <string_view>
// Free functions in a namespace, not a class: the whole process shares one
// verbosity level and one destination, so a second instance would have nothing
// of its own to hold.
namespace webserv::log {
// The "class" keyword here is overloaded: not a class but a scoped enum.
// Names must be written Level::Info, plus no implicit conversion to int.
enum class Level { Debug, Info, Warn, Error };
// Thin Template Idiom: vlog keeps one ordinary body instead of repeating it
// for every argument type combination. The small wrappers retain compile time
// checks, erase those types and converge here at the shared sink. Its v follows
// the C variadic convention. The sink filters by level before formatting.
void vlog(Level messageLevel, std::string_view formatString,
std::format_args args);
// format_string uses Args to check at compile time that every placeholder
// receives a value of the expected type. Args&& is a "forwarding reference",
// so the same wrapper accepts lvalues and rvalues, as in info("{}", value)
// and info("{}", 42), without copying them.
template <typename... Args>
void debug(std::format_string<Args...> formatString, Args&&... args) {
// make_format_args packs the arguments together with their type information.
// vlog then sees only std::format_args and does not need a separate template
// version for every combination of argument types.
vlog(Level::Debug, formatString.get(), std::make_format_args(args...));
}
template <typename... Args>
void info(std::format_string<Args...> formatString, Args&&... args) {
vlog(Level::Info, formatString.get(), std::make_format_args(args...));
}
template <typename... Args>
void warn(std::format_string<Args...> formatString, Args&&... args) {
vlog(Level::Warn, formatString.get(), std::make_format_args(args...));
}
template <typename... Args>
void error(std::format_string<Args...> formatString, Args&&... args) {
vlog(Level::Error, formatString.get(), std::make_format_args(args...));
}
} // namespace webserv::log
+27
View File
@@ -0,0 +1,27 @@
/* ************************************************************************** */
/* */
/* ::: :::::::: */
/* Result.hpp :+: :+: :+: */
/* +:+ +:+ +:+ */
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */
/* Created: 2026/08/20 20:02:19 by acossari #+# #+# */
/* Updated: 2026/08/21 20:02:26 by acossari ### ########.fr */
/* */
/* ************************************************************************** */
#pragma once
#include <expected>
#include "http/HttpStatus.hpp"
namespace webserv {
// Discarding a Result can hide a request error. Neither this alias nor
// std::expected can enforce [[nodiscard]], so every function returning a
// Result must declare the attribute itself.
template <typename T>
using Result = std::expected<T, HttpStatus>;
} // namespace webserv
+78
View File
@@ -0,0 +1,78 @@
/* ************************************************************************** */
/* */
/* ::: :::::::: */
/* Server.hpp :+: :+: :+: */
/* +:+ +:+ +:+ */
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */
/* Created: 2026/09/01 09:13:02 by acossari #+# #+# */
/* Updated: 2026/09/01 19:00:54 by acossari ### ########.fr */
/* */
/* ************************************************************************** */
#pragma once
#include <sys/socket.h>
#include <chrono>
#include <cstdint>
#include <optional>
#include <span>
#include <vector>
#include "event/Epoll.hpp"
#include "net/Listener.hpp"
#include "net/PauseMask.hpp"
#include "net/SlotPool.hpp"
namespace webserv {
class Server final {
public:
// Both constants use direct-list-initialization to call the explicit
// duration constructor. A bare "= 500" would need an implicit conversion
// from int to std::chrono::milliseconds.
// After accept() fails, listeners are temporarily disarmed before
// retrying. Level-triggered epoll would otherwise keep
// reporting the ready listener and create a busy loop. The 500 ms delay
// matches nginx's default.
static constexpr std::chrono::milliseconds ACCEPT_BACKOFF{500};
// A rare, documented race exists if SIGINT arrives after the shutdown
// flag is checked but before epoll_wait begins. "epoll_pwait" closes it
// atomically, but the subject does not allow it. Bounding an idle wait lets
// run() recheck the flag within 500 ms. This interval is a trade-off, not a
// measured optimum, and causes at most two timeout wakeups per idle second.
static constexpr std::chrono::milliseconds MAX_IDLE_WAIT{500};
// TEMP
// Endpoints are hardcoded in main() until the config parser is available.
explicit Server(std::span<const sockaddr_storage> endpoints);
void run();
private:
void dispatch(std::uint64_t packedToken, std::uint32_t events);
void acceptFrom(std::uint32_t listenerIndex);
void handleAcceptFailure(std::uint32_t listenerIndex, int errnoCode);
void startBackoff();
void retire(std::uint32_t slotIndex) noexcept;
void stopListener(std::uint32_t listenerIndex) noexcept;
void updateRegistration(std::uint32_t slotIndex, std::uint32_t events);
void pause(ListenerPause reason);
void resume(ListenerPause reason);
void applyListenerEvents(std::uint32_t events);
void expireBackoff();
int millisUntilDeadline() const;
Epoll epoll_;
std::vector<Listener> listeners_;
SlotPool slotPool_;
PauseMask pauseMask_;
// Stores a fixed deadline because loop iterations have no fixed duration.
// Empty means no resource backoff is scheduled.
std::optional<std::chrono::steady_clock::time_point> backoffExpiry_;
};
} // namespace webserv
+57
View File
@@ -0,0 +1,57 @@
/* ************************************************************************** */
/* */
/* ::: :::::::: */
/* Epoll.hpp :+: :+: :+: */
/* +:+ +:+ +:+ */
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */
/* Created: 2026/08/26 11:34:23 by acossari #+# #+# */
/* Updated: 2026/08/26 11:34:25 by acossari ### ########.fr */
/* */
/* ************************************************************************** */
#pragma once
#include <sys/epoll.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <exception>
#include <span>
#include <string>
#include "net/FileDescriptor.hpp"
namespace webserv {
class Epoll final {
public:
class Error : public std::exception {
public:
explicit Error(std::string message);
const char* what() const noexcept override;
private:
std::string msg_;
};
// wait() returns at most this many ready events per call. Smaller batches
// require more calls, while larger batches delay the next loop iteration.
// The value is a middle ground. Excess events remain for later calls.
static constexpr std::size_t EVENT_BATCH_CAPACITY = 256;
Epoll();
void add(int fd, std::uint32_t events, std::uint64_t token);
void modify(int fd, std::uint32_t events, std::uint64_t token);
void remove(int fd) noexcept;
std::span<const epoll_event> wait(int timeoutMillis);
private:
FileDescriptor fd_;
std::array<epoll_event, EVENT_BATCH_CAPACITY> events_{};
};
} // namespace webserv
+56
View File
@@ -0,0 +1,56 @@
/* ************************************************************************** */
/* */
/* ::: :::::::: */
/* EventToken.hpp :+: :+: :+: */
/* +:+ +:+ +:+ */
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */
/* Created: 2026/08/25 13:26:42 by acossari #+# #+# */
/* Updated: 2026/08/25 19:06:13 by acossari ### ########.fr */
/* */
/* ************************************************************************** */
#pragma once
#include <cstdint>
namespace webserv {
enum class EndpointKind {
ClientSocket,
CgiStdin,
CgiStdout,
Listener,
};
// Identifies which registered endpoint produced an epoll event without storing
// a pointer. For Listener, index selects a listener; for every other kind it
// selects a connection slot, while kind identifies its client socket or CGI
// pipe. epoll_wait() returns a batch in which an old event may remain after its
// endpoint has been retired and re-registered. generation distinguishes that
// old registration from the current one even when index and kind are equal.
//
// Packed layout in epoll_event.data.u64:
// bits 0-29 | index | 30 bits
// bits 30-31 | kind | 2 bits
// bits 32-63 | generation | 32 bits
struct EventToken {
static constexpr std::uint64_t INDEX_BITS = 30;
static constexpr std::uint64_t KIND_BITS = 2;
// List-initialization gives 1 the exact std::uint32_t type before shifting.
// std::uint32_t{1} << INDEX_BITS = 1,073,741,824
// (std::uint32_t{1} << INDEX_BITS) - 1 = 1,073,741,823
// Binary: 100...000 (30 zeros) - 1 = 011...111 (30 ones).
static constexpr std::uint32_t MAX_INDEX =
(std::uint32_t{1} << INDEX_BITS) - 1;
std::uint32_t index;
EndpointKind kind;
std::uint32_t generation;
};
std::uint64_t pack(EventToken token);
EventToken unpack(std::uint64_t token);
} // namespace webserv
+50
View File
@@ -0,0 +1,50 @@
/* ************************************************************************** */
/* */
/* ::: :::::::: */
/* HttpStatus.hpp :+: :+: :+: */
/* +:+ +:+ +:+ */
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */
/* Created: 2026/08/20 19:29:20 by acossari #+# #+# */
/* Updated: 2026/08/21 19:32:48 by acossari ### ########.fr */
/* */
/* ************************************************************************** */
#pragma once
namespace webserv {
enum class HttpStatus {
// Success
Ok = 200,
Created = 201,
NoContent = 204,
// Redirection
MovedPermanently = 301,
Found = 302,
SeeOther = 303,
TemporaryRedirect = 307,
PermanentRedirect = 308,
// Client errors
BadRequest = 400,
Forbidden = 403,
NotFound = 404,
MethodNotAllowed = 405,
RequestTimeout = 408,
LengthRequired = 411,
ContentTooLarge = 413,
UriTooLong = 414,
RequestHeaderFieldsTooLarge = 431,
// Server errors
InternalServerError = 500,
NotImplemented = 501,
BadGateway = 502,
ServiceUnavailable = 503,
GatewayTimeout = 504,
HttpVersionNotSupported = 505,
};
} // namespace webserv
+61
View File
@@ -0,0 +1,61 @@
/* ************************************************************************** */
/* */
/* ::: :::::::: */
/* ByteBuffer.hpp :+: :+: :+: */
/* +:+ +:+ +:+ */
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */
/* Created: 2026/08/25 09:30:45 by acossari #+# #+# */
/* Updated: 2026/08/25 13:26:00 by acossari ### ########.fr */
/* */
/* ************************************************************************** */
#pragma once
#include <cstddef>
#include <span>
#include <vector>
namespace webserv {
class ByteBuffer final {
public:
// The fixed 64 KiB capacity is also the maximum request-head size. It
// leaves room for large headers while keeping memory bounded. The server
// accepts at most 256 simultaneous connections, each with one receive and
// one send buffer. In the worst-case scenario, their storage is 32 MiB, a
// modest fixed ceiling for all connection buffers combined.
// The C++23 uz suffix makes 64 a std::size_t, so multiplication occurs in
// std::size_t instead of int and avoids implicit widening (clang-tidy).
static constexpr std::size_t CAPACITY = 64uz * 1024;
std::span<const char> readable() const {
return {storage_.data() + readPos_, writePos_ - readPos_};
}
std::span<char> writableTail() {
return {storage_.data() + writePos_, CAPACITY - writePos_};
}
// Makes count bytes already written in writableTail() readable. count must
// not exceed writableTail().size().
void commit(std::size_t count);
// Discards count bytes from the start of readable(). count must not exceed
// readable().size().
void consume(std::size_t count);
void compact();
private:
// C++17 guaranteed copy elision constructs this vector directly in
// storage_, with no temporary vector or move.
std::vector<char> storage_ = std::vector<char>(CAPACITY);
// Only bytes from readPos_ up to, but not including, writePos_ are live.
// Values outside this range can remain in storage_ and are ignored.
std::size_t readPos_ = 0;
std::size_t writePos_ = 0;
};
} // namespace webserv
+71
View File
@@ -0,0 +1,71 @@
/* ************************************************************************** */
/* */
/* ::: :::::::: */
/* Connection.hpp :+: :+: :+: */
/* +:+ +:+ +:+ */
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */
/* Created: 2026/08/28 09:21:04 by acossari #+# #+# */
/* Updated: 2026/08/29 18:07:36 by acossari ### ########.fr */
/* */
/* ************************************************************************** */
#pragma once
#include <cstddef>
#include <cstdint>
#include "net/ByteBuffer.hpp"
#include "net/FileDescriptor.hpp"
namespace webserv {
class Connection final {
public:
enum class Lifecycle { Alive, Done, Fatal };
struct Outcome {
Lifecycle lifecycle;
std::uint32_t desiredEvents;
};
// ByteBuffer::CAPACITY is 64 KiB.
// At most 16 KiB are copied per 48 KiB appended: 16 / 48 = 1 / 3.
// A lower threshold reduces copying but leaves less unread work available.
// The 16 KiB value is a fixed compromise, not a measured optimum.
static constexpr std::size_t LOW_WATERMARK = 16uz * 1024;
explicit Connection(FileDescriptor socket);
// Each Connection has a unique identity and remains in its original slot.
Connection(const Connection&) = delete;
Connection& operator=(const Connection&) = delete;
Connection(Connection&&) = delete;
Connection& operator=(Connection&&) = delete;
int fd() const noexcept { return socket_.get(); }
Outcome onClientEvent(std::uint32_t events);
std::uint32_t desiredEvents() noexcept;
void close() noexcept { socket_.reset(); }
private:
// recv() returning zero means the client has finished sending data.
// readClosed_ records this. receive() still returns true because this
// is not an error and the socket may still send its buffered response.
// Only a recv() error returns false.
bool receive();
bool transmit();
// TEMP echo path: queues received bytes unchanged for sending back to
// the client.
void forwardReceivedBytes();
FileDescriptor socket_;
ByteBuffer receiveBuffer_;
ByteBuffer sendBuffer_;
bool readClosed_ = false;
};
} // namespace webserv
+61
View File
@@ -0,0 +1,61 @@
/* ************************************************************************** */
/* */
/* ::: :::::::: */
/* FileDescriptor.hpp :+: :+: :+: */
/* +:+ +:+ +:+ */
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */
/* Created: 2026/08/24 16:24:03 by acossari #+# #+# */
/* Updated: 2026/08/24 20:05:59 by acossari ### ########.fr */
/* */
/* ************************************************************************** */
#pragma once
#include <exception>
#include <string>
namespace webserv {
// Sole owner of one file descriptor, move-only. Copying would give two owners
// the same number, and the second close would land on whatever the kernel had
// handed out in the meantime, killing an unrelated connection.
class FileDescriptor final {
public:
class Error : public std::exception {
public:
explicit Error(std::string message);
const char* what() const noexcept override;
private:
std::string msg_;
};
FileDescriptor() = default;
// Takes ownership and sets FD_CLOEXEC, so a successful execve() in a
// forked child closes the descriptor before the new program starts. A
// negative fd is accepted and gives an invalid object instead of throwing,
// so the result of socket() can be passed in and checked with valid().
explicit FileDescriptor(int fd);
~FileDescriptor();
FileDescriptor(const FileDescriptor&) = delete;
FileDescriptor& operator=(const FileDescriptor&) = delete;
FileDescriptor(FileDescriptor&& other) noexcept;
FileDescriptor& operator=(FileDescriptor&& other) noexcept;
int get() const noexcept { return fd_; }
bool valid() const noexcept { return fd_ >= 0; }
void setNonBlocking();
// Closes now instead of at destruction. Idempotent.
void reset() noexcept;
private:
int fd_ = -1;
};
} // namespace webserv
+48
View File
@@ -0,0 +1,48 @@
/* ************************************************************************** */
/* */
/* ::: :::::::: */
/* Listener.hpp :+: :+: :+: */
/* +:+ +:+ +:+ */
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */
/* Created: 2026/08/27 15:52:02 by acossari #+# #+# */
/* Updated: 2026/09/01 18:10:13 by acossari ### ########.fr */
/* */
/* ************************************************************************** */
#pragma once
#include <sys/socket.h>
#include <exception>
#include <string>
#include "net/FileDescriptor.hpp"
namespace webserv {
class Listener final {
public:
class Error : public std::exception {
public:
explicit Error(std::string message);
const char* what() const noexcept override;
private:
std::string msg_;
};
explicit Listener(const sockaddr_storage& address);
int fd() const noexcept { return fd_.get(); }
// Delegates the validity check without exposing the owned FileDescriptor.
bool valid() const noexcept { return fd_.valid(); }
void close() noexcept { fd_.reset(); }
private:
FileDescriptor fd_;
};
} // namespace webserv
+37
View File
@@ -0,0 +1,37 @@
/* ************************************************************************** */
/* */
/* ::: :::::::: */
/* PauseMask.hpp :+: :+: :+: */
/* +:+ +:+ +:+ */
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */
/* Created: 2026/08/25 20:00:34 by acossari #+# #+# */
/* Updated: 2026/08/25 20:00:38 by acossari ### ########.fr */
/* */
/* ************************************************************************** */
#pragma once
#include <cstdint>
namespace webserv {
// PauseMask stores its flags in an 8-bit integer. Using the same type here
// makes the compiler reject a ninth flag, which would not fit in the mask.
enum class ListenerPause : std::uint8_t {
PoolFull = 1U << 0,
AcceptBackoff = 1U << 1,
};
class PauseMask final {
public:
// set() returns true when the first reason is added; clear() returns true
// when the last is removed. Those transitions disarm and rearm listeners.
[[nodiscard]] bool set(ListenerPause reason);
[[nodiscard]] bool clear(ListenerPause reason);
private:
std::uint8_t bits_ = 0;
};
} // namespace webserv
+104
View File
@@ -0,0 +1,104 @@
/* ************************************************************************** */
/* */
/* ::: :::::::: */
/* SlotPool.hpp :+: :+: :+: */
/* +:+ +:+ +:+ */
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */
/* Created: 2026/08/28 21:51:32 by acossari #+# #+# */
/* Updated: 2026/08/29 20:33:13 by acossari ### ########.fr */
/* */
/* ************************************************************************** */
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <vector>
#include "event/EventToken.hpp"
#include "net/Connection.hpp"
#include "net/FileDescriptor.hpp"
namespace webserv {
// SlotPool uses fixed storage instead of growing a container for every new
// connection. This representation provides three guarantees:
//
// 1. Every slot keeps a permanent index and stable storage. EventToken pairs
// that index with an endpoint generation so stale events can be rejected.
//
// 2. The slot array is allocated once. Connections are constructed and
// destroyed in place, so their slots are never relocated.
//
// 3. The free list limits the pool to CAPACITY connections. acquire() returns
// no index when it is exhausted, and drain() makes retired slots reusable.
class SlotPool final {
public:
// Slot indices use uint32_t to match their field in the 64-bit EventToken.
// 256 is a calculated tradeoff, not an optimal value. It doubles the
// target of 128 concurrent clients exercised by the provided tester. It
// assumes a conservative limit of 1024 fds per process, common on Linux.
//
// fd budget = 4 base fds (standard streams and epoll)
// + L listener fds
// + 2 * 256 connection fds (socket + possible temporary file)
// + 2 * 24 CGI pipe fds (the tester reaches 20, leaving 4 spare)
// + 8 spare fds (temporary setup and general working margin)
// = 572 + L
//
// This leaves space for up to 452 listeners, giving substantial headroom.
// A capacity of 512 would require 1084 + L fds, already exceeding 1024.
static constexpr std::uint32_t CAPACITY = 256;
// Each slot tracks the client socket and the CGI stdin and stdout pipes.
static constexpr std::size_t ENDPOINTS_PER_SLOT = 3;
struct Endpoint {
std::uint32_t generation = 0;
std::uint32_t registeredEvents = 0;
bool registered = false;
};
SlotPool();
bool full() const noexcept { return freeHead_ == CAPACITY; }
// Constructs a Connection for the socket in a free slot and returns its
// index. Returns std::nullopt when the pool is full.
std::optional<std::uint32_t> acquire(FileDescriptor socket);
// Returns true only if the token still identifies a registered endpoint
// in an active slot with the same generation.
bool isCurrent(const EventToken& token) const noexcept;
// Accesses the connection or endpoint metadata already stored in a slot.
Connection& connection(std::uint32_t index);
Endpoint& endpoint(std::uint32_t index, EndpointKind kind);
// Quarantines an active slot until the current event batch is complete.
void retire(std::uint32_t index) noexcept;
// Recycles all quarantined slots and returns how many became available.
std::size_t drain();
private:
enum class SlotState { Free, InUse, Retired };
struct Slot {
std::array<Endpoint, ENDPOINTS_PER_SLOT> endpoints;
std::optional<Connection> connection;
SlotState state = SlotState::Free;
std::uint32_t nextFree = 0;
};
std::array<Slot, CAPACITY> slots_;
// Holds retired slot indices until they can return to the free list.
// With storage reserved for CAPACITY entries, it acts as if it were a fixed
// array while tracking how many entries are active without another counter.
std::vector<std::uint32_t> pendingFree_;
std::uint32_t freeHead_ = 0;
};
} // namespace webserv