Skip to content

maestriny/webserv

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

59 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Webserv

✨ This is where dreams go to die ✨

Table of Contents

Description

Lightweight HTTP server implementation written in C++98, capable of serving static content, handling file uploads, processing CGI scripts, and more. This project follows the HTTP/1.1 standard and implements a non-blocking I/O architecture using poll() (or equivalent) for handling multiple client connections simultaneously.

Features

  • HTTP/1.1 compliant
  • Configurable via NGINX-like configuration file
  • Multiple virtual servers with customizable routes
  • Static file serving with directory listing
  • Support for GET, POST, and DELETE methods
  • CGI execution (PHP, Python)
  • File uploads
  • Custom error pages
  • HTTP redirects
  • Non-blocking architecture

Requirements

  • C++98 compatible compiler (g++, clang++)
  • POSIX-compliant operating system (Linux, macOS)

Compilation

make

Running

./webserv [config_file]

If no configuration file is specified, conf/default.conf will be used.

Project Structure

webserv/
├── Makefile
├── conf/
|   └── default.conf
│ 
└── srcs/
    ├── config/
    ├── http/
    ├── tcp/ 
    └── main.cpp

Team Organization

Person 1: Core Server and Networking

Responsibility: Implement the core server functionality and low-level networking

  • Main server class, poll() implementation, connection lifecycle
  • Client connection management
  • Socket creation, binding, and listening
  • Non-blocking I/O operations
  • Main server loop and event handling
  • Stress testing and performance tuning

Person 2: HTTP Protocol Implementation

Responsibility: Implement the HTTP protocol handling

  • HTTP request parsing and validation
  • HTTP response generation
  • HTTP methods (GET, POST, DELETE) implementation
  • Headers and status codes handling
  • File upload mechanism
  • HTTP protocol compliance testing

Person 3: Configuration and File System

Responsibility: Implement configuration parsing and file system operations

  • Configuration file parsing
  • CGI script execution
  • Path manipulation, directory operations
  • Directory listing implementation
  • File type detection and MIME types
  • Configuration file format development

Allowed Functions

Socket Programming

Function Signature Description
socket int socket(int domain, int type, int protocol); Creates an endpoint for communication and returns a file descriptor
bind int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); Assigns a local address to a socket
listen int listen(int sockfd, int backlog); Marks the socket as a passive socket, used to accept incoming connection requests
accept int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); Accepts a connection on a socket
connect int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); Initiates a connection on a socket
send ssize_t send(int sockfd, const void *buf, size_t len, int flags); Sends a message on a socket
recv ssize_t recv(int sockfd, void *buf, size_t len, int flags); Receives a message from a socket
socketpair int socketpair(int domain, int type, int protocol, int sv[2]); Creates a pair of connected sockets
setsockopt int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); Sets options on sockets
getsockname int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen); Retrieves the current address to which the socket is bound
getprotobyname struct protoent *getprotobyname(const char *name); Returns protocol entry for the given protocol name

Network Byte Order Conversion

Function Signature Description
htons uint16_t htons(uint16_t hostshort); Converts a 16-bit number from host byte order to network byte order
htonl uint32_t htonl(uint32_t hostlong); Converts a 32-bit number from host byte order to network byte order
ntohs uint16_t ntohs(uint16_t netshort); Converts a 16-bit number from network byte order to host byte order
ntohl uint32_t ntohl(uint32_t netlong); Converts a 32-bit number from network byte order to host byte order

Address Resolution

Function Signature Description
getaddrinfo int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); Resolves hostnames and service names into a list of addrinfo structures
freeaddrinfo void freeaddrinfo(struct addrinfo *res); Frees memory allocated by getaddrinfo
gai_strerror const char *gai_strerror(int errcode); Returns a human-readable string describing an error from getaddrinfo

Event Polling Mechanisms

POSIX (Linux)

Function Signature Description
select int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); Waits for file descriptors to be ready for I/O operations
poll int poll(struct pollfd *fds, nfds_t nfds, int timeout); Monitors multiple file descriptors for I/O readiness without blocking

Linux-specific (epoll)

Function Signature Description
epoll_create int epoll_create(int size); Creates an epoll instance
epoll_ctl int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); Controls the epoll instance—adding, modifying, or deleting monitored fds
epoll_wait int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); Waits for events on the epoll instance

BSD/macOS-specific (kqueue)

Function Signature Description
kqueue int kqueue(void); Creates a new kernel event queue
kevent int kevent(int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents, const struct timespec *timeout); Registers events with the queue or waits for triggered events

Directory Operations

Function Signature Description
opendir DIR *opendir(const char *name); Opens a directory stream corresponding to the directory name
readdir struct dirent *readdir(DIR *dirp); Reads the next directory entry in the opened directory stream
closedir int closedir(DIR *dirp); Closes the opened directory stream

File and Path Operations

Function Signature Description
access int access(const char *pathname, int mode); Checks file accessibility (existence, read/write permissions)
stat int stat(const char *pathname, struct stat *statbuf); Retrieves information about the file (size, type, permissions)
open int open(const char *pathname, int flags, mode_t mode); Opens or creates a file with the specified flags and mode
chdir int chdir(const char *path); Changes the current working directory

Error Handling

Function Signature Description
errno int errno; (global variable) Stores the last error code set by a system call
strerror char *strerror(int errnum); Returns a human-readable string describing the error code




Understanding poll() and I/O multiplexing

What is I/O Multiplexing?

I/O multiplexing is a technique that lets a program monitor multiple file descriptors (like sockets, pipes, or files) to see which ones are ready for I/O (input or output), without blocking on just one.
Instead of calling read() on each socket and waiting (which would block execution), we can use I/O multiplexing to wait for any of them to be ready, and act only when one is.

There are three common system calls for this:

  • select()
  • poll()
  • epoll() (Linux-specific, more scalable)

What is poll()?

struct pollfd {
    int fd;         // The file descriptor to monitor
    short events;   // Events we are interested in (input)
    short revents;  // Events that actually occurred (output)
};

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

//parameter:
// - nfds_t nfds Number of elements in fds
// - struct pollfd *fds Array of struct pollfd (each representing one fd you want to monitor)
// - int timeout  Time to wait (in milliseconds)
//   - > 0 → wait that many milliseconds
//   - 0 → return immediately (non-blocking)
//   - < 0 → wait forever

poll() is a system call used for I/O multiplexing. It waits for one or more file descriptors to become ready to perform I/O (e.g., reading or writing).

It improves on select() by avoiding hard limits on the number of file descriptors and having a cleaner interface.

Common Events

Event Meaning
POLLIN Ready to read
POLLOUT Ready to write
POLLERR Error occurred
POLLHUP Disconnected (hang up)
POLLNVAL Invalid file descriptor

Example

#include <stdio.h>
#include <poll.h>
#include <unistd.h>

int main() {
    struct pollfd pfd;
    pfd.fd = 0;            // stdin (fd 0)
    pfd.events = POLLIN;   // wait for data to read

    printf("Waiting for input on stdin...\n");

    int ret = poll(&pfd, 1, 5000);  // wait up to 5 seconds

    if (ret == -1) {
        perror("poll");
    } else if (ret == 0) {
        printf("Timeout! No input.\n");
    } else {
        if (pfd.revents & POLLIN) {
            printf("Data is ready to read on stdin!\n");
        }
    }

    return 0;
}




Select() and Macros

select() is a system call used to monitor multiple file descriptors (like sockets or files) at the same time, waiting until one or more of them become ready for some class of I/O (like read, write, or exception).
watch this: How one thread listens to many sockets with select in C

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

It blocks (or not, depending on timeout) until one or more of the file descriptors are ready.

fd_set is a data structure used by select() to keep track of which file descriptors you want to monitor. But we can't manipulate fd_set directly — that's where these macros come in:

FD_ZERO(fd_set *set)

Use this before anything else. It clears the set — makes sure it's empty.

FD_ZERO(&readfds);

FD_SET(int fd, fd_set *set)

Adds a file descriptor to the set. You tell select(): "Hey, I want to monitor this fd!"

FD_SET(sockfd, &readfds);  // I want to know when sockfd is ready to read

FD_ISSET(int fd, fd_set *set)

After select() returns, you check if a file descriptor is ready using this.

if (FD_ISSET(sockfd, &readfds)) {
    // sockfd is ready to read
}

FD_CLR(int fd, fd_set *set)

Removes a file descriptor from the set (you rarely use this with select itself, but can use it when managing sets manually).

FD_CLR(sockfd, &readfds);  // Remove fd from the set




Handle HTTP POST Request

A POST request is typically used when the client needs to send data to the server.
Common scenarios include:

  • Submitting a login form (username and password).
  • Uploading a file (e.g., an image, PDF, or document).
  • Sending form data that may include multiple fields at once.
  • Creating or updating resources in APIs.

Unlike GET, which appends data to the URL, POST includes the data in the body of the request.

Structure of a POST Request

A POST request consists of:

  1. Request line: e.g., POST /upload HTTP/1.0
  2. Headers: contain metadata such as Content-Type and Content-Length.
  3. Body: contains the actual data being sent.

Example (with form data and file upload)

POST /upload HTTP/1.0
Host: localhost
Content-Type: multipart/form-data; boundary=----MyBoundary
Content-Length: 1234

------MyBoundary
Content-Disposition: form-data; name="username"

Luca
------MyBoundary
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain

This is the content of the file.
------MyBoundary--

About the Boundary

  • When using multipart/form-data, a boundary string is defined in the header.
  • This boundary separates each field in the body. In the example boundary=----MyBoundary
  • Each part contains:
    • A disposition header (e.g., field name, filename).
    • Optionally a Content-Type.
    • A blank line.
    • The field content or file content.

POST Request Scenarios

  1. application/x-www-form-urlencoded
  • Default for forms without files.
  • Data is encoded as key-value pairs separated by &.
  • Example:
    POST /login HTTP/1.0
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 27
    
    username=Luca&password=1234
  1. multipart/form-data
  • Required for forms that include file uploads.
  • Each field or file is separated by the boundary string.
  • Example:
    POST /upload HTTP/1.0
    Content-Type: multipart/form-data; boundary=----MyBoundary
    Content-Length: 1234
    
    ------MyBoundary
    Content-Disposition: form-data; name="username"
    
    Luca
    ------MyBoundary
    Content-Disposition: form-data; name="file"; filename="test.txt"
    Content-Type: text/plain
    
    File content goes here...
    ------MyBoundary--
  1. application/json (modern APIs)
  • Common in APIs but not originally typical for HTTP/1.0.
  • Data is sent in JSON format.
  • Example:
    POST /api/data HTTP/1.0
    Content-Type: application/json
    Content-Length: 35
    
    {"username":"Luca","password":"1234"}
  1. text/plain
  • Rare but possible.
  • Body contains plain text without formatting.
  • Example:
    POST /echo HTTP/1.0
    Content-Type: text/plain
    Content-Length: 11
    
    Hello World

Chunked Transfer Encoding

When a client sends an HTTP POST request, the message body can be delivered in two ways:

  • Using Content-Length The client specifies the total size of the body in advance:

    POST /upload HTTP/1.1
    Content-Length: 20
    
    name=Luca&age=30

    The server reads exactly 20 bytes after the headers — and that’s it.

  • Using Transfer-Encoding: chunked In this case, the total length of the body is not specified. The client sends the body in pieces (chunks), and each chunk is preceded by a hexadecimal number indicating its size.

    Example:

    POST /upload HTTP/1.1
    Transfer-Encoding: chunked
    
    4\r\n
    Wiki\r\n
    5\r\n
    pedia\r\n
    E\r\n
    in\r\nchunks.\r\n
    0\r\n
    \r\n

    Let’s break it down:

Part Meaning
4\r\nWiki\r\n Chunk of 4 bytes"Wiki"
5\r\npedia\r\n Chunk of 5 bytes"pedia"
E\r\n in\r\nchunks.\r\n Chunk of 14 bytes" in\r\nchunks."
0\r\n\r\n Final chunk of 0 bytes, indicates the end

When the server decodes this message, it must:

  • Read each hexadecimal number (the chunk length).
  • Extract the corresponding chunk.
  • Stop when it encounters 0\r\n\r\n.
  • Concatenate all chunks into a single body:
Wikipedia in
chunks.

This process is known as “unchunking the body.”





SOURCES

articles & books

Github

Video

About

this is where dreams go to die

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors