A complete Java implementation of the Gemini Lite protocol — a lightweight, TCP-based alternative to HTTP — featuring a command-line client, a multi-threaded file-serving server, and a transparent intermediating proxy.
Course: BCS2110, Maastricht University
Author: Andreas Constantinou
The client and proxy are built around a State Machine (FSM) design pattern:
User Input → CONNECTING → SENDING_REQUEST → AWAITING_REPLY
↓
PROCESSING_REPLY
↓
┌──────────────────────────────┼──────────────────────────────┐
↓ ↓ ↓
Status 1x (10-19) Status 2x (20-29) Status 3x (30-39)
AWAITING_INPUT DISPLAYING_CONTENT REDIRECTING
↓ ↓ ↓
SENDING_REQUEST CLOSED → Exit CONNECTING (loop)
↑
Status 44 (Slow Down)
CONNECTING (exponential backoff)
Status 4x/5x (Errors) → ERROR → Exit
All shared state is held in a ClientContext object (URI, sockets, reply, redirect count, backoff timer, proxy flag), enabling the same state machine to power both the client and the proxy with minimal duplication.
- Parses
gemini-lite://URIs with strict validation (no userinfo, no fragment, 1024-byte limit) - Connects via TCP (default port
1958, configurable via URI orGEMINI_LITE_PROXYenv var) - Handles all Gemini Lite reply types:
- Input (10-19): Prompts user for input with password masking support
- Success (20-29): Streams response body to stdout
- Redirect (30-39): Follows redirects (max 5, with loop detection)
- Slow Down (44): Retries with exponential backoff
- Errors (4x, 5x): Exits with appropriate status code
- Multi-threaded — one thread per client connection
- Serves files from a configurable filesystem root directory
- MIME type detection:
.gmi→text/gemini, others useFiles.probeContentType()withapplication/octet-streamfallback - Directory listing in Gemtext format (
=> filenamelinks) - Path traversal protection — rejects requests attempting to escape the root directory
- Transparent relaying — handles redirections and slow-down responses just as the client does
- Returns status code 43 (proxy error) for:
- Unreachable upstream servers
- Connection resets or drops
- Malformed or invalid upstream responses
- Timeouts
- Internal I/O failures
- Environment-variable-based proxy configuration (
GEMINI_LITE_PROXY)
| File Size | p50 Latency | p99 Latency | Throughput |
|---|---|---|---|
| 64 B | 3 ms | 113 ms | 5,766 B/s |
| 1 KB | 3 ms | 4 ms | 371,071 B/s |
| 128 KB | 3 ms | 4 ms | 39,723,333 B/s |
| 100 MB | 155 ms | 283 ms | 624,896,395 B/s |
Prerequisites: Java 23+, Apache Maven
git clone https://gitlab.maastrichtuniversity.nl/I6389371/bcs2110-project.git
cd bcs2110-project
mvn clean packageThe compiled JAR will be at target/bcs2110-2025.jar.
java -cp target/bcs2110-2025.jar gemini_lite.Client <URL> [<input>]<URL>— Agemini-lite://URI. Scheme must begemini-lite://, userinfo and fragments are forbidden.[<input>]— Optional input string (for status 10-19 replies that prompt the user).
Examples:
# Fetch a file
java -cp target/bcs2110-2025.jar gemini_lite.Client gemini-lite://localhost:1958/index.gmi
# Fetch with a query string
java -cp target/bcs2110-2025.jar gemini_lite.Client "gemini-lite://localhost/search?q=hello"java -cp target/bcs2110-2025.jar gemini_lite.Server <directory> [<port>]<directory>— Path to the directory to serve (required).[<port>]— Port to listen on (default:1958).
Example:
java -cp target/bcs2110-2025.jar gemini_lite.Server src/main/resources/root 1958java -cp target/bcs2110-2025.jar gemini_lite.Proxy [<port>][<port>]— Port to listen on (default:9999).
Configure clients to route through the proxy by setting the environment variable:
export GEMINI_LITE_PROXY=localhost:9999Example:
java -cp target/bcs2110-2025.jar gemini_lite.Proxy 9999src/main/java/gemini_lite/
├── Client.java # Client entry point
├── Server.java # Server entry point
├── Proxy.java # Proxy entry point
├── client_engine/
│ ├── ClientEngine.java # FSM runner (loops through states)
│ ├── ClientContext.java # Shared state object
│ └── states/
│ ├── ClientState.java # State interface
│ ├── ConnectingState.java # TCP socket connection
│ ├── SendingRequestState.java # Format and send Request
│ ├── AwaitingReplyState.java # Parse Reply from server
│ ├── ProcessingReplyState.java # Route to next state based on status
│ ├── AwaitingInputState.java # Prompt user for input
│ ├── DisplayingContentState.java # Dump body to stdout
│ ├── RedirectingState.java # Follow redirect
│ ├── ProxyRelayState.java # Relay reply to downstream client
│ ├── ClosedState.java # Close sockets, exit
│ └── ErrorState.java # Handle errors, exit
├── io/
│ ├── Request.java # Request model
│ ├── Reply.java # Reply model (status + meta + body)
│ ├── replies/ # Status-specific reply subtypes
│ └── exceptions/ # Custom exception hierarchy
└── util/
├── RequestUtils.java # URI validation
├── ReplyUtils.java # MIME validation, control char detection
├── RequestHandler.java # Request handling interface
└── FileSystemRequestHandler.java # Filesystem-backed handler
Run the unit test suite with:
mvn testTests cover:
- Request parsing — valid URIs, long URIs, userinfo, fragments, path normalization
- Reply parsing — encoded replies, input replies, emoji handling
- URI behavior — Java URI edge cases
- Client context — input encoding, state transitions
- State machine transitions — correctness of all FSM paths
The repository also includes 9 custom integration test cases in test-cases.json (3 client, 4 server, 2 proxy) covering edge cases like unrecognized MIME parameters, query string redirects, path traversal attacks, and proxy error reporting.
| Category | Tools & Libraries |
|---|---|
| Language | Java 23 |
| Build system | Apache Maven |
| Testing | JUnit Jupiter 5.11.3 |
| Networking | TCP sockets (java.net.Socket, java.net.ServerSocket) |
| Concurrency | Java threads (one per client connection) |
| Design pattern | State Machine (FSM) |
| Security | Path traversal prevention, URI validation, input sanitization |
This project is an academic assignment for BCS2110 at Maastricht University.