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