This project is built from the wish to deepen my knowledge in networking and software development and to understand better how the web works under the hood.
The project is a minimalist implementation of a static web server which works on protocol HTTP/1.1 and has the ability to serve multiple clients.
- Concurrency (fixed size thread pool with explicit flow control)
Acceptor thread - accepts connections, assigns a connection handler to each of them, submits the tasks to the executor
Bounded Thread Pool - keeps a fixed number of worker threads to prevent out-of-memory problems
Bounded Work Queue - buffers bursts of traffic
Rejection Policy (Caller Runs) - when the server is overloaded, the acceptor processes requests to provide an automatic backpressure (naturally slow down new connections)
Normal Load -> queue is mostly empty, the workers are processing requests at an acceptable rate
High Load -> queue is starting to fill, workers are busy accepting requests one after one at a high rate
Overload -> queue is full, all workers are busy and the policy kicks in telling the acceptor thread to start handle requests instead of accepting them so that the accept rate decreases and the system starts to stabilize
-
Static Files Serving
-
Accepted request methods
- GET
The client can make GET requests for static files stored on the server (.html, .css, .js, .jpg, .jpeg, .png, .gif, .svg)
- POST
Being a static server, it does not have capabilities of rendering dynamic pages but it offers an echo endpoint for testing the POST requests. The client can send POST requests to the echo endpoint (/api/echo) and it will respond with the data from the request body to prove that the server parses, processes and stores even the POST requests.
Content-Type: application/json, application/x-www-form-urlencoded, text/plain
- Request Size Limits
- Request Line up to 8kb
- Request Header Field Value up to 8kb
- Request Header Field Name up to 256b
- Number of Request Headers up to 100
- Routing
The server offers the capability of creating custom routes for resources.
e.g. the client can access the resource example.html by sending a GET request with the path /example.html or can register the resource under the name /something/example and request it by sending a GET request with the path /something/example
-
Proper Status Code for Responses (e.g. 200 OK, 400 Bad Request, 404 Not Found)
-
Keep-Alive Connections (reusing a connection for multiple requests, if header Connection: close is not specified)
-
Security Measures
- Block Path Traversal (when the client makes a request to a path that contains path traversal sequences like ../ it will get a response with a 404 Not Found)
- Correct MIME types for served files
- Security Headers (X-Content-Type-Options, Referrer-Policy, X-Frame-Options, Content-Security-Policy)
- Restricted file serving (the allowed files to be served are the ones with the registered extensions)
- Minimum protection measures for DoS (Denial of Service) - request size limits specified above, limit the number of requests per connection, socket timeout so that a slow client does not keep a worker busy for too long, use of a safe library for Regex operations to mitigate ReDoS
Programming language - Java v25.0.1
SDK - openjdk-25
Build System - Gradle Kotlin DSL
Gradle JVM - openjdk-25
External Libraries
-
Google RE2/J (Maven Repository) - Regex operations
-
Jackson Databing (Maven Repository) - JSON operations
Docker Build
Multi-stage build (Build Stage + Runtime Stage)
- Build Stage
Image: gradle:9.2-jdk 25
The role of this stage is to copy all necessary gradle files from the project folder into the image workdir, install project dependencies specified in the gradle build script and run a custom gradle task which creates a Fat JAR of the project (JAR file that contains the Java program and all dependencies).
- Runtime Stage
Image: eclipse-temurin:25-jdk
The role of this stage is to copy the JAR from the Build Stage and run it.
The project contains a .env file where should be set two variables:
- PORT (the port number on which the server will start running)
- DOCUMENT_ROOT (the path to the folder from where the static files will be served)
The folder /static from the project root folder will be mount on the Docker container at the location specified in DOCUMENT_ROOT so that all files uploaded in the /static folder to be accessible from the container.
The project can be also run without Docker (by installing the dependencies and running the Main class) because all configurations made for Docker container compatibility have default values relative to the local system.
-
Clone the project repository
-
Create a .env file with the same format as .env.example and set the variables e.g.
PORT=80 DOCUMENT_ROOT=/var/www/html
-
Run the following command
docker compose up --build- If you want to upload files on the server -> before running the project, place your files inside of the /static folder according to the rules from that folder (you will find README files inside of every subfolder with the rules)
- If you want to register routes for your files -> before running the project, open the Main.java file (/src/main/java/org/example/Main.java) and register your routes after line 21 and before the method from line 23 as in the given example from line 21 with the following method
server.getRouter().registerRoute("/example", "/example.html");