- Project Overview
- Key Features
- Architecture & Directory Structure
- Program Flow & Request Lifecycle
- Flowchart
- Configuration File
- CGI Execution
- Session Management
- Technical Constraints
- How to Build & Run
- Testing
- Authors
Webserv is a fully functional HTTP/1.1 web server implemented from scratch in C++98, developed as part of the 42 Network curriculum. The project demands a deep understanding of network programming, the HTTP protocol, and Unix system calls β all without the aid of external libraries or frameworks.
The server is capable of:
- Handling multiple simultaneous client connections using I/O multiplexing (
poll()). - Serving static files, processing CGI scripts (Python, Shell), and managing file uploads.
- Being configured via a custom Nginx-inspired configuration file.
- Maintaining lightweight session state through HTTP cookies.
This project is the direct analogue of building a simplified version of Nginx or Apache β understanding every byte that flows between client and server.
| Feature | Details |
|---|---|
| Methods | GET, POST, DELETE |
| HTTP Version | HTTP/1.1 (persistent connections) |
| Request Parsing | Request line, headers, body, chunked transfer encoding |
| Response Generation | Status line, headers, body with correct Content-Length |
| Redirections | 301 permanent redirects via return directive |
| Directory Listing | autoindex on/off per location block |
| Custom Error Pages | Configurable per server via error_page directive |
| Feature | Details |
|---|---|
| Multiplexing | poll() β single-threaded, event-driven I/O loop |
| Non-Blocking Sockets | All sockets set with O_NONBLOCK via fcntl() |
| Multiple Ports | A single server block can listen on multiple ports simultaneously |
| Virtual Hosting | Multiple server blocks resolved by Host header and server_name |
| Connection Handling | Supports persistent (keep-alive) and short-lived connections |
| Feature | Details |
|---|---|
| CGI Execution | Executes .py (Python 3) and .sh (Bash) scripts via fork()/execve() |
| File Uploads | POST to /upload writes multipart data to a configurable upload_path |
| Cookie Sessions | Server-side session generation and validation via Set-Cookie / Cookie headers |
| Config Parser | Custom Nginx-like parser supporting nested server and location blocks |
Webserv_42/
βββ conf/
β βββ default.conf # Default server configuration
βββ includes/
β βββ Config.hpp # Top-level config aggregator
β βββ ServerConfig.hpp # Per-server block configuration
β βββ Location.hpp # Per-location block configuration
β βββ Parser.hpp # Config file parser
β βββ HTTPRequest.hpp # HTTP request model
β βββ HTTPResponse.hpp # HTTP response builder
β βββ HTTPServerManager.hpp # Core event loop & socket manager
β βββ handler/
β βββ IHandler.hpp # Abstract handler interface
β βββ HandlerFactory.hpp# Routes requests to the correct handler
β βββ staticHandler.hpp # Serves static files
β βββ CGIHandler.hpp # Executes CGI scripts
β βββ UploadHandler.hpp # Handles file uploads
β βββ deleteHandler.hpp # Handles DELETE requests
β βββ RedirectHandler.hpp # Handles HTTP redirects
β βββ errorHandler.hpp # Serves error pages
βββ srcs/
β βββ main.cpp # Entry point
β βββ config/ # Config parsing implementation
β βββ http/
β β βββ HTTPRequest/ # Request parsing (line, headers, body)
β β βββ HTTPResponse/ # Response serialization
β β βββ handler/ # Handler implementations
β βββ server/ # HTTPServerManager (event loop)
β βββ utils/ # Shared utility functions
βββ www/
β βββ html/ # Static HTML files & index
β βββ cgi-bin/ # CGI scripts (.py, .sh)
β βββ upload/ # Upload destination directory
β βββ error_pages/ # Custom HTML error pages
βββ Makefile
The server operates as a single-threaded, event-driven system. All I/O is non-blocking and orchestrated by a central poll() loop. The lifecycle of every HTTP request proceeds through the following distinct stages:
At launch, main.cpp passes the config file path to the Config parser. Once all ServerConfig objects are populated, HTTPServerManager iterates over every (host, port) pair defined across all server blocks.
For each pair:
- A TCP socket is created with
socket(AF_INET, SOCK_STREAM, 0). SO_REUSEADDRis set to avoidAddress already in useerrors on restart.- The socket is bound to its address with
bind(). listen()is called to mark it as a passive socket.fcntl(fd, F_SETFL, O_NONBLOCK)is called to make all I/O non-blocking.- The listening file descriptor is registered in the
poll()array withPOLLINinterest.
HTTPServerManager enters an infinite poll() loop. On each iteration:
poll()blocks until at least one file descriptor becomes ready.- For each
pollfdwith aPOLLINevent:- If it is a listening socket β
accept()a new client connection, set it non-blocking, and add it to thepollset. - If it is a client socket β read incoming data into a per-client buffer.
- If it is a listening socket β
- For each
pollfdwith aPOLLOUTevent:- If the client has a pending response β write the response data and advance a send-position pointer.
- Closed or errored connections are detected via
POLLHUP/POLLERRand removed from the set.
Note: No
fork(),thread, or blockingread()/write()is ever called on the main event loop. All heavy work (e.g., CGI) is done with carefully managed child processes whose pipe descriptors are also registered inpoll().
Once sufficient data arrives in a client's read buffer, it is fed to the HTTPRequest parser, which operates as a state machine across three independent compilation units:
- Tokenizes the first line into method, URI (with optional query string), and HTTP version.
- Validates the method against the set
{GET, POST, DELETE}. - Rejects malformed request lines with
400 Bad Request.
- Reads headers line by line until the blank
\r\n\r\nseparator. - Populates a
std::map<std::string, std::string>of header key-value pairs. - Key headers extracted:
Host,Content-Length,Transfer-Encoding,Content-Type,Cookie.
- Fixed-length body: reads exactly
Content-Lengthbytes. - Chunked Transfer Encoding: Decodes chunk-size hex values and reassembles the body incrementally. Each chunk follows the format:
<hex-size>\r\n<data>\r\n. client_max_body_size: If the incoming body exceeds the limit configured for the matchedLocation, parsing is aborted and a413 Content Too Largeresponse is generated.
After a complete request is parsed, HandlerFactory::createHandler() inspects the request and matched Location block to decide which handler to instantiate:
Request URI
β
βΌ
Match Location block (longest-prefix match)
β
ββ Location has `return` directive? βββΊ RedirectHandler (301/302)
β
ββ Method is DELETE? βββΊ deleteHandler
β
ββ URI maps to a file with a `cgi_ext` match?
β ββ e.g., `.py` or `.sh` βββΊ CGIHandler
β
ββ Method is POST & `upload_path` set?
β βββΊ UploadHandler
β
ββ Target is a regular file? βββΊ staticHandler
β
ββ None matched / access denied? βββΊ errorHandler (403/404/405)
All handlers implement the IHandler interface, exposing a single handle(HTTPRequest&, HTTPResponse&) method.
The CGI handler bridges the HTTP server with external interpreter processes:
-
Environment Setup: A
char**array of environment variables is constructed from the request metadata, following the CGI/1.1 specification:REQUEST_METHOD,QUERY_STRING,CONTENT_TYPE,CONTENT_LENGTHSCRIPT_FILENAME,SCRIPT_NAME,PATH_INFOSERVER_NAME,SERVER_PORT,HTTP_COOKIE
-
Pipe Creation: Two
pipe()pairs are created β one for stdin (sending the request body to the script) and one for stdout (capturing the script's output). -
fork()&execve(): A child process is created. It:dup2()s the pipe ends onto itsSTDIN_FILENO/STDOUT_FILENO.- Calls
execve()with the interpreter path (e.g.,/usr/bin/python3) and the script path asargv[1].
-
Non-blocking I/O on pipes: The write-end of stdin and read-end of stdout are registered in
poll(). Data is fed and collected incrementally without blocking. -
Timeout handling: A deadline is tracked per CGI process. If the child does not exit within the configured timeout,
kill(pid, SIGKILL)is called and504 Gateway Timeoutis returned. -
Output parsing: The CGI output is split at
\r\n\r\n. AnyStatus:header in the CGI output is mapped to the HTTP status code. Remaining headers and the body are forwarded in the HTTP response.
The server implements a lightweight, stateless-on-disk session mechanism:
-
Session Creation: When a CGI script or handler logic requires a session, a unique session token is generated using a hash of a timestamp, the client IP, and a pseudo-random seed β producing a hexadecimal string (e.g.,
a3f8c21d...). -
Cookie Issuance: The response includes:
Set-Cookie: session_id=a3f8c21d...; HttpOnly; Path=/ -
Session Verification: On subsequent requests, the
Cookie: session_id=...header is parsed. The token is looked up in the server's in-memory session store (std::map<std::string, SessionData>). If found and not expired, the session is considered valid. -
CGI Access: The raw
HTTP_COOKIEenvironment variable is passed to CGI scripts, allowing Python/Shell scripts to read and set their own session-level data.
Once a handler completes:
-
HTTPResponseconstructs the response buffer:- Status line:
HTTP/1.1 <code> <reason>\r\n - Headers:
Content-Type,Content-Length,Connection,Set-Cookie(if applicable), etc. - Blank line:
\r\n - Body: file content, CGI output, or generated HTML.
- Status line:
-
The complete serialized response is stored in the client's send buffer.
-
The client's
pollfdentry is updated to watch forPOLLOUT. -
On the next
poll()iteration, the data is written in chunks viasend()until the buffer is drained, preventing any single response from monopolizing the loop.
flowchart TD
A([π Client Request]) --> B[TCP Accept\nnew connection fd]
B --> C{poll loop\nPOLLIN ready?}
C -->|Yes| D[Read data\ninto buffer]
D --> E[HTTPRequest Parser]
E --> E1[Parse Request Line\nMethod Β· URI Β· Version]
E1 --> E2[Parse Headers\nHost Β· Content-Type Β· Cookie ...]
E2 --> E3{Body?}
E3 -->|Content-Length| E4[Read fixed body]
E3 -->|Chunked| E5[Decode chunks]
E3 -->|No body| F
E4 --> F[HandlerFactory\nRouting Decision]
E5 --> F
F --> G{Route Match}
G -->|return directive| H[RedirectHandler\n301 / 302]
G -->|DELETE method| I[deleteHandler\nUnlink file]
G -->|cgi_ext match| J[CGIHandler]
G -->|upload_path set| K[UploadHandler\nWrite to disk]
G -->|Static file| L[staticHandler\nRead & serve file]
G -->|No match / Error| M[errorHandler\n4xx / 5xx]
J --> J1[Build env vars\nCGI/1.1 spec]
J1 --> J2[pipe + fork + execve\ninterpreter script]
J2 --> J3[Collect stdout\nvia poll on pipe]
J3 --> J4[Parse CGI headers\nStatus Β· Content-Type]
L --> N[HTTPResponse Builder]
H --> N
I --> N
K --> N
M --> N
J4 --> N
N --> O[Serialize\nStatus Β· Headers Β· Body]
O --> P{Cookie / Session?}
P -->|New session| Q[Generate token\nSet-Cookie header]
P -->|Existing| R[Validate token\nfrom session store]
Q --> S
R --> S
S[Add to send buffer\nmark POLLOUT] --> T{poll loop\nPOLLOUT ready?}
T -->|Yes| U[send in chunks\nnon-blocking]
U --> V([β
Response delivered\nto Client])
The configuration file follows an Nginx-inspired syntax, supporting multiple server blocks, each with one or more location blocks.
| Directive | Scope | Description |
|---|---|---|
port |
server |
One or more ports to listen on (space-separated) |
host |
server |
IP address to bind |
server_name |
server |
Virtual host names (space-separated) |
root |
server / location |
Document root for file resolution |
index |
server / location |
Default file for directory requests |
client_max_body_size |
server / location |
Max allowed body size (e.g., 1M, 5M, 1G) |
error_page |
server |
Map status codes to custom error page paths |
allow_methods |
location |
Whitelist of HTTP methods |
autoindex |
location |
on enables directory listing, off disables |
return |
location |
HTTP redirect (e.g., 301 /new-path) |
upload_path |
location |
Directory to store uploaded files |
cgi_ext |
location |
Maps file extension to interpreter (e.g., .py /usr/bin/python3) |
server {
port 8080 6060;
host 127.0.0.1;
server_name mysite.com;
root /Webserv_42/www/html/;
index index.html;
client_max_body_size 1M;
error_page 404 /Webserv_42/www/error_pages/404.html;
location / {
root /Webserv_42/www/html;
allow_methods GET;
autoindex off;
index index.html;
}
location /upload {
allow_methods POST GET DELETE;
root /Webserv_42/www/;
client_max_body_size 1G;
upload_path /Webserv_42/www/upload;
}
location /cgi-bin {
allow_methods GET POST;
root /Webserv_42/www;
index main.py;
client_max_body_size 5M;
cgi_ext .py /usr/bin/python3;
cgi_ext .sh /bin/bash;
}
location /old {
return 301 /;
}
}CGI (Common Gateway Interface) is the mechanism by which the server delegates response generation to an external script. The implementation strictly follows CGI/1.1 conventions.
REQUEST_METHOD=POST
QUERY_STRING=name=foo&bar=baz
CONTENT_TYPE=application/x-www-form-urlencoded
CONTENT_LENGTH=18
SCRIPT_FILENAME=/Webserv_42/www/cgi-bin/form.py
SCRIPT_NAME=/cgi-bin/form.py
PATH_INFO=/cgi-bin/form.py
SERVER_NAME=127.0.0.1
SERVER_PORT=8080
HTTP_COOKIE=session_id=a3f8c21d...
Content-Type: text/html
Status: 200
<html>
<body><p>Hello from Python CGI!</p></body>
</html>
β οΈ C++98 Note: The environment array is built as a rawchar**(notstd::vector<std::string>) to remain compatible withexecve(). Noauto, range-basedfor, or<thread>is used anywhere in the codebase.
Session management is implemented at the HTTP layer using the Cookie mechanism, requiring no external database:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β First Request (no cookie) β
β β
β Client ββGET /βββΊ Server β
β βββ Set-Cookie: session_id=<token>; HttpOnly βββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Subsequent Requests β
β β
β Client ββGET /dashboard β
β Cookie: session_id=<token> βββββββββββββββΊ Serverβ
β βββ (Validates token in session store) βββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Token generation uses a combination of:
- Current Unix timestamp (
time()) - Client IP address string
- A server-side secret seed
The resulting hash is stored in a std::map<std::string, SessionData> keyed by token string.
The entire codebase is compiled with -std=c++98 -Wall -Wextra -Werror. This means:
- β No
autokeyword for type deduction - β No range-based
forloops (for (auto& x : v)) - β No
nullptr(useNULL) - β No
<thread>,<mutex>,<chrono>, or any C++11 headers - β No lambda expressions or
std::function - β
std::string,std::vector,std::map,std::listare all permitted - β
Manual memory management with
new/deletewhere needed
- Configurable per
serverblock and overridable perlocationblock. - Accepts size suffixes:
K(kilobytes),M(megabytes),G(gigabytes). - Enforced during body parsing. If exceeded, parsing halts and
413 Content Too Largeis returned before reading further data from the socket.
- Custom error pages are mapped via
error_page <code> <path>in the server block. - If no custom page is configured for a given status code, a minimal built-in HTML error page is generated dynamically.
- Supported codes include:
400,403,404,405,408,413,500,502,504.
- A C++98-compatible compiler (
g++orclang++) - GNU
make - Python 3 (
/usr/bin/python3) and/or Bash (/bin/bash) for CGI support
# Clone the repository
git clone https://github.com/<team>/Webserv_42.git
cd Webserv_42
# Compile with make
makeThe binary webserv will be produced in the project root.
# Run with the default configuration
./webserv conf/default.conf
# Run with a custom configuration file
./webserv /path/to/your/custom.conf| Target | Description |
|---|---|
make / make all |
Build the webserv binary |
make clean |
Remove all .o object files |
make fclean |
Remove object files and the webserv binary |
make re |
Full rebuild (fclean + all) |
# GET a static page
curl -v http://127.0.0.1:8080/
# POST to a CGI script
curl -v -X POST -d "name=World" http://127.0.0.1:8080/cgi-bin/hello.py
# Upload a file
curl -v -X POST -F "file=@/path/to/file.txt" http://127.0.0.1:8080/upload
# DELETE a resource
curl -v -X DELETE http://127.0.0.1:8080/upload/file.txt
# Test redirect
curl -v http://127.0.0.1:8080/old
# Test max body size limit
curl -v -X POST --data-binary @large_file.bin http://127.0.0.1:8080/uploadNavigate to http://127.0.0.1:8080/ to access the server's HTML interface.
# Install siege (macOS)
brew install siege
# Run a concurrency test
siege -c 100 -r 50 http://127.0.0.1:8080/| Name | Role | GitHub | |
|---|---|---|---|
| π©βπ» | Mohamed | Config Parser . Core Infrastructure Β· I/O Multiplexing Β· CGI Execution | @Mohamed |
| π©βπ» | Hayat | HTTP Parsing Β· Upload Handling Β· Session Management | @Hayat |
| π¨βπ» | Wafae | Response Generation Β· Handler Factory Β· Static Files (GET/DELETE) | @Wafae |
Built with πͺ and β at 42 Network
