Skip to content

mdbentaleb/Webserv_42

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

50 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🌐 WebServ

HTTP WebServer

An HTTP/1.1-compliant web server built from scratch in C++98

42 Network C++98 HTTP/1.1 License


Team: Mohamed Β· Wafae Β· Hayat


πŸ“‹ Table of Contents


🧭 Project Overview

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.


✨ Key Features

βš™οΈ Core β€” HTTP/1.1 Protocol

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

πŸ—οΈ Infrastructure β€” I/O Multiplexing & Sockets

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

πŸš€ Advanced β€” CGI, Upload & Sessions

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

πŸ—‚οΈ Architecture & Directory Structure

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

πŸ”„ Program Flow & Request Lifecycle

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:


Stage 1 β€” Startup: Socket Initialization & Binding

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:

  1. A TCP socket is created with socket(AF_INET, SOCK_STREAM, 0).
  2. SO_REUSEADDR is set to avoid Address already in use errors on restart.
  3. The socket is bound to its address with bind().
  4. listen() is called to mark it as a passive socket.
  5. fcntl(fd, F_SETFL, O_NONBLOCK) is called to make all I/O non-blocking.
  6. The listening file descriptor is registered in the poll() array with POLLIN interest.

Stage 2 β€” The Event Loop: Connection Acceptance & I/O Dispatch

HTTPServerManager enters an infinite poll() loop. On each iteration:

  1. poll() blocks until at least one file descriptor becomes ready.
  2. For each pollfd with a POLLIN event:
    • If it is a listening socket β†’ accept() a new client connection, set it non-blocking, and add it to the poll set.
    • If it is a client socket β†’ read incoming data into a per-client buffer.
  3. For each pollfd with a POLLOUT event:
    • If the client has a pending response β†’ write the response data and advance a send-position pointer.
  4. Closed or errored connections are detected via POLLHUP/POLLERR and removed from the set.

Note: No fork(), thread, or blocking read()/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 in poll().


Stage 3 β€” Request Parsing

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:

3a. Request Line (parseLine.cpp)

  • 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.

3b. Headers (parseHeaders.cpp)

  • Reads headers line by line until the blank \r\n\r\n separator.
  • Populates a std::map<std::string, std::string> of header key-value pairs.
  • Key headers extracted: Host, Content-Length, Transfer-Encoding, Content-Type, Cookie.

3c. Body (parseBody.cpp)

  • Fixed-length body: reads exactly Content-Length bytes.
  • 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 matched Location, parsing is aborted and a 413 Content Too Large response is generated.

Stage 4 β€” Routing Logic: HandlerFactory

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.


Stage 5 β€” CGI Execution (CGIHandler)

The CGI handler bridges the HTTP server with external interpreter processes:

  1. 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_LENGTH
    • SCRIPT_FILENAME, SCRIPT_NAME, PATH_INFO
    • SERVER_NAME, SERVER_PORT, HTTP_COOKIE
  2. 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).

  3. fork() & execve(): A child process is created. It:

    • dup2()s the pipe ends onto its STDIN_FILENO / STDOUT_FILENO.
    • Calls execve() with the interpreter path (e.g., /usr/bin/python3) and the script path as argv[1].
  4. 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.

  5. Timeout handling: A deadline is tracked per CGI process. If the child does not exit within the configured timeout, kill(pid, SIGKILL) is called and 504 Gateway Timeout is returned.

  6. Output parsing: The CGI output is split at \r\n\r\n. Any Status: header in the CGI output is mapped to the HTTP status code. Remaining headers and the body are forwarded in the HTTP response.


Stage 6 β€” Session Management (Cookies)

The server implements a lightweight, stateless-on-disk session mechanism:

  1. 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...).

  2. Cookie Issuance: The response includes:

    Set-Cookie: session_id=a3f8c21d...; HttpOnly; Path=/
    
  3. 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.

  4. CGI Access: The raw HTTP_COOKIE environment variable is passed to CGI scripts, allowing Python/Shell scripts to read and set their own session-level data.


Stage 7 β€” Response Generation & Transmission

Once a handler completes:

  1. HTTPResponse constructs 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.
  2. The complete serialized response is stored in the client's send buffer.

  3. The client's pollfd entry is updated to watch for POLLOUT.

  4. On the next poll() iteration, the data is written in chunks via send() until the buffer is drained, preventing any single response from monopolizing the loop.


πŸ“Š Flowchart

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])
Loading

βš™οΈ Configuration File

The configuration file follows an Nginx-inspired syntax, supporting multiple server blocks, each with one or more location blocks.

Directives Reference

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)

Example Configuration

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 Execution

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.

Environment Variables Passed to Scripts

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...

Expected CGI Script Output Format

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 raw char** (not std::vector<std::string>) to remain compatible with execve(). No auto, range-based for, or <thread> is used anywhere in the codebase.


πŸ”’ Session Management

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.


πŸ›‘οΈ Technical Constraints

C++98 Standard

The entire codebase is compiled with -std=c++98 -Wall -Wextra -Werror. This means:

  • ❌ No auto keyword for type deduction
  • ❌ No range-based for loops (for (auto& x : v))
  • ❌ No nullptr (use NULL)
  • ❌ No <thread>, <mutex>, <chrono>, or any C++11 headers
  • ❌ No lambda expressions or std::function
  • βœ… std::string, std::vector, std::map, std::list are all permitted
  • βœ… Manual memory management with new/delete where needed

client_max_body_size

  • Configurable per server block and overridable per location block.
  • Accepts size suffixes: K (kilobytes), M (megabytes), G (gigabytes).
  • Enforced during body parsing. If exceeded, parsing halts and 413 Content Too Large is returned before reading further data from the socket.

Error Pages

  • 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.

πŸ› οΈ How to Build & Run

Prerequisites

  • A C++98-compatible compiler (g++ or clang++)
  • GNU make
  • Python 3 (/usr/bin/python3) and/or Bash (/bin/bash) for CGI support

Build

# Clone the repository
git clone https://github.com/<team>/Webserv_42.git
cd Webserv_42

# Compile with make
make

The binary webserv will be produced in the project root.

Run

# Run with the default configuration
./webserv conf/default.conf

# Run with a custom configuration file
./webserv /path/to/your/custom.conf

Makefile Targets

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)

πŸ§ͺ Testing

Using curl

# 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/upload

Using a Browser

Navigate to http://127.0.0.1:8080/ to access the server's HTML interface.

Stress Testing

# Install siege (macOS)
brew install siege

# Run a concurrency test
siege -c 100 -r 50 http://127.0.0.1:8080/

πŸ‘₯ Authors

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

About

🌐 HTTP/1.1 web server built from scratch in C++98 with non-blocking I/O, CGI, sessions, file uploads, and Nginx-inspired architecture πŸš€

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors