Welcome to my little world, i will walk you through my code, what it does and how you can try it out if you wish to.
working branch will be
v0.10
A production-ready, security-focused HTTP server built in Rust for handling web requests, form submissions, and email notifications.
- Overview
- Architecture
- Security Features
- Getting Started
- Request Flow
- Core Components
- Configuration
- API Routes
- Security Implementation Details
- Deployment
NOX Server is a custom HTTP server implementation that prioritizes security, performance, and reliability. Built entirely in Rust without heavyweight web frameworks, it demonstrates low-level network programming with enterprise-grade security practices.
Key Features:
- Multi-threaded request handling with custom thread pool
- Comprehensive input validation and sanitization
- Rate limiting per IP address
- CORS support (same-origin and cross-origin modes)
- XSS and injection attack prevention
- Email notification system with HTML support
- Static file serving with MIME type detection
- Graceful error handling and recovery
Tech Stack:
- Language:
Rust - Key Dependencies:
regex- Pattern matching with ReDoS protectionlettre- Email sending via SMTP
┌──────────────────────────────────────────────────────────┐
│ TCP Listener │
│ (Port 8080/Custom) │
└────────────────────┬─────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ Thread Pool (4 Workers) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Rate Limiter Check (100 req/hour per IP) │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Connection Handler (Per Request) │
│ 1. Parse request line │
│ 2. Parse headers (with security limits) │
│ 3. Validate content length │
│ 4. Check CORS origin │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Route Matching │
│ ┌──────────┬──────────┬──────────┬──────────────────┐ │
│ │ Static │ Contact │ API │ Not Found │ │
│ │ Files │ Form │ Endpoints│ │ │
│ └──────────┴──────────┴──────────┴──────────────────┘ │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Request Handlers │
│ • Form validation & sanitization │
│ • Email generation & sending │
│ • Static file serving │
│ • JSON API responses │
└────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ HTTP Response │
│ • CORS headers │
│ • Security headers (X-Content-Type-Options, etc.) │
│ • Proper status codes │
└─────────────────────────────────────────────────────────┘
- Per-IP tracking: 100 requests per hour per unique IP address
- Sliding window: 60-minute rolling window that cleans expired entries
- Early rejection: Rate limit checks happen before request processing
- Thread-safe: Uses
Arc<Mutex>for concurrent access
// Rate limiter configuration
const WINDOW_LIMIT_MINS: u64 = 60; // 1 hour window
max_requests_per_hour: 100 // Limit per IPRequest Line Protection:
- Maximum request line size: 8KB (
MAX_REQUEST_LINE_SIZE) - Maximum header line size: 8KB (
MAX_HEADER_LINE_SIZE) - Maximum header count: 50 headers
- Empty request rejection
Content Length Limits:
- Maximum content length: 50KB (
MAX_CONTENT_LENGTH) - Maximum form data: 10KB (
MAX_FORM_DATA_LENGTH) - Early validation before memory allocation
Form Field Validation:
// Field constraints enforced
- name: max 40 chars, required
- email: max 80 chars, required, validated with regex
- message: max 2000 chars, required
- checkboxes: only "on" value allowed if present
- HTML escaping: All user inputs are escaped before rendering
- Pattern detection: Scans for 25+ malicious patterns including:
- Script tags (
<script,</script>) - Event handlers (
onclick=,onerror=, etc.) - Protocol handlers (
javascript:,data:, etc.) - Encoding attempts (
&#,\u,\x)
- Script tags (
// Detected patterns include
"<script", "javascript:", "onload=", "onerror=",
"<iframe", "data:text/html", "&#", "\\u",
//etc.- Strips carriage returns (
\r), newlines (\n), and null bytes (\0) - Validates email format with ReDoS-resistant regex
- Enforces RFC 5321 limits:
- Local part: max 64 characters
- Domain part: max 253 characters
- Total email: max 254 characters
- Path sanitization: Removes
..,., and leading slashes - Canonical path validation: Ensures file access stays within
/distdirectory - URL decoding: Prevents encoded path traversal attempts
- Two modes:
same-origin(default) andcross-origin - Origin validation: Only allowed origins receive CORS headers
- Preflight handling: Proper OPTIONS request validation
- Credentials support: Enables secure cookie/auth handling
- Uses simplified email regex to avoid catastrophic backtracking
- Pattern:
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ - Additional length checks before regex evaluation
- Rust 1.70+ (
rustc --version) - Cargo (comes with Rust)
- Clone the repository
git clone https://github.com/nonso01/nox.git
cd nox/server- Set up environment variables
Create a
.envfile in the project root:
# Server Configuration
HOST=127.0.0.1
PORT=8080
CORS_MODE=same-origin # or "cross-origin"
# Email Configuration (for contact form)
# you might consider using quotes
MY_EMAIL=your-email@gmail.com
MY_PASSWORD=your-app-password
SMTP_SERVER=smtp.gmail.com
SMTP_FROM=your-email@gmail.com- Build, and Run the project
# grant permission first
./server-manager start- Preview logs
./server-magager logsThe server will start on http://127.0.0.1:8080 by default.
// Main loop accepts incoming TCP connections
for stream in listener.incoming() {
match stream {
Ok(stream) => pool.execute(stream),
// Send to thread pool
Err(e) => eprintln!("Connection failed: {}", e),
}
}Each worker thread:
- Receives TCP stream from channel
- Sets socket timeout (30 seconds)
- Checks rate limit for client IP
- Reads request line with size validation
- Parses headers securely
- Validates method against CORS config
enum Route {
Static, // GET / or static files
ContactForm, // POST /contact or /api/contact
ApiStatus, // GET /api/status
ApiHealth, // GET /api/health
NotFound, // Everything else
}POST /contact Flow:
1. Validate content length (< 50KB)
2. Read request body
3. Parse form data (URL-encoded or multipart)
4. Validate all fields against constraints
5. Check for XSS/injection attempts
6. Sanitize email addresses
7. Send confirmation email (HTML + plaintext)
8. Return success/error HTML response
GET /api/status Flow:
1. Generate timestamp
2. Build JSON response: {"status":"healthy","version":"1.0.0","timestamp":...}
3. Add CORS headers if origin allowed
4. Return 200 OK
GET / (Static Files) Flow:
1. Sanitize requested path
2. Check path traversal protection
3. Resolve to index.html if directory
4. Read file content
5. Determine MIME type
6. Return file with proper Content-Type
Defines global security constraints for the server.
struct SecurityConfig {
max_content_length: usize, // 50KB - prevents memory exhaustion
max_connections: usize, // 100 - max concurrent connections
connection_timeout: Duration, // 30s - prevents slowloris attacks
max_requests_per_hour: usize, // 100 - rate limit threshold
enable_rate_limiting: bool, // true - toggle rate limiting
}Usage:
let security_config = SecurityConfig::new(); // Uses default valuesManages Cross-Origin Resource Sharing settings.
struct CorsConfig {
allow_origins: Vec<String>, // Whitelist of allowed origins
allow_all_origins: bool, // Allow * (dev only)
allow_methods: Vec<String>, // Allowed HTTP methods
allow_headers: Vec<String>, // Allowed request headers
max_age: u32, // Preflight cache duration (seconds)
}Modes:
Same-Origin Mode (production default):
allow_origins: vec![], // No explicit origins needed
allow_methods: ["GET", "POST", "OPTIONS"]
max_age: 3600 // 1 hour cacheCross-Origin Mode (for separate frontends):
allow_origins: vec![
"http://localhost:5173", // React dev server
"https://nox-dev.vercel.app" // Production frontend
]
allow_methods: ["GET", "POST", "OPTIONS"]
allow_headers: ["Content-Type", "Authorization", ...]
max_age: 86400 // 24 hour cacheCustom implementation for concurrent request handling.
struct ThreadPool {
workers: Vec<Worker>, // Worker threads
sender: Option<mpsc::Sender<TcpStream>>, // Channel sender
cors_config: CorsConfig, // CORS configuration
rate_limiter: Arc<RateLimiter>, // Shared rate limiter
security_config: SecurityConfig, // Security settings
}Design:
- Fixed size: 4 worker threads by default
- Shared channel: Workers receive tasks via
mpsc::channel - Shared state: Rate limiter wrapped in
Arc<Mutex>for thread safety - Graceful shutdown:
Dropimplementation ensures clean worker termination
Execution Flow:
pool.execute(stream) // Main thread
↓
sender.send(stream) // Send to channel
↓
receiver.recv() // Worker receives
↓
handle_connection() // Process requestTracks request counts per client IP with sliding window.
pub struct RateLimiter {
requests: Arc<Mutex<HashMap<String, Vec<Instant>>>>,
max_requests: usize, // 100 requests
window_duration: Duration, // 60 minutes
}Algorithm:
- Lock the shared HashMap
- Get current timestamp
- Remove expired entries (older than window)
- Check if count < max_requests
- If allowed, add new timestamp
- Return true/false
Example:
if rate_limiter.is_allowed(&client_ip) {
// Process request
} else {
// Return 429 Too Many Requests
}Custom error type for comprehensive error handling.
pub enum ServerError {
IoError(std::io::Error), // Network/file errors
ParseError(String), // Malformed requests
ValidationError(String), // Invalid input data
EmailError(Box<dyn Error>), // SMTP failures
ThreadPoolError(String), // Worker failures
NetworkError(String), // Connection issues
}Benefits:
- Type safety: Compiler ensures all errors are handled
- Context: Each variant carries specific error information
- Propagation: Uses
?operator for clean error bubbling - Display: Implements
fmt::Displayfor user-friendly messages
| Variable | Description | Default | Required |
|---|---|---|---|
HOST |
Server bind address | 127.0.0.1 |
No |
PORT |
Server port | 8080 |
No |
CORS_MODE |
CORS mode (same-origin or cross-origin) |
same-origin |
No |
MY_EMAIL |
SMTP username | - | Yes (for email) |
MY_PASSWORD |
SMTP password/app password | - | Yes (for email) |
SMTP_SERVER |
SMTP server address | smtp.gmail.com |
No |
SMTP_FROM |
From email address | MY_EMAIL value |
No |
To enable cross-origin requests:
export CORS_MODE=cross-originEdit allowed origins in main.rs:
fn cross_origin_config() -> Self {
CorsConfig {
allow_origins: vec![
"http://localhost:5173".to_string(),
"https://your-frontend.com".to_string(),
],
// ...
}
}Adjust rate limits:
const WINDOW_LIMIT_MINS: u64 = 60; // Change window duration
max_requests_per_hour: 100, // Change request limitAdjust content limits:
const MAX_CONTENT_LENGTH: usize = 50 * 1024; // 50KB
const MAX_FORM_DATA_LENGTH: usize = 10 * 1024; // 10KB
const MAX_REQUEST_LINE_SIZE: usize = 8192; // 8KBServes index.html from the ../dist directory.
Response:
- Status:
200 OK - Content-Type:
text/html - Body: HTML file content
Fallback:
If ../dist doesn't exist, serves hello.html from src/ directory.
Serves static files with automatic MIME type detection.
Supported types:
- HTML, CSS, JavaScript
- Images: PNG, JPG, GIF, SVG, ICO
- Fonts: WOFF, WOFF2, TTF, EOT
- 3D Models: GLB, GLTF
- Media: MP4, WEBM, MP3, WAV
- Documents: PDF, ZIP, TXT
Security:
- Path traversal protection enabled
- Files must be within
../distdirectory - Returns 403 Forbidden for unsafe paths
- Returns 404 Not Found for missing files
Handles form submissions with email notifications.
Request Format:
Content-Type: application/x-www-form-urlencoded
or
Content-Type: multipart/form-data
Required Fields:
{
"name": "string (max 40 chars)",
"email": "string (max 80 chars, valid email)",
"message": "string (max 2000 chars)"
}Optional Fields:
{
"frontend": "on", // Checkbox
"webDevelopment": "on", // Checkbox
"blender": "on" // Checkbox
}Validation Rules:
- All required fields must be present
- Email must match regex pattern
- No XSS patterns allowed
- No control characters (except \n, \r, \t)
- Total form data < 10KB
- Maximum 2 suspicious patterns in message
Success Response:
Status: 200 OK
Content-Type: text/html
[HTML page with success message]
Error Response:
Status: 200 OK
Content-Type: text/html
[HTML page with error details]
Email Behavior:
- Sends HTML + plain text confirmation email
- Uses SMTP credentials from environment
- Falls back to plain text if HTML fails
- Logs email success/failure
Example with cURL:
curl -X POST http://localhost:8080/contact \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "name=John%20Doe&email=john@example.com&message=Hello%20World"Health check endpoint with version information.
Response:
{
"status": "healthy",
"version": "1.0.0",
"timestamp": 1697385600
}- Status:
200 OK - Content-Type:
application/json - CORS: Allowed origins receive proper headers
Simple health check endpoint.
Response:
Status: 200 OK
Content-Type: text/plain
OK
Returned for:
- Non-existent API endpoints (paths starting with
/api/) - Invalid routes
- Missing static files
Response:
Status: 404 Not Found
Content-Type: text/html
<h1>404 Not Found</h1>
Returned when HTTP method is not in CORS allowed methods.
Response:
Status: 405 Method Not Allowed
Returned when rate limit is exceeded.
Response:
Status: 429 Too Many Requests
Rate limit exceeded. Please try again later.
Step 1: Field Presence Check
// Verify no unexpected fields
let allowed_fields: HashSet<&str> = FIELD_CONSTRAINTS.iter()
.map(|c| c.name)
.collect();
let unexpected_fields: Vec<&String> = form_data.keys()
.filter(|k| !allowed_fields.contains(k.as_str()))
.collect();Step 2: Character Validation
// Check for null bytes and control characters
if value.contains('\0') || value.chars().any(|c|
c.is_control() && c != '\n' && c != '\r' && c != '\t'
) {
// Reject field
}Step 3: Length Validation
if value.len() > constraint.max_length {
errors.push(format!("{} too long", constraint.name));
}Step 4: Email Validation
// ReDoS-resistant regex
let email_regex = Regex::new(
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
)?;
if !email_regex.is_match(value) {
errors.push("Invalid email format");
}
// Additional RFC 5321 checks
let parts: Vec<&str> = value.split('@').collect();
if parts[0].len() > 64 { // Local part limit
errors.push("Email local part too long");
}Step 5: Spam Pattern Detection
let suspicious_patterns = [
"http://", "https://", "www.", ".com", ".org",
"<script", "<iframe", "javascript:", "data:"
];
let count = suspicious_patterns.iter()
.filter(|p| value_lower.contains(*p))
.count();
if count > 2 {
errors.push("Message contains suspicious content");
}Step 6: XSS Scan
for (field_name, value) in form_data {
if contains_potential_xss(value) {
errors.push(format!("Field '{}' contains malicious content", field_name));
}
}Limits Enforced:
const MAX_HEADER_LINE_SIZE: usize = 8192; // 8KB per header
const MAX_HEADERS: usize = 50; // Max header count
const MAX_ORIGIN_LENGTH: usize = 200; // Max origin header lengthParsing Logic:
fn parse_headers_secure(
buf_reader: &mut BufReader<&mut TcpStream>,
security_config: &SecurityConfig,
) -> ServerResult<(usize, Option<String>)> {
let mut header_count = 0;
loop {
line.clear();
buf_reader.read_line(&mut line)?;
if line.trim().is_empty() { break; } // End of headers
header_count += 1;
if header_count > MAX_HEADERS {
return Err(ParseError("Too many headers"));
}
if line.len() > MAX_HEADER_LINE_SIZE {
return Err(ParseError("Header line too long"));
}
// Parse Content-Length and Origin headers
// Validate content length against max_content_length
}
}Origin Validation:
fn is_origin_allowed(&self, origin: &str) -> bool {
if self.allow_all_origins {
return true; // Dev mode only!
}
if self.allow_origins.is_empty() {
return true; // Same-origin mode
}
self.allow_origins.contains(&origin.to_string())
}Header Generation:
fn get_cors_origin_header(cors_config: &CorsConfig, origin: Option<&str>) -> String {
match origin {
Some(origin_value) => {
if cors_config.is_origin_allowed(origin_value) {
// Send CORS headers for allowed origins
if cors_config.allow_all_origins {
"Access-Control-Allow-Origin: *".to_string()
} else {
format!(
"Access-Control-Allow-Origin: {}\r\n\
Access-Control-Allow-Credentials: true",
origin_value
)
}
} else {
// CRITICAL: No CORS headers for disallowed origins
"".to_string()
}
}
None => {
// Same-origin request
if cors_config.allow_all_origins {
"Access-Control-Allow-Origin: *".to_string()
} else {
"".to_string()
}
}
}
}Preflight Handling:
fn handle_preflight_request(stream, cors_config, origin) {
let origin_header = get_cors_origin_header(cors_config, origin);
// Block unauthorized origins
if origin_header.is_empty() && origin.is_some() {
let response = "HTTP/1.1 403 Forbidden\r\n\r\n";
stream.write_all(response.as_bytes())?;
return Ok(());
}
// Send preflight response for allowed origins
let response = format!(
"HTTP/1.1 200 OK\r\n\
{}\r\n\
Access-Control-Allow-Methods: {}\r\n\
Access-Control-Allow-Headers: {}\r\n\
Access-Control-Max-Age: {}\r\n\r\n",
origin_header,
cors_config.allow_methods.join(", "),
cors_config.allow_headers.join(", "),
cors_config.max_age
);
stream.write_all(response.as_bytes())?;
}Sanitization Function:
pub fn sanitize_email_content(input: &str) -> String {
input.chars()
.filter(|c| !matches!(c, '\r' | '\n' | '\0'))
.collect()
}Why This Matters: Prevents email header injection attacks where attackers inject additional headers:
To: victim@example.com\r\nBcc: attacker@evil.com
Applied To:
- Recipient email address
- User name (displayed in email body)
- User message content
HTML Email Generation:
pub fn generate_email_html(name: &str, message: &str) -> String {
// name and message are already sanitized before calling
format!(
r#"<!DOCTYPE html>
<html>
<body>
<p>Hello <span class="highlight">{}</span>,</p>
<blockquote>{}</blockquote>
</body>
</html>"#,
name, message
)
}The server-manager.sh script provides process management for production deployment.
Commands:
Start the server:
./server-manager.sh start- Loads environment variables from
.env - Builds release binary
- Starts server in background
- Creates PID file at
$PREFIX/tmp/nox.pid - Logs output to
$PREFIX/tmp/nox.log
Stop the server:
./server-manager.sh stop- Reads PID from file
- Sends kill signal to process
- Removes PID file
Restart the server:
./server-manager.sh restart- Stops the server
- Waits 2 seconds
- Starts the server
Check status:
./server-manager.sh status- Shows if server is running
- Displays PID if active
- Detects stale PID files
View logs:
./server-manager.sh logs- Tails the log file in real-time
- Use Ctrl+C to exit
todo
- Set
CORS_MODE=same-originfor same-domain deployment - Use strong SMTP app password (not account password)
- Configure firewall to allow only port 80/443
- Run server as non-root user
- Set up reverse proxy (nginx/Apache) for HTTPS
- Enable log rotation for
nox.log - Monitor rate limiter performance
- Set up health check monitoring (
/api/health) - Configure automated backups
- Test form submission end-to-end
todo