Project
http
Implementation of the RFC 2616/7230-7235 and 6455 specifications for HTTP 1.1 and Websocket protocols in Rust
A from-scratch implementation of HTTP/1.1 and WebSocket (RFC 6455) protocols in
Rust, built on tokio. This is a learning and portfolio project with a
networking + cybersecurity angle: the goal is not just to make the protocols
work, but to understand them deeply enough to document compliance honestly and
to prove, with tests, that the server holds up under attack.
The code is organized as a Cargo workspace of small, single-purpose crates. The security audit and hardening program is complete: every finding now has a documented control with regression tests (docs/security/controls.md), and the parsers are fuzzed in CI.
Table of Contents
- Features
- Workspace layout
- Documentation
- Usage
- Trying it out
- Using the libraries
- Dependencies
- Security
- Roadmap
- License
Features
HTTP/1.1 (RFC 7230-7235)
- Request parsing for all common methods (GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH, TRACE, CONNECT)
- Persistent connections (keep-alive): idle timeout and request budget
enforced to match the advertised
Keep-Aliveheader - Pipelining: a connection-owned buffer never discards bytes (F1 closed)
- Request bodies via
Content-Lengthand chunked transfer-encoding - CL/TE conflict rejection per RFC 9112 §6.3 (request-smuggling defense)
- Response builder with strongly-typed status codes (including
413/431) - Auto-generated standard headers (
Date,Server,Connection,Keep-Alive) - Malformed input answered with proper 4xx responses, never silent drops
- Read timeouts against Slow-Loris style attacks
- Static file serving with content-type detection, percent-decoding, and canonicalized traversal protection
Full requirement-by-requirement status: docs/rfc-compliance/http-1.1.md
WebSocket (RFC 6455)
- Opening handshake with full §4.2.1 validation (GET, HTTP/1.1+, base64
16-byte key) and
Sec-WebSocket-Acceptcomputation per §4.2 - Strict frame codec: masking (§5.3), RSV bits, opcodes, 64-bit length MSB, control-frame rules (§5.5)
- Message fragmentation and reassembly per §5.4, with interleaved control frames handled immediately
- Text and binary messages, echo behavior in the demo server
- Data-frame and message size caps (close 1009 before buffering)
- Protocol failures answered with the right close code (1002/1007/1009)
- Server-initiated ping/pong liveness checks with timeout
- Clean close handshake with status codes and reasons
Full requirement-by-requirement status: docs/rfc-compliance/websocket-rfc6455.md
Engineering practices
- Cargo workspace: two protocol libraries + one demo binary (see below)
- Strong typing for methods, status codes, frames, limits and errors
(
thiserror) - Per-crate error types:
http::Error,websocket::Error - Pure parsers over byte slices; connection drivers generic over
AsyncRead + AsyncWrite(duplex-tested, fuzz-targeted) - Async/await throughout, one
tokiotask per connection - Structured logging with
tracing - 90 tests (8 unit + 14 integration + 46 security/conformance + 20
end-to-end + 2 doctests), clippy-clean with
all/pedanticwarnings denied workspace-wide,missing_docsdenied,unsafe_codeforbidden
Workspace layout
1├── Cargo.toml # workspace root: shared deps + lints2├── justfile # task runner: test, lint, fuzz, image, bench, demo3├── container/ # pinned multi-stage image + compose (bench/attack profiles)4├── examples/ # client-side examples: ws_echo_client, ws_bench5├── fuzz/ # cargo-fuzz harnesses (own workspace, nightly)6└── crates/7 ├── http/ # HTTP/1.1 protocol library8 │ ├── src/9 │ │ ├── request.rs # pure request parsing (head + body framing)10 │ │ ├── response.rs # response builder11 │ │ ├── body.rs # chunked body decoding (pure)12 │ │ ├── connection.rs# generic-IO request reader (persistent buffer)13 │ │ ├── limits.rs # typed security limits14 │ │ └── error.rs # http::Error with status mapping15 │ └── tests/ # protocol + security integration tests16 ├── websocket/ # WebSocket library (depends on http)17 │ ├── src/18 │ │ ├── frame.rs # strict frame codec (RFC 6455 §5)19 │ │ ├── handshake.rs# upgrade validation + accept key20 │ │ ├── connection.rs# lifecycle: echo, reassembly, ping/pong, close21 │ │ ├── limits.rs # typed security limits22 │ │ └── error.rs # websocket::Error23 │ └── tests/24 └── server/ # demo application (binary)25 ├── src/ # config, connection dispatch, handlers26 ├── static/ # files served by the demo27 └── tests/ # end-to-end tests over real TCPDependency direction is strictly websocket → http (the WebSocket handshake is
an HTTP upgrade) and server → {http, websocket}. No cycles, no shared "common"
crate; each protocol crate carries only what it needs, so they can be read and
reused independently.
Documentation
The full documentation system lives in docs/:
- Architecture: crates, concurrency and error models, request/connection lifecycles
- RFC compliance matrices: HTTP/1.1 and WebSocket, one row per RFC requirement, honest ❌ rows included
- Security: threat model, controls catalog, hardening guide, fuzzing
- Protocol deep-dives: HTTP, WebSocket
- Testing · Development · Benchmarking
- ADRs: architecture decision records (tokio, workspace split, error model, httpdate, generic IO, security limits, keep-alive policy, explicit bind, containerized demos)
Usage
Running the server
1cargo run -p serverThe demo server starts on http://127.0.0.1:8000. The bind address is explicit
(ADR-0008): set SERVER_ADDR to change it, and a taken port fails startup
rather than drifting to another one.
Enable detailed logging with RUST_LOG:
1RUST_LOG=server=debug cargo run -p serverTests, lints and docs
1cargo test --workspace # all 90 tests (incl. doctests)2cargo test -p http # just one crate3cargo clippy --workspace --all-targets # all + pedantic warnings denied4cargo doc --workspace --open # API documentationCI runs fmt, clippy, tests, a docs build, a boot-and-curl smoke test (including the POST echo regression probe), a containerized build-and-smoke test and 60-second fuzz smokes on every push. See docs/development.md.
Containers and demos
Everything runs reproducibly with Podman — defender and adversary in one compose file (ADR-0009):
1just image # pinned multi-stage build (digest-pinned bases, --locked)2just up # demo server on localhost:80803just bench # wrk keep-alive ON/OFF + ws_bench, recorded in docs/benchmarking.md4just bench-http # HTTP benchmark only (auto-starts the server)5just bench-ws # WebSocket benchmark only (auto-starts the server)6just demo-container slowloris # attack demos print EXPECTED vs OBSERVED7just demo-container header_bomb8just demo-container unmasked_framesThe attack scripts cite their control IDs (SEC-HTTP-001/002, SEC-WS-001) and
exit non-zero if a mitigation does not hold. Results and methodology:
docs/benchmarking.md.
HTTP endpoints
GET /: servescrates/server/static/index.htmlGET /: serves files from the static directoryPOST /: echo endpoint, returns the body as JSONOPTIONS /: permissive CORS preflight response
WebSocket endpoint
Any path with valid upgrade headers is accepted (e.g. ws://127.0.0.1:8000).
The server echoes text messages back prefixed with Echo:, echoes binary
messages as-is, answers pings with pongs, and performs a proper close handshake.
Trying it out
1# Fetch the index page2curl -i http://127.0.0.1:8000/34# Post some data to the echo endpoint5curl -X POST http://127.0.0.1:8000/api/test -d "Hello, Server!"From a browser (already wired up in the served index.html, which connects back
to whatever host and port served the page):
1const wsScheme = location.protocol === "https:" ? "wss:" : "ws:";2const socket = new WebSocket(`${wsScheme}//${location.host}/`);3socket.onopen = () => socket.send("Hello, Rust!");4socket.onmessage = (e) => console.log("Received:", e.data);5socket.onclose = (e) => console.log("Closed:", e.code, e.reason);Using the libraries
Building an HTTP response with the http crate:
1use http::response::{HttpResponse, HttpStatusCode};23// Simple text response4let response = HttpResponse::ok().with_text("Hello, World!");56// JSON with a custom status code7let response = HttpResponse::new(HttpStatusCode::Created)8 .with_json(r#"{"message": "Resource created"}"#);910// Custom headers; to_bytes() serializes the full response11let bytes = HttpResponse::ok()12 .with_header("cache-control", "no-cache")13 .with_html("<h1>Hello</h1>")14 .to_bytes();Working with frames from the websocket crate:
1use websocket::{frame::{Frame, OpCode}, limits::Limits};23// Build and serialize a frame (server-to-client frames are unmasked)4let bytes = Frame::text("Hello").to_bytes();56// Parsing expects client-to-server traffic: client frames must be7// masked (RFC 6455 §5.3), unmasked input is rejected as a protocol error.8match Frame::parse(&wire_bytes, &Limits::default()) {9 Ok((frame, consumed)) => {10 if frame.opcode == OpCode::Text {11 println!("got: {}", String::from_utf8_lossy(&frame.payload));12 }13 }14 Err(e) => eprintln!("frame error: {e:?}"),15}Dependencies
Kept deliberately small; versions are managed once in the workspace root.
| Dependency | Used by | Purpose |
|---|---|---|
tokio |
all crates | Async runtime (each crate opts into only the features it needs) |
bytes |
websocket, server | Byte-buffer utilities for frame/request buffering |
thiserror |
all crates | Derive macros for the per-crate error types |
tracing + tracing-subscriber |
websocket, server | Structured, level-filtered logging |
base64, sha1 |
websocket | Sec-WebSocket-Accept handshake computation |
httpdate |
http | IMF-fixdate formatting for the Date header |
Security
Security is a stated goal of this project, and honesty about the current state is part of it. The hardening work is complete; in place today:
typed limits per protocol crate (head 16 KiB → 431, body 10 MiB → 413, WS
frame 1 MiB → close 1009), read timeouts against Slow-Loris, enforced keep-alive
(idle timeout, request budget, HTTP/1.0 semantics), CL/TE conflict rejection,
canonicalized path handling with canonical-path reads, strict WebSocket frame
validation with close codes 1002/1007/1009, full §4.2.1 handshake validation,
and complete §5.4 fragmentation/reassembly.
Every control carries an ID (SEC-HTTP-003, SEC-WS-009) in
docs/security/controls.md with implementation and
test pointers; 46 security tests and 20 end-to-end socket tests pin the
behavior, and CI fuzzes both parsers for 60 seconds per push. The compliance
matrices mark the remaining declared gap (browser Origin validation, an
accepted risk recorded in
ADR-0011) and the out-of-scope
features explicitly.
Roadmap
The refactor program that shaped this repo (workspace split, documentation system, security hardening, reproducible demos) is complete; the decisions are recorded in the ADRs and the controls in docs/security/controls.md.
Delivered milestones:
- Workspace split into
http/websocket/servercrates - Documentation system: architecture docs, ADRs, RFC compliance
matrices, rustdoc with
missing_docsdenied, docs built in CI - Security hardening: timeouts and limits, request-smuggling fixes, WebSocket message fragmentation/reassembly (RFC 6455 §5.4), security test catalog, fuzzing
- Reproducible demos: containers, benchmarks, attack-mitigation demos (docs/benchmarking.md, ADR-0009)
Explicit non-goals for now, recorded as future work rather than silently
missing: HTTP/2, TLS, WebSocket extensions (permessage-deflate), response
compression, a routing/middleware framework, rate limiting beyond the
connection-level limits, and graceful-shutdown draining (the server has no
signal handler; containers rely on tini and process death).
License
This project is licensed under the MIT License: see the LICENSE file for details.
Want to see more? Visit my GitHub profile for all of my repositories and contributions.
View all repositories on GitHub