Skip to content

Project

http

Implementation of the RFC 2616/7230-7235 and 6455 specifications for HTTP 1.1 and Websocket protocols in Rust

Rust80Updated Oct 2026
View on GitHub ↗

HTTP & WebSockets in Rust

CI License: MIT Unsafe code: forbidden Sponsor Other sponsorship options


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

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-Alive header
  • Pipelining: a connection-owned buffer never discards bytes (F1 closed)
  • Request bodies via Content-Length and 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-Accept computation 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 tokio task 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/pedantic warnings denied workspace-wide, missing_docs denied, unsafe_code forbidden

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 TCP

Dependency 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/:

Usage

Running the server

1cargo run -p server

The 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 server

Tests, 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 documentation

CI 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_frames

The 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 /: serves crates/server/static/index.html
  • GET /: serves files from the static directory
  • POST /: echo endpoint, returns the body as JSON
  • OPTIONS /: 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 / server crates
  • Documentation system: architecture docs, ADRs, RFC compliance matrices, rustdoc with missing_docs denied, 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