first commit: echo server
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# Style contract for this repo. Taken from the CPP modules, so both codebases
|
||||
# read the same way, with the Standard line corrected — see below.
|
||||
# Google as the base (2-space indent, K&R braces, west-const pointers).
|
||||
---
|
||||
Language: Cpp
|
||||
BasedOnStyle: Google
|
||||
|
||||
# `Standard` only tells clang-format how to *parse* the source; it has nothing to
|
||||
# do with what the compiler is given. There is no `c++23` value — the enum stops
|
||||
# at `c++20` plus `Latest` and `Auto` (verified against clang-format 18.1.3:
|
||||
# c++23 rejected, c++20/Latest/Auto accepted), and an unknown value makes
|
||||
# clang-format refuse the whole file with "unknown enumerated scalar", not fall
|
||||
# back to a default. `Latest` is the value that means "the newest this binary
|
||||
# knows". The language standard is set once, in the Makefile: `-std=c++23`.
|
||||
Standard: Latest
|
||||
|
||||
# 80 columns, straight from the Google C++ Style Guide ("Line Length"). This is
|
||||
# the dominant limit across C++ house styles: Google, LLVM, Chromium, Mozilla,
|
||||
# Abseil, folly. The 100 used by Rust/Kotlin/Qt is a newer-language convention
|
||||
# and does not carry over. Nothing to do with norminette, which is a C rule.
|
||||
ColumnLimit: 80
|
||||
|
||||
IndentWidth: 2
|
||||
# Access specifiers get their own indent level: `public:` at 2, members at 4.
|
||||
IndentAccessModifiers: true
|
||||
|
||||
# `T& name` / `T* name` — west style.
|
||||
PointerAlignment: Left
|
||||
ReferenceAlignment: Pointer
|
||||
|
||||
# One-liner bodies allowed only for functions defined inside a class — trivial
|
||||
# accessors in headers. An out-of-line definition always gets a real body.
|
||||
# (The CPP modules use None; Google and LLVM both default to All. Inline is the
|
||||
# middle ground, and the only part of this file that deviates from them.)
|
||||
AllowShortFunctionsOnASingleLine: Inline
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
|
||||
# Include order is deliberate (own header first, then std by topic) — don't shuffle.
|
||||
SortIncludes: false
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
# Quality contract for this repo, machine-readable half. ".clang-format" decides
|
||||
# how the code is laid out; this file decides what it is allowed to say. The two
|
||||
# are meant to agree: FormatStyle below points clang-tidy at ".clang-format", so
|
||||
# "--fix" never reformats against the house style.
|
||||
#
|
||||
# Run it with "make tidy" — a separate target, not part of "make", and it does
|
||||
# not fail the build (WarningsAsErrors is empty). The compile flags reach
|
||||
# clang-tidy after the "--", which is why no compile_commands.json is needed.
|
||||
---
|
||||
Checks: >
|
||||
bugprone-*,
|
||||
performance-*,
|
||||
modernize-*,
|
||||
portability-*,
|
||||
clang-analyzer-*,
|
||||
misc-definitions-in-headers,
|
||||
misc-misplaced-const,
|
||||
misc-unused-parameters,
|
||||
misc-unused-using-decls,
|
||||
readability-identifier-naming,
|
||||
readability-redundant-*,
|
||||
readability-container-size-empty,
|
||||
readability-misleading-indentation,
|
||||
readability-simplify-boolean-expr,
|
||||
readability-string-compare,
|
||||
-bugprone-easily-swappable-parameters,
|
||||
-modernize-use-trailing-return-type,
|
||||
-modernize-use-nodiscard,
|
||||
-performance-enum-size
|
||||
|
||||
# Why those four are off:
|
||||
# bugprone-easily-swappable-parameters — fires on any two adjacent parameters
|
||||
# of the same type, so every "(int fd, int events)" in a network codebase.
|
||||
# modernize-use-trailing-return-type — would rewrite every signature as
|
||||
# "auto f() -> T", which contradicts the Google base of ".clang-format".
|
||||
# modernize-use-nodiscard — wants [[nodiscard]] on essentially every const
|
||||
# accessor; the contract puts it where it earns its place, on Result<T>.
|
||||
# performance-enum-size — suggests a narrower underlying type for every enum;
|
||||
# pure churn on enums that hold a handful of values.
|
||||
|
||||
# Only our own headers. System headers are excluded by clang-tidy anyway unless
|
||||
# -system-headers is passed, and standard library headers have no extension, so
|
||||
# this pattern cannot reach them.
|
||||
HeaderFilterRegex: '.*\.(hpp|tpp|ipp)$'
|
||||
|
||||
# Empty on purpose: "make tidy" reports, it does not block. A single overzealous
|
||||
# check must never be able to stop the build.
|
||||
WarningsAsErrors: ''
|
||||
|
||||
FormatStyle: file
|
||||
|
||||
CheckOptions:
|
||||
# Types.
|
||||
readability-identifier-naming.ClassCase: CamelCase
|
||||
readability-identifier-naming.StructCase: CamelCase
|
||||
readability-identifier-naming.UnionCase: CamelCase
|
||||
readability-identifier-naming.EnumCase: CamelCase
|
||||
readability-identifier-naming.TypeAliasCase: CamelCase
|
||||
readability-identifier-naming.TypedefCase: CamelCase
|
||||
readability-identifier-naming.TemplateParameterCase: CamelCase
|
||||
|
||||
# Enumerators follow the type: EndpointKind::Listener, ListenerPause::PoolFull.
|
||||
readability-identifier-naming.EnumConstantCase: CamelCase
|
||||
|
||||
readability-identifier-naming.NamespaceCase: lower_case
|
||||
|
||||
# Functions and variables.
|
||||
readability-identifier-naming.FunctionCase: camelBack
|
||||
readability-identifier-naming.MethodCase: camelBack
|
||||
readability-identifier-naming.VariableCase: camelBack
|
||||
readability-identifier-naming.ParameterCase: camelBack
|
||||
|
||||
# Data members: trailing underscore on the ones that are not public. A public
|
||||
# member belongs to a plain aggregate (Header{name, value}) and takes none.
|
||||
readability-identifier-naming.MemberCase: camelBack
|
||||
readability-identifier-naming.PublicMemberCase: camelBack
|
||||
readability-identifier-naming.PrivateMemberCase: camelBack
|
||||
readability-identifier-naming.PrivateMemberSuffix: _
|
||||
readability-identifier-naming.ProtectedMemberCase: camelBack
|
||||
readability-identifier-naming.ProtectedMemberSuffix: _
|
||||
|
||||
# Constants are UPPER_SNAKE_CASE only where they are constants in the sense
|
||||
# that matters — namespace scope, static, or a class-level constant such as
|
||||
# ByteBuffer::CAPACITY. ConstexprVariableCase and LocalConstantCase are left
|
||||
# unset deliberately: clang-tidy only uses a style kind it has been given a
|
||||
# value for, so an unset one falls through to the next candidate, and a
|
||||
# "constexpr" inside a function lands on VariableCase and stays camelBack.
|
||||
readability-identifier-naming.GlobalConstantCase: UPPER_CASE
|
||||
readability-identifier-naming.StaticConstantCase: UPPER_CASE
|
||||
readability-identifier-naming.ClassConstantCase: UPPER_CASE
|
||||
readability-identifier-naming.MacroDefinitionCase: UPPER_CASE
|
||||
|
||||
# "_foo" at namespace scope, and anything with "__", is reserved to the
|
||||
# implementation. This is the check that keeps the trailing-underscore rule
|
||||
# from drifting back to a leading one.
|
||||
bugprone-reserved-identifier.AllowedIdentifiers: ''
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "webserv (Ubuntu 22.04 / g++ 14)",
|
||||
|
||||
// Reuses the same compose file the terminal workflow uses, so the editor and
|
||||
// `docker compose run` always land in an identical environment.
|
||||
"dockerComposeFile": "../docker/compose.yml",
|
||||
"service": "dev",
|
||||
"workspaceFolder": "/webserv",
|
||||
"remoteUser": "dev",
|
||||
|
||||
// Keep the container alive when the VS Code window is closed, instead of
|
||||
// tearing it down and rebuilding on the next open.
|
||||
"shutdownAction": "none",
|
||||
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"ms-vscode.cpptools",
|
||||
"ms-vscode.makefile-tools",
|
||||
"twxs.cmake"
|
||||
],
|
||||
"settings": {
|
||||
"C_Cpp.default.compilerPath": "/usr/bin/g++-14",
|
||||
"C_Cpp.default.cppStandard": "c++23",
|
||||
"C_Cpp.default.intelliSenseMode": "linux-gcc-x64",
|
||||
"editor.formatOnSave": false,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"files.insertFinalNewline": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
# Build output
|
||||
obj/
|
||||
*.o
|
||||
*.d
|
||||
webserv
|
||||
webserv-clang
|
||||
|
||||
# Container user ids, taken from the machine the image is built on
|
||||
docker/.env
|
||||
|
||||
# Runtime artifacts
|
||||
*.log
|
||||
www/uploads/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
@@ -0,0 +1,105 @@
|
||||
# **************************************************************************** #
|
||||
# #
|
||||
# ::: :::::::: #
|
||||
# Makefile :+: :+: :+: #
|
||||
# +:+ +:+ +:+ #
|
||||
# By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ #
|
||||
# +#+#+#+#+#+ +#+ #
|
||||
# Created: 2026/08/19 10:05:52 by acossari #+# #+# #
|
||||
# Updated: 2026/08/20 22:18:47 by acossari ### ########.fr #
|
||||
# #
|
||||
# **************************************************************************** #
|
||||
|
||||
NAME := webserv
|
||||
|
||||
SRC_DIR := src
|
||||
OBJ_DIR := obj
|
||||
INC_DIR := include
|
||||
|
||||
CXX := g++
|
||||
# EXTRA_CXXFLAGS is the slot for flags added from the command line, for example
|
||||
# make EXTRA_CXXFLAGS="-g3 -fsanitize=address". CXXFLAGS itself must never be
|
||||
# overridden that way, it carries the warning flags that have to stay on.
|
||||
CXXFLAGS := -Wall -Wextra -Werror -std=c++23 -I$(INC_DIR) $(EXTRA_CXXFLAGS)
|
||||
# The C++ standard library goes inside the binary instead of being loaded from
|
||||
# the system at startup. A recent compiler emits references to symbol versions
|
||||
# that an older system libstdc++.so.6 does not carry, and that failure shows up
|
||||
# when the program is started, not when it is linked.
|
||||
LDFLAGS := -static-libstdc++ -static-libgcc
|
||||
|
||||
CLANG ?= clang++
|
||||
CLANG_TIDY ?= clang-tidy
|
||||
CLANG_FORMAT ?= clang-format
|
||||
|
||||
# Separate object directory and binary for the clang build. Sharing obj/ would
|
||||
# link a fresh g++ object with a leftover clang one, and the error looks like a
|
||||
# bug in the source instead of an old file.
|
||||
CLANG_NAME := $(NAME)-clang
|
||||
CLANG_OBJ_DIR := $(OBJ_DIR)/clang
|
||||
|
||||
# libstdc++ is the GNU library and ships with g++, libc++ is the LLVM one and
|
||||
# ships with clang. On Linux clang usually sits on libstdc++ anyway, and that
|
||||
# pairing cannot build this project:
|
||||
#
|
||||
# clang 18 with libstdc++ : std::expected not available
|
||||
# clang 18 with libc++ : std::expected available
|
||||
#
|
||||
# So every clang pass gets libc++. g++ never sees this flag.
|
||||
CLANG_STDLIB ?= -stdlib=libc++
|
||||
|
||||
# find returns files in filesystem order, which is not the same on every
|
||||
# machine. sort keeps the compile and link lines stable, so build logs and
|
||||
# clang-tidy output stay comparable.
|
||||
SRCS := $(sort $(shell find $(SRC_DIR) -type f -name '*.cpp'))
|
||||
OBJS := $(SRCS:$(SRC_DIR)/%.cpp=$(OBJ_DIR)/%.o)
|
||||
# Make only knows that obj/main.o comes from src/main.cpp, so editing Log.hpp
|
||||
# would leave the old main.o in place. Each .d adds the missing line, obj/main.o
|
||||
# also depends on include/Log.hpp, and -include at the bottom pulls them in.
|
||||
DEPS := $(OBJS:.o=.d)
|
||||
HDRS := $(sort $(shell find $(INC_DIR) $(SRC_DIR) -type f \
|
||||
\( -name '*.hpp' -o -name '*.tpp' \)))
|
||||
|
||||
all: $(NAME)
|
||||
|
||||
$(NAME): $(OBJS)
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) -o $(NAME)
|
||||
|
||||
# -MMD writes the .d file described above. -MP adds an empty rule for every
|
||||
# header in it, which matters after a rename: the old .d still asks for the old
|
||||
# path and make stops, having no way to build that file. The empty rule says the
|
||||
# header needs no recipe, so make moves on and recompiles.
|
||||
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp
|
||||
@mkdir -p $(dir $@)
|
||||
$(CXX) $(CXXFLAGS) -MMD -MP -c $< -o $@
|
||||
|
||||
# Reports only, never fails the build: WarningsAsErrors is empty in .clang-tidy
|
||||
# and all does not depend on this. clang-tidy parses the code itself, so it
|
||||
# needs the same flags the compiler gets (everything after the --).
|
||||
tidy: $(SRCS)
|
||||
$(CLANG_TIDY) $(SRCS) -- $(CXXFLAGS) $(CLANG_STDLIB)
|
||||
|
||||
# Same sources through the other compiler and standard library, into files of
|
||||
# its own, so the normal build is untouched. Worth running because the two
|
||||
# compilers do not warn about the same things. Needs libc++ 18 or newer, the
|
||||
# first release that ships <print>.
|
||||
check-clang:
|
||||
$(MAKE) --no-print-directory NAME=$(CLANG_NAME) OBJ_DIR=$(CLANG_OBJ_DIR) \
|
||||
CXX=$(CLANG) EXTRA_CXXFLAGS="$(CLANG_STDLIB)"
|
||||
|
||||
format:
|
||||
$(CLANG_FORMAT) -i $(SRCS) $(HDRS)
|
||||
|
||||
format-check:
|
||||
$(CLANG_FORMAT) --dry-run --Werror $(SRCS) $(HDRS)
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJ_DIR)
|
||||
|
||||
fclean: clean
|
||||
rm -f $(NAME) $(CLANG_NAME)
|
||||
|
||||
re: fclean all
|
||||
|
||||
-include $(DEPS)
|
||||
|
||||
.PHONY: all clean fclean re tidy check-clang format format-check
|
||||
@@ -0,0 +1,52 @@
|
||||
# Default configuration loaded when no configuration path is provided.
|
||||
|
||||
server {
|
||||
listen 0.0.0.0:8000;
|
||||
server_name localhost;
|
||||
|
||||
root ./www;
|
||||
index index.html;
|
||||
client_max_body_size 1m;
|
||||
|
||||
error_page 404 /errors/404.html;
|
||||
error_page 500 502 503 504 /errors/50x.html;
|
||||
|
||||
location / {
|
||||
allow_methods GET;
|
||||
}
|
||||
|
||||
location /files {
|
||||
allow_methods GET DELETE;
|
||||
autoindex on;
|
||||
}
|
||||
|
||||
location /upload {
|
||||
alias ./www/uploads;
|
||||
allow_methods GET POST DELETE;
|
||||
upload_store ./www/uploads;
|
||||
client_max_body_size 10m;
|
||||
}
|
||||
|
||||
location /old {
|
||||
return 301 /;
|
||||
}
|
||||
}
|
||||
|
||||
# Both server blocks share one listening socket because they use the same
|
||||
# endpoint. The Host header selects a block, and the first one is used when no
|
||||
# server name matches.
|
||||
#
|
||||
# A browser needs the "127.0.0.1 webserv.local" entry in /etc/hosts.
|
||||
# curl can supply that mapping for one request:
|
||||
# curl --resolve webserv.local:8000:127.0.0.1 http://webserv.local:8000/
|
||||
server {
|
||||
listen 0.0.0.0:8000;
|
||||
server_name webserv.local;
|
||||
|
||||
root ./www/vhost;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
allow_methods GET;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
# Configuration for the official 42 tester, tester42/official/tester.
|
||||
#
|
||||
# Every path below is relative to the repository root, so run both from there:
|
||||
# ./webserv conf/tester.conf
|
||||
# ./tester42/official/tester http://localhost:8000
|
||||
|
||||
server {
|
||||
# 0.0.0.0 accepts connections arriving on any interface. 127.0.0.1 would
|
||||
# accept only those from the same machine.
|
||||
listen 0.0.0.0:8000;
|
||||
server_name webserv.tester;
|
||||
|
||||
# The tester POSTs a 100 MB body, so the ceiling here has to sit above it.
|
||||
# /post_body lowers it again for that one location.
|
||||
client_max_body_size 200m;
|
||||
|
||||
root ./tester42/YoupiBanane;
|
||||
index index.html;
|
||||
|
||||
# GET only, so that POST and HEAD on / answer 405.
|
||||
location / {
|
||||
allow_methods GET;
|
||||
}
|
||||
|
||||
# alias and not root, and this is the case that shows they differ. The
|
||||
# server root is already YoupiBanane, so root would map /directory/nop onto
|
||||
# YoupiBanane/directory/nop, which does not exist. alias replaces the
|
||||
# matched prefix instead of keeping it.
|
||||
#
|
||||
# index youpi.bad_extension makes GET /directory and GET /directory/nop
|
||||
# succeed while GET /directory/Yeah gives 404, since Yeah is the one
|
||||
# subdirectory without that file. autoindex must stay off, or the listing
|
||||
# would answer 200 there instead.
|
||||
location /directory {
|
||||
alias ./tester42/YoupiBanane;
|
||||
allow_methods GET;
|
||||
index youpi.bad_extension;
|
||||
autoindex off;
|
||||
}
|
||||
|
||||
# Matched by extension, and this match wins over the /directory prefix
|
||||
# above, so POST /directory/youpi.bla reaches the CGI and not the static
|
||||
# handler. GET is allowed as well because the same path is also requested
|
||||
# with GET, and a location matched by extension sends every method to the
|
||||
# CGI.
|
||||
location ~ \.bla$ {
|
||||
allow_methods GET POST;
|
||||
cgi_pass ./tester42/official/cgi_tester;
|
||||
}
|
||||
|
||||
# The limit is inclusive: a body of exactly 100 bytes succeeds, 101 gives
|
||||
# 413. Bodies arrive chunked with no Content-Length, so the count is taken
|
||||
# on decoded bytes as they stream in.
|
||||
location /post_body {
|
||||
allow_methods POST;
|
||||
client_max_body_size 100;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
# Development environment for webserv.
|
||||
#
|
||||
# Target platform is Linux, because that is what the Codam VMs run and what the
|
||||
# official testers are built for. The base is Ubuntu 22.04, the same release
|
||||
# those VMs run, so this image is that environment rather than something close
|
||||
# to it. A newer base would be the permissive side of the difference: its glibc
|
||||
# accepts everything the older one does and more, so code that cannot run at
|
||||
# school would still build clean here. On 24.04 a plain std::strtoul compiles
|
||||
# into a call to __isoc23_strtoul, a GLIBC_2.38 symbol that 22.04 does not have,
|
||||
# at every -std= and not only at C++23.
|
||||
#
|
||||
# 22.04 ships neither compiler this project needs, so two repositories are
|
||||
# added: ubuntu-toolchain-r/test for g++ 14, apt.llvm.org for clang 18. Both
|
||||
# publish builds made for 22.04, which is what keeps the produced binary
|
||||
# runnable there.
|
||||
#
|
||||
# libc++ is installed alongside libstdc++ for one specific reason: clang 18
|
||||
# reports __cpp_concepts as 201907, and libstdc++ gates <expected> on 202002L,
|
||||
# so clang + libstdc++ compiles "#include <expected>" into nothing at all. The
|
||||
# control build with clang — a second opinion on warnings, and the sanitizers —
|
||||
# would die on the first function returning Result<T>. "make check-clang" passes
|
||||
# -stdlib=libc++ for exactly this.
|
||||
FROM ubuntu:22.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
gnupg \
|
||||
wget \
|
||||
lsb-release \
|
||||
software-properties-common \
|
||||
&& add-apt-repository -y ppa:ubuntu-toolchain-r/test \
|
||||
&& wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key \
|
||||
| gpg --dearmor -o /usr/share/keyrings/llvm-archive-keyring.gpg \
|
||||
&& echo "deb [signed-by=/usr/share/keyrings/llvm-archive-keyring.gpg]" \
|
||||
"http://apt.llvm.org/jammy/ llvm-toolchain-jammy-18 main" \
|
||||
> /etc/apt/sources.list.d/llvm.list \
|
||||
&& apt-get update
|
||||
|
||||
RUN apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
g++-14 \
|
||||
clang-18 \
|
||||
clang-tidy-18 \
|
||||
clang-format-18 \
|
||||
libc++-18-dev \
|
||||
libc++abi-18-dev \
|
||||
libclang-rt-18-dev \
|
||||
gdb \
|
||||
valgrind \
|
||||
strace \
|
||||
make \
|
||||
git \
|
||||
curl \
|
||||
siege \
|
||||
python3 \
|
||||
python3-pip \
|
||||
net-tools \
|
||||
iproute2 \
|
||||
procps \
|
||||
less \
|
||||
vim \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Make the versioned tools the defaults, so "c++", "g++", "clang++",
|
||||
# "clang-tidy" and "clang-format" all point at something that understands C++23.
|
||||
# Without the last two the Makefile would have to hardcode Ubuntu's versioned
|
||||
# names, which are wrong everywhere else — Codam included.
|
||||
RUN update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-14 100 \
|
||||
&& update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-14 100 \
|
||||
&& update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-18 100 \
|
||||
&& update-alternatives --install /usr/bin/c++ c++ /usr/bin/g++-14 100 \
|
||||
&& update-alternatives --install /usr/bin/clang-tidy clang-tidy \
|
||||
/usr/bin/clang-tidy-18 100 \
|
||||
&& update-alternatives --install /usr/bin/clang-format clang-format \
|
||||
/usr/bin/clang-format-18 100
|
||||
|
||||
# Run as a normal user whose uid/gid match the host account, so files created
|
||||
# inside the container (object files, uploads, test output) do not end up owned
|
||||
# by root on the macOS side of the bind mount.
|
||||
ARG UID=1000
|
||||
ARG GID=1000
|
||||
RUN if getent passwd ${UID} >/dev/null; then userdel -r "$(getent passwd ${UID} | cut -d: -f1)"; fi \
|
||||
&& groupadd -g ${GID} dev 2>/dev/null || true \
|
||||
&& useradd -m -u ${UID} -g ${GID} -s /bin/bash dev
|
||||
|
||||
USER dev
|
||||
WORKDIR /webserv
|
||||
|
||||
CMD ["/bin/bash"]
|
||||
@@ -0,0 +1,29 @@
|
||||
services:
|
||||
dev:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
# Read from docker/.env, which is machine-specific and gitignored.
|
||||
# The fallbacks are the usual first-user ids on Linux, so a fresh clone
|
||||
# without a .env still builds correctly there.
|
||||
args:
|
||||
UID: "${UID:-1000}"
|
||||
GID: "${GID:-1000}"
|
||||
image: webserv-dev
|
||||
container_name: webserv-dev
|
||||
# The project directory lives on the Mac and is mounted here: you keep editing
|
||||
# in VS Code on the host, while compiling and running on Linux.
|
||||
volumes:
|
||||
- ..:/webserv
|
||||
working_dir: /webserv
|
||||
# Ports the config files listen on, exposed to the Mac so a host browser,
|
||||
# curl or siege can reach the server running inside.
|
||||
ports:
|
||||
- "8000:8000"
|
||||
- "8001:8001"
|
||||
- "8002:8002"
|
||||
- "8080:8080"
|
||||
# Keeps the container alive so you can `docker compose exec dev bash` into it.
|
||||
stdin_open: true
|
||||
tty: true
|
||||
command: /bin/bash
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/* ************************************************************************** */
|
||||
/* */
|
||||
/* ::: :::::::: */
|
||||
/* Log.cpp :+: :+: :+: */
|
||||
/* +:+ +:+ +:+ */
|
||||
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
|
||||
/* +#+#+#+#+#+ +#+ */
|
||||
/* Created: 2026/08/20 16:55:22 by acossari #+# #+# */
|
||||
/* Updated: 2026/08/21 17:04:33 by acossari ### ########.fr */
|
||||
/* */
|
||||
/* ************************************************************************** */
|
||||
|
||||
#include "Log.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <print>
|
||||
|
||||
namespace webserv::log {
|
||||
|
||||
// Anonymous namespace: nothing in here is visible outside this file. Writing
|
||||
// "static" on each name does the same, this keeps the rule in one place. The
|
||||
// constant below would be local anyway, constexpr already implies it.
|
||||
namespace {
|
||||
|
||||
// Verbosity floor: vlog drops anything below it. To quiet a noisy run, edit
|
||||
// this line and rebuild.
|
||||
constexpr Level ACTIVE_LEVEL = Level::Info;
|
||||
|
||||
constexpr std::string_view tag(Level level) {
|
||||
switch (level) {
|
||||
case Level::Debug:
|
||||
return "DEBUG";
|
||||
case Level::Info:
|
||||
return "INFO";
|
||||
case Level::Warn:
|
||||
return "WARN";
|
||||
case Level::Error:
|
||||
return "ERROR";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void vlog(Level messageLevel, std::string_view formatString,
|
||||
std::format_args args) {
|
||||
if (messageLevel < ACTIVE_LEVEL) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto now = std::chrono::floor<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now());
|
||||
|
||||
// Printing can throw, both while building the string and while writing it.
|
||||
// A logger that takes down its caller is worse than one that misses a line,
|
||||
// so the failure stops here.
|
||||
try {
|
||||
// Format specifiers:
|
||||
// %F date, 2026-08-21
|
||||
// %T time, 12:58:41
|
||||
// %Z zone name, UTC because now comes from the system clock
|
||||
// <5 left aligned in five columns, so the level tags stay lined up
|
||||
std::print(stderr, "[{:%F %T %Z}] [{:<5}] {}\n", now, tag(messageLevel),
|
||||
// vformat replaces the placeholders in formatString with the
|
||||
// values stored in args and returns the completed message.
|
||||
std::vformat(formatString, args));
|
||||
// "Exception swallowing": there is nowhere left to report a logging
|
||||
// failure to, since the reporting channel is precisely what just failed.
|
||||
// NOLINTNEXTLINE(bugprone-empty-catch)
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace webserv::log
|
||||
+389
@@ -0,0 +1,389 @@
|
||||
/* ************************************************************************** */
|
||||
/* */
|
||||
/* ::: :::::::: */
|
||||
/* Server.cpp :+: :+: :+: */
|
||||
/* +:+ +:+ +:+ */
|
||||
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
|
||||
/* +#+#+#+#+#+ +#+ */
|
||||
/* Created: 2026/09/01 19:00:50 by acossari #+# #+# */
|
||||
/* Updated: 2026/09/04 07:58:42 by acossari ### ########.fr */
|
||||
/* */
|
||||
/* ************************************************************************** */
|
||||
|
||||
#include "Server.hpp"
|
||||
|
||||
#include <sys/epoll.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <cerrno>
|
||||
#include <csignal>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <utility>
|
||||
|
||||
#include "Log.hpp"
|
||||
#include "event/EventToken.hpp"
|
||||
|
||||
namespace webserv {
|
||||
|
||||
namespace {
|
||||
|
||||
// volatile makes the loop read this variable again at every check instead of
|
||||
// reusing an earlier value. sig_atomic_t makes each simple read or write
|
||||
// indivisible when a signal handler interrupts normal execution.
|
||||
volatile std::sig_atomic_t stopRequested = 0;
|
||||
|
||||
// signal() passes the signal number to a void(int) handler. On Linux, SIGINT
|
||||
// is 2 and SIGTERM is 15. Leaving the unused int unnamed avoids a
|
||||
// -Werror=unused-parameter compilation error.
|
||||
void requestStop(int) {
|
||||
stopRequested = 1;
|
||||
}
|
||||
|
||||
std::uint64_t listenerToken(std::uint32_t listenerIndex) {
|
||||
// Designated aggregate initialization creates an EventToken prvalue. It
|
||||
// directly initializes the parameter of pack(), so no copy or move occurs.
|
||||
return pack(EventToken{
|
||||
.index = listenerIndex,
|
||||
.kind = EndpointKind::Listener,
|
||||
.generation = 0,
|
||||
});
|
||||
}
|
||||
|
||||
std::uint64_t clientToken(std::uint32_t slotIndex, std::uint32_t generation) {
|
||||
return pack(EventToken{
|
||||
.index = slotIndex,
|
||||
.kind = EndpointKind::ClientSocket,
|
||||
.generation = generation,
|
||||
});
|
||||
}
|
||||
|
||||
// Linux accept(2) documents the failures classified by these helpers. The
|
||||
// groups distinguish a failed connection attempt, temporary resource
|
||||
// exhaustion, and a listener or argument that is no longer valid.
|
||||
|
||||
// These failures concern one connection attempt. They do not invalidate the
|
||||
// listener, so a later connection may still be accepted.
|
||||
bool isTransientAcceptFailure(int errnoCode) {
|
||||
constexpr std::array codes{
|
||||
ECONNABORTED, EPERM, EPROTO, ENETDOWN, ENETUNREACH,
|
||||
ENOPROTOOPT, EHOSTDOWN, EHOSTUNREACH, ENONET, EOPNOTSUPP,
|
||||
ETIMEDOUT, ECONNRESET, ESOCKTNOSUPPORT, EPROTONOSUPPORT};
|
||||
return std::ranges::contains(codes, errnoCode);
|
||||
}
|
||||
|
||||
// These failures report exhausted process or kernel resources. They do not
|
||||
// invalidate the listener and may clear before a later accept attempt.
|
||||
bool isResourceFailure(int errnoCode) {
|
||||
constexpr std::array codes{EMFILE, ENFILE, ENOBUFS, ENOMEM, ENOSR};
|
||||
return std::ranges::contains(codes, errnoCode);
|
||||
}
|
||||
|
||||
// These failures report an invalid descriptor, listener state, or address
|
||||
// argument. Retrying the unchanged call cannot restore a valid accept state.
|
||||
bool isBrokenListenerFailure(int errnoCode) {
|
||||
constexpr std::array codes{EBADF, EFAULT, EINVAL, ENOTSOCK};
|
||||
return std::ranges::contains(codes, errnoCode);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Server::Server(std::span<const sockaddr_storage> endpoints) {
|
||||
listeners_.reserve(endpoints.size());
|
||||
for (const sockaddr_storage& address : endpoints) {
|
||||
listeners_.emplace_back(address);
|
||||
}
|
||||
|
||||
for (std::uint32_t listenerIndex = 0; listenerIndex < listeners_.size();
|
||||
++listenerIndex) {
|
||||
epoll_.add(listeners_[listenerIndex].fd(), EPOLLIN,
|
||||
listenerToken(listenerIndex));
|
||||
}
|
||||
}
|
||||
|
||||
void Server::run() {
|
||||
// SIGINT handles Ctrl+C. SIGTERM is the usual request sent by the kill
|
||||
// command. Both stop the loop so shutdown code and destructors can run.
|
||||
if (::signal(SIGINT, requestStop) == SIG_ERR) {
|
||||
log::error("signal(SIGINT): {}", std::strerror(errno));
|
||||
}
|
||||
if (::signal(SIGTERM, requestStop) == SIG_ERR) {
|
||||
log::error("signal(SIGTERM): {}", std::strerror(errno));
|
||||
}
|
||||
|
||||
while (stopRequested == 0) {
|
||||
// Use whichever comes first: the shutdown check or the resource retry.
|
||||
const auto readyEvents = epoll_.wait(millisUntilDeadline());
|
||||
// If backoffExpiry_ has been reached, clear AcceptBackoff. If PoolFull
|
||||
// is still set, listeners remain disabled. Otherwise, rearm each valid
|
||||
// listener with EPOLLIN so the server can accept connections again.
|
||||
expireBackoff();
|
||||
for (const auto& event : readyEvents) {
|
||||
// data.u64 is the token stored when the descriptor was registered.
|
||||
// events is the 32 bit mask reported by epoll, such as EPOLLIN
|
||||
dispatch(event.data.u64, event.events);
|
||||
}
|
||||
// The batch is complete, so quarantined slots can return to the free list.
|
||||
// Recovering any slot clears the PoolFull pause reason if it was active.
|
||||
if (slotPool_.drain() > 0) {
|
||||
resume(ListenerPause::PoolFull);
|
||||
}
|
||||
}
|
||||
|
||||
log::info("shutting down");
|
||||
}
|
||||
|
||||
void Server::dispatch(std::uint64_t packedToken, std::uint32_t events) {
|
||||
const EventToken token = unpack(packedToken);
|
||||
|
||||
// A listener event means a new client may be waiting. Accept one connection
|
||||
// and initialize its socket and pool slot.
|
||||
if (token.kind == EndpointKind::Listener) {
|
||||
assert(token.index < listeners_.size());
|
||||
// An earlier listener event may accept the final available client and fill
|
||||
// the pool. The server then tries to disable every listener by setting its
|
||||
// epoll event mask to zero. If updating this listener throws, the error
|
||||
// handler closes it, but its event may already be in the current batch.
|
||||
// Check the descriptor so that queued event is ignored after the close.
|
||||
if (listeners_[token.index].valid()) {
|
||||
acceptFrom(token.index);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// epoll may return an event after its endpoint was retired or replaced.
|
||||
// Ignore it unless its token still matches the current registration.
|
||||
if (!slotPool_.isCurrent(token)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Outcome reports whether the connection stays alive and which events
|
||||
// epoll should monitor next.
|
||||
const Connection::Outcome outcome =
|
||||
slotPool_.connection(token.index).onClientEvent(events);
|
||||
if (outcome.lifecycle == Connection::Lifecycle::Alive) {
|
||||
// Processing may change the buffer state and therefore the readiness
|
||||
// events needed for a live connection.
|
||||
updateRegistration(token.index, outcome.desiredEvents);
|
||||
return;
|
||||
}
|
||||
} catch (const std::exception& error) {
|
||||
log::error("connection {}: {}", token.index, error.what());
|
||||
}
|
||||
|
||||
// Done and Fatal both close the connection. A caught processing exception
|
||||
// follows the same path. The slot remains quarantined until the current
|
||||
// event batch ends, then becomes available for a new connection.
|
||||
retire(token.index);
|
||||
}
|
||||
|
||||
// Accepts one pending connection from the ready listener and obtains a socket
|
||||
// dedicated to that client.
|
||||
void Server::acceptFrom(std::uint32_t listenerIndex) {
|
||||
// The nullptr arguments currently omit the peer address and its byte length.
|
||||
const int clientFd =
|
||||
::accept(listeners_[listenerIndex].fd(), nullptr, nullptr);
|
||||
if (clientFd == -1) {
|
||||
// The subject forbids errno only after read, recv, write, and send.
|
||||
// accept errors distinguish normal retries from resource backoff.
|
||||
handleAcceptFailure(listenerIndex, errno);
|
||||
return;
|
||||
}
|
||||
|
||||
// This state must remain visible after try ends so catch can inspect the
|
||||
// slot acquisition result.
|
||||
std::optional<std::uint32_t> slotIndex;
|
||||
try {
|
||||
FileDescriptor clientSocket(clientFd);
|
||||
clientSocket.setNonBlocking();
|
||||
|
||||
// Transfers the accepted socket into a pool slot. On success the new
|
||||
// Connection owns it and acquire returns the slot index. A full pool
|
||||
// returns no index and closes the socket through its FileDescriptor.
|
||||
slotIndex = slotPool_.acquire(std::move(clientSocket));
|
||||
if (!slotIndex.has_value()) {
|
||||
pause(ListenerPause::PoolFull);
|
||||
return;
|
||||
}
|
||||
|
||||
Connection& connection = slotPool_.connection(*slotIndex);
|
||||
SlotPool::Endpoint& endpoint =
|
||||
slotPool_.endpoint(*slotIndex, EndpointKind::ClientSocket);
|
||||
const std::uint32_t events = connection.desiredEvents();
|
||||
epoll_.add(connection.fd(), events,
|
||||
clientToken(*slotIndex, endpoint.generation));
|
||||
endpoint.registered = true;
|
||||
endpoint.registeredEvents = events;
|
||||
} catch (const std::exception& error) {
|
||||
log::error("accept on listener {}: {}", listenerIndex, error.what());
|
||||
// If epoll_.add() throws after acquire() succeeds, the stored index
|
||||
// identifies the slot to retire. If setup fails before acquire() returns,
|
||||
// the FileDescriptor that owns the client socket closes it through RAII.
|
||||
if (slotIndex.has_value()) {
|
||||
retire(*slotIndex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (slotPool_.full()) {
|
||||
pause(ListenerPause::PoolFull);
|
||||
}
|
||||
}
|
||||
|
||||
void Server::handleAcceptFailure(std::uint32_t listenerIndex, int errnoCode) {
|
||||
// Case 1: Nonfatal accept failure
|
||||
// A network error can empty the accept queue after epoll reports readiness,
|
||||
// so nonblocking accept() returns EAGAIN. The other failures concern the
|
||||
// connection attempt, not the listener, so leave the listener registered.
|
||||
if (errnoCode == EAGAIN || isTransientAcceptFailure(errnoCode)) {
|
||||
log::debug("accept: {}", std::strerror(errnoCode));
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 2: Temporary resource exhaustion
|
||||
// Stop accepting new clients temporarily and wait before retrying.
|
||||
if (isResourceFailure(errnoCode)) {
|
||||
log::warn("accept: {}", std::strerror(errnoCode));
|
||||
startBackoff();
|
||||
return;
|
||||
}
|
||||
|
||||
log::error("accept on listener {}: {}", listenerIndex,
|
||||
std::strerror(errnoCode));
|
||||
|
||||
// Case 3: Known invariant violation
|
||||
// An internal server error broke a condition accept() always requires: a
|
||||
// valid listening socket and valid arguments. Retrying cannot change that
|
||||
// state, so remove the listener instead of repeating the failure forever.
|
||||
if (isBrokenListenerFailure(errnoCode)) {
|
||||
stopListener(listenerIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 4: Unrecognized accept failure
|
||||
// Preserve the listener because the error does not prove it is unusable.
|
||||
// Timed backoff prevents a busy loop, but a persistent failure can keep
|
||||
// producing one error log for every retry indefinitely.
|
||||
startBackoff();
|
||||
}
|
||||
|
||||
// Removes the client socket from epoll when registered, then asks the pool to
|
||||
// close the Connection and quarantine its slot until the event batch ends.
|
||||
void Server::retire(std::uint32_t slotIndex) noexcept {
|
||||
if (slotPool_.endpoint(slotIndex, EndpointKind::ClientSocket).registered) {
|
||||
epoll_.remove(slotPool_.connection(slotIndex).fd());
|
||||
}
|
||||
slotPool_.retire(slotIndex);
|
||||
}
|
||||
|
||||
void Server::stopListener(std::uint32_t listenerIndex) noexcept {
|
||||
Listener& listener = listeners_[listenerIndex];
|
||||
epoll_.remove(listener.fd());
|
||||
listener.close();
|
||||
log::error("listener {} left the event loop", listenerIndex);
|
||||
|
||||
// std::ranges::any_of scans the whole container and stops at the first true.
|
||||
// The pointer to member function Listener::valid is called on each Listener,
|
||||
// so anyLeft reports whether at least one valid listener remains.
|
||||
const bool anyLeft = std::ranges::any_of(listeners_, &Listener::valid);
|
||||
// No shutdown is requested when the final listener stops. Existing clients
|
||||
// remain active until completion. The process then stays idle without
|
||||
// accepting new connections and keeps running until a shutdown signal.
|
||||
if (!anyLeft) {
|
||||
log::error(
|
||||
"no listener left: open connections are served to the end, no new "
|
||||
"connection is accepted");
|
||||
}
|
||||
}
|
||||
|
||||
void Server::startBackoff() {
|
||||
pause(ListenerPause::AcceptBackoff);
|
||||
backoffExpiry_ = std::chrono::steady_clock::now() + ACCEPT_BACKOFF;
|
||||
}
|
||||
|
||||
// Modifies a registered client socket only when its desired epoll events have
|
||||
// changed. The cached mask is updated only after epoll accepts the change.
|
||||
void Server::updateRegistration(std::uint32_t slotIndex, std::uint32_t events) {
|
||||
SlotPool::Endpoint& endpoint =
|
||||
slotPool_.endpoint(slotIndex, EndpointKind::ClientSocket);
|
||||
assert(endpoint.registered);
|
||||
if (endpoint.registeredEvents == events) {
|
||||
return;
|
||||
}
|
||||
|
||||
epoll_.modify(slotPool_.connection(slotIndex).fd(), events,
|
||||
clientToken(slotIndex, endpoint.generation));
|
||||
endpoint.registeredEvents = events;
|
||||
}
|
||||
|
||||
// The first pause reason disarms listeners. Removing the last reason rearms
|
||||
// them, preventing one cleared reason from resuming listeners too early.
|
||||
void Server::pause(ListenerPause reason) {
|
||||
if (pauseMask_.set(reason)) {
|
||||
applyListenerEvents(0);
|
||||
}
|
||||
}
|
||||
|
||||
void Server::resume(ListenerPause reason) {
|
||||
if (pauseMask_.clear(reason)) {
|
||||
applyListenerEvents(EPOLLIN);
|
||||
}
|
||||
}
|
||||
|
||||
void Server::applyListenerEvents(std::uint32_t events) {
|
||||
for (std::uint32_t listenerIndex = 0; listenerIndex < listeners_.size();
|
||||
++listenerIndex) {
|
||||
if (!listeners_[listenerIndex].valid()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
epoll_.modify(listeners_[listenerIndex].fd(), events,
|
||||
listenerToken(listenerIndex));
|
||||
} catch (const Epoll::Error& error) {
|
||||
log::error("{}", error.what());
|
||||
stopListener(listenerIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Server::expireBackoff() {
|
||||
if (backoffExpiry_.has_value() &&
|
||||
std::chrono::steady_clock::now() >= *backoffExpiry_) {
|
||||
// Removes the stored value, leaving the optional empty (std::nullopt).
|
||||
backoffExpiry_.reset();
|
||||
resume(ListenerPause::AcceptBackoff);
|
||||
}
|
||||
}
|
||||
|
||||
int Server::millisUntilDeadline() const {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
auto deadline = now + MAX_IDLE_WAIT;
|
||||
|
||||
// Dereferencing backoffExpiry_ accesses its stored time after has_value()
|
||||
// confirms that one exists. Choose the earlier of that time and the periodic
|
||||
// shutdown deadline, so a new loop iteration does not delay the retry.
|
||||
if (backoffExpiry_.has_value() && *backoffExpiry_ < deadline) {
|
||||
deadline = *backoffExpiry_;
|
||||
}
|
||||
|
||||
// Example: backoffExpiry_ becomes the deadline at 500.80 ms. If now is
|
||||
// 101.61 ms, 399.19 ms remain. Rounding down asks epoll_wait for 399 ms,
|
||||
// so its timeout can end at 500.61 ms with 0.19 ms of backoff remaining.
|
||||
//
|
||||
// On the next loop, rounding 0.19 ms down produces a zero timeout, so
|
||||
// epoll_wait returns immediately. At 0.01 ms per empty loop, this could
|
||||
// waste about 19 iterations. Rounding 399.19 ms up to 400 ms makes the
|
||||
// timeout end only after backoffExpiry_ has been reached.
|
||||
const auto remaining =
|
||||
std::chrono::ceil<std::chrono::milliseconds>(deadline - now);
|
||||
// The deadline can pass while events are handled or this process is not
|
||||
// scheduled. A negative timeout would make epoll_wait block indefinitely.
|
||||
// Returning zero makes epoll_wait check for events without waiting. run()
|
||||
// then calls expireBackoff(), which resumes the paused listeners.
|
||||
return remaining.count() > 0 ? static_cast<int>(remaining.count()) : 0;
|
||||
}
|
||||
|
||||
} // namespace webserv
|
||||
@@ -0,0 +1,89 @@
|
||||
/* ************************************************************************** */
|
||||
/* */
|
||||
/* ::: :::::::: */
|
||||
/* Epoll.cpp :+: :+: :+: */
|
||||
/* +:+ +:+ +:+ */
|
||||
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
|
||||
/* +#+#+#+#+#+ +#+ */
|
||||
/* Created: 2026/08/26 16:25:24 by acossari #+# #+# */
|
||||
/* Updated: 2026/09/03 13:08:22 by acossari ### ########.fr */
|
||||
/* */
|
||||
/* ************************************************************************** */
|
||||
|
||||
#include "event/Epoll.hpp"
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
#include "Log.hpp"
|
||||
|
||||
namespace webserv {
|
||||
|
||||
namespace {
|
||||
|
||||
void control(int epollFd, int operation, int fd, std::uint32_t events,
|
||||
std::uint64_t token) {
|
||||
// epoll_event contains events and the data union. The outer braces initialize
|
||||
// the struct, while the inner braces initialize data.u64 with the token.
|
||||
epoll_event event{.events = events, .data = {.u64 = token}};
|
||||
if (::epoll_ctl(epollFd, operation, fd, &event) == -1) {
|
||||
const std::string_view operationName =
|
||||
operation == EPOLL_CTL_ADD ? "EPOLL_CTL_ADD" : "EPOLL_CTL_MOD";
|
||||
// The subject forbids errno only after read/recv/write/send.
|
||||
throw Epoll::Error(std::format("epoll_ctl({}) on fd {}: {}", operationName,
|
||||
fd, std::strerror(errno)));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Epoll::Error::Error(std::string message) : msg_(std::move(message)) {}
|
||||
|
||||
const char* Epoll::Error::what() const noexcept {
|
||||
return msg_.c_str();
|
||||
}
|
||||
|
||||
// The legacy size argument estimated how many file descriptors would be
|
||||
// registered in this epoll instance. Current Linux ignores it but still
|
||||
// requires a positive value, so one is the smallest valid argument.
|
||||
Epoll::Epoll() : fd_(::epoll_create(1)) {
|
||||
if (!fd_.valid()) {
|
||||
throw Error(std::format("epoll_create: {}", std::strerror(errno)));
|
||||
}
|
||||
}
|
||||
|
||||
void Epoll::add(int fd, std::uint32_t events, std::uint64_t token) {
|
||||
control(fd_.get(), EPOLL_CTL_ADD, fd, events, token);
|
||||
}
|
||||
|
||||
void Epoll::modify(int fd, std::uint32_t events, std::uint64_t token) {
|
||||
control(fd_.get(), EPOLL_CTL_MOD, fd, events, token);
|
||||
}
|
||||
|
||||
void Epoll::remove(int fd) noexcept {
|
||||
if (::epoll_ctl(fd_.get(), EPOLL_CTL_DEL, fd, nullptr) == -1) {
|
||||
log::error("epoll_ctl(EPOLL_CTL_DEL) on fd {}: {}", fd,
|
||||
std::strerror(errno));
|
||||
}
|
||||
}
|
||||
|
||||
std::span<const epoll_event> Epoll::wait(int timeoutMillis) {
|
||||
const int readyCount =
|
||||
::epoll_wait(fd_.get(), events_.data(),
|
||||
static_cast<int>(EVENT_BATCH_CAPACITY), timeoutMillis);
|
||||
if (readyCount == -1) {
|
||||
if (errno == EINTR) {
|
||||
// A signal caught by the program can interrupt epoll_wait.
|
||||
// An empty span lets the caller stop or wait again. epoll_wait reports
|
||||
// readiness only, so returning an empty span does not consume data.
|
||||
return {};
|
||||
}
|
||||
throw Error(std::format("epoll_wait: {}", std::strerror(errno)));
|
||||
}
|
||||
return {events_.data(), static_cast<std::size_t>(readyCount)};
|
||||
}
|
||||
|
||||
} // namespace webserv
|
||||
@@ -0,0 +1,53 @@
|
||||
/* ************************************************************************** */
|
||||
/* */
|
||||
/* ::: :::::::: */
|
||||
/* EventToken.cpp :+: :+: :+: */
|
||||
/* +:+ +:+ +:+ */
|
||||
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
|
||||
/* +#+#+#+#+#+ +#+ */
|
||||
/* Created: 2026/08/25 18:00:56 by acossari #+# #+# */
|
||||
/* Updated: 2026/08/25 19:45:51 by acossari ### ########.fr */
|
||||
/* */
|
||||
/* ************************************************************************** */
|
||||
|
||||
#include "event/EventToken.hpp"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
namespace webserv {
|
||||
|
||||
// INDEX_MASK = binary 111...111 (30 low ones) = decimal 1,073,741,823.
|
||||
// KIND_MASK = binary 11 (2 low ones) = decimal 3.
|
||||
constexpr std::uint64_t INDEX_MASK =
|
||||
(std::uint64_t{1} << EventToken::INDEX_BITS) - 1;
|
||||
constexpr std::uint64_t KIND_MASK =
|
||||
(std::uint64_t{1} << EventToken::KIND_BITS) - 1;
|
||||
|
||||
std::uint64_t pack(EventToken token) {
|
||||
assert(token.index <= EventToken::MAX_INDEX);
|
||||
assert(static_cast<std::uint64_t>(token.kind) <= KIND_MASK);
|
||||
// Shift each field into place, then combine the disjoint bit ranges with OR:
|
||||
// [ generation: 32 bits | kind: 2 bits | index: 30 bits ].
|
||||
return static_cast<std::uint64_t>(token.index) |
|
||||
(static_cast<std::uint64_t>(token.kind) << EventToken::INDEX_BITS) |
|
||||
(static_cast<std::uint64_t>(token.generation)
|
||||
<< (EventToken::INDEX_BITS + EventToken::KIND_BITS));
|
||||
}
|
||||
|
||||
// Designated initialization constructs the returned EventToken by naming its
|
||||
// fields in their declaration order.
|
||||
//
|
||||
// AND keeps only positions where a mask has 1. index already occupies the low
|
||||
// 30 bits; kind is shifted right first so its 2 bits move there. generation is
|
||||
// shifted right by 32 and needs no mask because no higher field remains.
|
||||
EventToken unpack(std::uint64_t token) {
|
||||
return EventToken{
|
||||
.index = static_cast<std::uint32_t>(token & INDEX_MASK),
|
||||
.kind = static_cast<EndpointKind>((token >> EventToken::INDEX_BITS) &
|
||||
KIND_MASK),
|
||||
.generation = static_cast<std::uint32_t>(
|
||||
token >> (EventToken::INDEX_BITS + EventToken::KIND_BITS)),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace webserv
|
||||
@@ -0,0 +1,77 @@
|
||||
/* ************************************************************************** */
|
||||
/* */
|
||||
/* ::: :::::::: */
|
||||
/* main.cpp :+: :+: :+: */
|
||||
/* +:+ +:+ +:+ */
|
||||
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
|
||||
/* +#+#+#+#+#+ +#+ */
|
||||
/* Created: 2026/08/19 12:36:23 by acossari #+# #+# */
|
||||
/* Updated: 2026/08/20 22:18:42 by acossari ### ########.fr */
|
||||
/* */
|
||||
/* ************************************************************************** */
|
||||
|
||||
#include <netinet/in.h>
|
||||
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <csignal>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <string_view>
|
||||
|
||||
#include "Log.hpp"
|
||||
#include "Server.hpp"
|
||||
|
||||
constexpr std::string_view DEFAULT_CONFIG_PATH = "conf/default.conf";
|
||||
|
||||
namespace {
|
||||
|
||||
// TEMP: Builds wildcard IPv4 addresses for the fixed echo server ports.
|
||||
// Parsed configuration will eventually provide resolved listener addresses.
|
||||
sockaddr_storage anyAddress(std::uint16_t port) {
|
||||
const sockaddr_in address{.sin_family = AF_INET,
|
||||
.sin_port = ::htons(port),
|
||||
.sin_addr = {.s_addr = ::htonl(INADDR_ANY)},
|
||||
.sin_zero = {}};
|
||||
|
||||
sockaddr_storage storage{};
|
||||
std::memcpy(&storage, &address, sizeof(address));
|
||||
return storage;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc > 2) {
|
||||
std::cerr << "usage: " << argv[0] << " [configuration file]\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::string_view configPath =
|
||||
(argc == 2) ? argv[1] : DEFAULT_CONFIG_PATH;
|
||||
|
||||
webserv::log::info("configuration file {}", configPath);
|
||||
|
||||
// Ignore SIGPIPE so writing to a closed socket or pipe reports an error
|
||||
// instead of terminating the server. Other clients can then continue.
|
||||
if (::signal(SIGPIPE, SIG_IGN) == SIG_ERR) {
|
||||
std::cerr << "signal(SIGPIPE): " << std::strerror(errno) << '\n';
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
// TEMP: The echo server listens on three fixed ports until main obtains
|
||||
// the listener addresses from the configuration file.
|
||||
const std::array<sockaddr_storage, 3> endpoints{
|
||||
anyAddress(8000), anyAddress(8001), anyAddress(8002)};
|
||||
webserv::Server server(endpoints);
|
||||
server.run();
|
||||
} catch (const std::exception& error) {
|
||||
webserv::log::error("{}", error.what());
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/* ************************************************************************** */
|
||||
/* */
|
||||
/* ::: :::::::: */
|
||||
/* ByteBuffer.cpp :+: :+: :+: */
|
||||
/* +:+ +:+ +:+ */
|
||||
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
|
||||
/* +#+#+#+#+#+ +#+ */
|
||||
/* Created: 2026/08/25 11:41:13 by acossari #+# #+# */
|
||||
/* Updated: 2026/08/25 13:08:04 by acossari ### ########.fr */
|
||||
/* */
|
||||
/* ************************************************************************** */
|
||||
|
||||
#include "net/ByteBuffer.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
|
||||
namespace webserv {
|
||||
|
||||
void ByteBuffer::commit(std::size_t count) {
|
||||
assert(count <= CAPACITY - writePos_);
|
||||
writePos_ += count;
|
||||
}
|
||||
|
||||
void ByteBuffer::consume(std::size_t count) {
|
||||
assert(count <= writePos_ - readPos_);
|
||||
readPos_ += count;
|
||||
if (readPos_ == writePos_) {
|
||||
readPos_ = 0;
|
||||
writePos_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void ByteBuffer::compact() {
|
||||
if (readPos_ == 0) {
|
||||
return;
|
||||
}
|
||||
// Copies live bytes to the start of storage_. The caller decides when
|
||||
// compaction is appropriate.
|
||||
const std::size_t live = writePos_ - readPos_;
|
||||
std::copy_n(storage_.data() + readPos_, live, storage_.data());
|
||||
readPos_ = 0;
|
||||
writePos_ = live;
|
||||
}
|
||||
|
||||
} // namespace webserv
|
||||
@@ -0,0 +1,168 @@
|
||||
/* ************************************************************************** */
|
||||
/* */
|
||||
/* ::: :::::::: */
|
||||
/* Connection.cpp :+: :+: :+: */
|
||||
/* +:+ +:+ +:+ */
|
||||
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
|
||||
/* +#+#+#+#+#+ +#+ */
|
||||
/* Created: 2026/08/28 13:52:38 by acossari #+# #+# */
|
||||
/* Updated: 2026/08/28 21:42:49 by acossari ### ########.fr */
|
||||
/* */
|
||||
/* ************************************************************************** */
|
||||
|
||||
#include "net/Connection.hpp"
|
||||
|
||||
#include <sys/epoll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <span>
|
||||
#include <utility>
|
||||
|
||||
namespace webserv {
|
||||
|
||||
namespace {
|
||||
|
||||
// Writable tail still has space -> keep using it
|
||||
//
|
||||
// Writable tail is exhausted, but more than 16 KiB are still live
|
||||
// -> wait for more bytes to be consumed because moving them is not worth it
|
||||
//
|
||||
// Writable tail is exhausted and at most 16 KiB are still live
|
||||
// -> move the live bytes to the start and recover the writable tail
|
||||
//
|
||||
// Before: [ 48 KiB consumed ][ 16 KiB live ]
|
||||
// After: [ 16 KiB live ][ 48 KiB writable tail ]
|
||||
void compactIfDrained(ByteBuffer& buffer) {
|
||||
if (buffer.writableTail().empty() &&
|
||||
buffer.readable().size() <= Connection::LOW_WATERMARK) {
|
||||
buffer.compact();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Connection::Connection(FileDescriptor socket) : socket_(std::move(socket)) {}
|
||||
|
||||
// Client can still send data -> Alive
|
||||
// Read side closed, buffered data remains -> Alive
|
||||
// Read side closed, both buffers empty -> Done
|
||||
// recv() or send() failed -> Fatal
|
||||
Connection::Outcome Connection::onClientEvent(std::uint32_t events) {
|
||||
// 1) Interpret the readiness reported by epoll.
|
||||
// EPOLLIN says recv() may progress.
|
||||
// EPOLLOUT says send() may progress.
|
||||
// EPOLLRDHUP (epoll read hangup) says the peer closed its write side.
|
||||
// It counts as read readiness because recv() still has to return any buffered
|
||||
// bytes before it returns zero.
|
||||
const bool readReady = (events & (EPOLLIN | EPOLLRDHUP)) != 0;
|
||||
const bool writeReady = (events & EPOLLOUT) != 0;
|
||||
|
||||
// 2) Combine kernel readiness with the connection's current state.
|
||||
const bool canRead =
|
||||
readReady && !readClosed_ && !receiveBuffer_.writableTail().empty();
|
||||
const bool canWrite = writeReady && !sendBuffer_.readable().empty();
|
||||
|
||||
// 3) Perform at most one socket operation. Read wins a tie, but this priority
|
||||
// ends when receiveBuffer_ has no writable tail and canRead becomes false.
|
||||
bool socketUsable = true;
|
||||
if (canRead) {
|
||||
socketUsable = receive();
|
||||
} else if (canWrite) {
|
||||
socketUsable = transmit();
|
||||
}
|
||||
|
||||
if (!socketUsable) {
|
||||
return {.lifecycle = Lifecycle::Fatal, .desiredEvents = 0};
|
||||
}
|
||||
|
||||
// 4) TEMP echo path. Compact sendBuffer_ if needed, forward received bytes
|
||||
// into it, then compact receiveBuffer_ after those bytes have been consumed.
|
||||
compactIfDrained(sendBuffer_);
|
||||
forwardReceivedBytes();
|
||||
compactIfDrained(receiveBuffer_);
|
||||
|
||||
// 5) A closed read side ends the connection only after both buffers drain.
|
||||
const bool drained =
|
||||
sendBuffer_.readable().empty() && receiveBuffer_.readable().empty();
|
||||
return {.lifecycle =
|
||||
(readClosed_ && drained) ? Lifecycle::Done : Lifecycle::Alive,
|
||||
.desiredEvents = desiredEvents()};
|
||||
}
|
||||
|
||||
// Read open, receive space, nothing to send -> EPOLLIN | EPOLLRDHUP
|
||||
// Read open, receive space, output pending -> EPOLLIN | EPOLLRDHUP | EPOLLOUT
|
||||
// Read open, receive full, nothing to send -> EPOLLRDHUP
|
||||
// Read open, receive full, output pending -> EPOLLRDHUP | EPOLLOUT
|
||||
// Read closed, output pending -> EPOLLOUT
|
||||
// Read closed, nothing to send -> 0
|
||||
std::uint32_t Connection::desiredEvents() noexcept {
|
||||
std::uint32_t events = 0;
|
||||
if (!readClosed_) {
|
||||
events |= EPOLLRDHUP;
|
||||
// Backpressure: omit EPOLLIN while the receive buffer has no writable tail.
|
||||
// TCP slows the peer until space returns.
|
||||
if (!receiveBuffer_.writableTail().empty()) {
|
||||
events |= EPOLLIN;
|
||||
}
|
||||
}
|
||||
if (!sendBuffer_.readable().empty()) {
|
||||
events |= EPOLLOUT;
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
// true keeps the socket usable, while false retires the connection.
|
||||
// recv() > 0 (bytes received and committed to the buffer) -> true
|
||||
// recv() == 0 (the client has finished sending) -> true
|
||||
// recv() < 0 (socket error) -> false
|
||||
bool Connection::receive() {
|
||||
const std::span<char> tail = receiveBuffer_.writableTail();
|
||||
// May write at most tail.size() bytes into the free buffer space.
|
||||
// The final zero requests no special options.
|
||||
const ssize_t count = ::recv(socket_.get(), tail.data(), tail.size(), 0);
|
||||
if (count > 0) {
|
||||
// Advances writePos_ so the bytes written become readable.
|
||||
// Before: [ readable bytes ][ writable tail ]
|
||||
// After: [ readable bytes ][ received bytes ][ writable tail ]
|
||||
receiveBuffer_.commit(static_cast<std::size_t>(count));
|
||||
return true;
|
||||
}
|
||||
// EOF is not a socket error. Pending response bytes may still be sent, so the
|
||||
// socket remains usable.
|
||||
if (count == 0) {
|
||||
readClosed_ = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Connection::transmit() {
|
||||
const std::span<const char> pending = sendBuffer_.readable();
|
||||
const ssize_t count =
|
||||
::send(socket_.get(), pending.data(), pending.size(), 0);
|
||||
if (count <= 0) {
|
||||
return false;
|
||||
}
|
||||
// Advances readPos_ so the bytes sent are no longer readable.
|
||||
// Before: [ bytes pending to send ]
|
||||
// After: [ bytes accepted ][ bytes still pending to send ]
|
||||
sendBuffer_.consume(static_cast<std::size_t>(count));
|
||||
return true;
|
||||
}
|
||||
|
||||
// TEMP echo
|
||||
void Connection::forwardReceivedBytes() {
|
||||
const std::span<const char> pending = receiveBuffer_.readable();
|
||||
const std::span<char> tail = sendBuffer_.writableTail();
|
||||
const std::size_t count = std::min(pending.size(), tail.size());
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
std::copy_n(pending.begin(), count, tail.begin());
|
||||
sendBuffer_.commit(count);
|
||||
receiveBuffer_.consume(count);
|
||||
}
|
||||
|
||||
} // namespace webserv
|
||||
@@ -0,0 +1,96 @@
|
||||
/* ************************************************************************** */
|
||||
/* */
|
||||
/* ::: :::::::: */
|
||||
/* FileDescriptor.cpp :+: :+: :+: */
|
||||
/* +:+ +:+ +:+ */
|
||||
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
|
||||
/* +#+#+#+#+#+ +#+ */
|
||||
/* Created: 2026/08/24 16:23:52 by acossari #+# #+# */
|
||||
/* Updated: 2026/08/28 16:13:23 by acossari ### ########.fr */
|
||||
/* */
|
||||
/* ************************************************************************** */
|
||||
|
||||
#include "net/FileDescriptor.hpp"
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
#include <utility>
|
||||
|
||||
namespace webserv {
|
||||
|
||||
// "Sink parameter": consumes the local parameter by moving its value into msg_
|
||||
// lvalue -> copy into message -> move into msg_ (1 copy, worst case scenario)
|
||||
// rvalue -> move into message -> move into msg_ (0 copies)
|
||||
FileDescriptor::Error::Error(std::string message) : msg_(std::move(message)) {}
|
||||
|
||||
const char* FileDescriptor::Error::what() const noexcept {
|
||||
return msg_.c_str();
|
||||
}
|
||||
|
||||
// A valid fd is either made close-on-exec or closed before construction fails.
|
||||
FileDescriptor::FileDescriptor(int fd) : fd_(fd) {
|
||||
if (fd_ < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// fork() inherits this fd, but a successful execve() closes it before the
|
||||
// new program starts, preventing CGI processes from retaining server fds.
|
||||
if (::fcntl(fd_, F_SETFD, FD_CLOEXEC) == -1) {
|
||||
// The subject forbids errno only after read/recv/write/send.
|
||||
// Save it before close(), which may overwrite the original fcntl error.
|
||||
const int errnoCode = errno;
|
||||
::close(fd_);
|
||||
fd_ = -1;
|
||||
throw Error(std::format("fcntl(F_SETFD, FD_CLOEXEC) on fd {}: {}", fd,
|
||||
std::strerror(errnoCode)));
|
||||
}
|
||||
}
|
||||
|
||||
FileDescriptor::~FileDescriptor() {
|
||||
reset();
|
||||
}
|
||||
|
||||
// The FileDescriptor&& parameter is an rvalue reference and a sink. It accepts
|
||||
// only a temporary or an object passed with std::move(). The constructor takes
|
||||
// ownership of that object's fd, so only the new FileDescriptor will close it.
|
||||
FileDescriptor::FileDescriptor(FileDescriptor&& other) noexcept
|
||||
: fd_(std::exchange(other.fd_, -1)) {}
|
||||
|
||||
FileDescriptor& FileDescriptor::operator=(FileDescriptor&& other) noexcept {
|
||||
if (this != &other) {
|
||||
reset();
|
||||
fd_ = std::exchange(other.fd_, -1);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
void FileDescriptor::setNonBlocking() {
|
||||
if (!valid()) {
|
||||
throw Error("setNonBlocking() on an invalid file descriptor");
|
||||
}
|
||||
|
||||
// Read, modify, write pattern: F_SETFL replaces the whole status flags word,
|
||||
// so passing O_NONBLOCK alone would clear whatever else is set.
|
||||
const int flags = ::fcntl(fd_, F_GETFL);
|
||||
if (flags == -1) {
|
||||
throw Error(
|
||||
std::format("fcntl(F_GETFL) on fd {}: {}", fd_, std::strerror(errno)));
|
||||
}
|
||||
if (::fcntl(fd_, F_SETFL, flags | O_NONBLOCK) == -1) {
|
||||
throw Error(std::format("fcntl(F_SETFL, O_NONBLOCK) on fd {}: {}", fd_,
|
||||
std::strerror(errno)));
|
||||
}
|
||||
}
|
||||
|
||||
void FileDescriptor::reset() noexcept {
|
||||
if (fd_ >= 0) {
|
||||
::close(fd_);
|
||||
fd_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace webserv
|
||||
@@ -0,0 +1,87 @@
|
||||
/* ************************************************************************** */
|
||||
/* */
|
||||
/* ::: :::::::: */
|
||||
/* Listener.cpp :+: :+: :+: */
|
||||
/* +:+ +:+ +:+ */
|
||||
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
|
||||
/* +#+#+#+#+#+ +#+ */
|
||||
/* Created: 2026/08/27 15:51:53 by acossari #+# #+# */
|
||||
/* Updated: 2026/08/27 18:12:50 by acossari ### ########.fr */
|
||||
/* */
|
||||
/* ************************************************************************** */
|
||||
|
||||
#include "net/Listener.hpp"
|
||||
|
||||
#include <netinet/in.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
#include <utility>
|
||||
|
||||
namespace webserv {
|
||||
|
||||
namespace {
|
||||
|
||||
socklen_t addressLength(const sockaddr_storage& address) {
|
||||
// AF_INET identifies IPv4, and AF_INET6 identifies IPv6.
|
||||
// sockaddr_in and sockaddr_in6 are the matching structures that hold the
|
||||
// socket address, including the family, IP address, and port.
|
||||
switch (address.ss_family) {
|
||||
case AF_INET:
|
||||
return static_cast<socklen_t>(sizeof(sockaddr_in));
|
||||
case AF_INET6:
|
||||
return static_cast<socklen_t>(sizeof(sockaddr_in6));
|
||||
default:
|
||||
throw Listener::Error(
|
||||
std::format("unsupported address family {}", address.ss_family));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Listener::Error::Error(std::string message) : msg_(std::move(message)) {}
|
||||
|
||||
const char* Listener::Error::what() const noexcept {
|
||||
return msg_.c_str();
|
||||
}
|
||||
|
||||
Listener::Listener(const sockaddr_storage& address)
|
||||
// Creates a socket in the IPv4 or IPv6 family stored in address.
|
||||
// SOCK_STREAM requests a byte stream, and protocol 0 selects TCP as the
|
||||
// default stream protocol for both supported families.
|
||||
: fd_(::socket(address.ss_family, SOCK_STREAM, 0)) {
|
||||
if (!fd_.valid()) {
|
||||
throw Error(std::format("socket: {}", std::strerror(errno)));
|
||||
}
|
||||
|
||||
// Allows the local address and port to be reused without waiting for old TCP
|
||||
// connection states to expire in the kernel. SO_REUSEADDR belongs to
|
||||
// SOL_SOCKET, the group of general socket options.
|
||||
// The old C socket API uses an int for this on/off setting.
|
||||
const int enabled = 1;
|
||||
if (::setsockopt(fd_.get(), SOL_SOCKET, SO_REUSEADDR, &enabled,
|
||||
sizeof(enabled)) == -1) {
|
||||
throw Error(
|
||||
std::format("setsockopt(SO_REUSEADDR): {}", std::strerror(errno)));
|
||||
}
|
||||
|
||||
fd_.setNonBlocking();
|
||||
|
||||
const socklen_t length = addressLength(address);
|
||||
// Assigns the local IP address and port in address to the socket.
|
||||
// reinterpret_cast presents the same pointer as the generic sockaddr pointer.
|
||||
// length limits the read to the bytes of sockaddr_in or sockaddr_in6.
|
||||
if (::bind(fd_.get(), reinterpret_cast<const sockaddr*>(&address), length) ==
|
||||
-1) {
|
||||
throw Error(std::format("bind: {}", std::strerror(errno)));
|
||||
}
|
||||
|
||||
// Marks the bound socket as a listener for incoming TCP connections.
|
||||
// SOMAXCONN requests the max pending connection queue allowed by the system.
|
||||
if (::listen(fd_.get(), SOMAXCONN) == -1) {
|
||||
throw Error(std::format("listen: {}", std::strerror(errno)));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace webserv
|
||||
@@ -0,0 +1,43 @@
|
||||
/* ************************************************************************** */
|
||||
/* */
|
||||
/* ::: :::::::: */
|
||||
/* PauseMask.cpp :+: :+: :+: */
|
||||
/* +:+ +:+ +:+ */
|
||||
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
|
||||
/* +#+#+#+#+#+ +#+ */
|
||||
/* Created: 2026/08/25 20:03:02 by acossari #+# #+# */
|
||||
/* Updated: 2026/08/25 21:35:31 by acossari ### ########.fr */
|
||||
/* */
|
||||
/* ************************************************************************** */
|
||||
|
||||
#include "net/PauseMask.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace webserv {
|
||||
|
||||
bool PauseMask::set(ListenerPause reason) {
|
||||
const std::uint8_t before = bits_;
|
||||
// std::to_underlying converts any enum value to its declared integer type.
|
||||
// Here ListenerPause becomes uint8_t. The |= bitwise OR assignment sets its
|
||||
// flag in bits_ without clearing the pause reasons already present.
|
||||
bits_ |= std::to_underlying(reason);
|
||||
return before == 0 && bits_ != 0;
|
||||
}
|
||||
|
||||
bool PauseMask::clear(ListenerPause reason) {
|
||||
const std::uint8_t before = bits_;
|
||||
// Cast reason to unsigned before applying ~ (bitwise NOT operator), which
|
||||
// flips every bit. C++ promotes uint8_t to signed int because int can
|
||||
// represent all its values; casting directly to unsigned avoids that.
|
||||
|
||||
// Ex. Clearing it while both pause reasons are active:
|
||||
// bits_ 00000011
|
||||
// reason 00000010
|
||||
// & ~reason 11111101
|
||||
// result 00000001
|
||||
bits_ &= static_cast<std::uint8_t>(~static_cast<unsigned>(reason));
|
||||
return before != 0 && bits_ == 0;
|
||||
}
|
||||
|
||||
} // namespace webserv
|
||||
@@ -0,0 +1,123 @@
|
||||
/* ************************************************************************** */
|
||||
/* */
|
||||
/* ::: :::::::: */
|
||||
/* SlotPool.cpp :+: :+: :+: */
|
||||
/* +:+ +:+ +:+ */
|
||||
/* By: acossari <acossari@student.codam.nl> +#+ +:+ +#+ */
|
||||
/* +#+#+#+#+#+ +#+ */
|
||||
/* Created: 2026/08/28 22:07:22 by acossari #+# #+# */
|
||||
/* Updated: 2026/08/29 21:10:35 by acossari ### ########.fr */
|
||||
/* */
|
||||
/* ************************************************************************** */
|
||||
|
||||
#include "net/SlotPool.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <utility>
|
||||
|
||||
namespace webserv {
|
||||
|
||||
SlotPool::SlotPool() {
|
||||
// Reserves room for at most one retired index per slot.
|
||||
pendingFree_.reserve(CAPACITY);
|
||||
|
||||
// Links each free slot to the next. The sentinel value 256 ends the list.
|
||||
for (std::uint32_t index = 0; index < CAPACITY; ++index) {
|
||||
slots_[index].nextFree = index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Receives ownership of the socket and transfers it to the Connection built in
|
||||
// the selected slot. With no free slot, the local FileDescriptor closes the
|
||||
// socket when this function returns.
|
||||
std::optional<std::uint32_t> SlotPool::acquire(FileDescriptor socket) {
|
||||
if (full()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const std::uint32_t index = freeHead_;
|
||||
Slot& slot = slots_[index];
|
||||
// emplace constructs the Connection directly inside the optional because it
|
||||
// cannot be copied or moved. Moving socket makes that Connection the owner
|
||||
// of the accepted file descriptor.
|
||||
slot.connection.emplace(std::move(socket));
|
||||
freeHead_ = slot.nextFree;
|
||||
slot.state = SlotState::InUse;
|
||||
return index;
|
||||
}
|
||||
|
||||
// Rejects tokens for inactive slots, unregistered endpoints, or an earlier
|
||||
// endpoint generation so queued stale events cannot reach a Connection.
|
||||
bool SlotPool::isCurrent(const EventToken& token) const noexcept {
|
||||
if (token.kind == EndpointKind::Listener || token.index >= CAPACITY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Slot& slot = slots_[token.index];
|
||||
if (slot.state != SlotState::InUse) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Converts token.kind from an enum value to the array index 0, 1, or 2.
|
||||
const Endpoint& endpoint =
|
||||
slot.endpoints[static_cast<std::size_t>(token.kind)];
|
||||
return endpoint.registered && endpoint.generation == token.generation;
|
||||
}
|
||||
|
||||
Connection& SlotPool::connection(std::uint32_t index) {
|
||||
assert(index < CAPACITY);
|
||||
Slot& slot = slots_[index];
|
||||
assert(slot.connection.has_value());
|
||||
// Dereferences the optional to return its stored Connection by reference.
|
||||
return *slot.connection;
|
||||
}
|
||||
|
||||
SlotPool::Endpoint& SlotPool::endpoint(std::uint32_t index, EndpointKind kind) {
|
||||
assert(index < CAPACITY);
|
||||
const auto endpointIndex = static_cast<std::size_t>(kind);
|
||||
assert(endpointIndex < ENDPOINTS_PER_SLOT);
|
||||
return slots_[index].endpoints[endpointIndex];
|
||||
}
|
||||
|
||||
// Quarantines an active slot until the current event batch is complete.
|
||||
// pendingFree_ reserves CAPACITY entries and holds at most one per slot, so
|
||||
// push_back() cannot reallocate or throw inside this noexcept function.
|
||||
// NOLINTNEXTLINE(bugprone-exception-escape)
|
||||
void SlotPool::retire(std::uint32_t index) noexcept {
|
||||
assert(index < CAPACITY);
|
||||
Slot& slot = slots_[index];
|
||||
assert(slot.state == SlotState::InUse);
|
||||
assert(slot.connection.has_value());
|
||||
|
||||
// The arrow accesses the Connection stored inside the optional. Its socket
|
||||
// is released now, but the object stays alive until drain() destroys it.
|
||||
slot.connection->close();
|
||||
// References make the loop modify the Endpoint objects stored in the slot.
|
||||
for (Endpoint& endpoint : slot.endpoints) {
|
||||
endpoint.registered = false;
|
||||
endpoint.registeredEvents = 0;
|
||||
++endpoint.generation;
|
||||
}
|
||||
// Keeps the slot unavailable until drain() returns it to the free list.
|
||||
slot.state = SlotState::Retired;
|
||||
pendingFree_.push_back(index);
|
||||
}
|
||||
|
||||
// Recycles all quarantined slots and returns how many became available.
|
||||
std::size_t SlotPool::drain() {
|
||||
// Saves how many Retired slots this call will return to the free list.
|
||||
const std::size_t recycled = pendingFree_.size();
|
||||
for (const std::uint32_t index : pendingFree_) {
|
||||
Slot& slot = slots_[index];
|
||||
// The batch is complete, so destroy the Connection and empty the optional.
|
||||
slot.connection.reset();
|
||||
slot.state = SlotState::Free;
|
||||
// Prepends the recycled slot to the free list.
|
||||
slot.nextFree = freeHead_;
|
||||
freeHead_ = index;
|
||||
}
|
||||
pendingFree_.clear();
|
||||
return recycled;
|
||||
}
|
||||
|
||||
} // namespace webserv
|
||||
@@ -0,0 +1,105 @@
|
||||
# tester42 — official 42 webserv testers
|
||||
|
||||
Binaries attached to the intra evaluation scale, plus the fixtures they require.
|
||||
All of them are Go programs, not stripped, so their expectations can be read out of the
|
||||
binaries directly — `strings`, `nm`, and `go tool objdump`, which still shows the original
|
||||
source file and line numbers. Nothing below is guesswork, and the whole sequence has also
|
||||
been executed once against a probe stub.
|
||||
|
||||
## Layout and provenance
|
||||
|
||||
```
|
||||
official/ tester, cgi_tester ← downloaded from intra. THE ONLY TRUSTED ONES.
|
||||
YoupiBanane/ the fixture tree the tester requires (contents dictated by the tester
|
||||
itself — see its printed instructions below, so it is reconstructable)
|
||||
```
|
||||
|
||||
Both official binaries are **Linux ELF x86-64** and cannot run on macOS. They run in the
|
||||
project's Linux container, which is also the platform the school VMs use.
|
||||
|
||||
## What the tester requires (verbatim from the binary)
|
||||
|
||||
```
|
||||
- Download the cgi_test executable on the host
|
||||
- Create a directory YoupiBanane with:
|
||||
- A file name youpi.bad_extension
|
||||
- A file name youpi.bla
|
||||
- A sub directory called nop
|
||||
- A file name youpi.bad_extension in nop
|
||||
- A file name other.pouic in nop
|
||||
- A sub directory called Yeah
|
||||
- A file name not_happy.bad_extension in Yeah
|
||||
- / must answer to GET request ONLY
|
||||
- /directory/ must answer to GET request and the root of it would be the repository
|
||||
YoupiBanane and if no file are requested, it should search for youpi.bad_extension files
|
||||
- /post_body must answer anything to POST request with a maxBody of 100
|
||||
- Any file with .bla as extension must answer to POST request by calling the cgi_test executable
|
||||
```
|
||||
|
||||
## Test cases found in the binary
|
||||
|
||||
The complete executed order has now been verified against a passing probe stub.
|
||||
|
||||
| Area | Evidence in binary |
|
||||
|---|---|
|
||||
| `Test GET` | `GET on /directory/nop`, `directory/Yeah/not_happy.bad_extension`, `directory/nop/other.pouic` |
|
||||
| 404 paths | `directory/oulalala`, `directory/nop/other.pouac` |
|
||||
| `Test HEAD` | `HEAD /` is exercised and must return `405`; general HEAD support is not tested |
|
||||
| `Test POST` | `/post_body` with `maxBody 100` |
|
||||
| CGI | `Post on /directory/youpi.bla with size 100000000` (100 MB body into CGI), `bad cgi returned body content` |
|
||||
| Concurrency | `Test multiple workers(%d) doing multiple times(%d)`, `client disconnected` |
|
||||
|
||||
`FATAL ERROR ON LAST TEST:` is the message printed when a case fails.
|
||||
|
||||
## CGI environment the cgi_tester reads
|
||||
|
||||
```
|
||||
REQUEST_METHOD SERVER_PROTOCOL CONTENT_LENGTH CONTENT_TYPE
|
||||
QUERY_STRING PATH_INFO HTTP_HOST HTTP_<any request header>
|
||||
```
|
||||
|
||||
The binary vendors Go's `net/http/cgi`, so the contract is that package's, not folklore.
|
||||
Three of these are failure modes rather than niceties:
|
||||
|
||||
- **`SERVER_PROTOCOL` is mandatory.** `RequestFromMap` calls `http.ParseHTTPVersion` on it and
|
||||
aborts with `cgi: invalid SERVER_PROTOCOL version` if it is missing. The CGI never runs.
|
||||
- **`PATH_INFO` must equal the request path**, with `SCRIPT_NAME` left empty. The handler
|
||||
compares `os.Getenv("PATH_INFO")` against `r.URL.Path` and answers `500 PATH_INFO incorrect`
|
||||
otherwise. Splitting them the way RFC 3875 prescribes **fails this tester**.
|
||||
- **Arbitrary headers must be exported as `HTTP_*`** (uppercase, dashes to underscores). One
|
||||
test reads `X-SECRET-HEADER-FOR-TEST`, so `HTTP_X_SECRET_HEADER_FOR_TEST` has to be there.
|
||||
- `CONTENT_LENGTH` must be exact — including for the 100 MB POST. Wrong value gives
|
||||
`cgi: bad CONTENT_LENGTH in environment`; a missing one makes the CGI read zero bytes,
|
||||
because `Request()` wraps stdin in `io.LimitReader(os.Stdin, r.ContentLength)`.
|
||||
- `CONTENT_TYPE` arrives as `test/file`. Not a real MIME type — forward it verbatim.
|
||||
|
||||
Its output starts with a `Status: NNN Text` header, which the server must turn into the HTTP
|
||||
status line rather than forward. No `Content-Length` is emitted: EOF ends the body.
|
||||
|
||||
## Scope implications
|
||||
|
||||
Passing this tester requires more than the subject text asks for:
|
||||
|
||||
- **location matched by extension** (`*.bla`), not only by path prefix, and taking precedence
|
||||
over the prefix match so that `POST /directory/youpi.bla` reaches the CGI while
|
||||
`GET` on the same path serves the file
|
||||
- **`alias` semantics** distinct from `root`: `/directory/` maps onto `YoupiBanane`, which has
|
||||
no `directory/` subtree, so plain `root` cannot express it
|
||||
- **per-location body limit** (`/post_body`, 100 bytes), enforced on the **decoded** body —
|
||||
every POST the tester sends is chunked, so there is no `Content-Length` to check
|
||||
- a 100 MB request body streamed into a CGI without buffering it all in memory
|
||||
|
||||
Nested `location` blocks are *not* required by this binary; they are kept in the config
|
||||
grammar because the grammar is expensive to change later, not because a test needs them.
|
||||
|
||||
This official binary does **not** execute PUT or DELETE. DELETE and uploads remain mandatory
|
||||
because the subject and evaluation checklist require them; they need project-owned tests.
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
# inside the Linux container, from the directory that contains YoupiBanane
|
||||
./official/tester http://localhost:8000
|
||||
```
|
||||
|
||||
The config must point the `.bla` CGI at `official/cgi_tester`.
|
||||
@@ -0,0 +1 @@
|
||||
unhappyyyyy
|
||||
@@ -0,0 +1 @@
|
||||
this is YoupiBanane/directory/youpi.bla
|
||||
@@ -0,0 +1 @@
|
||||
This is the default index yo!
|
||||
@@ -0,0 +1 @@
|
||||
pouic content
|
||||
@@ -0,0 +1 @@
|
||||
bad extension?
|
||||
@@ -0,0 +1 @@
|
||||
Nope
|
||||
@@ -0,0 +1 @@
|
||||
yololo
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>404 Not Found</title>
|
||||
<h1>404 Not Found</h1>
|
||||
<p>Configured error page.</p>
|
||||
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Server error</title>
|
||||
<h1>Server error</h1>
|
||||
<p>Configured error page.</p>
|
||||
@@ -0,0 +1 @@
|
||||
A plain file, so /files/ has something to list.
|
||||
@@ -0,0 +1,9 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>webserv</title>
|
||||
<h1>webserv</h1>
|
||||
<p>Static root served from <code>./www</code>.</p>
|
||||
<ul>
|
||||
<li><a href="/files/">/files/</a> — directory listing</li>
|
||||
<li><a href="/old">/old</a> — 301 redirect</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,5 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>webserv.local</title>
|
||||
<h1>webserv.local</h1>
|
||||
<p>Different server block, same interface and port. Picked by the Host header.</p>
|
||||
Reference in New Issue
Block a user