Compare commits

...

51 Commits

Author SHA1 Message Date
8ae8ccdfe0 feat: implement zero-copy splice forwarding on Linux and fix upstream connection leak
Some checks failed
Build and Test / build (push) Failing after 27s
Build and Test / coverage (push) Successful in 38s
- Add forwarding_try_splice() with Linux splice syscall for zero-copy data transfer between client and upstream sockets, reducing CPU overhead by avoiding userspace buffer copies
- Remove stale config list management in config hot reload, replacing it with direct config reference counting to simplify memory handling and eliminate potential use-after-free bugs
- Fix connection event handling to process buffered data when upstream closes, preventing data loss during connection teardown
- Close existing upstream connection before establishing new one in upstream_connect(), preventing duplicate connections for the same client
2026-01-27 19:53:29 +00:00
6783b80844 chore: add retoor attribution and expand source file list in Makefile
- Add author comment with retoor <retoor@molodetz.nl> at top of Makefile
- Include 12 new source files (config_parser, http_response, client_handler, upstream, forwarding, base64, socket_utils, epoll_utils, time_utils, histogram, deque, rate_tracker, stats_collector) to SOURCES and TEST_LIB_SOURCES
- Add corresponding backup copies of auth.c, config.c, connection.c, and monitor.c to src/backup/ directory
2026-01-27 19:34:43 +00:00
ab70643e1f feat: reject HTTP pipelined requests with 400 error and close upstream connection
Add detection of pipelined requests in handle_client_read by checking if buffered data starts with a new HTTP request. When a pipelined request is detected, the server now sends a 400 Bad Request response with a descriptive body, closes the upstream connection, and transitions the client state to CLIENT_STATE_CLOSING. Previously, the server would silently close the upstream connection and continue reading headers. The change also includes a comprehensive test that validates the rejection behavior, response content, and connection state transitions for pipelined requests.
2026-01-27 16:15:20 +00:00
e914b08c29 chore: switch from -flto to -flto=auto for automatic job count in Makefile 2026-01-27 15:24:10 +00:00
69ab2f4fd9 feat: process buffered client data after upstream connection closes to prevent data loss
Add logic in connection_close to detect remaining buffered data on client read buffer when an upstream connection is closed, and invoke handle_client_read to process it. Also add a forward declaration for handle_client_read and include debug logging for routing decisions. A new test validates that pipelined requests buffered before upstream close are correctly handled.
2026-01-27 15:19:36 +00:00
f75581963a chore: add http_uri_is_internal_route and http_normalize_uri_path with tests for internal routing 2026-01-06 14:12:10 +00:00
a4438c6b60 chore: remove unused stdio.h include from main.c and fix typo in comment 2026-01-01 20:55:23 +00:00
0eac08394c feat: enable keep-alive for internal routes by resetting connection state and handling pending reads
Reset multiple connection flags (content_type_checked, is_textual_content, response_headers_parsed, original_content_length, content_length_delta, patch_blocked) when reusing connection for internal route keep-alive. Add immediate handling of any buffered read data after state reset to prevent stale data blocking subsequent requests. Add comprehensive test verifying second request processing on internal dashboard route via Unix socket pair.
2025-12-29 01:22:20 +00:00
45005f904b feat: add formatNum and formatMs helpers for dashboard metric display 2025-12-29 00:50:08 +00:00
598668ca8a feat: integrate monitoring metrics into connection handling and redesign dashboard with advanced charts
Add real-time tracking of accepted connections, upstream connection success/failure, DNS errors, and retries to connection.c. Introduce histogram, rate tracker, and health score data structures in types.h and monitor.h/c. Redesign dashboard HTML/CSS with color-coded health metrics, responsive charts grid, and tall chart containers for improved visualization of system performance data.
2025-12-29 00:37:24 +00:00
ca8bdcf2cd feat: add constant-time credential comparison, rate limiting, SSL hostname verification, and update coverage threshold to 69% 2025-12-28 04:16:15 +00:00
155df9ad3e chore: add test_logging.c to build system and raise minimum coverage threshold from 60 to 69 2025-12-15 01:36:54 +00:00
353e6b08b6 feat: add TCP_NODELAY socket optimization and upstream handling with connection caching tests 2025-12-15 00:31:27 +00:00
9e7c5940dc perf: cache epoll events in connection struct and add socket optimization routines 2025-12-15 00:28:34 +00:00
c84d8c9d21 feat: add splice pipe fields and patch buffer to connection struct for zero-copy forwarding 2025-12-15 00:12:09 +00:00
521bd1eb5b chore: add bc dependency and coverage job with gcovr/lcov to build pipeline 2025-12-12 23:21:55 +00:00
87ede0b4c9 chore: add trailing whitespace to author line in readme for placeholder formatting 2025-12-12 21:37:32 +00:00
5cea9db29e chore: add test results section to README and tag routing logs with hostname 2025-12-12 21:33:27 +00:00
8158576fb6 feat: forward buffered client request data to upstream on write completion
When the write buffer is fully drained on an upstream connection, check if the paired client connection has buffered request data that does not start with an HTTP request line. If so, copy that data directly into the upstream's write buffer and re-arm epoll for both read and write events, enabling transparent forwarding of non-HTTP payloads (e.g., WebSocket upgrade data or raw TCP streams) without waiting for the next epoll cycle.
2025-12-12 21:29:45 +00:00
0461f21d5a chore: update README with author, testing section, and per-route auth config 2025-12-12 21:20:34 +00:00
1ec7101289 fix: replace pthread primitives with atomics and add log rotation in config and monitor
- Migrate config.c from pthread_rwlock and __sync builtins to stdatomic.h atomic_fetch_add/sub for ref counting, removing global config_lock
- Replace pthread_mutex_t in health_check.c and monitor.c with lock-free patterns, eliminating vhost_stats_mutex and health_mutex
- Add log file rotation in logging.c with 10MB max size and 5 rotation backups, triggered on fstat size check
- Introduce SSL handshake timeout (SSL_HANDSHAKE_TIMEOUT_SEC) in connection.c with elapsed time tracking via ssl_handshake_start
- Extend upstream connection setup to copy config reference and call config_ref_inc in connection_connect_to_upstream
- Add WAL journal mode and synchronous NORMAL pragmas to monitor.c SQLite init, plus data retention constant and vhost_totals table schema
- Update Makefile with -Werror, -O3, -march=native, -flto flags, separate CFLAGS_DEBUG, add valgrind phony target, and lower min coverage to 60% with new test modules
- Expand .gitignore to cover CLAUDE.md, *.db-wal, and *.db-shm files
- Refactor health_check.c to use snprintf instead of strncpy for hostname/upstream_host with HOSTNAME_MAX_LEN bounds
2025-12-12 21:02:24 +00:00
ef3cb0fc54 chore: remove entire original rproxy.c source tree from repository history 2025-12-12 19:43:24 +00:00
493a77a7ce chore: add coverage build flags, new test suites, and fix config pointer access in health_check and test_config 2025-12-01 23:50:05 +00:00
d86022d49e feat: add http_find_header_line_bounds helper and refactor host header rewrite in connection.c
Extract host header line detection into a reusable http_find_header_line_bounds function in http.c/http.h, replacing the inline manual scanning loop in connection_connect_to_upstream. The new helper returns the start and end pointers of a named header line, enabling cleaner host rewrite logic that also appends the upstream port only when it is non-default (443 for SSL, 80 for plain).
2025-11-29 12:27:04 +00:00
5a07aeffba refactor: convert global config struct to heap-allocated ref-counted pointer with hot-reload support 2025-11-29 12:18:07 +00:00
4c17705239 feat: add stream data patching with find-replace and content blocking for HTTP routes
Implement configurable patch rules for rewriting or blocking textual content in HTTP streams. New `patch` configuration object supports string replacement and content blocking via null values, applied bidirectionally to requests and responses. Blocked responses return 502, blocked requests return 403.
2025-11-29 04:56:34 +00:00
eeb9544b83 fix: correct sni_hostname selection logic in connection_connect_to_upstream
The ternary condition for sni_hostname was inverted: when route->rewrite_host is true,
the upstream_host should be used for SNI, not the client's request host. This fix swaps
the operands so that rewrite_host correctly selects the upstream hostname for TLS
Server Name Indication extension.
2025-11-29 04:21:38 +00:00
b7957fd5f6 chore: add rate limiting, auth, health checks, and config hot-reload to build system and source 2025-11-29 03:58:34 +00:00
949034bf74 fix: correct SNI hostname field name and SSL API call in connection setup 2025-11-29 03:18:39 +00:00
04eefd07eb fix: correct SNI hostname selection logic in connection_connect_to_upstream
The SNI hostname assignment was incorrectly using `route->rewrite_host` to decide between `route->upstream_host` and `client->request.host`. The corrected logic now uses `route->rewritehost` and swaps the hostname sources: when rewrite is enabled, the client's original host is used; otherwise the upstream host is passed. Additionally, the SSL function call was updated from `SSL_set_tlsext_host_name` to `SSL_set_tlsext_hostname` to match the actual OpenSSL API.
2025-11-29 03:16:59 +00:00
988dff1b20 fix: correct default port detection to respect SSL context in connection routing 2025-11-29 02:15:45 +00:00
8436787928 fix: correct SSL read error handling and replace magic strings with macros in connection.c 2025-11-29 01:51:08 +00:00
a6650d66c2 chore: remove obsolete test scripts for proxy routing verification
Delete test_routing.sh, test_routing_comprehensive.sh, test_routing_fix.sh,
test_upstream.py, and verify_routing.sh as they are no longer needed after
routing logic has been stabilized and integrated into the main test suite.
2025-11-29 00:58:16 +00:00
50a682c865 chore: remove trailing whitespace from README.md formatting and update project name to lowercase 2025-11-29 00:57:12 +00:00
feb2fb8a05 chore: add Gitea CI workflow for build and test on main/master pushes 2025-11-29 00:53:11 +00:00
3643909bd5 chore: add .gitignore entries for build artifacts and test files, restructure Makefile with proper build system 2025-11-29 00:49:14 +00:00
2a492dd934 fix: prefix internal dashboard and stats routes with /rproxy/ in rproxy.c 2025-11-29 00:04:25 +00:00
94df53367b chore: remove trailing whitespace and clean up comment blocks in rproxy.c 2025-11-20 05:29:28 +00:00
5783c0ec9c fix: replace hardcoded 10MB buffer limit with SIZE_MAX/2 overflow check and remove stale comments 2025-11-20 05:27:32 +00:00
126faf6082 feat: extend valid HTTP method list with WebDAV verbs in is_valid_http_method
Add PROPFIND, PROPPATCH, MKCOL, MOVE, COPY, PROPDEL, LOCK, and UNLOCK to the
valid_methods array in rproxy.c to support WebDAV protocol routing through the
reverse proxy without rejecting these methods as invalid.
2025-11-01 18:30:46 +00:00
ed58bae194 fix: intercept internal HTTP routes on keep-alive connections in handle_forwarding
Add heuristic detection of new HTTP requests for internal routes (/dashboard, /api/stats) arriving on persistent connections in handle_forwarding. When such a request is detected, close the paired upstream connection to break the keep-alive link and allow re-routing through the connection lifecycle logic. This fixes the pipeline issue where internal requests were incorrectly forwarded to the original upstream server.
2025-09-25 22:17:16 +00:00
9485b7a618 chore: remove extraneous blank lines between function definitions in rproxy.c 2025-09-25 22:07:06 +00:00
30c664660c feat: add chunked transfer encoding detection for git client compatibility in http request parser
- Add `stdbool.h` include for boolean type support
- Introduce `is_chunked` field to `http_request_t` struct for tracking chunked encoding
- Implement Transfer-Encoding header parsing to detect chunked transfers used by git push/pull operations
- Increase `MAX_EVENTS` from 1024 to 4096 to handle higher connection concurrency
2025-09-25 21:59:27 +00:00
d2eabd26a3 chore: remove trailing whitespace from README.md line 42 2025-09-25 17:36:47 +00:00
b9d0423b1a fix: reset client state and process buffered request on upstream close to fix keep-alive race 2025-09-25 17:31:04 +00:00
a47849b005 chore: remove trailing whitespace from README.md line 42 2025-09-25 17:23:20 +00:00
9d7087c7c0 chore: remove verbose routing debug logs and simplify forwarding state reset logic in handle_client_read 2025-09-25 17:16:07 +00:00
5c4c357bc3 fix: reset orphaned forwarding state to reading headers and add routing debug logging 2025-09-25 04:37:51 +00:00
33b230f722 fix: correct SSL upstream configuration for dr endpoint to resolve connection failure 2025-09-25 03:43:10 +00:00
84293be85e feat: add connection timeout constant and new client states for enhanced stats tracking
Implement CONNECTION_TIMEOUT macro (300s), extend client_state_t with CLIENT_STATE_SERVING_INTERNAL and CLIENT_STATE_CLOSING, add connection_close field to http_request_t, and bump version to 3.0 with updated author string.
2025-09-25 03:21:40 +00:00
0fbf53e0bf fix: simplify client state machine and fix buffer management in rproxy.c 2025-09-25 03:11:52 +00:00
81 changed files with 15872 additions and 2212 deletions

50
.gitea/workflows/build.yaml Executable file
View File

@ -0,0 +1,50 @@
# retoor <retoor@molodetz.nl>
name: Build and Test
on:
push:
branches:
- main
- master
pull_request:
branches:
- main
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y gcc make libssl-dev libsqlite3-dev bc
- name: Build
run: make all
- name: Run tests
run: make test
- name: Build legacy
run: make legacy
- name: Clean
run: make clean
coverage:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y gcc make libssl-dev libsqlite3-dev bc gcovr lcov
- name: Run coverage
run: make coverage

10
.gitignore vendored Normal file → Executable file
View File

@ -2,4 +2,14 @@
*.bak
*.bak2
*.db
*.txt
*.log
*_test
*.py
*.sh
build
__pycache__/
rproxy
CLAUDE.md
*.db-wal
*.db-shm

114
CHANGELOG.md Executable file
View File

@ -0,0 +1,114 @@
# Changelog
## Version 0.14.0 - 2026-01-27
add zero-copy forwarding using splice syscall on linux
**Changes:** 4 files, 105 lines
**Languages:** C (105 lines)
## Version 0.13.0 - 2026-01-27
extract base64 decoding into separate module
**Changes:** 37 files, 7660 lines
**Languages:** C (7469 lines), Other (191 lines)
## Version 0.12.0 - 2026-01-27
The server now rejects HTTP pipelined requests with a 400 Bad Request error.
**Changes:** 2 files, 268 lines
**Languages:** C (268 lines)
## Version 0.11.0 - 2026-01-27
The system now processes buffered client data after an upstream connection closes, ensuring no data loss in connection scenarios. A corresponding test validates this behavior.
**Changes:** 2 files, 103 lines
**Languages:** C (103 lines)
## Version 0.10.0 - 2026-01-06
update c, h files
**Changes:** 4 files, 145 lines
**Languages:** C (145 lines)
## Version 0.9.0 - 2026-01-01
update c files
**Changes:** 1 files, 5 lines
**Languages:** C (5 lines)
## Version 0.8.0 - 2025-12-29
Enables keep-alive connections for internal routes, allowing multiple requests over the same connection. Adds a test to verify handling of the second request on an internal route.
**Changes:** 2 files, 99 lines
**Languages:** C (99 lines)
## Version 0.7.0 - 2025-12-29
The dashboard now formats numbers and times in metrics for improved readability.
**Changes:** 1 files, 14 lines
**Languages:** C (14 lines)
## Version 0.6.0 - 2025-12-29
Integrates monitoring metrics into connection handling to provide real-time insights into network performance. Redesigns the dashboard with advanced metrics and charts for enhanced visualization of system data.
**Changes:** 9 files, 1245 lines
**Languages:** C (1245 lines)
## Version 0.5.0 - 2025-12-28
Enhances authentication security by preventing timing attacks and clearing sensitive memory, while adding rate limiting to protect against abusive client requests. Enables SSL hostname verification and preferred cipher suites for improved connection security, and fixes request denial when rate limit allocation fails.
**Changes:** 49 files, 138 lines
**Languages:** C (134 lines), Markdown (4 lines)
## Version 0.4.0 - 2025-12-15
Add comprehensive tests for the auth, buffer, and connection modules. Enhance the build process with logging test support and an increased minimum coverage threshold.
**Changes:** 8 files, 2046 lines
**Languages:** C (2036 lines), Other (10 lines)
## Version 0.3.0 - 2025-12-15
Adds socket optimization functions that enable TCP_NODELAY for reduced connection latency and enhance upstream handling. Includes tests for connection optimizations, caching, and cleanup to verify functionality.
**Changes:** 2 files, 146 lines
**Languages:** C (146 lines)
## Version 0.2.0 - 2025-12-15
Optimizes performance through enhanced socket settings and caching, reducing latency in connections. Adds host header validation to client requests and improves DNS resolution error handling for more reliable network operations.
**Changes:** 4 files, 79 lines
**Languages:** C (79 lines)
## Version 0.1.0 - 2025-12-15
Implements zero-copy data forwarding to enhance connection performance. Caches patch buffers in connection structures and optimizes buffer compaction during read handling to reduce memory allocations.
**Changes:** 2 files, 153 lines
**Languages:** C (153 lines)

226
Makefile Normal file → Executable file
View File

@ -1,11 +1,225 @@
# retoor <retoor@molodetz.nl>
all: build run
CC = gcc
CFLAGS = -Wall -Wextra -Werror -O3 -march=native -flto=auto -fomit-frame-pointer -D_GNU_SOURCE
CFLAGS_DEBUG = -Wall -Wextra -Werror -O0 -g -D_GNU_SOURCE
CFLAGS_COV = -Wall -Wextra -Werror -O0 -g -D_GNU_SOURCE --coverage -fprofile-arcs -ftest-coverage
LDFLAGS = -flto -lssl -lcrypto -lsqlite3 -lm -lpthread
LDFLAGS_COV = -lssl -lcrypto -lsqlite3 -lm -lpthread --coverage
build:
gcc -Wall -Wextra -O2 -g -pthread cJSON.c rproxy.c -o rproxy -lssl -lcrypto -lsqlite3 -lm
SRC_DIR = src
BUILD_DIR = build
TESTS_DIR = tests
run:
./rproxy
SOURCES = $(SRC_DIR)/main.c \
$(SRC_DIR)/buffer.c \
$(SRC_DIR)/logging.c \
$(SRC_DIR)/config.c \
$(SRC_DIR)/config_parser.c \
$(SRC_DIR)/monitor.c \
$(SRC_DIR)/http.c \
$(SRC_DIR)/http_response.c \
$(SRC_DIR)/ssl_handler.c \
$(SRC_DIR)/connection.c \
$(SRC_DIR)/client_handler.c \
$(SRC_DIR)/upstream.c \
$(SRC_DIR)/forwarding.c \
$(SRC_DIR)/dashboard.c \
$(SRC_DIR)/rate_limit.c \
$(SRC_DIR)/auth.c \
$(SRC_DIR)/base64.c \
$(SRC_DIR)/health_check.c \
$(SRC_DIR)/patch.c \
$(SRC_DIR)/socket_utils.c \
$(SRC_DIR)/epoll_utils.c \
$(SRC_DIR)/time_utils.c \
$(SRC_DIR)/histogram.c \
$(SRC_DIR)/deque.c \
$(SRC_DIR)/rate_tracker.c \
$(SRC_DIR)/stats_collector.c \
cJSON.c
OBJECTS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(notdir $(SOURCES)))
TARGET = rproxy
TEST_SOURCES = $(TESTS_DIR)/test_main.c \
$(TESTS_DIR)/test_http.c \
$(TESTS_DIR)/test_buffer.c \
$(TESTS_DIR)/test_config.c \
$(TESTS_DIR)/test_routing.c \
$(TESTS_DIR)/test_host_rewrite.c \
$(TESTS_DIR)/test_http_helpers.c \
$(TESTS_DIR)/test_patch.c \
$(TESTS_DIR)/test_auth.c \
$(TESTS_DIR)/test_rate_limit.c \
$(TESTS_DIR)/test_monitor.c \
$(TESTS_DIR)/test_dashboard.c \
$(TESTS_DIR)/test_health_check.c \
$(TESTS_DIR)/test_ssl_handler.c \
$(TESTS_DIR)/test_connection.c \
$(TESTS_DIR)/test_logging.c
TEST_OBJECTS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(notdir $(TEST_SOURCES)))
TEST_LIB_SOURCES = $(SRC_DIR)/buffer.c \
$(SRC_DIR)/logging.c \
$(SRC_DIR)/config.c \
$(SRC_DIR)/config_parser.c \
$(SRC_DIR)/monitor.c \
$(SRC_DIR)/http.c \
$(SRC_DIR)/http_response.c \
$(SRC_DIR)/ssl_handler.c \
$(SRC_DIR)/connection.c \
$(SRC_DIR)/client_handler.c \
$(SRC_DIR)/upstream.c \
$(SRC_DIR)/forwarding.c \
$(SRC_DIR)/dashboard.c \
$(SRC_DIR)/rate_limit.c \
$(SRC_DIR)/auth.c \
$(SRC_DIR)/base64.c \
$(SRC_DIR)/health_check.c \
$(SRC_DIR)/patch.c \
$(SRC_DIR)/socket_utils.c \
$(SRC_DIR)/epoll_utils.c \
$(SRC_DIR)/time_utils.c \
$(SRC_DIR)/histogram.c \
$(SRC_DIR)/deque.c \
$(SRC_DIR)/rate_tracker.c \
$(SRC_DIR)/stats_collector.c \
cJSON.c
TEST_LIB_OBJECTS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(notdir $(TEST_LIB_SOURCES)))
TEST_TARGET = rproxy_test
MIN_COVERAGE = 60
COVERAGE_MODULES = auth.c buffer.c config.c http.c logging.c patch.c rate_limit.c monitor.c dashboard.c health_check.c ssl_handler.c connection.c
.PHONY: all clean test legacy run coverage coverage-html valgrind
all: $(BUILD_DIR) $(TARGET)
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
$(TARGET): $(OBJECTS)
$(CC) $(OBJECTS) -o $@ $(LDFLAGS)
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR)/cJSON.o: cJSON.c | $(BUILD_DIR)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR)/test_%.o: $(TESTS_DIR)/test_%.c | $(BUILD_DIR)
$(CC) $(CFLAGS) -I$(SRC_DIR) -c $< -o $@
$(TEST_TARGET): $(BUILD_DIR) $(TEST_OBJECTS) $(TEST_LIB_OBJECTS)
$(CC) $(TEST_OBJECTS) $(TEST_LIB_OBJECTS) -o $@ $(LDFLAGS)
test: $(TEST_TARGET)
./$(TEST_TARGET)
legacy: rproxy.c cJSON.c cJSON.h
$(CC) $(CFLAGS) rproxy.c cJSON.c -o rproxy_legacy $(LDFLAGS)
run: $(TARGET)
./$(TARGET)
coverage: clean
mkdir -p $(BUILD_DIR)
@for src in $(TEST_LIB_SOURCES); do \
obj=$(BUILD_DIR)/$$(basename $${src%.c}.o); \
$(CC) $(CFLAGS_COV) -c $$src -o $$obj; \
done
@for src in $(TEST_SOURCES); do \
obj=$(BUILD_DIR)/$$(basename $${src%.c}.o); \
$(CC) $(CFLAGS_COV) -I$(SRC_DIR) -c $$src -o $$obj; \
done
$(CC) $(TEST_OBJECTS) $(TEST_LIB_OBJECTS) -o $(TEST_TARGET) $(LDFLAGS_COV)
./$(TEST_TARGET)
@echo ""
@echo "=== Coverage Report ==="
@gcov -n build/*.gcno 2>/dev/null | grep -A 1 "^File.*src/" | grep -v "^--$$" || true
@echo ""
@echo "=== Coverage Check (minimum $(MIN_COVERAGE)%) ==="
@echo "Checking modules: $(COVERAGE_MODULES)"
@total_lines=0; covered_lines=0; \
for gcno in build/*.gcno; do \
output=$$(gcov -n "$$gcno" 2>/dev/null); \
file=$$(echo "$$output" | grep "^File.*src/" | head -1 | sed "s/File '\\(.*\\)'/\\1/"); \
if [ -n "$$file" ]; then \
basename=$$(basename "$$file"); \
is_tracked=0; \
for mod in $(COVERAGE_MODULES); do \
if [ "$$basename" = "$$mod" ]; then is_tracked=1; break; fi; \
done; \
if [ $$is_tracked -eq 1 ]; then \
lines=$$(echo "$$output" | grep "Lines executed:" | head -1 | sed 's/.*of \([0-9]*\)/\1/'); \
pct=$$(echo "$$output" | grep "Lines executed:" | head -1 | sed 's/Lines executed:\([0-9.]*\)%.*/\1/'); \
if [ -n "$$lines" ] && [ -n "$$pct" ]; then \
covered=$$(echo "$$pct * $$lines / 100" | bc -l | cut -d. -f1); \
covered=$${covered:-0}; \
total_lines=$$((total_lines + lines)); \
covered_lines=$$((covered_lines + covered)); \
echo " $$basename: $$pct% ($$covered/$$lines lines)"; \
fi; \
fi; \
fi; \
done; \
if [ $$total_lines -gt 0 ]; then \
avg=$$(echo "scale=2; $$covered_lines * 100 / $$total_lines" | bc -l); \
echo ""; \
echo "Total: $$covered_lines/$$total_lines lines covered ($$avg%)"; \
avg_int=$$(echo "$$avg" | cut -d. -f1); \
if [ $$avg_int -lt $(MIN_COVERAGE) ]; then \
echo "FAILED: Coverage $$avg% is below minimum $(MIN_COVERAGE)%"; \
exit 1; \
else \
echo "PASSED: Coverage $$avg% meets minimum $(MIN_COVERAGE)%"; \
fi; \
else \
echo "FAILED: No coverage data found"; \
exit 1; \
fi
coverage-html: coverage
mkdir -p coverage_report
lcov --capture --directory build --output-file coverage_report/coverage.info --ignore-errors source 2>/dev/null || true
genhtml coverage_report/coverage.info --output-directory coverage_report 2>/dev/null || echo "Install lcov for HTML reports: sudo apt install lcov"
@echo "Coverage report generated in coverage_report/index.html"
valgrind: clean
mkdir -p $(BUILD_DIR)
@for src in $(TEST_LIB_SOURCES); do \
obj=$(BUILD_DIR)/$$(basename $${src%.c}.o); \
$(CC) $(CFLAGS_DEBUG) -c $$src -o $$obj; \
done
@for src in $(TEST_SOURCES); do \
obj=$(BUILD_DIR)/$$(basename $${src%.c}.o); \
$(CC) $(CFLAGS_DEBUG) -I$(SRC_DIR) -c $$src -o $$obj; \
done
$(CC) $(TEST_OBJECTS) $(TEST_LIB_OBJECTS) -o $(TEST_TARGET) -lssl -lcrypto -lsqlite3 -lm -lpthread
valgrind --leak-check=full --show-leak-kinds=definite,indirect --error-exitcode=1 ./$(TEST_TARGET) 2>&1 | tee valgrind.log; \
VALGRIND_EXIT=$$?; \
echo ""; \
echo "=== Valgrind Summary ==="; \
grep -E "(definitely|indirectly) lost:" valgrind.log || true; \
grep "All heap blocks were freed" valgrind.log || true; \
grep "ERROR SUMMARY:" valgrind.log || true; \
if [ $$VALGRIND_EXIT -ne 0 ]; then \
echo "FAILED: Valgrind detected errors"; rm -f valgrind.log; exit 1; \
fi; \
if grep -q "definitely lost: [1-9]" valgrind.log; then \
echo "FAILED: Definite memory leaks detected"; rm -f valgrind.log; exit 1; \
fi; \
if grep -q "indirectly lost: [1-9]" valgrind.log; then \
echo "FAILED: Indirect memory leaks detected"; rm -f valgrind.log; exit 1; \
fi; \
rm -f valgrind.log
@echo ""
@echo "=== Valgrind Check Passed ==="
clean:
rm -f rproxy
rm -rf $(BUILD_DIR) $(TARGET) $(TEST_TARGET) rproxy_legacy *.gcov coverage_report

229
README.md Normal file → Executable file
View File

@ -1,68 +1,201 @@
# RProxy
# rproxy
RProxy is a reverse proxy server designed to route requests to various upstream services based on the hostname. It is built using C and leverages the cJSON library for JSON parsing. This README provides a comprehensive guide on how to get started, configure the server, and understand its features.
Author: retoor <retoor@molodetz.nl>
## Getting Started
A high-performance reverse proxy server written in C. Routes HTTP and WebSocket requests to upstream services based on hostname, with support for SSL/TLS connections and real-time monitoring.
### Prerequisites
- GCC (GNU Compiler Collection)
- Make
- OpenSSL libraries (`libssl` and `libcrypto`)
- SQLite3 library
## Features
### Installation
1. Clone the repository:
```bash
git clone <repository-url>
cd <repository-directory>
```
2. Build the project using the Makefile:
```bash
make
```
3. Run the server:
```bash
make run
```
- Reverse proxy routing by hostname
- SSL/TLS support for upstream connections with certificate verification
- WebSocket proxying
- Connection pooling and idle timeout management
- Real-time monitoring and statistics
- Web-based dashboard for metrics visualization
- SQLite-based persistent statistics storage
- Epoll-based event handling for high concurrency
- Graceful shutdown with connection draining
- Live configuration reload via SIGHUP
- Per-route authentication (HTTP Basic Auth)
- Dashboard authentication
- Rate limiting per client IP
- Health checks for upstream servers
- Automatic upstream connection retries
- File logging support
- Stream data patching/rewriting for textual content
### Configuration
## Dependencies
The configuration for RProxy is stored in the `proxy_config.json` file. Below is a breakdown of its structure:
- GCC
- OpenSSL (libssl, libcrypto)
- SQLite3
- pthreads
- cJSON (bundled)
## Build
```bash
make
```
Compiles the source files in `src/` and produces the `rproxy` executable.
## Testing
```bash
make test # Run unit tests
make coverage # Run tests with coverage report (minimum 69% required)
make coverage-html # Generate HTML coverage report
make valgrind # Run tests with memory leak detection
```
### Test Results
```
Test Results: 741/741 passed
HEAP SUMMARY:
in use at exit: 0 bytes in 0 blocks
total heap usage: 155,794 allocs, 155,794 frees, 13,900,573 bytes allocated
All heap blocks were freed -- no leaks are possible
ERROR SUMMARY: 0 errors from 0 contexts
```
## Configuration
Configuration is defined in `proxy_config.json`:
```json
{
"port": 8585,
"port": 9998,
"reverse_proxy": [
{ "hostname": "example.com", "upstream_host": "127.0.0.1", "upstream_port": 3000, "use_ssl": false }
{
"hostname": "example.com",
"upstream_host": "127.0.0.1",
"upstream_port": 5000,
"use_ssl": false,
"rewrite_host": true,
"use_auth": true,
"username": "admin",
"password": "secret",
"patch": {
"old_string": "new_string",
"blocked_content": null
}
}
]
}
```
- **port**: The port on which the RProxy server will listen for incoming requests.
- **reverse_proxy**: An array of objects, each representing a reverse proxy configuration.
- **hostname**: The hostname that the server will respond to.
- **upstream_host**: The IP address or hostname of the upstream service.
- **upstream_port**: The port on which the upstream service is running.
- **use_ssl**: A boolean indicating whether to use SSL for the upstream connection.
### Route Options
### Features
- **Reverse Proxying**: RProxy can route requests to multiple upstream services based on the hostname.
- **SSL Support**: Optionally, RProxy can connect to upstream services using SSL.
- **Dynamic Configuration**: The configuration can be easily modified by editing the `proxy_config.json` file.
- **Multi-Host Support**: RProxy can handle requests for multiple hostnames, making it suitable for microservices architectures.
| Option | Type | Description |
|--------|------|-------------|
| `hostname` | string | Host header to match for routing |
| `upstream_host` | string | Target server hostname or IP |
| `upstream_port` | integer | Target server port (1-65535) |
| `use_ssl` | boolean | Enable SSL/TLS for upstream connection |
| `rewrite_host` | boolean | Rewrite Host header to upstream hostname |
| `use_auth` | boolean | Enable HTTP Basic Auth for this route |
| `username` | string | Authentication username |
| `password` | string | Authentication password |
| `patch` | object | Stream data patching rules |
### Use Cases
- **Microservices Architecture**: RProxy can be used to route requests to different microservices based on the hostname, simplifying service discovery.
- **Load Balancing**: By configuring multiple upstream services for a single hostname, RProxy can distribute traffic among them.
- **API Gateway**: RProxy can serve as an API gateway, providing a single entry point for various backend services.
### Data Patching
### Monitoring and Statistics
RProxy can be extended to monitor various statistics such as:
- **Request Count**: The total number of requests handled by the server.
- **Response Times**: The time taken to respond to requests.
- **Error Rates**: The number of failed requests to upstream services.
The `patch` configuration allows rewriting or blocking content in HTTP streams. Patch rules are applied to textual content only. Binary content passes through unmodified.
```json
{
"patch": {
"find_this": "replace_with_this",
"another_string": "replacement",
"blocked_term": null
}
}
```
- **String replacement**: Each key-value pair defines a find-replace rule
- **Content blocking**: Setting value to `null` blocks the entire response/request when the key is found
- **Bidirectional**: Patches apply to both requests and responses
Blocked responses return `502 Bad Gateway`. Blocked requests return `403 Forbidden`.
Supported textual content types:
- `text/*`
- `application/json`
- `application/xml`
- `application/javascript`
- `application/x-www-form-urlencoded`
- Content types with `+xml` or `+json` suffix
## Environment Variables
| Variable | Description |
|----------|-------------|
| `DEBUG` | Enable debug logging (set to `1`) |
| `LOG_FILE` | Path to log file (default: stdout) |
| `RATE_LIMIT` | Max requests per minute per IP |
| `DASHBOARD_USER` | Dashboard authentication username |
| `DASHBOARD_PASS` | Dashboard authentication password |
| `SSL_VERIFY` | Disable SSL verification (set to `0`) |
| `SSL_CA_FILE` | Path to custom CA certificate file |
| `SSL_CA_PATH` | Path to CA certificate directory |
## Usage
```bash
./rproxy [config_file]
```
If no config file is specified, defaults to `proxy_config.json`.
```bash
./rproxy
./rproxy /etc/rproxy/config.json
DEBUG=1 ./rproxy
LOG_FILE=/var/log/rproxy.log ./rproxy
RATE_LIMIT=100 ./rproxy
DASHBOARD_USER=admin DASHBOARD_PASS=secret ./rproxy
SSL_VERIFY=0 ./rproxy
kill -HUP $(pidof rproxy) # Reload configuration
```
## Endpoints
| Path | Description |
|------|-------------|
| `/rproxy/dashboard` | Web-based monitoring dashboard |
| `/rproxy/api/stats` | JSON API for statistics |
## Signals
| Signal | Action |
|--------|--------|
| `SIGINT` | Graceful shutdown |
| `SIGTERM` | Graceful shutdown |
| `SIGHUP` | Reload configuration |
## Architecture
| Module | Description |
|--------|-------------|
| `main.c` | Entry point, event loop, signal handling |
| `connection.c` | Connection management, epoll handling |
| `http.c` | HTTP request/response parsing |
| `ssl_handler.c` | SSL/TLS connection handling |
| `monitor.c` | System and per-vhost statistics collection |
| `dashboard.c` | Web dashboard generation |
| `config.c` | JSON configuration parsing with hot-reload |
| `buffer.c` | Circular buffer implementation |
| `logging.c` | Logging utilities |
| `rate_limit.c` | Per-IP rate limiting with sliding window |
| `auth.c` | HTTP Basic Auth implementation |
| `health_check.c` | Upstream health monitoring |
| `patch.c` | Stream data patching engine |
## License
Copyright (c) retoor. All rights reserved.
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
See LICENSE file for details.

0
cJSON.c Normal file → Executable file
View File

0
cJSON.h Normal file → Executable file
View File

2158
rproxy.c

File diff suppressed because it is too large Load Diff

170
src/auth.c Normal file
View File

@ -0,0 +1,170 @@
// retoor <retoor@molodetz.nl>
#include "auth.h"
#include "base64.h"
#include "logging.h"
#include <stdlib.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/crypto.h>
static char g_dashboard_username[128] = "";
static char g_dashboard_password_hash[256] = "";
static int g_auth_enabled = 0;
static int constant_time_compare(const char *a, const char *b, size_t len) {
return CRYPTO_memcmp(a, b, len) == 0;
}
static void compute_sha256(const char *input, char *output, size_t output_size) {
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
if (!ctx) return;
unsigned char hash[EVP_MAX_MD_SIZE];
unsigned int hash_len = 0;
EVP_DigestInit_ex(ctx, EVP_sha256(), NULL);
EVP_DigestUpdate(ctx, input, strlen(input));
EVP_DigestFinal_ex(ctx, hash, &hash_len);
EVP_MD_CTX_free(ctx);
for (unsigned int i = 0; i < hash_len && (i * 2 + 2) < output_size; i++) {
snprintf(output + (i * 2), 3, "%02x", hash[i]);
}
}
void auth_init(const char *username, const char *password) {
if (!username || !password || strlen(username) == 0 || strlen(password) == 0) {
g_auth_enabled = 0;
return;
}
strncpy(g_dashboard_username, username, sizeof(g_dashboard_username) - 1);
g_dashboard_username[sizeof(g_dashboard_username) - 1] = '\0';
compute_sha256(password, g_dashboard_password_hash, sizeof(g_dashboard_password_hash));
g_auth_enabled = 1;
log_info("Dashboard authentication enabled for user: %s", username);
}
int auth_is_enabled(void) {
return g_auth_enabled;
}
int auth_check_credentials(const char *username, const char *password) {
if (!g_auth_enabled) return 1;
if (!username || !password) return 0;
char password_hash[256];
compute_sha256(password, password_hash, sizeof(password_hash));
size_t username_len = strlen(username);
size_t expected_username_len = strlen(g_dashboard_username);
int username_match = (username_len == expected_username_len) &&
constant_time_compare(username, g_dashboard_username, username_len);
int hash_match = constant_time_compare(password_hash, g_dashboard_password_hash, 64);
memset(password_hash, 0, sizeof(password_hash));
return username_match && hash_match;
}
typedef struct {
const char *expected_username;
const char *expected_password_hash;
} auth_credentials_t;
static int auth_parse_and_verify(const char *auth_header, const auth_credentials_t *creds,
char *error_msg, size_t error_size) {
if (!auth_header) {
if (error_msg && error_size > 0) {
strncpy(error_msg, "Authentication required", error_size - 1);
}
return 0;
}
if (strncmp(auth_header, "Basic ", 6) != 0) {
if (error_msg && error_size > 0) {
strncpy(error_msg, "Invalid authentication method", error_size - 1);
}
return 0;
}
const char *encoded = auth_header + 6;
size_t encoded_len = strlen(encoded);
if (encoded_len > 680) {
if (error_msg && error_size > 0) {
strncpy(error_msg, "Credentials too long", error_size - 1);
}
return 0;
}
char decoded[512];
int decoded_len = base64_decode(encoded, decoded, sizeof(decoded));
if (decoded_len < 0) {
if (error_msg && error_size > 0) {
strncpy(error_msg, "Invalid credentials format", error_size - 1);
}
return 0;
}
char *colon = strchr(decoded, ':');
if (!colon) {
memset(decoded, 0, sizeof(decoded));
if (error_msg && error_size > 0) {
strncpy(error_msg, "Invalid credentials format", error_size - 1);
}
return 0;
}
*colon = '\0';
const char *username = decoded;
const char *password = colon + 1;
char password_hash[256];
compute_sha256(password, password_hash, sizeof(password_hash));
size_t username_len = strlen(username);
size_t expected_username_len = strlen(creds->expected_username);
int username_match = (username_len == expected_username_len) &&
constant_time_compare(username, creds->expected_username, username_len);
int hash_match = constant_time_compare(password_hash, creds->expected_password_hash, 64);
memset(decoded, 0, sizeof(decoded));
memset(password_hash, 0, sizeof(password_hash));
if (!username_match || !hash_match) {
if (error_msg && error_size > 0) {
strncpy(error_msg, "Invalid username or password", error_size - 1);
}
return 0;
}
return 1;
}
int auth_check_basic_auth(const char *auth_header, char *error_msg, size_t error_size) {
if (!g_auth_enabled) return 1;
auth_credentials_t creds = {
.expected_username = g_dashboard_username,
.expected_password_hash = g_dashboard_password_hash
};
return auth_parse_and_verify(auth_header, &creds, error_msg, error_size);
}
int auth_check_route_basic_auth(const route_config_t *route, const char *auth_header,
char *error_msg, size_t error_size) {
if (!route || !route->use_auth) return 1;
auth_credentials_t creds = {
.expected_username = route->username,
.expected_password_hash = route->password_hash
};
return auth_parse_and_verify(auth_header, &creds, error_msg, error_size);
}

13
src/auth.h Executable file
View File

@ -0,0 +1,13 @@
#ifndef RPROXY_AUTH_H
#define RPROXY_AUTH_H
#include <stddef.h>
#include "types.h"
void auth_init(const char *username, const char *password);
int auth_is_enabled(void);
int auth_check_credentials(const char *username, const char *password);
int auth_check_basic_auth(const char *auth_header, char *error_msg, size_t error_size);
int auth_check_route_basic_auth(const route_config_t *route, const char *auth_header, char *error_msg, size_t error_size);
#endif

235
src/backup/auth.c Executable file
View File

@ -0,0 +1,235 @@
#include "auth.h"
#include "logging.h"
#include <stdlib.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <openssl/crypto.h>
static char g_dashboard_username[128] = "";
static char g_dashboard_password_hash[256] = "";
static int g_auth_enabled = 0;
static int constant_time_compare(const char *a, const char *b, size_t len) {
return CRYPTO_memcmp(a, b, len) == 0;
}
static void compute_sha256(const char *input, char *output, size_t output_size) {
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
if (!ctx) return;
unsigned char hash[EVP_MAX_MD_SIZE];
unsigned int hash_len = 0;
EVP_DigestInit_ex(ctx, EVP_sha256(), NULL);
EVP_DigestUpdate(ctx, input, strlen(input));
EVP_DigestFinal_ex(ctx, hash, &hash_len);
EVP_MD_CTX_free(ctx);
for (unsigned int i = 0; i < hash_len && (i * 2 + 2) < output_size; i++) {
snprintf(output + (i * 2), 3, "%02x", hash[i]);
}
}
void auth_init(const char *username, const char *password) {
if (!username || !password || strlen(username) == 0 || strlen(password) == 0) {
g_auth_enabled = 0;
return;
}
strncpy(g_dashboard_username, username, sizeof(g_dashboard_username) - 1);
g_dashboard_username[sizeof(g_dashboard_username) - 1] = '\0';
compute_sha256(password, g_dashboard_password_hash, sizeof(g_dashboard_password_hash));
g_auth_enabled = 1;
log_info("Dashboard authentication enabled for user: %s", username);
}
int auth_is_enabled(void) {
return g_auth_enabled;
}
int auth_check_credentials(const char *username, const char *password) {
if (!g_auth_enabled) return 1;
if (!username || !password) return 0;
char password_hash[256];
compute_sha256(password, password_hash, sizeof(password_hash));
size_t username_len = strlen(username);
size_t expected_username_len = strlen(g_dashboard_username);
int username_match = (username_len == expected_username_len) &&
constant_time_compare(username, g_dashboard_username, username_len);
int hash_match = constant_time_compare(password_hash, g_dashboard_password_hash, 64);
memset(password_hash, 0, sizeof(password_hash));
return username_match && hash_match;
}
static int base64_decode_char(char c) {
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
if (c >= '0' && c <= '9') return c - '0' + 52;
if (c == '+') return 62;
if (c == '/') return 63;
return -1;
}
static int base64_decode(const char *input, char *output, size_t output_size) {
size_t input_len = strlen(input);
size_t output_idx = 0;
for (size_t i = 0; i < input_len && output_idx < output_size - 1; i += 4) {
int v[4] = {0, 0, 0, 0};
int pad = 0;
for (int j = 0; j < 4; j++) {
if (i + j >= input_len || input[i + j] == '=') {
pad++;
v[j] = 0;
} else {
v[j] = base64_decode_char(input[i + j]);
if (v[j] < 0) return -1;
}
}
if (output_idx < output_size - 1) output[output_idx++] = (v[0] << 2) | (v[1] >> 4);
if (pad < 2 && output_idx < output_size - 1) output[output_idx++] = (v[1] << 4) | (v[2] >> 2);
if (pad < 1 && output_idx < output_size - 1) output[output_idx++] = (v[2] << 6) | v[3];
}
output[output_idx] = '\0';
return output_idx;
}
int auth_check_basic_auth(const char *auth_header, char *error_msg, size_t error_size) {
if (!g_auth_enabled) return 1;
if (!auth_header) {
if (error_msg && error_size > 0) {
strncpy(error_msg, "Authentication required", error_size - 1);
}
return 0;
}
if (strncmp(auth_header, "Basic ", 6) != 0) {
if (error_msg && error_size > 0) {
strncpy(error_msg, "Invalid authentication method", error_size - 1);
}
return 0;
}
const char *encoded = auth_header + 6;
size_t encoded_len = strlen(encoded);
if (encoded_len > 680) {
if (error_msg && error_size > 0) {
strncpy(error_msg, "Credentials too long", error_size - 1);
}
return 0;
}
char decoded[512];
int decoded_len = base64_decode(encoded, decoded, sizeof(decoded));
if (decoded_len < 0) {
if (error_msg && error_size > 0) {
strncpy(error_msg, "Invalid credentials format", error_size - 1);
}
return 0;
}
char *colon = strchr(decoded, ':');
if (!colon) {
if (error_msg && error_size > 0) {
strncpy(error_msg, "Invalid credentials format", error_size - 1);
}
return 0;
}
*colon = '\0';
const char *username = decoded;
const char *password = colon + 1;
int result = auth_check_credentials(username, password);
memset(decoded, 0, sizeof(decoded));
if (!result) {
if (error_msg && error_size > 0) {
strncpy(error_msg, "Invalid username or password", error_size - 1);
}
return 0;
}
return 1;
}
int auth_check_route_basic_auth(const route_config_t *route, const char *auth_header, char *error_msg, size_t error_size) {
if (!route || !route->use_auth) return 1;
if (!auth_header) {
if (error_msg && error_size > 0) {
strncpy(error_msg, "Authentication required", error_size - 1);
}
return 0;
}
if (strncmp(auth_header, "Basic ", 6) != 0) {
if (error_msg && error_size > 0) {
strncpy(error_msg, "Invalid authentication method", error_size - 1);
}
return 0;
}
const char *encoded = auth_header + 6;
size_t encoded_len = strlen(encoded);
if (encoded_len > 680) {
if (error_msg && error_size > 0) {
strncpy(error_msg, "Credentials too long", error_size - 1);
}
return 0;
}
char decoded[512];
int decoded_len = base64_decode(encoded, decoded, sizeof(decoded));
if (decoded_len < 0) {
if (error_msg && error_size > 0) {
strncpy(error_msg, "Invalid credentials format", error_size - 1);
}
return 0;
}
char *colon = strchr(decoded, ':');
if (!colon) {
memset(decoded, 0, sizeof(decoded));
if (error_msg && error_size > 0) {
strncpy(error_msg, "Invalid credentials format", error_size - 1);
}
return 0;
}
*colon = '\0';
const char *username = decoded;
const char *password = colon + 1;
char password_hash[256];
compute_sha256(password, password_hash, sizeof(password_hash));
size_t username_len = strlen(username);
size_t expected_username_len = strlen(route->username);
int username_match = (username_len == expected_username_len) &&
constant_time_compare(username, route->username, username_len);
int hash_match = constant_time_compare(password_hash, route->password_hash, 64);
memset(decoded, 0, sizeof(decoded));
memset(password_hash, 0, sizeof(password_hash));
if (!username_match || !hash_match) {
if (error_msg && error_size > 0) {
strncpy(error_msg, "Invalid username or password", error_size - 1);
}
return 0;
}
return 1;
}

534
src/backup/config.c Executable file
View File

@ -0,0 +1,534 @@
#include "config.h"
#include "logging.h"
#include "../cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <openssl/evp.h>
#include <sys/stat.h>
#include <stdatomic.h>
static time_t config_file_mtime = 0;
static void compute_password_hash(const char *password, char *output, size_t output_size) {
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
if (!ctx) return;
unsigned char hash[EVP_MAX_MD_SIZE];
unsigned int hash_len = 0;
EVP_DigestInit_ex(ctx, EVP_sha256(), NULL);
EVP_DigestUpdate(ctx, password, strlen(password));
EVP_DigestFinal_ex(ctx, hash, &hash_len);
EVP_MD_CTX_free(ctx);
for (unsigned int i = 0; i < hash_len && (i * 2 + 2) < output_size; i++) {
snprintf(output + (i * 2), 3, "%02x", hash[i]);
}
}
app_config_t *config = NULL;
static app_config_t *stale_configs_head = NULL;
static int is_valid_hostname(const char *hostname) {
if (!hostname || strlen(hostname) == 0 || strlen(hostname) > 253) return 0;
const char *p = hostname;
int label_len = 0;
while (*p) {
char c = *p;
if (c == '.') {
if (label_len == 0) return 0;
label_len = 0;
} else if (isalnum((unsigned char)c) || c == '-' || c == '_') {
label_len++;
if (label_len > 63) return 0;
} else {
return 0;
}
p++;
}
return 1;
}
static int is_valid_ip(const char *ip) {
if (!ip) return 0;
int dots = 0;
int num = 0;
int has_digit = 0;
while (*ip) {
if (*ip == '.') {
if (!has_digit || num > 255) return 0;
dots++;
num = 0;
has_digit = 0;
} else if (isdigit((unsigned char)*ip)) {
num = num * 10 + (*ip - '0');
has_digit = 1;
} else {
return 0;
}
ip++;
}
return dots == 3 && has_digit && num <= 255;
}
static int is_valid_host(const char *host) {
return is_valid_hostname(host) || is_valid_ip(host);
}
static char* read_file_to_string(const char *filename) {
FILE *f = fopen(filename, "rb");
if (!f) return NULL;
if (fseek(f, 0, SEEK_END) != 0) {
fclose(f);
return NULL;
}
long length = ftell(f);
if (length < 0 || length > 1024*1024) {
fclose(f);
return NULL;
}
if (fseek(f, 0, SEEK_SET) != 0) {
fclose(f);
return NULL;
}
char *buffer = malloc((size_t)length + 1);
if (!buffer) {
fclose(f);
return NULL;
}
size_t read_len = fread(buffer, 1, (size_t)length, f);
buffer[read_len] = '\0';
fclose(f);
return buffer;
}
int config_load(const char *filename) {
log_info("Loading configuration from %s", filename);
char *json_string = read_file_to_string(filename);
if (!json_string) {
log_error("Could not read config file");
return 0;
}
cJSON *root = cJSON_Parse(json_string);
if (!root) {
const char *error_ptr = cJSON_GetErrorPtr();
fprintf(stderr, "JSON parse error: %s\n", error_ptr ? error_ptr : "unknown");
free(json_string);
return 0;
}
free(json_string);
app_config_t *new_config = calloc(1, sizeof(app_config_t));
if (!new_config) {
log_error("Failed to allocate memory for new config");
cJSON_Delete(root);
return 0;
}
new_config->ref_count = 1; // Start with one reference for the global 'config' pointer
cJSON *port_item = cJSON_GetObjectItem(root, "port");
new_config->port = cJSON_IsNumber(port_item) ? port_item->valueint : 8080;
if (new_config->port < 1 || new_config->port > 65535) {
fprintf(stderr, "Invalid port number: %d\n", new_config->port);
free(new_config);
cJSON_Delete(root);
return 0;
}
cJSON *proxy_array = cJSON_GetObjectItem(root, "reverse_proxy");
if (cJSON_IsArray(proxy_array)) {
new_config->route_count = cJSON_GetArraySize(proxy_array);
if (new_config->route_count <= 0) {
free(new_config);
cJSON_Delete(root);
return 0;
}
new_config->routes = calloc(new_config->route_count, sizeof(route_config_t));
if (!new_config->routes) {
log_error("Failed to allocate memory for routes");
free(new_config);
cJSON_Delete(root);
return 0;
}
int i = 0;
cJSON *route_item;
cJSON_ArrayForEach(route_item, proxy_array) {
route_config_t *route = &new_config->routes[i];
cJSON *hostname = cJSON_GetObjectItem(route_item, "hostname");
cJSON *upstream_host = cJSON_GetObjectItem(route_item, "upstream_host");
cJSON *upstream_port = cJSON_GetObjectItem(route_item, "upstream_port");
if (!cJSON_IsString(hostname) || !cJSON_IsString(upstream_host) || !cJSON_IsNumber(upstream_port)) {
fprintf(stderr, "Invalid route configuration at index %d\n", i);
continue;
}
if (!is_valid_host(hostname->valuestring)) {
fprintf(stderr, "Invalid hostname at index %d: %s\n", i, hostname->valuestring);
continue;
}
if (!is_valid_host(upstream_host->valuestring)) {
fprintf(stderr, "Invalid upstream_host at index %d: %s\n", i, upstream_host->valuestring);
continue;
}
strncpy(route->hostname, hostname->valuestring, sizeof(route->hostname) - 1);
route->hostname[sizeof(route->hostname) - 1] = '\0';
strncpy(route->upstream_host, upstream_host->valuestring, sizeof(route->upstream_host) - 1);
route->upstream_host[sizeof(route->upstream_host) - 1] = '\0';
route->upstream_port = upstream_port->valueint;
if (route->upstream_port < 1 || route->upstream_port > 65535) {
fprintf(stderr, "Invalid upstream port for %s: %d\n", route->hostname, route->upstream_port);
continue;
}
route->use_ssl = cJSON_IsTrue(cJSON_GetObjectItem(route_item, "use_ssl"));
route->rewrite_host = cJSON_IsTrue(cJSON_GetObjectItem(route_item, "rewrite_host"));
route->use_auth = 0;
route->username[0] = '\0';
route->password_hash[0] = '\0';
cJSON *use_auth = cJSON_GetObjectItem(route_item, "use_auth");
cJSON *auth_username = cJSON_GetObjectItem(route_item, "username");
cJSON *auth_password = cJSON_GetObjectItem(route_item, "password");
if (cJSON_IsTrue(use_auth) && cJSON_IsString(auth_username) && cJSON_IsString(auth_password)) {
if (strlen(auth_username->valuestring) > 0 && strlen(auth_password->valuestring) > 0) {
route->use_auth = 1;
strncpy(route->username, auth_username->valuestring, sizeof(route->username) - 1);
route->username[sizeof(route->username) - 1] = '\0';
compute_password_hash(auth_password->valuestring, route->password_hash, sizeof(route->password_hash));
}
}
route->patches.rule_count = 0;
cJSON *patch_obj = cJSON_GetObjectItem(route_item, "patch");
if (cJSON_IsObject(patch_obj)) {
cJSON *patch_item = NULL;
cJSON_ArrayForEach(patch_item, patch_obj) {
if (route->patches.rule_count >= MAX_PATCH_RULES) {
log_info("Maximum patch rules reached for %s", route->hostname);
break;
}
if (!patch_item->string) continue;
size_t key_len = strlen(patch_item->string);
if (key_len == 0 || key_len >= MAX_PATCH_KEY_SIZE) continue;
patch_rule_t *rule = &route->patches.rules[route->patches.rule_count];
strncpy(rule->key, patch_item->string, MAX_PATCH_KEY_SIZE - 1);
rule->key[MAX_PATCH_KEY_SIZE - 1] = '\0';
rule->key_len = key_len;
if (cJSON_IsNull(patch_item)) {
rule->is_null = 1;
rule->value[0] = '\0';
rule->value_len = 0;
} else if (cJSON_IsString(patch_item)) {
rule->is_null = 0;
size_t val_len = strlen(patch_item->valuestring);
if (val_len >= MAX_PATCH_VALUE_SIZE) val_len = MAX_PATCH_VALUE_SIZE - 1;
strncpy(rule->value, patch_item->valuestring, MAX_PATCH_VALUE_SIZE - 1);
rule->value[MAX_PATCH_VALUE_SIZE - 1] = '\0';
rule->value_len = val_len;
} else {
continue;
}
route->patches.rule_count++;
}
if (route->patches.rule_count > 0) {
log_info("Loaded %d patch rules for %s", route->patches.rule_count, route->hostname);
}
}
log_info("Route configured: %s -> %s:%d (SSL: %s, Rewrite Host: %s, Auth: %s)",
route->hostname, route->upstream_host, route->upstream_port,
route->use_ssl ? "yes" : "no", route->rewrite_host ? "yes" : "no",
route->use_auth ? "yes" : "no");
i++;
}
}
cJSON_Delete(root);
if (config) {
config_ref_dec(config);
}
config = new_config;
log_info("Loaded %d routes from %s", config->route_count, filename);
return 1;
}
void config_ref_inc(app_config_t *conf) {
if (conf) {
atomic_fetch_add(&conf->ref_count, 1);
}
}
void config_ref_dec(app_config_t *conf) {
if (!conf) return;
if (atomic_fetch_sub(&conf->ref_count, 1) == 1) {
log_debug("Freeing configuration with port %d", conf->port);
if (conf->routes) {
free(conf->routes);
}
free(conf);
}
}
void config_free(void) {
if (config) {
config_ref_dec(config);
config = NULL;
}
app_config_t *current = stale_configs_head;
while (current) {
app_config_t *next = current->next;
config_ref_dec(current);
current = next;
}
stale_configs_head = NULL;
}
void config_create_default(const char *filename) {
FILE *f = fopen(filename, "r");
if (f) {
fclose(f);
return;
}
f = fopen(filename, "w");
if (!f) {
log_error("Cannot create default config file");
return;
}
fprintf(f, "{\n"
" \"port\": 8080,\n"
" \"reverse_proxy\": [\n"
" {\n"
" \"hostname\": \"localhost\",\n"
" \"upstream_host\": \"127.0.0.1\",\n"
" \"upstream_port\": 3000,\n"
" \"use_ssl\": false,\n"
" \"rewrite_host\": true\n"
" },\n"
" {\n"
" \"hostname\": \"example.com\",\n"
" \"upstream_host\": \"127.0.0.1\",\n"
" \"upstream_port\": 5000,\n"
" \"use_ssl\": false,\n"
" \"rewrite_host\": false\n"
" }\n"
" ]\n"
"}\n");
fclose(f);
log_info("Created default config file: %s", filename);
}
route_config_t *config_find_route(const char *hostname) {
if (!hostname) return NULL;
app_config_t *current_config = config;
if (!current_config) {
return NULL;
}
for (int i = 0; i < current_config->route_count; i++) {
if (strcasecmp(hostname, current_config->routes[i].hostname) == 0) {
return &current_config->routes[i];
}
}
return NULL;
}
int config_check_file_changed(const char *filename) {
struct stat st;
if (stat(filename, &st) != 0) {
return 0;
}
if (config_file_mtime == 0) {
config_file_mtime = st.st_mtime;
return 0;
}
if (st.st_mtime != config_file_mtime) {
config_file_mtime = st.st_mtime;
return 1;
}
return 0;
}
int config_hot_reload(const char *filename) {
log_info("Hot-reloading configuration from %s", filename);
app_config_t *new_config = calloc(1, sizeof(app_config_t));
if (!new_config) {
log_error("Hot-reload: Failed to allocate memory for new config");
return 0;
}
new_config->ref_count = 1;
char *json_string = read_file_to_string(filename);
if (!json_string) {
log_error("Hot-reload: Could not read config file");
free(new_config);
return 0;
}
cJSON *root = cJSON_Parse(json_string);
if (!root) {
const char *error_ptr = cJSON_GetErrorPtr();
log_error("Hot-reload: JSON parse error: %s", error_ptr ? error_ptr : "unknown");
free(json_string);
free(new_config);
return 0;
}
free(json_string);
cJSON *port_item = cJSON_GetObjectItem(root, "port");
new_config->port = cJSON_IsNumber(port_item) ? port_item->valueint : 8080;
if (new_config->port < 1 || new_config->port > 65535) {
log_error("Hot-reload: Invalid port number: %d", new_config->port);
cJSON_Delete(root);
free(new_config);
return 0;
}
cJSON *proxy_array = cJSON_GetObjectItem(root, "reverse_proxy");
if (cJSON_IsArray(proxy_array)) {
new_config->route_count = cJSON_GetArraySize(proxy_array);
if (new_config->route_count <= 0) {
cJSON_Delete(root);
free(new_config);
return 0;
}
new_config->routes = calloc(new_config->route_count, sizeof(route_config_t));
if (!new_config->routes) {
log_error("Hot-reload: Failed to allocate memory for routes");
cJSON_Delete(root);
free(new_config);
return 0;
}
int i = 0;
cJSON *route_item;
cJSON_ArrayForEach(route_item, proxy_array) {
route_config_t *route = &new_config->routes[i];
cJSON *hostname = cJSON_GetObjectItem(route_item, "hostname");
cJSON *upstream_host = cJSON_GetObjectItem(route_item, "upstream_host");
cJSON *upstream_port = cJSON_GetObjectItem(route_item, "upstream_port");
if (!cJSON_IsString(hostname) || !cJSON_IsString(upstream_host) || !cJSON_IsNumber(upstream_port)) {
continue;
}
if (!is_valid_host(hostname->valuestring) || !is_valid_host(upstream_host->valuestring)) {
continue;
}
strncpy(route->hostname, hostname->valuestring, sizeof(route->hostname) - 1);
route->hostname[sizeof(route->hostname) - 1] = '\0';
strncpy(route->upstream_host, upstream_host->valuestring, sizeof(route->upstream_host) - 1);
route->upstream_host[sizeof(route->upstream_host) - 1] = '\0';
route->upstream_port = upstream_port->valueint;
if (route->upstream_port < 1 || route->upstream_port > 65535) {
continue;
}
route->use_ssl = cJSON_IsTrue(cJSON_GetObjectItem(route_item, "use_ssl"));
route->rewrite_host = cJSON_IsTrue(cJSON_GetObjectItem(route_item, "rewrite_host"));
route->use_auth = 0;
route->username[0] = '\0';
route->password_hash[0] = '\0';
cJSON *use_auth = cJSON_GetObjectItem(route_item, "use_auth");
cJSON *auth_username = cJSON_GetObjectItem(route_item, "username");
cJSON *auth_password = cJSON_GetObjectItem(route_item, "password");
if (cJSON_IsTrue(use_auth) && cJSON_IsString(auth_username) && cJSON_IsString(auth_password)) {
if (strlen(auth_username->valuestring) > 0 && strlen(auth_password->valuestring) > 0) {
route->use_auth = 1;
strncpy(route->username, auth_username->valuestring, sizeof(route->username) - 1);
route->username[sizeof(route->username) - 1] = '\0';
compute_password_hash(auth_password->valuestring, route->password_hash, sizeof(route->password_hash));
}
}
route->patches.rule_count = 0;
cJSON *patch_obj = cJSON_GetObjectItem(route_item, "patch");
if (cJSON_IsObject(patch_obj)) {
cJSON *patch_item = NULL;
cJSON_ArrayForEach(patch_item, patch_obj) {
if (route->patches.rule_count >= MAX_PATCH_RULES) break;
if (!patch_item->string) continue;
size_t key_len = strlen(patch_item->string);
if (key_len == 0 || key_len >= MAX_PATCH_KEY_SIZE) continue;
patch_rule_t *rule = &route->patches.rules[route->patches.rule_count];
strncpy(rule->key, patch_item->string, MAX_PATCH_KEY_SIZE - 1);
rule->key[MAX_PATCH_KEY_SIZE - 1] = '\0';
rule->key_len = key_len;
if (cJSON_IsNull(patch_item)) {
rule->is_null = 1;
rule->value[0] = '\0';
rule->value_len = 0;
} else if (cJSON_IsString(patch_item)) {
rule->is_null = 0;
size_t val_len = strlen(patch_item->valuestring);
if (val_len >= MAX_PATCH_VALUE_SIZE) val_len = MAX_PATCH_VALUE_SIZE - 1;
strncpy(rule->value, patch_item->valuestring, MAX_PATCH_VALUE_SIZE - 1);
rule->value[MAX_PATCH_VALUE_SIZE - 1] = '\0';
rule->value_len = val_len;
} else {
continue;
}
route->patches.rule_count++;
}
}
log_info("Hot-reload route: %s -> %s:%d (SSL: %s, Auth: %s, Patches: %d)",
route->hostname, route->upstream_host, route->upstream_port,
route->use_ssl ? "yes" : "no", route->use_auth ? "yes" : "no",
route->patches.rule_count);
i++;
}
new_config->route_count = i;
}
cJSON_Delete(root);
app_config_t *old_config = config;
config = new_config;
if (old_config) {
old_config->next = stale_configs_head;
stale_configs_head = old_config;
}
log_info("Hot-reload complete: %d routes loaded", new_config->route_count);
return 1;
}

1381
src/backup/connection.c Executable file

File diff suppressed because it is too large Load Diff

813
src/backup/monitor.c Executable file
View File

@ -0,0 +1,813 @@
#include "monitor.h"
#include "logging.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/sysinfo.h>
#include <math.h>
system_monitor_t monitor;
void history_deque_init(history_deque_t *dq, int capacity) {
dq->points = calloc(capacity, sizeof(history_point_t));
dq->capacity = capacity;
dq->head = 0;
dq->count = 0;
}
void history_deque_push(history_deque_t *dq, double time, double value) {
if (!dq || !dq->points) return;
dq->points[dq->head] = (history_point_t){ .time = time, .value = value };
dq->head = (dq->head + 1) % dq->capacity;
if (dq->count < dq->capacity) dq->count++;
}
void network_history_deque_init(network_history_deque_t *dq, int capacity) {
dq->points = calloc(capacity, sizeof(network_history_point_t));
dq->capacity = capacity;
dq->head = 0;
dq->count = 0;
}
void network_history_deque_push(network_history_deque_t *dq, double time, double rx, double tx) {
if (!dq || !dq->points) return;
dq->points[dq->head] = (network_history_point_t){ .time = time, .rx_kbps = rx, .tx_kbps = tx };
dq->head = (dq->head + 1) % dq->capacity;
if (dq->count < dq->capacity) dq->count++;
}
void disk_history_deque_init(disk_history_deque_t *dq, int capacity) {
dq->points = calloc(capacity, sizeof(disk_history_point_t));
dq->capacity = capacity;
dq->head = 0;
dq->count = 0;
}
void disk_history_deque_push(disk_history_deque_t *dq, double time, double read_mbps, double write_mbps) {
if (!dq || !dq->points) return;
dq->points[dq->head] = (disk_history_point_t){ .time = time, .read_mbps = read_mbps, .write_mbps = write_mbps };
dq->head = (dq->head + 1) % dq->capacity;
if (dq->count < dq->capacity) dq->count++;
}
void request_time_deque_init(request_time_deque_t *dq, int capacity) {
dq->times = calloc(capacity, sizeof(double));
dq->capacity = capacity;
dq->head = 0;
dq->count = 0;
}
void request_time_deque_push(request_time_deque_t *dq, double time_ms) {
if (!dq || !dq->times) return;
dq->times[dq->head] = time_ms;
dq->head = (dq->head + 1) % dq->capacity;
if (dq->count < dq->capacity) dq->count++;
}
#define DATA_RETENTION_SECONDS (24 * 60 * 60)
static void init_db(void) {
if (!monitor.db) return;
char *err_msg = 0;
sqlite3_exec(monitor.db, "PRAGMA journal_mode=WAL;", 0, 0, NULL);
sqlite3_exec(monitor.db, "PRAGMA synchronous=NORMAL;", 0, 0, NULL);
const char *sql_create_stats =
"CREATE TABLE IF NOT EXISTS vhost_stats ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" vhost TEXT NOT NULL,"
" timestamp REAL NOT NULL,"
" http_requests INTEGER DEFAULT 0,"
" websocket_requests INTEGER DEFAULT 0,"
" total_requests INTEGER DEFAULT 0,"
" bytes_sent INTEGER DEFAULT 0,"
" bytes_recv INTEGER DEFAULT 0,"
" avg_request_time_ms REAL DEFAULT 0,"
" UNIQUE(vhost, timestamp)"
");";
const char *sql_create_totals =
"CREATE TABLE IF NOT EXISTS vhost_totals ("
" vhost TEXT PRIMARY KEY,"
" http_requests INTEGER DEFAULT 0,"
" websocket_requests INTEGER DEFAULT 0,"
" total_requests INTEGER DEFAULT 0,"
" bytes_sent INTEGER DEFAULT 0,"
" bytes_recv INTEGER DEFAULT 0"
");";
const char *sql_idx_vhost_ts = "CREATE INDEX IF NOT EXISTS idx_vhost_timestamp ON vhost_stats(vhost, timestamp);";
const char *sql_idx_ts = "CREATE INDEX IF NOT EXISTS idx_timestamp ON vhost_stats(timestamp);";
if (sqlite3_exec(monitor.db, sql_create_stats, 0, 0, &err_msg) != SQLITE_OK) {
fprintf(stderr, "SQL error: %s\n", err_msg);
sqlite3_free(err_msg);
}
if (sqlite3_exec(monitor.db, sql_create_totals, 0, 0, &err_msg) != SQLITE_OK) {
fprintf(stderr, "SQL error: %s\n", err_msg);
sqlite3_free(err_msg);
}
if (sqlite3_exec(monitor.db, sql_idx_vhost_ts, 0, 0, &err_msg) != SQLITE_OK) {
fprintf(stderr, "SQL error: %s\n", err_msg);
sqlite3_free(err_msg);
}
if (sqlite3_exec(monitor.db, sql_idx_ts, 0, 0, &err_msg) != SQLITE_OK) {
fprintf(stderr, "SQL error: %s\n", err_msg);
sqlite3_free(err_msg);
}
}
static void load_stats_from_db(void) {
if (!monitor.db) return;
sqlite3_stmt *res;
const char *sql =
"SELECT vhost, http_requests, websocket_requests, total_requests, "
"bytes_sent, bytes_recv FROM vhost_totals";
if (sqlite3_prepare_v2(monitor.db, sql, -1, &res, 0) != SQLITE_OK) {
fprintf(stderr, "Failed to execute statement: %s\n", sqlite3_errmsg(monitor.db));
return;
}
int vhost_count = 0;
while (sqlite3_step(res) == SQLITE_ROW) {
vhost_stats_t *stats = monitor_get_or_create_vhost_stats((const char*)sqlite3_column_text(res, 0));
if (stats) {
stats->http_requests = sqlite3_column_int64(res, 1);
stats->websocket_requests = sqlite3_column_int64(res, 2);
stats->total_requests = sqlite3_column_int64(res, 3);
stats->bytes_sent = sqlite3_column_int64(res, 4);
stats->bytes_recv = sqlite3_column_int64(res, 5);
vhost_count++;
}
}
sqlite3_finalize(res);
log_info("Loaded statistics for %d vhosts from database", vhost_count);
}
void monitor_init(const char *db_file) {
memset(&monitor, 0, sizeof(system_monitor_t));
monitor.start_time = time(NULL);
monitor.uptime_start = time(NULL);
monitor.health_score = 100.0;
history_deque_init(&monitor.cpu_history, HISTORY_SECONDS);
history_deque_init(&monitor.memory_history, HISTORY_SECONDS);
network_history_deque_init(&monitor.network_history, HISTORY_SECONDS);
disk_history_deque_init(&monitor.disk_history, HISTORY_SECONDS);
history_deque_init(&monitor.throughput_history, HISTORY_SECONDS);
history_deque_init(&monitor.load1_history, HISTORY_SECONDS);
history_deque_init(&monitor.load5_history, HISTORY_SECONDS);
history_deque_init(&monitor.load15_history, HISTORY_SECONDS);
histogram_init(&monitor.global_latency);
histogram_init(&monitor.connection_lifetime);
rate_tracker_init(&monitor.global_rps);
if (sqlite3_open(db_file, &monitor.db) != SQLITE_OK) {
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(monitor.db));
if (monitor.db) {
sqlite3_close(monitor.db);
monitor.db = NULL;
}
} else {
init_db();
load_stats_from_db();
}
monitor_update();
}
void monitor_cleanup(void) {
if (monitor.db) {
sqlite3_close(monitor.db);
monitor.db = NULL;
}
vhost_stats_t *current = monitor.vhost_stats_head;
while (current) {
vhost_stats_t *next = current->next;
if (current->throughput_history.points) free(current->throughput_history.points);
if (current->request_times.times) free(current->request_times.times);
free(current);
current = next;
}
monitor.vhost_stats_head = NULL;
if (monitor.cpu_history.points) { free(monitor.cpu_history.points); monitor.cpu_history.points = NULL; }
if (monitor.memory_history.points) { free(monitor.memory_history.points); monitor.memory_history.points = NULL; }
if (monitor.network_history.points) { free(monitor.network_history.points); monitor.network_history.points = NULL; }
if (monitor.disk_history.points) { free(monitor.disk_history.points); monitor.disk_history.points = NULL; }
if (monitor.throughput_history.points) { free(monitor.throughput_history.points); monitor.throughput_history.points = NULL; }
if (monitor.load1_history.points) { free(monitor.load1_history.points); monitor.load1_history.points = NULL; }
if (monitor.load5_history.points) { free(monitor.load5_history.points); monitor.load5_history.points = NULL; }
if (monitor.load15_history.points) { free(monitor.load15_history.points); monitor.load15_history.points = NULL; }
}
static void cleanup_old_stats(void) {
if (!monitor.db) return;
double cutoff = (double)time(NULL) - DATA_RETENTION_SECONDS;
sqlite3_stmt *stmt;
const char *sql = "DELETE FROM vhost_stats WHERE timestamp < ?;";
if (sqlite3_prepare_v2(monitor.db, sql, -1, &stmt, NULL) != SQLITE_OK) return;
sqlite3_bind_double(stmt, 1, cutoff);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
}
static void save_stats_to_db(void) {
if (!monitor.db) return;
sqlite3_exec(monitor.db, "BEGIN TRANSACTION;", 0, 0, NULL);
sqlite3_stmt *stmt_totals;
const char *sql_totals =
"INSERT OR REPLACE INTO vhost_totals "
"(vhost, http_requests, websocket_requests, total_requests, bytes_sent, bytes_recv) "
"VALUES (?, ?, ?, ?, ?, ?);";
sqlite3_stmt *stmt_stats;
const char *sql_stats =
"INSERT OR REPLACE INTO vhost_stats "
"(vhost, timestamp, http_requests, websocket_requests, total_requests, "
"bytes_sent, bytes_recv, avg_request_time_ms) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?);";
if (sqlite3_prepare_v2(monitor.db, sql_totals, -1, &stmt_totals, NULL) != SQLITE_OK) {
sqlite3_exec(monitor.db, "ROLLBACK;", 0, 0, NULL);
return;
}
if (sqlite3_prepare_v2(monitor.db, sql_stats, -1, &stmt_stats, NULL) != SQLITE_OK) {
sqlite3_finalize(stmt_totals);
sqlite3_exec(monitor.db, "ROLLBACK;", 0, 0, NULL);
return;
}
double current_time = (double)time(NULL);
for (vhost_stats_t *s = monitor.vhost_stats_head; s != NULL; s = s->next) {
if (s->request_times.count > 0) {
double total_time = 0;
for(int i = 0; i < s->request_times.count; i++) {
total_time += s->request_times.times[i];
}
s->avg_request_time_ms = total_time / s->request_times.count;
}
sqlite3_bind_text(stmt_totals, 1, s->vhost_name, -1, SQLITE_STATIC);
sqlite3_bind_int64(stmt_totals, 2, s->http_requests);
sqlite3_bind_int64(stmt_totals, 3, s->websocket_requests);
sqlite3_bind_int64(stmt_totals, 4, s->total_requests);
sqlite3_bind_int64(stmt_totals, 5, s->bytes_sent);
sqlite3_bind_int64(stmt_totals, 6, s->bytes_recv);
sqlite3_step(stmt_totals);
sqlite3_reset(stmt_totals);
sqlite3_bind_text(stmt_stats, 1, s->vhost_name, -1, SQLITE_STATIC);
sqlite3_bind_double(stmt_stats, 2, current_time);
sqlite3_bind_int64(stmt_stats, 3, s->http_requests);
sqlite3_bind_int64(stmt_stats, 4, s->websocket_requests);
sqlite3_bind_int64(stmt_stats, 5, s->total_requests);
sqlite3_bind_int64(stmt_stats, 6, s->bytes_sent);
sqlite3_bind_int64(stmt_stats, 7, s->bytes_recv);
sqlite3_bind_double(stmt_stats, 8, s->avg_request_time_ms);
sqlite3_step(stmt_stats);
sqlite3_reset(stmt_stats);
}
sqlite3_finalize(stmt_totals);
sqlite3_finalize(stmt_stats);
sqlite3_exec(monitor.db, "COMMIT;", 0, 0, NULL);
static time_t last_cleanup = 0;
if (current_time - last_cleanup >= 3600) {
cleanup_old_stats();
last_cleanup = current_time;
}
}
static double get_cpu_usage(void) {
static long long prev_user = 0, prev_nice = 0, prev_system = 0, prev_idle = 0;
long long user, nice, system, idle, iowait, irq, softirq;
FILE *f = fopen("/proc/stat", "r");
if (!f) return 0.0;
if (fscanf(f, "cpu %lld %lld %lld %lld %lld %lld %lld",
&user, &nice, &system, &idle, &iowait, &irq, &softirq) != 7) {
fclose(f);
return 0.0;
}
fclose(f);
long long prev_total = prev_user + prev_nice + prev_system + prev_idle;
long long total = user + nice + system + idle;
long long totald = total - prev_total;
long long idled = idle - prev_idle;
prev_user = user; prev_nice = nice; prev_system = system; prev_idle = idle;
return totald == 0 ? 0.0 : (double)(totald - idled) * 100.0 / totald;
}
static void get_memory_usage(double *used_gb) {
struct sysinfo info;
if (sysinfo(&info) != 0) {
*used_gb = 0;
return;
}
*used_gb = (double)(info.totalram - info.freeram - info.bufferram) * info.mem_unit / (1024.0 * 1024.0 * 1024.0);
}
static void get_network_stats(long long *bytes_sent, long long *bytes_recv) {
FILE *f = fopen("/proc/net/dev", "r");
if (!f) {
*bytes_sent = 0;
*bytes_recv = 0;
return;
}
char line[256];
if (!fgets(line, sizeof(line), f) || !fgets(line, sizeof(line), f)) {
fclose(f);
*bytes_sent = 0;
*bytes_recv = 0;
return;
}
long long total_recv = 0, total_sent = 0;
while (fgets(line, sizeof(line), f)) {
char iface[32];
long long r, t;
if (sscanf(line, "%31[^:]: %lld %*d %*d %*d %*d %*d %*d %*d %lld", iface, &r, &t) == 3) {
char *trimmed = iface;
while (*trimmed == ' ') trimmed++;
if (strcmp(trimmed, "lo") != 0) {
total_recv += r;
total_sent += t;
}
}
}
fclose(f);
*bytes_sent = total_sent;
*bytes_recv = total_recv;
}
static void get_disk_stats(long long *sectors_read, long long *sectors_written) {
FILE *f = fopen("/proc/diskstats", "r");
if (!f) {
*sectors_read = 0;
*sectors_written = 0;
return;
}
char line[2048];
long long total_read = 0, total_written = 0;
while (fgets(line, sizeof(line), f)) {
char device[64];
long long sectors_r = 0, sectors_w = 0;
int nfields = 0;
char major[16], minor[16], dev[64];
char rc[32], rm[32], sr[32], rtm[32], rtm2[32], wc[32], wm[32], sw[32];
nfields = sscanf(line, "%15s %15s %63s %31s %31s %31s %31s %31s %31s %31s %31s %31s",
major, minor, dev, rc, rm, sr, rtm, rtm2, wc, wm, sw, sw);
if (nfields >= 11) {
strncpy(device, dev, sizeof(device)-1);
device[sizeof(device)-1] = '\0';
char *endptr;
sectors_r = strtoll(sr, &endptr, 10);
if (endptr == sr) sectors_r = 0;
sectors_w = strtoll(sw, &endptr, 10);
if (endptr == sw) sectors_w = 0;
if (strncmp(device, "loop", 4) != 0 && strncmp(device, "ram", 3) != 0) {
int len = strlen(device);
if ((strncmp(device, "sd", 2) == 0 && len == 3) ||
(strncmp(device, "nvme", 4) == 0 && strstr(device, "n1p") == NULL) ||
(strncmp(device, "vd", 2) == 0 && len == 3) ||
(strncmp(device, "hd", 2) == 0 && len == 3)) {
total_read += sectors_r;
total_written += sectors_w;
}
}
}
}
fclose(f);
*sectors_read = total_read;
*sectors_written = total_written;
}
static void get_load_averages(double *load1, double *load5, double *load15) {
FILE *f = fopen("/proc/loadavg", "r");
if (!f) {
*load1 = *load5 = *load15 = 0.0;
return;
}
if (fscanf(f, "%lf %lf %lf", load1, load5, load15) != 3) {
*load1 = *load5 = *load15 = 0.0;
}
fclose(f);
}
void monitor_update(void) {
double current_time = time(NULL);
history_deque_push(&monitor.cpu_history, current_time, get_cpu_usage());
double mem_used_gb;
get_memory_usage(&mem_used_gb);
history_deque_push(&monitor.memory_history, current_time, mem_used_gb);
long long net_sent, net_recv;
get_network_stats(&net_sent, &net_recv);
double time_delta = current_time - monitor.last_net_update_time;
if (time_delta > 0 && monitor.last_net_update_time > 0) {
double rx = (net_recv - monitor.last_net_recv) / time_delta / 1024.0;
double tx = (net_sent - monitor.last_net_sent) / time_delta / 1024.0;
network_history_deque_push(&monitor.network_history, current_time, fmax(0, rx), fmax(0, tx));
history_deque_push(&monitor.throughput_history, current_time, fmax(0, rx + tx));
}
monitor.last_net_sent = net_sent;
monitor.last_net_recv = net_recv;
monitor.last_net_update_time = current_time;
long long disk_read, disk_write;
get_disk_stats(&disk_read, &disk_write);
double disk_time_delta = current_time - monitor.last_disk_update_time;
if (disk_time_delta > 0 && monitor.last_disk_update_time > 0) {
double read_mbps = (disk_read - monitor.last_disk_read) * 512.0 / disk_time_delta / (1024.0 * 1024.0);
double write_mbps = (disk_write - monitor.last_disk_write) * 512.0 / disk_time_delta / (1024.0 * 1024.0);
disk_history_deque_push(&monitor.disk_history, current_time, fmax(0, read_mbps), fmax(0, write_mbps));
}
monitor.last_disk_read = disk_read;
monitor.last_disk_write = disk_write;
monitor.last_disk_update_time = current_time;
double load1, load5, load15;
get_load_averages(&load1, &load5, &load15);
history_deque_push(&monitor.load1_history, current_time, load1);
history_deque_push(&monitor.load5_history, current_time, load5);
history_deque_push(&monitor.load15_history, current_time, load15);
for (vhost_stats_t *s = monitor.vhost_stats_head; s != NULL; s = s->next) {
double vhost_delta = current_time - s->last_update;
if (vhost_delta >= 1.0) {
double kbps = 0;
if (s->last_update > 0) {
long long bytes_diff = (s->bytes_sent - s->last_bytes_sent) + (s->bytes_recv - s->last_bytes_recv);
kbps = bytes_diff / vhost_delta / 1024.0;
}
history_deque_push(&s->throughput_history, current_time, fmax(0, kbps));
s->last_bytes_sent = s->bytes_sent;
s->last_bytes_recv = s->bytes_recv;
s->last_update = current_time;
}
}
static time_t last_db_save = 0;
if (current_time - last_db_save >= 10) {
save_stats_to_db();
last_db_save = current_time;
}
}
vhost_stats_t* monitor_get_or_create_vhost_stats(const char *vhost_name) {
if (!vhost_name || strlen(vhost_name) == 0) return NULL;
for (vhost_stats_t *curr = monitor.vhost_stats_head; curr; curr = curr->next) {
if (strcmp(curr->vhost_name, vhost_name) == 0) {
return curr;
}
}
vhost_stats_t *new_stats = calloc(1, sizeof(vhost_stats_t));
if (!new_stats) {
return NULL;
}
strncpy(new_stats->vhost_name, vhost_name, sizeof(new_stats->vhost_name) - 1);
new_stats->last_update = time(NULL);
history_deque_init(&new_stats->throughput_history, 60);
request_time_deque_init(&new_stats->request_times, 100);
histogram_init(&new_stats->latency_histogram);
histogram_init(&new_stats->request_size_histogram);
histogram_init(&new_stats->response_size_histogram);
histogram_init(&new_stats->ttfb_histogram);
histogram_init(&new_stats->upstream_connect_latency);
rate_tracker_init(&new_stats->requests_per_second);
new_stats->next = monitor.vhost_stats_head;
monitor.vhost_stats_head = new_stats;
return new_stats;
}
void monitor_record_request_start(vhost_stats_t *stats, int is_websocket) {
if (!stats) return;
if (is_websocket) {
stats->websocket_requests++;
} else {
stats->http_requests++;
}
stats->total_requests++;
rate_tracker_increment(&stats->requests_per_second);
rate_tracker_increment(&monitor.global_rps);
}
void monitor_record_request_end(vhost_stats_t *stats, double start_time) {
if (!stats || start_time <= 0) return;
struct timespec end_time;
clock_gettime(CLOCK_MONOTONIC, &end_time);
double duration_ms = ((end_time.tv_sec + end_time.tv_nsec / 1e9) - start_time) * 1000.0;
if (duration_ms >= 0 && duration_ms < 60000) {
request_time_deque_push(&stats->request_times, duration_ms);
histogram_add(&stats->latency_histogram, duration_ms);
histogram_add(&monitor.global_latency, duration_ms);
}
}
void monitor_record_bytes(vhost_stats_t *stats, long long sent, long long recv) {
if (!stats) return;
stats->bytes_sent += sent;
stats->bytes_recv += recv;
}
const double LATENCY_BUCKET_BOUNDS[HISTOGRAM_BUCKETS] = {
1.0, 2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0,
500.0, 1000.0, 2500.0, 5000.0, 10000.0, 30000.0, 60000.0, 1e9
};
const char* LATENCY_BUCKET_LABELS[HISTOGRAM_BUCKETS] = {
"0-1ms", "1-2ms", "2-5ms", "5-10ms", "10-25ms", "25-50ms", "50-100ms", "100-250ms",
"250-500ms", "500ms-1s", "1-2.5s", "2.5-5s", "5-10s", "10-30s", "30-60s", "60s+"
};
const double SIZE_BUCKET_BOUNDS[HISTOGRAM_BUCKETS] = {
128.0, 512.0, 1024.0, 4096.0, 16384.0, 65536.0, 262144.0, 1048576.0,
4194304.0, 16777216.0, 67108864.0, 268435456.0, 1073741824.0, 4294967296.0, 1e15, 1e18
};
void histogram_init(histogram_t *h) {
if (!h) return;
memset(h, 0, sizeof(histogram_t));
h->min_value = 1e18;
h->max_value = -1e18;
}
void histogram_add(histogram_t *h, double value) {
if (!h) return;
h->total_count++;
h->sum += value;
if (value < h->min_value) h->min_value = value;
if (value > h->max_value) h->max_value = value;
for (int i = 0; i < HISTOGRAM_BUCKETS; i++) {
if (value <= LATENCY_BUCKET_BOUNDS[i]) {
h->buckets[i]++;
return;
}
}
h->overflow++;
}
static void histogram_add_with_bounds(histogram_t *h, double value, const double *bounds) {
if (!h) return;
h->total_count++;
h->sum += value;
if (value < h->min_value) h->min_value = value;
if (value > h->max_value) h->max_value = value;
for (int i = 0; i < HISTOGRAM_BUCKETS; i++) {
if (value <= bounds[i]) {
h->buckets[i]++;
return;
}
}
h->overflow++;
}
double histogram_percentile(histogram_t *h, double p) {
if (!h || h->total_count == 0) return 0.0;
uint64_t target = (uint64_t)(h->total_count * p);
uint64_t cumulative = 0;
for (int i = 0; i < HISTOGRAM_BUCKETS; i++) {
cumulative += h->buckets[i];
if (cumulative >= target) {
return LATENCY_BUCKET_BOUNDS[i];
}
}
return LATENCY_BUCKET_BOUNDS[HISTOGRAM_BUCKETS - 1];
}
double histogram_mean(histogram_t *h) {
if (!h || h->total_count == 0) return 0.0;
return h->sum / h->total_count;
}
void rate_tracker_init(rate_tracker_t *rt) {
if (!rt) return;
memset(rt, 0, sizeof(rate_tracker_t));
rt->slot_start = time(NULL);
}
void rate_tracker_increment(rate_tracker_t *rt) {
if (!rt) return;
time_t now = time(NULL);
int slot = now % RATE_TRACKER_SLOTS;
if (now != rt->slot_start) {
int slots_to_clear = (int)(now - rt->slot_start);
if (slots_to_clear >= RATE_TRACKER_SLOTS) {
memset(rt->counts, 0, sizeof(rt->counts));
} else {
for (int i = 1; i <= slots_to_clear; i++) {
int clear_slot = (rt->current_slot + i) % RATE_TRACKER_SLOTS;
rt->counts[clear_slot] = 0;
}
}
rt->current_slot = slot;
rt->slot_start = now;
}
rt->counts[slot]++;
}
uint32_t rate_tracker_get_rps(rate_tracker_t *rt) {
if (!rt) return 0;
time_t now = time(NULL);
int prev_slot = (now - 1) % RATE_TRACKER_SLOTS;
if ((now - rt->slot_start) > RATE_TRACKER_SLOTS) return 0;
return rt->counts[prev_slot];
}
uint32_t rate_tracker_get_total_last_minute(rate_tracker_t *rt) {
if (!rt) return 0;
time_t now = time(NULL);
if ((now - rt->slot_start) > RATE_TRACKER_SLOTS) {
return 0;
}
uint32_t total = 0;
for (int i = 0; i < RATE_TRACKER_SLOTS; i++) {
total += rt->counts[i];
}
return total;
}
http_method_t http_method_from_string(const char *method) {
if (!method) return HTTP_METHOD_OTHER;
if (strcmp(method, "GET") == 0) return HTTP_METHOD_GET;
if (strcmp(method, "POST") == 0) return HTTP_METHOD_POST;
if (strcmp(method, "PUT") == 0) return HTTP_METHOD_PUT;
if (strcmp(method, "DELETE") == 0) return HTTP_METHOD_DELETE;
if (strcmp(method, "PATCH") == 0) return HTTP_METHOD_PATCH;
if (strcmp(method, "HEAD") == 0) return HTTP_METHOD_HEAD;
if (strcmp(method, "OPTIONS") == 0) return HTTP_METHOD_OPTIONS;
return HTTP_METHOD_OTHER;
}
void monitor_record_method(vhost_stats_t *stats, http_method_t method) {
if (!stats || method >= HTTP_METHOD_COUNT) return;
stats->method_counts.counts[method]++;
}
void monitor_record_status(vhost_stats_t *stats, int status_code) {
if (!stats) return;
if (status_code >= 100 && status_code < 200) {
stats->status_counts.status_1xx++;
} else if (status_code >= 200 && status_code < 300) {
stats->status_counts.status_2xx++;
} else if (status_code >= 300 && status_code < 400) {
stats->status_counts.status_3xx++;
} else if (status_code >= 400 && status_code < 500) {
stats->status_counts.status_4xx++;
} else if (status_code >= 500 && status_code < 600) {
stats->status_counts.status_5xx++;
} else {
stats->status_counts.status_unknown++;
}
}
void monitor_record_request_size(vhost_stats_t *stats, long size) {
if (!stats || size < 0) return;
histogram_add_with_bounds(&stats->request_size_histogram, (double)size, SIZE_BUCKET_BOUNDS);
}
void monitor_record_response_size(vhost_stats_t *stats, long size) {
if (!stats || size < 0) return;
histogram_add_with_bounds(&stats->response_size_histogram, (double)size, SIZE_BUCKET_BOUNDS);
stats->response_bytes_total += size;
}
void monitor_record_ttfb(vhost_stats_t *stats, double ttfb_ms) {
if (!stats || ttfb_ms < 0) return;
histogram_add(&stats->ttfb_histogram, ttfb_ms);
}
void monitor_record_upstream_connect(vhost_stats_t *stats, int success, double latency_ms) {
if (!stats) return;
if (success) {
stats->upstream_connect_success++;
if (latency_ms >= 0) {
histogram_add(&stats->upstream_connect_latency, latency_ms);
}
} else {
stats->upstream_connect_failures++;
}
}
void monitor_record_splice_transfer(vhost_stats_t *stats, long long bytes) {
if (!stats) return;
stats->splice_transfers++;
stats->bytes_via_splice += bytes;
}
void monitor_record_buffer_transfer(vhost_stats_t *stats, long long bytes) {
if (!stats) return;
stats->buffered_transfers++;
stats->bytes_via_buffer += bytes;
}
void monitor_record_connection_opened(vhost_stats_t *stats) {
if (!stats) return;
stats->connections_opened++;
monitor.total_connections_accepted++;
}
void monitor_record_connection_closed(vhost_stats_t *stats) {
if (!stats) return;
stats->connections_closed++;
}
void monitor_record_keepalive_reuse(vhost_stats_t *stats) {
if (!stats) return;
stats->keep_alive_reused++;
}
void monitor_record_error(vhost_stats_t *stats, int error_type) {
if (!stats) return;
switch (error_type) {
case ERROR_TYPE_DNS:
stats->dns_failures++;
break;
case ERROR_TYPE_SSL:
stats->ssl_failures++;
break;
case ERROR_TYPE_TIMEOUT:
stats->timeout_errors++;
break;
case ERROR_TYPE_CONNECTION:
stats->connection_errors++;
break;
}
}
void monitor_compute_health_score(void) {
uint64_t total_requests = 0;
uint64_t total_errors = 0;
uint64_t total_upstream_failures = 0;
uint64_t total_upstream_attempts = 0;
uint64_t total_timeouts = 0;
for (vhost_stats_t *s = monitor.vhost_stats_head; s != NULL; s = s->next) {
total_requests += s->total_requests;
total_errors += s->status_counts.status_5xx;
total_upstream_failures += s->upstream_connect_failures;
total_upstream_attempts += s->upstream_connect_success + s->upstream_connect_failures;
total_timeouts += s->timeout_errors;
}
double error_rate = total_requests > 0 ? (double)total_errors / total_requests : 0;
double upstream_fail_rate = total_upstream_attempts > 0 ? (double)total_upstream_failures / total_upstream_attempts : 0;
double timeout_rate = total_requests > 0 ? (double)total_timeouts / total_requests : 0;
monitor.health_score = 100.0 * (1.0 - (error_rate * 0.5 + upstream_fail_rate * 0.3 + timeout_rate * 0.2));
if (monitor.health_score < 0) monitor.health_score = 0;
if (monitor.health_score > 100) monitor.health_score = 100;
monitor.error_rate_1m = error_rate;
}
void monitor_update_connection_states(void) {
memset(monitor.connections_by_state, 0, sizeof(monitor.connections_by_state));
}
double monitor_get_current_rps(void) {
uint32_t total_rps = 0;
for (vhost_stats_t *s = monitor.vhost_stats_head; s != NULL; s = s->next) {
total_rps += rate_tracker_get_rps(&s->requests_per_second);
}
double rps = (double)total_rps;
if (rps > monitor.peak_rps) {
monitor.peak_rps = rps;
monitor.peak_rps_time = time(NULL);
}
return rps;
}

42
src/base64.c Normal file
View File

@ -0,0 +1,42 @@
// retoor <retoor@molodetz.nl>
#include "base64.h"
#include <string.h>
static int base64_decode_char(char c) {
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
if (c >= '0' && c <= '9') return c - '0' + 52;
if (c == '+') return 62;
if (c == '/') return 63;
return -1;
}
int base64_decode(const char *input, char *output, size_t output_size) {
if (!input || !output || output_size == 0) return -1;
size_t input_len = strlen(input);
size_t output_idx = 0;
for (size_t i = 0; i < input_len && output_idx < output_size - 1; i += 4) {
int v[4] = {0, 0, 0, 0};
int pad = 0;
for (int j = 0; j < 4; j++) {
if (i + (size_t)j >= input_len || input[i + j] == '=') {
pad++;
v[j] = 0;
} else {
v[j] = base64_decode_char(input[i + j]);
if (v[j] < 0) return -1;
}
}
if (output_idx < output_size - 1) output[output_idx++] = (char)((v[0] << 2) | (v[1] >> 4));
if (pad < 2 && output_idx < output_size - 1) output[output_idx++] = (char)((v[1] << 4) | (v[2] >> 2));
if (pad < 1 && output_idx < output_size - 1) output[output_idx++] = (char)((v[2] << 6) | v[3]);
}
output[output_idx] = '\0';
return (int)output_idx;
}

10
src/base64.h Normal file
View File

@ -0,0 +1,10 @@
// retoor <retoor@molodetz.nl>
#ifndef RPROXY_BASE64_H
#define RPROXY_BASE64_H
#include <stddef.h>
int base64_decode(const char *input, char *output, size_t output_size);
#endif

100
src/buffer.c Executable file
View File

@ -0,0 +1,100 @@
#include "buffer.h"
#include "logging.h"
#include "types.h"
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
int buffer_init(buffer_t *buf, size_t capacity) {
if (!buf) return -1;
buf->data = malloc(capacity);
if (!buf->data) {
log_error("Failed to allocate buffer");
return -1;
}
buf->capacity = capacity;
buf->head = 0;
buf->tail = 0;
return 0;
}
void buffer_free(buffer_t *buf) {
if (!buf) return;
if (buf->data) {
free(buf->data);
buf->data = NULL;
}
buf->capacity = 0;
buf->head = 0;
buf->tail = 0;
}
size_t buffer_available_read(buffer_t *buf) {
if (!buf) return 0;
return buf->tail - buf->head;
}
size_t buffer_available_write(buffer_t *buf) {
if (!buf) return 0;
return buf->capacity - buf->tail;
}
int buffer_ensure_capacity(buffer_t *buf, size_t required) {
if (!buf) return -1;
if (buf->capacity >= required) return 0;
if (required > MAX_BUFFER_SIZE) {
log_error("Buffer size limit exceeded: requested %zu, max %d", required, MAX_BUFFER_SIZE);
return -1;
}
size_t new_capacity = buf->capacity;
while (new_capacity < required) {
if (new_capacity > SIZE_MAX / 2) {
log_error("Buffer size overflow");
return -1;
}
new_capacity *= 2;
if (new_capacity > MAX_BUFFER_SIZE) {
new_capacity = MAX_BUFFER_SIZE;
break;
}
}
if (new_capacity < required) {
log_error("Cannot satisfy buffer capacity requirement");
return -1;
}
char *new_data = realloc(buf->data, new_capacity);
if (!new_data) {
log_error("Failed to reallocate buffer");
return -1;
}
buf->data = new_data;
buf->capacity = new_capacity;
return 0;
}
void buffer_compact(buffer_t *buf) {
if (!buf || buf->head == 0) return;
size_t len = buf->tail - buf->head;
if (len > 0) {
memmove(buf->data, buf->data + buf->head, len);
}
buf->head = 0;
buf->tail = len;
}
void buffer_consume(buffer_t *buf, size_t bytes) {
if (!buf) return;
size_t available = buf->tail - buf->head;
if (bytes > available) {
bytes = available;
}
buf->head += bytes;
if (buf->head >= buf->tail) {
buf->head = 0;
buf->tail = 0;
}
}

14
src/buffer.h Executable file
View File

@ -0,0 +1,14 @@
#ifndef RPROXY_BUFFER_H
#define RPROXY_BUFFER_H
#include "types.h"
int buffer_init(buffer_t *buf, size_t capacity);
void buffer_free(buffer_t *buf);
size_t buffer_available_read(buffer_t *buf);
size_t buffer_available_write(buffer_t *buf);
int buffer_ensure_capacity(buffer_t *buf, size_t required);
void buffer_compact(buffer_t *buf);
void buffer_consume(buffer_t *buf, size_t bytes);
#endif

141
src/client_handler.c Normal file
View File

@ -0,0 +1,141 @@
// retoor <retoor@molodetz.nl>
#include "client_handler.h"
#include "buffer.h"
#include "http.h"
#include "http_response.h"
#include "config.h"
#include "rate_limit.h"
#include "auth.h"
#include "monitor.h"
#include "dashboard.h"
#include "upstream.h"
#include <string.h>
#include <unistd.h>
#include <errno.h>
extern connection_t connections[MAX_FDS];
extern time_t cached_time;
static int client_has_complete_request(connection_t *conn) {
size_t available = conn->read_buf.tail - conn->read_buf.head;
if (available < 4) return 0;
char *headers_end = memmem(conn->read_buf.data + conn->read_buf.head, available, "\r\n\r\n", 4);
return headers_end != NULL;
}
int client_check_rate_limit(connection_t *conn) {
if (!rate_limit_check(conn->client_ip)) {
http_response_send_error(conn, 429, "Too Many Requests",
"429 Too Many Requests - Rate limit exceeded");
return 0;
}
return 1;
}
int client_check_route_auth(connection_t *conn, route_config_t *route, const char *data, size_t len) {
if (!route || !route->use_auth) return 1;
char auth_header[512] = "";
http_find_header_value(data, len, "Authorization", auth_header, sizeof(auth_header));
char error_msg[256] = "";
if (!auth_check_route_basic_auth(route, auth_header[0] ? auth_header : NULL, error_msg, sizeof(error_msg))) {
http_response_send_auth_required(conn, route->hostname);
return 0;
}
return 1;
}
int client_handle_internal_route(connection_t *conn, const char *data, size_t len) {
if (!http_uri_is_internal_route(conn->request.uri)) {
return 0;
}
conn->state = CLIENT_STATE_SERVING_INTERNAL;
if (strcmp(conn->request.uri, "/rproxy/dashboard") == 0) {
dashboard_serve(conn, data, len);
} else if (strcmp(conn->request.uri, "/rproxy/api/stats") == 0) {
dashboard_serve_stats_api(conn, data, len);
} else {
http_response_send_error(conn, 404, "Not Found", "404 Not Found");
}
return 1;
}
static void client_setup_vhost_stats(connection_t *conn) {
if (!conn->vhost_stats && conn->request.host[0] != '\0') {
conn->vhost_stats = monitor_get_or_create_vhost_stats(conn->request.host);
if (conn->vhost_stats) {
monitor_record_request_start(conn->vhost_stats, conn->request.is_websocket);
monitor_record_method(conn->vhost_stats, http_method_from_string(conn->request.method));
}
}
}
void client_handle_read(connection_t *conn) {
if (!conn || conn->state == CLIENT_STATE_CLOSING) return;
size_t available = buffer_available_write(&conn->read_buf);
if (available == 0) {
if (buffer_ensure_capacity(&conn->read_buf, conn->read_buf.capacity * 2) < 0) {
http_response_send_error(conn, 413, "Request Entity Too Large",
"413 Request Entity Too Large");
return;
}
available = buffer_available_write(&conn->read_buf);
}
ssize_t n = read(conn->fd, conn->read_buf.data + conn->read_buf.tail, available);
if (n > 0) {
conn->read_buf.tail += (size_t)n;
conn->last_activity = cached_time;
} else if (n == 0 || (errno != EAGAIN && errno != EWOULDBLOCK)) {
conn->state = CLIENT_STATE_CLOSING;
return;
}
if (conn->state == CLIENT_STATE_READING_HEADERS) {
if (!client_has_complete_request(conn)) return;
size_t data_len = conn->read_buf.tail - conn->read_buf.head;
char *data = conn->read_buf.data + conn->read_buf.head;
char *headers_end = memmem(data, data_len, "\r\n\r\n", 4);
size_t request_len = headers_end ? (size_t)(headers_end - data) + 4 : data_len;
int parse_result = http_parse_request(data, data_len, &conn->request);
if (parse_result <= 0) {
http_response_send_error(conn, 400, "Bad Request", "400 Bad Request - Invalid HTTP request");
return;
}
client_setup_vhost_stats(conn);
if (!client_check_rate_limit(conn)) return;
if (client_handle_internal_route(conn, data, data_len)) {
conn->read_buf.head += request_len;
if (conn->read_buf.head >= conn->read_buf.tail) {
conn->read_buf.head = 0;
conn->read_buf.tail = 0;
}
return;
}
route_config_t *route = config_find_route(conn->request.host);
if (!route) {
http_response_send_error(conn, 502, "Bad Gateway", "502 Bad Gateway - No route configured");
return;
}
if (!client_check_route_auth(conn, route, data, data_len)) return;
conn->state = CLIENT_STATE_FORWARDING;
upstream_connect(conn, data, data_len);
}
}

13
src/client_handler.h Normal file
View File

@ -0,0 +1,13 @@
// retoor <retoor@molodetz.nl>
#ifndef RPROXY_CLIENT_HANDLER_H
#define RPROXY_CLIENT_HANDLER_H
#include "types.h"
void client_handle_read(connection_t *conn);
int client_check_rate_limit(connection_t *conn);
int client_check_route_auth(connection_t *conn, route_config_t *route, const char *data, size_t len);
int client_handle_internal_route(connection_t *conn, const char *data, size_t len);
#endif

251
src/config.c Normal file
View File

@ -0,0 +1,251 @@
// retoor <retoor@molodetz.nl>
#include "config.h"
#include "config_parser.h"
#include "logging.h"
#include "../cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <stdatomic.h>
static time_t config_file_mtime = 0;
app_config_t *config = NULL;
static app_config_t *config_create_from_json(cJSON *root) {
app_config_t *new_config = calloc(1, sizeof(app_config_t));
if (!new_config) {
log_error("Failed to allocate memory for config");
return NULL;
}
new_config->ref_count = 1;
cJSON *port_item = cJSON_GetObjectItem(root, "port");
new_config->port = cJSON_IsNumber(port_item) ? port_item->valueint : 8080;
if (new_config->port < 1 || new_config->port > 65535) {
log_error("Invalid port number: %d", new_config->port);
free(new_config);
return NULL;
}
cJSON *proxy_array = cJSON_GetObjectItem(root, "reverse_proxy");
if (!cJSON_IsArray(proxy_array)) {
log_error("No reverse_proxy array found");
free(new_config);
return NULL;
}
int total_routes = cJSON_GetArraySize(proxy_array);
if (total_routes <= 0) {
log_error("Empty reverse_proxy array");
free(new_config);
return NULL;
}
new_config->routes = calloc((size_t)total_routes, sizeof(route_config_t));
if (!new_config->routes) {
log_error("Failed to allocate memory for routes");
free(new_config);
return NULL;
}
int valid_routes = 0;
int index = 0;
cJSON *route_item;
cJSON_ArrayForEach(route_item, proxy_array) {
route_config_t *route = &new_config->routes[valid_routes];
if (config_parse_route(route_item, route, index)) {
log_info("Route: %s -> %s:%d (SSL: %s, Auth: %s, Patches: %d)",
route->hostname, route->upstream_host, route->upstream_port,
route->use_ssl ? "yes" : "no", route->use_auth ? "yes" : "no",
route->patches.rule_count);
valid_routes++;
}
index++;
}
if (valid_routes == 0) {
log_error("No valid routes parsed");
free(new_config->routes);
free(new_config);
return NULL;
}
new_config->route_count = valid_routes;
return new_config;
}
int config_load(const char *filename) {
log_info("Loading configuration from %s", filename);
char *json_string = config_read_file(filename);
if (!json_string) {
log_error("Could not read config file");
return 0;
}
cJSON *root = cJSON_Parse(json_string);
if (!root) {
const char *error_ptr = cJSON_GetErrorPtr();
log_error("JSON parse error: %s", error_ptr ? error_ptr : "unknown");
free(json_string);
return 0;
}
free(json_string);
app_config_t *new_config = config_create_from_json(root);
cJSON_Delete(root);
if (!new_config) {
return 0;
}
struct stat st;
if (stat(filename, &st) == 0) {
config_file_mtime = st.st_mtime;
}
if (config) {
config_ref_dec(config);
}
config = new_config;
log_info("Loaded %d routes from %s", config->route_count, filename);
return 1;
}
void config_ref_inc(app_config_t *conf) {
if (conf) {
atomic_fetch_add(&conf->ref_count, 1);
}
}
void config_ref_dec(app_config_t *conf) {
if (!conf) return;
if (atomic_fetch_sub(&conf->ref_count, 1) == 1) {
log_debug("Freeing configuration with port %d", conf->port);
if (conf->routes) {
free(conf->routes);
}
free(conf);
}
}
void config_free(void) {
if (config) {
config_ref_dec(config);
config = NULL;
}
}
void config_create_default(const char *filename) {
FILE *f = fopen(filename, "r");
if (f) {
fclose(f);
return;
}
f = fopen(filename, "w");
if (!f) {
log_error("Cannot create default config file");
return;
}
fprintf(f, "{\n"
" \"port\": 8080,\n"
" \"reverse_proxy\": [\n"
" {\n"
" \"hostname\": \"localhost\",\n"
" \"upstream_host\": \"127.0.0.1\",\n"
" \"upstream_port\": 3000,\n"
" \"use_ssl\": false,\n"
" \"rewrite_host\": true\n"
" },\n"
" {\n"
" \"hostname\": \"example.com\",\n"
" \"upstream_host\": \"127.0.0.1\",\n"
" \"upstream_port\": 5000,\n"
" \"use_ssl\": false,\n"
" \"rewrite_host\": false\n"
" }\n"
" ]\n"
"}\n");
fclose(f);
log_info("Created default config file: %s", filename);
}
route_config_t *config_find_route(const char *hostname) {
if (!hostname) return NULL;
app_config_t *current_config = config;
if (!current_config) {
return NULL;
}
for (int i = 0; i < current_config->route_count; i++) {
if (strcasecmp(hostname, current_config->routes[i].hostname) == 0) {
return &current_config->routes[i];
}
}
return NULL;
}
int config_check_file_changed(const char *filename) {
struct stat st;
if (stat(filename, &st) != 0) {
return 0;
}
if (config_file_mtime == 0) {
config_file_mtime = st.st_mtime;
return 0;
}
if (st.st_mtime != config_file_mtime) {
config_file_mtime = st.st_mtime;
return 1;
}
return 0;
}
int config_hot_reload(const char *filename) {
log_info("Hot-reloading configuration from %s", filename);
char *json_string = config_read_file(filename);
if (!json_string) {
log_error("Hot-reload: Could not read config file");
return 0;
}
cJSON *root = cJSON_Parse(json_string);
if (!root) {
const char *error_ptr = cJSON_GetErrorPtr();
log_error("Hot-reload: JSON parse error: %s", error_ptr ? error_ptr : "unknown");
free(json_string);
return 0;
}
free(json_string);
app_config_t *new_config = config_create_from_json(root);
cJSON_Delete(root);
if (!new_config) {
return 0;
}
struct stat st;
if (stat(filename, &st) == 0) {
config_file_mtime = st.st_mtime;
}
if (config) {
config_ref_dec(config);
}
config = new_config;
log_info("Hot-reload complete: %d routes loaded", new_config->route_count);
return 1;
}

20
src/config.h Executable file
View File

@ -0,0 +1,20 @@
#ifndef RPROXY_CONFIG_H
#define RPROXY_CONFIG_H
#include "types.h"
#define CONFIG_RELOAD_INTERVAL_SECONDS 3
extern app_config_t *config;
int config_load(const char *filename);
void config_free(void);
void config_create_default(const char *filename);
route_config_t *config_find_route(const char *hostname);
int config_check_file_changed(const char *filename);
int config_hot_reload(const char *filename);
void config_ref_inc(app_config_t *conf);
void config_ref_dec(app_config_t *conf);
#endif

198
src/config_parser.c Normal file
View File

@ -0,0 +1,198 @@
// retoor <retoor@molodetz.nl>
#include "config_parser.h"
#include "logging.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <openssl/evp.h>
void config_compute_password_hash(const char *password, char *output, size_t output_size) {
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
if (!ctx) return;
unsigned char hash[EVP_MAX_MD_SIZE];
unsigned int hash_len = 0;
EVP_DigestInit_ex(ctx, EVP_sha256(), NULL);
EVP_DigestUpdate(ctx, password, strlen(password));
EVP_DigestFinal_ex(ctx, hash, &hash_len);
EVP_MD_CTX_free(ctx);
for (unsigned int i = 0; i < hash_len && (i * 2 + 2) < output_size; i++) {
snprintf(output + (i * 2), 3, "%02x", hash[i]);
}
}
int config_is_valid_hostname(const char *hostname) {
if (!hostname || strlen(hostname) == 0 || strlen(hostname) > 253) return 0;
const char *p = hostname;
int label_len = 0;
while (*p) {
char c = *p;
if (c == '.') {
if (label_len == 0) return 0;
label_len = 0;
} else if (isalnum((unsigned char)c) || c == '-' || c == '_') {
label_len++;
if (label_len > 63) return 0;
} else {
return 0;
}
p++;
}
return 1;
}
int config_is_valid_ip(const char *ip) {
if (!ip) return 0;
int dots = 0;
int num = 0;
int has_digit = 0;
while (*ip) {
if (*ip == '.') {
if (!has_digit || num > 255) return 0;
dots++;
num = 0;
has_digit = 0;
} else if (isdigit((unsigned char)*ip)) {
num = num * 10 + (*ip - '0');
has_digit = 1;
} else {
return 0;
}
ip++;
}
return dots == 3 && has_digit && num <= 255;
}
int config_is_valid_host(const char *host) {
return config_is_valid_hostname(host) || config_is_valid_ip(host);
}
char *config_read_file(const char *filename) {
FILE *f = fopen(filename, "rb");
if (!f) return NULL;
if (fseek(f, 0, SEEK_END) != 0) {
fclose(f);
return NULL;
}
long length = ftell(f);
if (length < 0 || length > 1024 * 1024) {
fclose(f);
return NULL;
}
if (fseek(f, 0, SEEK_SET) != 0) {
fclose(f);
return NULL;
}
char *buffer = malloc((size_t)length + 1);
if (!buffer) {
fclose(f);
return NULL;
}
size_t read_len = fread(buffer, 1, (size_t)length, f);
buffer[read_len] = '\0';
fclose(f);
return buffer;
}
int config_parse_route(cJSON *route_item, route_config_t *route, int index) {
if (!route_item || !route) return 0;
cJSON *hostname = cJSON_GetObjectItem(route_item, "hostname");
cJSON *upstream_host = cJSON_GetObjectItem(route_item, "upstream_host");
cJSON *upstream_port = cJSON_GetObjectItem(route_item, "upstream_port");
if (!cJSON_IsString(hostname) || !cJSON_IsString(upstream_host) || !cJSON_IsNumber(upstream_port)) {
log_debug("Invalid route configuration at index %d", index);
return 0;
}
if (!config_is_valid_host(hostname->valuestring)) {
log_debug("Invalid hostname at index %d: %s", index, hostname->valuestring);
return 0;
}
if (!config_is_valid_host(upstream_host->valuestring)) {
log_debug("Invalid upstream_host at index %d: %s", index, upstream_host->valuestring);
return 0;
}
strncpy(route->hostname, hostname->valuestring, sizeof(route->hostname) - 1);
route->hostname[sizeof(route->hostname) - 1] = '\0';
strncpy(route->upstream_host, upstream_host->valuestring, sizeof(route->upstream_host) - 1);
route->upstream_host[sizeof(route->upstream_host) - 1] = '\0';
route->upstream_port = upstream_port->valueint;
if (route->upstream_port < 1 || route->upstream_port > 65535) {
log_debug("Invalid upstream port for %s: %d", route->hostname, route->upstream_port);
return 0;
}
route->use_ssl = cJSON_IsTrue(cJSON_GetObjectItem(route_item, "use_ssl"));
route->rewrite_host = cJSON_IsTrue(cJSON_GetObjectItem(route_item, "rewrite_host"));
route->use_auth = 0;
route->username[0] = '\0';
route->password_hash[0] = '\0';
cJSON *use_auth = cJSON_GetObjectItem(route_item, "use_auth");
cJSON *auth_username = cJSON_GetObjectItem(route_item, "username");
cJSON *auth_password = cJSON_GetObjectItem(route_item, "password");
if (cJSON_IsTrue(use_auth) && cJSON_IsString(auth_username) && cJSON_IsString(auth_password)) {
if (strlen(auth_username->valuestring) > 0 && strlen(auth_password->valuestring) > 0) {
route->use_auth = 1;
strncpy(route->username, auth_username->valuestring, sizeof(route->username) - 1);
route->username[sizeof(route->username) - 1] = '\0';
config_compute_password_hash(auth_password->valuestring, route->password_hash, sizeof(route->password_hash));
}
}
route->patches.rule_count = 0;
cJSON *patch_obj = cJSON_GetObjectItem(route_item, "patch");
if (cJSON_IsObject(patch_obj)) {
cJSON *patch_item = NULL;
cJSON_ArrayForEach(patch_item, patch_obj) {
if (route->patches.rule_count >= MAX_PATCH_RULES) {
log_info("Maximum patch rules reached for %s", route->hostname);
break;
}
if (!patch_item->string) continue;
size_t key_len = strlen(patch_item->string);
if (key_len == 0 || key_len >= MAX_PATCH_KEY_SIZE) continue;
patch_rule_t *rule = &route->patches.rules[route->patches.rule_count];
strncpy(rule->key, patch_item->string, MAX_PATCH_KEY_SIZE - 1);
rule->key[MAX_PATCH_KEY_SIZE - 1] = '\0';
rule->key_len = key_len;
if (cJSON_IsNull(patch_item)) {
rule->is_null = 1;
rule->value[0] = '\0';
rule->value_len = 0;
} else if (cJSON_IsString(patch_item)) {
rule->is_null = 0;
size_t val_len = strlen(patch_item->valuestring);
if (val_len >= MAX_PATCH_VALUE_SIZE) val_len = MAX_PATCH_VALUE_SIZE - 1;
strncpy(rule->value, patch_item->valuestring, MAX_PATCH_VALUE_SIZE - 1);
rule->value[MAX_PATCH_VALUE_SIZE - 1] = '\0';
rule->value_len = val_len;
} else {
continue;
}
route->patches.rule_count++;
}
}
return 1;
}

16
src/config_parser.h Normal file
View File

@ -0,0 +1,16 @@
// retoor <retoor@molodetz.nl>
#ifndef RPROXY_CONFIG_PARSER_H
#define RPROXY_CONFIG_PARSER_H
#include "types.h"
#include "../cJSON.h"
int config_parse_route(cJSON *route_item, route_config_t *route, int index);
int config_is_valid_hostname(const char *hostname);
int config_is_valid_ip(const char *ip);
int config_is_valid_host(const char *host);
void config_compute_password_hash(const char *password, char *output, size_t output_size);
char *config_read_file(const char *filename);
#endif

550
src/connection.c Normal file
View File

@ -0,0 +1,550 @@
// retoor <retoor@molodetz.nl>
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "connection.h"
#include "types.h"
#include "buffer.h"
#include "logging.h"
#include "config.h"
#include "monitor.h"
#include "http.h"
#include "http_response.h"
#include "ssl_handler.h"
#include "socket_utils.h"
#include "epoll_utils.h"
#include "client_handler.h"
#include "upstream.h"
#include "forwarding.h"
#include <openssl/ssl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
connection_t connections[MAX_FDS];
time_t cached_time = 0;
void connection_update_cached_time(void) {
cached_time = time(NULL);
}
void connection_init_all(void) {
for (int i = 0; i < MAX_FDS; i++) {
connections[i].type = CONN_TYPE_UNUSED;
connections[i].fd = -1;
connections[i].splice_pipe[0] = -1;
connections[i].splice_pipe[1] = -1;
}
connection_update_cached_time();
}
void connection_setup_listener(int port) {
int listener = socket(AF_INET, SOCK_STREAM, 0);
if (listener < 0) {
log_error("Failed to create listener socket");
exit(EXIT_FAILURE);
}
int yes = 1;
setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
setsockopt(listener, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons((uint16_t)port);
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(listener, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
log_error("Failed to bind to port %d", port);
close(listener);
exit(EXIT_FAILURE);
}
if (listen(listener, SOMAXCONN) < 0) {
log_error("Failed to listen on port %d", port);
close(listener);
exit(EXIT_FAILURE);
}
socket_set_non_blocking(listener);
epoll_utils_add(listener, EPOLLIN);
connections[listener].type = CONN_TYPE_LISTENER;
connections[listener].fd = listener;
log_info("Listening on port %d", port);
}
void connection_accept(int listener_fd) {
while (1) {
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int client_fd = accept(listener_fd, (struct sockaddr *)&client_addr, &addr_len);
if (client_fd < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) break;
log_error("accept failed");
break;
}
if (client_fd >= MAX_FDS) {
close(client_fd);
continue;
}
socket_set_non_blocking(client_fd);
socket_optimize(client_fd, 0);
epoll_utils_add(client_fd, EPOLLIN);
connection_t *conn = &connections[client_fd];
memset(conn, 0, sizeof(connection_t));
conn->type = CONN_TYPE_CLIENT;
conn->fd = client_fd;
conn->state = CLIENT_STATE_READING_HEADERS;
conn->last_activity = cached_time;
conn->splice_pipe[0] = -1;
conn->splice_pipe[1] = -1;
inet_ntop(AF_INET, &client_addr.sin_addr, conn->client_ip, sizeof(conn->client_ip));
if (buffer_init(&conn->read_buf, CHUNK_SIZE) < 0 ||
buffer_init(&conn->write_buf, CHUNK_SIZE) < 0) {
connection_close(client_fd);
continue;
}
monitor.active_connections++;
monitor.total_connections_accepted++;
}
}
void connection_close(int fd) {
if (fd < 0 || fd >= MAX_FDS) return;
connection_t *conn = &connections[fd];
if (conn->type == CONN_TYPE_UNUSED) return;
epoll_utils_remove(fd);
connection_t *pair = conn->pair;
if (pair) {
if (conn->type == CONN_TYPE_UPSTREAM && pair->type == CONN_TYPE_CLIENT) {
pair->state = CLIENT_STATE_READING_HEADERS;
pair->pair = NULL;
pair->route = NULL;
pair->content_type_checked = 0;
pair->is_textual_content = 0;
pair->response_headers_parsed = 0;
pair->original_content_length = 0;
pair->content_length_delta = 0;
pair->patch_blocked = 0;
pair->can_splice = 0;
memset(&pair->request, 0, sizeof(http_request_t));
size_t buffered = pair->read_buf.tail - pair->read_buf.head;
if (buffered > 0) {
log_debug("Processing %zu buffered bytes from client fd %d after upstream fd %d closed", buffered, pair->fd, fd);
client_handle_read(pair);
}
} else if (conn->type == CONN_TYPE_CLIENT && pair->type == CONN_TYPE_UPSTREAM) {
pair->pair = NULL;
connection_close(pair->fd);
}
conn->pair = NULL;
}
if (conn->vhost_stats && conn->type == CONN_TYPE_CLIENT) {
monitor_record_connection_closed(conn->vhost_stats);
}
if (conn->ssl) {
SSL_shutdown(conn->ssl);
SSL_free(conn->ssl);
conn->ssl = NULL;
}
if (conn->splice_pipe[0] >= 0) {
close(conn->splice_pipe[0]);
close(conn->splice_pipe[1]);
conn->splice_pipe[0] = -1;
conn->splice_pipe[1] = -1;
}
buffer_free(&conn->read_buf);
buffer_free(&conn->write_buf);
if (conn->config) {
config_ref_dec(conn->config);
conn->config = NULL;
}
if (conn->patch_buf) {
free(conn->patch_buf);
conn->patch_buf = NULL;
conn->patch_buf_capacity = 0;
}
close(fd);
if (conn->type == CONN_TYPE_CLIENT || conn->type == CONN_TYPE_UPSTREAM) {
if (monitor.active_connections > 0) {
monitor.active_connections--;
}
}
memset(conn, 0, sizeof(connection_t));
conn->type = CONN_TYPE_UNUSED;
conn->fd = -1;
conn->splice_pipe[0] = -1;
conn->splice_pipe[1] = -1;
}
int connection_do_read(connection_t *conn) {
if (!conn) return -1;
size_t available = buffer_available_write(&conn->read_buf);
if (available == 0) {
if (buffer_ensure_capacity(&conn->read_buf, conn->read_buf.capacity * 2) < 0) {
return -1;
}
available = buffer_available_write(&conn->read_buf);
}
ssize_t total_read = 0;
ssize_t n;
if (conn->ssl && conn->ssl_handshake_done) {
do {
n = ssl_read(conn, conn->read_buf.data + conn->read_buf.tail, available);
if (n > 0) {
conn->read_buf.tail += (size_t)n;
total_read += n;
available = buffer_available_write(&conn->read_buf);
if (available == 0) {
if (buffer_ensure_capacity(&conn->read_buf, conn->read_buf.capacity * 2) < 0) {
break;
}
available = buffer_available_write(&conn->read_buf);
}
}
} while (n > 0 && available > 0);
log_debug("SSL read fd=%d total=%zd last_n=%zd pending=%d",
conn->fd, total_read, n, SSL_pending(conn->ssl));
if (total_read > 0) {
conn->last_activity = cached_time;
return (int)total_read;
}
if (n == 0) return 0;
if (n < 0) return -1;
return 0;
}
n = read(conn->fd, conn->read_buf.data + conn->read_buf.tail, available);
if (n > 0) {
conn->read_buf.tail += (size_t)n;
conn->last_activity = cached_time;
return (int)n;
}
if (n == 0) return 0;
if (errno == EAGAIN || errno == EWOULDBLOCK) return 0;
return -1;
}
int connection_do_write(connection_t *conn) {
if (!conn) return -1;
size_t available = conn->write_buf.tail - conn->write_buf.head;
if (available == 0) return 0;
char *data = conn->write_buf.data + conn->write_buf.head;
ssize_t n;
if (conn->ssl && conn->ssl_handshake_done) {
n = ssl_write(conn, data, available);
} else {
n = write(conn->fd, data, available);
}
if (n > 0) {
conn->write_buf.head += (size_t)n;
if (conn->write_buf.head >= conn->write_buf.tail) {
conn->write_buf.head = 0;
conn->write_buf.tail = 0;
}
conn->last_activity = cached_time;
return (int)n;
}
if (n == 0) return 0;
if (errno == EAGAIN || errno == EWOULDBLOCK) return 0;
return -1;
}
void connection_send_error_response(connection_t *conn, int code, const char *status, const char *body) {
http_response_send_error(conn, code, status, body);
}
void connection_send_auth_required(connection_t *conn, const char *realm) {
http_response_send_auth_required(conn, realm);
}
void connection_connect_to_upstream(connection_t *client, const char *data, size_t data_len) {
upstream_connect(client, data, data_len);
}
static void handle_ssl_handshake(connection_t *conn) {
if (!conn || !conn->ssl) return;
if (conn->ssl_handshake_done) return;
int result = ssl_do_handshake(conn);
if (result == 1) {
epoll_utils_modify(conn->fd, EPOLLIN | EPOLLOUT);
return;
}
if (result < 0) {
if (conn->vhost_stats) {
monitor_record_error(conn->vhost_stats, ERROR_TYPE_SSL);
}
conn->state = CLIENT_STATE_CLOSING;
if (conn->pair) {
http_response_send_error(conn->pair, 502, "Bad Gateway", "SSL handshake failed");
}
return;
}
time_t elapsed = time(NULL) - conn->ssl_handshake_start;
if (elapsed > SSL_HANDSHAKE_TIMEOUT_SEC) {
log_debug("SSL handshake timeout for fd %d", conn->fd);
if (conn->vhost_stats) {
monitor_record_error(conn->vhost_stats, ERROR_TYPE_TIMEOUT);
}
conn->state = CLIENT_STATE_CLOSING;
if (conn->pair) {
http_response_send_error(conn->pair, 504, "Gateway Timeout", "SSL handshake timeout");
}
}
}
static void handle_client_event(connection_t *conn, uint32_t events) {
if (conn->state == CLIENT_STATE_CLOSING) {
return;
}
if (events & (EPOLLERR | EPOLLHUP)) {
conn->state = CLIENT_STATE_CLOSING;
return;
}
if (events & EPOLLIN) {
if (conn->state == CLIENT_STATE_READING_HEADERS) {
client_handle_read(conn);
} else if (conn->state == CLIENT_STATE_FORWARDING && conn->pair) {
int n = connection_do_read(conn);
if (n > 0) {
forwarding_handle(conn, conn->pair, 0);
} else if (n <= 0) {
conn->state = CLIENT_STATE_CLOSING;
}
}
}
if (events & EPOLLOUT) {
if (conn->write_buf.tail > conn->write_buf.head) {
int n = connection_do_write(conn);
if (n < 0) {
conn->state = CLIENT_STATE_CLOSING;
return;
}
}
int write_buf_empty = conn->write_buf.tail == conn->write_buf.head;
if (write_buf_empty && conn->state == CLIENT_STATE_READING_HEADERS) {
size_t buffered = conn->read_buf.tail - conn->read_buf.head;
if (buffered > 0) {
client_handle_read(conn);
return;
}
epoll_utils_modify(conn->fd, EPOLLIN);
return;
}
if (write_buf_empty && conn->state == CLIENT_STATE_FORWARDING && conn->pair) {
size_t pending = conn->pair->read_buf.tail - conn->pair->read_buf.head;
if (pending > 0) {
forwarding_handle(conn->pair, conn, 1);
}
}
int has_pending_write = conn->write_buf.tail > conn->write_buf.head;
int has_pending_forward = conn->pair &&
(conn->pair->read_buf.tail > conn->pair->read_buf.head);
if (!has_pending_write && !has_pending_forward) {
if (conn->state == CLIENT_STATE_ERROR || conn->state == CLIENT_STATE_CLOSING) {
conn->state = CLIENT_STATE_CLOSING;
} else if (conn->state == CLIENT_STATE_SERVING_INTERNAL) {
if (!conn->request.keep_alive) {
conn->state = CLIENT_STATE_CLOSING;
} else {
conn->state = CLIENT_STATE_READING_HEADERS;
memset(&conn->request, 0, sizeof(conn->request));
if (buffer_available_read(&conn->read_buf) > 0) {
client_handle_read(conn);
}
}
} else {
epoll_utils_modify(conn->fd, EPOLLIN);
}
}
}
}
static void handle_upstream_event(connection_t *conn, uint32_t events) {
if (conn->state == CLIENT_STATE_CLOSING) {
return;
}
if (events & (EPOLLERR | EPOLLHUP)) {
conn->state = CLIENT_STATE_CLOSING;
return;
}
if (conn->ssl && !conn->ssl_handshake_done) {
handle_ssl_handshake(conn);
return;
}
if (events & EPOLLOUT) {
if (conn->write_buf.tail > conn->write_buf.head) {
int n = connection_do_write(conn);
if (n < 0) {
conn->state = CLIENT_STATE_CLOSING;
return;
}
}
int write_buf_empty = conn->write_buf.tail == conn->write_buf.head;
if (write_buf_empty && conn->pair) {
size_t pending = conn->pair->read_buf.tail - conn->pair->read_buf.head;
if (pending > 0) {
forwarding_handle(conn->pair, conn, 0);
}
}
int has_pending_write = conn->write_buf.tail > conn->write_buf.head;
int has_pending_forward = conn->pair &&
(conn->pair->read_buf.tail > conn->pair->read_buf.head);
if (!has_pending_write && !has_pending_forward) {
epoll_utils_modify(conn->fd, EPOLLIN);
}
}
if (events & EPOLLIN) {
int n = connection_do_read(conn);
if (n > 0 && conn->pair) {
forwarding_handle(conn, conn->pair, 1);
} else if (n < 0 || (n == 0 && !conn->ssl)) {
if (conn->pair) {
size_t remaining = conn->read_buf.tail - conn->read_buf.head;
if (remaining > 0) {
forwarding_handle(conn, conn->pair, 1);
}
}
conn->state = CLIENT_STATE_CLOSING;
}
}
}
void connection_handle_event(struct epoll_event *event) {
if (!event) return;
int fd = event->data.fd;
if (fd < 0 || fd >= MAX_FDS) return;
connection_t *conn = &connections[fd];
if (conn->type == CONN_TYPE_UNUSED) return;
if (conn->type == CONN_TYPE_LISTENER) {
connection_accept(fd);
return;
}
if (conn->type == CONN_TYPE_CLIENT) {
handle_client_event(conn, event->events);
} else if (conn->type == CONN_TYPE_UPSTREAM) {
handle_upstream_event(conn, event->events);
}
if (conn->state == CLIENT_STATE_CLOSING) {
int write_buf_empty = conn->write_buf.tail == conn->write_buf.head;
int read_buf_empty = conn->read_buf.tail == conn->read_buf.head;
if (!read_buf_empty && conn->pair) {
forwarding_handle(conn, conn->pair, conn->type == CONN_TYPE_UPSTREAM ? 1 : 0);
read_buf_empty = conn->read_buf.tail == conn->read_buf.head;
}
if (write_buf_empty && (read_buf_empty || !conn->pair)) {
connection_close(fd);
}
}
}
void connection_cleanup_idle(void) {
time_t now = cached_time;
for (int i = 0; i < MAX_FDS; i++) {
connection_t *conn = &connections[i];
if (conn->type == CONN_TYPE_UNUSED || conn->type == CONN_TYPE_LISTENER) continue;
int timeout = CONNECTION_TIMEOUT;
if (conn->ssl && !conn->ssl_handshake_done) {
timeout = SSL_HANDSHAKE_TIMEOUT_SEC;
}
if (now - conn->last_activity > timeout) {
log_debug("Closing idle connection fd %d (type %d)", conn->fd, conn->type);
connection_close(i);
}
}
}
int connection_set_non_blocking(int fd) {
return socket_set_non_blocking(fd);
}
void connection_set_tcp_keepalive(int fd) {
socket_set_tcp_keepalive(fd);
}
void connection_set_tcp_nodelay(int fd) {
socket_set_tcp_nodelay(fd);
}
void connection_optimize_socket(int fd, int is_upstream) {
socket_optimize(fd, is_upstream);
}
void connection_add_to_epoll(int fd, uint32_t events) {
epoll_utils_add(fd, events);
}
void connection_modify_epoll(int fd, uint32_t events) {
epoll_utils_modify(fd, events);
}

33
src/connection.h Executable file
View File

@ -0,0 +1,33 @@
#ifndef RPROXY_CONNECTION_H
#define RPROXY_CONNECTION_H
#include "types.h"
#include <sys/epoll.h>
extern connection_t connections[MAX_FDS];
extern int epoll_fd;
extern time_t cached_time;
void connection_update_cached_time(void);
void connection_init_all(void);
void connection_setup_listener(int port);
void connection_accept(int listener_fd);
void connection_close(int fd);
void connection_handle_event(struct epoll_event *event);
void connection_cleanup_idle(void);
int connection_set_non_blocking(int fd);
void connection_set_tcp_keepalive(int fd);
void connection_set_tcp_nodelay(int fd);
void connection_optimize_socket(int fd, int is_upstream);
void connection_add_to_epoll(int fd, uint32_t events);
void connection_modify_epoll(int fd, uint32_t events);
int connection_do_read(connection_t *conn);
int connection_do_write(connection_t *conn);
void connection_send_error_response(connection_t *conn, int code, const char* status, const char* body);
void connection_send_auth_required(connection_t *conn, const char *realm);
void connection_connect_to_upstream(connection_t *client, const char *data, size_t data_len);
#endif

520
src/dashboard.c Executable file
View File

@ -0,0 +1,520 @@
#include "dashboard.h"
#include "buffer.h"
#include "monitor.h"
#include "connection.h"
#include "auth.h"
#include "http.h"
#include "../cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <time.h>
static const char *DASHBOARD_HTML =
"<!DOCTYPE html>\n"
"<html>\n"
"<head>\n"
" <title>Reverse Proxy Monitor</title>\n"
" <style>\n"
" * { margin: 0; padding: 0; box-sizing: border-box; }\n"
" body { font-family: -apple-system, system-ui, sans-serif; background: #000; color: #fff; padding: 20px; }\n"
" .header { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); margin-bottom: 30px; gap: 15px; }\n"
" .metric { text-align: center; background: #111; border-radius: 8px; padding: 12px; }\n"
" .metric-value { font-size: 28px; font-weight: 300; }\n"
" .metric-label { font-size: 11px; opacity: 0.7; text-transform: uppercase; margin-top: 4px; }\n"
" .metric-value.health { color: #2ecc71; }\n"
" .metric-value.warning { color: #f39c12; }\n"
" .metric-value.danger { color: #e74c3c; }\n"
" .charts-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; margin-bottom: 20px; }\n"
" .chart-container { background: #111; border-radius: 8px; padding: 20px; height: 220px; position: relative; }\n"
" .chart-container.tall { height: 280px; }\n"
" .chart-title { position: absolute; top: 10px; left: 20px; font-size: 13px; opacity: 0.7; z-index: 10; }\n"
" canvas { width: 100% !important; height: 100% !important; }\n"
" .process-table { background: #111; border-radius: 8px; padding: 20px; overflow-x: auto; }\n"
" table { width: 100%; border-collapse: collapse; font-size: 13px; }\n"
" th, td { padding: 8px 10px; text-align: left; border-bottom: 1px solid #2a2e3e; }\n"
" th { font-weight: 500; opacity: 0.7; white-space: nowrap; }\n"
" .legend { position: absolute; top: 10px; right: 20px; display: flex; gap: 15px; font-size: 11px; z-index: 10; }\n"
" .legend-item { display: flex; align-items: center; gap: 4px; }\n"
" .legend-color { width: 10px; height: 10px; border-radius: 2px; }\n"
" .vhost-chart-container { height: 180px; background: #111; border-radius: 8px; position: relative; padding: 15px; margin-bottom: 10px; }\n"
" .status-indicator { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }\n"
" .status-ok { background: #2ecc71; }\n"
" .status-warn { background: #f39c12; }\n"
" .status-error { background: #e74c3c; }\n"
" </style>\n"
" <script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>\n"
"</head>\n"
"<body>\n"
" <div class=\"header\">\n"
" <div class=\"metric\"><div class=\"metric-value health\" id=\"healthScore\">100</div><div class=\"metric-label\">Health</div></div>\n"
" <div class=\"metric\"><div class=\"metric-value\" id=\"rps\">0</div><div class=\"metric-label\">Req/sec</div></div>\n"
" <div class=\"metric\"><div class=\"metric-value\" id=\"p50\">0</div><div class=\"metric-label\">p50 ms</div></div>\n"
" <div class=\"metric\"><div class=\"metric-value\" id=\"p99\">0</div><div class=\"metric-label\">p99 ms</div></div>\n"
" <div class=\"metric\"><div class=\"metric-value\" id=\"errorRate\">0%</div><div class=\"metric-label\">Errors</div></div>\n"
" <div class=\"metric\"><div class=\"metric-value\" id=\"connections\">0</div><div class=\"metric-label\">Conns</div></div>\n"
" <div class=\"metric\"><div class=\"metric-value\" id=\"cpu\">0</div><div class=\"metric-label\">CPU</div></div>\n"
" <div class=\"metric\"><div class=\"metric-value\" id=\"memory\">0</div><div class=\"metric-label\">Memory</div></div>\n"
" <div class=\"metric\"><div class=\"metric-value\" id=\"uptime\">0s</div><div class=\"metric-label\">Uptime</div></div>\n"
" </div>\n"
" <div class=\"charts-grid\">\n"
" <div class=\"chart-container\"><div class=\"chart-title\">Latency Distribution</div><canvas id=\"latencyHistChart\"></canvas></div>\n"
" <div class=\"chart-container\"><div class=\"chart-title\">Status Codes</div><canvas id=\"statusChart\"></canvas></div>\n"
" <div class=\"chart-container\"><div class=\"chart-title\">HTTP Methods</div><canvas id=\"methodsChart\"></canvas></div>\n"
" <div class=\"chart-container\"><div class=\"chart-title\">Efficiency</div><canvas id=\"efficiencyChart\"></canvas></div>\n"
" </div>\n"
" <div class=\"charts-grid\">\n"
" <div class=\"chart-container\"><div class=\"chart-title\">CPU Usage</div><canvas id=\"cpuChart\"></canvas></div>\n"
" <div class=\"chart-container\"><div class=\"chart-title\">Memory Usage</div><canvas id=\"memChart\"></canvas></div>\n"
" </div>\n"
" <div class=\"charts-grid\">\n"
" <div class=\"chart-container\"><div class=\"chart-title\">Network I/O</div>\n"
" <div class=\"legend\"><div class=\"legend-item\"><div class=\"legend-color\" style=\"background:#3498db\"></div><span>RX</span></div><div class=\"legend-item\"><div class=\"legend-color\" style=\"background:#2ecc71\"></div><span>TX</span></div></div>\n"
" <canvas id=\"netChart\"></canvas></div>\n"
" <div class=\"chart-container\"><div class=\"chart-title\">Disk I/O</div>\n"
" <div class=\"legend\"><div class=\"legend-item\"><div class=\"legend-color\" style=\"background:#9b59b6\"></div><span>Read</span></div><div class=\"legend-item\"><div class=\"legend-color\" style=\"background:#e67e22\"></div><span>Write</span></div></div>\n"
" <canvas id=\"diskChart\"></canvas></div>\n"
" </div>\n"
" <div class=\"chart-container tall\" style=\"margin-bottom:20px;\"><div class=\"chart-title\">Load Average</div>\n"
" <div class=\"legend\"><div class=\"legend-item\"><div class=\"legend-color\" style=\"background:#e74c3c\"></div><span>1m</span></div><div class=\"legend-item\"><div class=\"legend-color\" style=\"background:#f39c12\"></div><span>5m</span></div><div class=\"legend-item\"><div class=\"legend-color\" style=\"background:#3498db\"></div><span>15m</span></div></div>\n"
" <canvas id=\"loadChart\"></canvas></div>\n"
" <div class=\"process-table\"><table><thead><tr><th>Virtual Host</th><th>RPS</th><th>Total</th><th>p50</th><th>p99</th><th>2xx</th><th>4xx</th><th>5xx</th><th>Err%</th><th>Sent</th><th>Recv</th></tr></thead><tbody id=\"processTable\"></tbody></table></div>\n"
" <script>\n"
" const formatTime = v => { const s=Math.abs(Math.round(v/1000)); if(s===0)return'now'; const m=Math.floor(s/60),ss=s%60; return`-${m>0?m+'m ':''}${ss}s`; };\n"
" const formatSize = v => { if(v>=1048576)return(v/1048576).toFixed(1)+' GB/s'; if(v>=1024)return(v/1024).toFixed(1)+' MB/s'; return v.toFixed(0)+' KB/s'; };\n"
" const formatDisk = v => v>=1024?(v/1024).toFixed(1)+' GB/s':v.toFixed(1)+' MB/s';\n"
" const formatBytes = b => { if(b===0)return'0 B'; const k=1024,s=['B','KB','MB','GB','TB'],i=Math.floor(Math.log(b)/Math.log(k)); return(b/Math.pow(k,i)).toFixed(1)+' '+s[i]; };\n"
" const formatUptime = s => { const d=Math.floor(s/86400),h=Math.floor((s%86400)/3600),m=Math.floor((s%3600)/60); return d>0?d+'d '+h+'h':h>0?h+'h '+m+'m':m+'m'; };\n"
" const formatNum = n => { if(n===null||n===undefined)return'0'; n=parseFloat(n); if(isNaN(n))return'0'; if(n>=1e9)return(n/1e9).toFixed(1)+'B'; if(n>=1e6)return(n/1e6).toFixed(1)+'M'; if(n>=1e3)return(n/1e3).toFixed(1)+'K'; return n.toFixed(0); };\n"
" const formatMs = v => { if(v>=1000)return(v/1000).toFixed(1)+'s'; return v.toFixed(0)+'ms'; };\n"
" const baseOpts = (sec,yCb) => ({responsive:true,maintainAspectRatio:false,animation:false,layout:{padding:{top:30}},interaction:{mode:'nearest',axis:'x',intersect:false},scales:{x:{type:'linear',display:true,grid:{color:'#2a2e3e'},ticks:{color:'#666',maxTicksLimit:7,callback:formatTime},min:-sec*1000,max:0},y:{display:true,grid:{color:'#2a2e3e'},ticks:{color:'#666',beginAtZero:true,callback:yCb}}},plugins:{legend:{display:false}},elements:{point:{radius:0},line:{borderWidth:2,tension:0.4,fill:true}}});\n"
" const cpuChart = new Chart(document.getElementById('cpuChart'),{type:'line',data:{datasets:[{data:[],borderColor:'#f39c12',backgroundColor:'rgba(243,156,18,0.1)'}]},options:{...baseOpts(300,v=>v+'%'),scales:{...baseOpts(300).scales,y:{...baseOpts(300).scales.y,max:100}}}});\n"
" const memChart = new Chart(document.getElementById('memChart'),{type:'line',data:{datasets:[{data:[],borderColor:'#e74c3c',backgroundColor:'rgba(231,76,60,0.1)'}]},options:baseOpts(300,v=>v.toFixed(1)+' GB')});\n"
" const netChart = new Chart(document.getElementById('netChart'),{type:'line',data:{datasets:[{data:[],borderColor:'#3498db',backgroundColor:'rgba(52,152,219,0.1)'},{data:[],borderColor:'#2ecc71',backgroundColor:'rgba(46,204,113,0.1)'}]},options:baseOpts(300,formatSize)});\n"
" const diskChart = new Chart(document.getElementById('diskChart'),{type:'line',data:{datasets:[{data:[],borderColor:'#9b59b6',backgroundColor:'rgba(155,89,182,0.1)'},{data:[],borderColor:'#e67e22',backgroundColor:'rgba(230,126,34,0.1)'}]},options:baseOpts(300,formatDisk)});\n"
" const loadChart = new Chart(document.getElementById('loadChart'),{type:'line',data:{datasets:[{data:[],borderColor:'#e74c3c',backgroundColor:'rgba(231,76,60,0.1)'},{data:[],borderColor:'#f39c12',backgroundColor:'rgba(243,156,18,0.1)'},{data:[],borderColor:'#3498db',backgroundColor:'rgba(52,152,219,0.1)'}]},options:baseOpts(300,v=>v.toFixed(2))});\n"
" const latencyHistChart = new Chart(document.getElementById('latencyHistChart'),{type:'bar',data:{labels:[],datasets:[{data:[],backgroundColor:v=>{const i=v.dataIndex;return i<4?'#2ecc71':i<8?'#f39c12':'#e74c3c';}}]},options:{responsive:true,maintainAspectRatio:false,animation:false,layout:{padding:{top:30}},plugins:{legend:{display:false}},scales:{x:{grid:{display:false},ticks:{color:'#666',maxRotation:45}},y:{grid:{color:'#2a2e3e'},ticks:{color:'#666'}}}}});\n"
" const statusChart = new Chart(document.getElementById('statusChart'),{type:'doughnut',data:{labels:['2xx','3xx','4xx','5xx'],datasets:[{data:[0,0,0,0],backgroundColor:['#2ecc71','#3498db','#f39c12','#e74c3c']}]},options:{responsive:true,maintainAspectRatio:false,animation:false,layout:{padding:{top:30}},plugins:{legend:{display:true,position:'right',labels:{color:'#fff',font:{size:11}}}}}});\n"
" const methodsChart = new Chart(document.getElementById('methodsChart'),{type:'bar',data:{labels:['GET','POST','PUT','DEL','PATCH','HEAD','OPT'],datasets:[{data:[0,0,0,0,0,0,0],backgroundColor:'#3498db'}]},options:{indexAxis:'y',responsive:true,maintainAspectRatio:false,animation:false,layout:{padding:{top:30}},plugins:{legend:{display:false}},scales:{x:{grid:{color:'#2a2e3e'},ticks:{color:'#666'}},y:{grid:{display:false},ticks:{color:'#666'}}}}});\n"
" const efficiencyChart = new Chart(document.getElementById('efficiencyChart'),{type:'doughnut',data:{labels:['Zero-Copy','Buffered'],datasets:[{data:[0,0],backgroundColor:['#2ecc71','#9b59b6']}]},options:{responsive:true,maintainAspectRatio:false,animation:false,layout:{padding:{top:30}},plugins:{legend:{display:true,position:'right',labels:{color:'#fff',font:{size:11}}}}}});\n"
" window.vhostCharts={}; let prevNames=[];\n"
" async function updateStats(){\n"
" try{\n"
" const r=await fetch('/rproxy/api/stats'),d=await r.json();\n"
" const hs=d.current.health_score||100; document.getElementById('healthScore').textContent=hs.toFixed(0);\n"
" const hEl=document.getElementById('healthScore'); hEl.className='metric-value '+(hs>=80?'health':hs>=50?'warning':'danger');\n"
" document.getElementById('rps').textContent=formatNum(d.current.requests_per_second||0);\n"
" document.getElementById('p50').textContent=formatMs(d.latency?.p50_ms||0);\n"
" document.getElementById('p99').textContent=formatMs(d.latency?.p99_ms||0);\n"
" document.getElementById('errorRate').textContent=((d.current.error_rate_1m||0)*100).toFixed(1)+'%';\n"
" document.getElementById('connections').textContent=formatNum(d.current.active_connections);\n"
" document.getElementById('cpu').textContent=d.current.cpu_percent+'%';\n"
" document.getElementById('memory').textContent=d.current.memory_gb+'GB';\n"
" document.getElementById('uptime').textContent=formatUptime(d.current.uptime_seconds||0);\n"
" cpuChart.data.datasets[0].data=d.cpu_history; memChart.data.datasets[0].data=d.memory_history;\n"
" netChart.data.datasets[0].data=d.network_rx_history; netChart.data.datasets[1].data=d.network_tx_history;\n"
" diskChart.data.datasets[0].data=d.disk_read_history; diskChart.data.datasets[1].data=d.disk_write_history;\n"
" loadChart.data.datasets[0].data=d.load1_history; loadChart.data.datasets[1].data=d.load5_history; loadChart.data.datasets[2].data=d.load15_history;\n"
" [cpuChart,memChart,netChart,diskChart,loadChart].forEach(c=>c.update('none'));\n"
" if(d.latency?.histogram){latencyHistChart.data.labels=d.latency.bucket_labels||[]; latencyHistChart.data.datasets[0].data=d.latency.histogram; latencyHistChart.update('none');}\n"
" if(d.status_codes){statusChart.data.datasets[0].data=[d.status_codes['2xx']||0,d.status_codes['3xx']||0,d.status_codes['4xx']||0,d.status_codes['5xx']||0]; statusChart.update('none');}\n"
" if(d.methods){methodsChart.data.datasets[0].data=[d.methods.GET||0,d.methods.POST||0,d.methods.PUT||0,d.methods.DELETE||0,d.methods.PATCH||0,d.methods.HEAD||0,d.methods.OPTIONS||0]; methodsChart.update('none');}\n"
" if(d.efficiency){efficiencyChart.data.datasets[0].data=[d.efficiency.splice_transfers||0,d.efficiency.buffer_transfers||0]; efficiencyChart.update('none');}\n"
" const tbody=document.getElementById('processTable'),names=d.processes.map(p=>p.name).join(',');\n"
" if(names!==prevNames.join(',')){ Object.values(window.vhostCharts).forEach(c=>c.destroy()); window.vhostCharts={}; tbody.innerHTML=''; prevNames=d.processes.map(p=>p.name); }\n"
" d.processes.forEach((p,i)=>{\n"
" let row=tbody.children[i*2]; if(!row){row=document.createElement('tr'); tbody.appendChild(row);}\n"
" const errP=(p.error_rate*100).toFixed(1); const cls=parseFloat(errP)>5?'status-error':parseFloat(errP)>1?'status-warn':'status-ok';\n"
" row.innerHTML=`<td><span class='status-indicator ${cls}'></span>${p.name}</td><td>${formatNum(p.rps||0)}</td><td>${formatNum(p.total_requests)}</td><td>${formatMs(p.latency_p50||0)}</td><td>${formatMs(p.latency_p99||0)}</td><td>${formatNum(p.status_2xx||0)}</td><td>${formatNum(p.status_4xx||0)}</td><td>${formatNum(p.status_5xx||0)}</td><td>${errP}%</td><td>${formatBytes(p.bytes_sent)}</td><td>${formatBytes(p.bytes_recv)}</td>`;\n"
" let chartRow=tbody.children[i*2+1]; if(!chartRow){chartRow=document.createElement('tr'); chartRow.innerHTML=`<td colspan='11' style='padding:10px 0;border:none;'><div class='vhost-chart-container'><div class='chart-title'>Throughput - ${p.name}</div><canvas id='vc${i}'></canvas></div></td>`; tbody.appendChild(chartRow);}\n"
" const cid='vc'+i,cv=document.getElementById(cid); if(!cv)return;\n"
" if(!window.vhostCharts[cid]){const cols=[{b:'#3498db',bg:'rgba(52,152,219,0.1)'},{b:'#2ecc71',bg:'rgba(46,204,113,0.1)'},{b:'#f39c12',bg:'rgba(243,156,18,0.1)'},{b:'#e74c3c',bg:'rgba(231,76,60,0.1)'}],c=cols[i%4]; window.vhostCharts[cid]=new Chart(cv,{type:'line',data:{datasets:[{data:p.throughput_history||[],borderColor:c.b,backgroundColor:c.bg}]},options:baseOpts(60,formatSize)});}\n"
" else{window.vhostCharts[cid].data.datasets[0].data=p.throughput_history||[]; window.vhostCharts[cid].update('none');}\n"
" });\n"
" }catch(e){console.error('Stats error:',e);}\n"
" }\n"
" updateStats(); setInterval(updateStats,1000);\n"
" </script>\n"
"</body>\n"
"</html>\n";
static void send_unauthorized_response(connection_t *conn) {
time_t now = time(NULL);
struct tm *gmt = gmtime(&now);
char date_buf[64];
strftime(date_buf, sizeof(date_buf), "%a, %d %b %Y %H:%M:%S GMT", gmt);
const char *body = "Unauthorized";
char header[1024];
int len = snprintf(header, sizeof(header),
"HTTP/1.1 401 Unauthorized\r\n"
"Content-Type: text/plain; charset=utf-8\r\n"
"Content-Length: %zu\r\n"
"WWW-Authenticate: Basic realm=\"RProxy Dashboard\"\r\n"
"Date: %s\r\n"
"Connection: close\r\n"
"\r\n"
"%s",
strlen(body), date_buf, body);
if (buffer_ensure_capacity(&conn->write_buf, conn->write_buf.tail + len) == 0) {
memcpy(conn->write_buf.data + conn->write_buf.tail, header, len);
conn->write_buf.tail += len;
struct epoll_event event = { .data.fd = conn->fd, .events = EPOLLIN | EPOLLOUT };
epoll_ctl(epoll_fd, EPOLL_CTL_MOD, conn->fd, &event);
}
conn->state = CLIENT_STATE_ERROR;
conn->request.keep_alive = 0;
}
static int check_dashboard_auth(connection_t *conn, const char *request_data, size_t request_len) {
if (!auth_is_enabled()) return 1;
char auth_header[512] = "";
http_find_header_value(request_data, request_len, "Authorization", auth_header, sizeof(auth_header));
char error_msg[256] = "";
if (!auth_check_basic_auth(auth_header[0] ? auth_header : NULL, error_msg, sizeof(error_msg))) {
send_unauthorized_response(conn);
return 0;
}
return 1;
}
void dashboard_serve(connection_t *conn, const char *request_data, size_t request_len) {
if (!conn) return;
if (!check_dashboard_auth(conn, request_data, request_len)) {
return;
}
size_t content_len = strlen(DASHBOARD_HTML);
char header[512];
int len = snprintf(header, sizeof(header),
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Content-Length: %zu\r\n"
"Connection: %s\r\n"
"Cache-Control: no-cache\r\n"
"\r\n",
content_len,
conn->request.keep_alive ? "keep-alive" : "close");
if (buffer_ensure_capacity(&conn->write_buf, conn->write_buf.tail + len + content_len) < 0) {
connection_send_error_response(conn, 500, "Internal Server Error", "Memory allocation failed");
return;
}
memcpy(conn->write_buf.data + conn->write_buf.tail, header, len);
conn->write_buf.tail += len;
memcpy(conn->write_buf.data + conn->write_buf.tail, DASHBOARD_HTML, content_len);
conn->write_buf.tail += content_len;
struct epoll_event event = { .data.fd = conn->fd, .events = EPOLLIN | EPOLLOUT };
epoll_ctl(epoll_fd, EPOLL_CTL_MOD, conn->fd, &event);
}
static cJSON* format_history(history_deque_t *dq, int window_seconds) {
cJSON *arr = cJSON_CreateArray();
if (!arr) return NULL;
if (!dq || !dq->points || dq->count == 0) return arr;
double current_time = time(NULL);
int start_index = (dq->head - dq->count + dq->capacity) % dq->capacity;
for (int i = 0; i < dq->count; ++i) {
int current_index = (start_index + i) % dq->capacity;
history_point_t *p = &dq->points[current_index];
if ((current_time - p->time) <= window_seconds) {
cJSON *pt = cJSON_CreateObject();
if (pt) {
cJSON_AddNumberToObject(pt, "x", (long)((p->time - current_time) * 1000));
cJSON_AddNumberToObject(pt, "y", p->value);
cJSON_AddItemToArray(arr, pt);
}
}
}
return arr;
}
static cJSON* format_network_history(network_history_deque_t *dq, int window_seconds, const char *key) {
cJSON *arr = cJSON_CreateArray();
if (!arr) return NULL;
if (!dq || !dq->points || !key || dq->count == 0) return arr;
double current_time = time(NULL);
int start_index = (dq->head - dq->count + dq->capacity) % dq->capacity;
for (int i = 0; i < dq->count; ++i) {
int current_index = (start_index + i) % dq->capacity;
network_history_point_t *p = &dq->points[current_index];
if ((current_time - p->time) <= window_seconds) {
cJSON *pt = cJSON_CreateObject();
if (pt) {
cJSON_AddNumberToObject(pt, "x", (long)((p->time - current_time) * 1000));
cJSON_AddNumberToObject(pt, "y", strcmp(key, "rx_kbps") == 0 ? p->rx_kbps : p->tx_kbps);
cJSON_AddItemToArray(arr, pt);
}
}
}
return arr;
}
static cJSON* format_disk_history(disk_history_deque_t *dq, int window_seconds, const char *key) {
cJSON *arr = cJSON_CreateArray();
if (!arr) return NULL;
if (!dq || !dq->points || !key || dq->count == 0) return arr;
double current_time = time(NULL);
int start_index = (dq->head - dq->count + dq->capacity) % dq->capacity;
for (int i = 0; i < dq->count; ++i) {
int current_index = (start_index + i) % dq->capacity;
disk_history_point_t *p = &dq->points[current_index];
if ((current_time - p->time) <= window_seconds) {
cJSON *pt = cJSON_CreateObject();
if (pt) {
cJSON_AddNumberToObject(pt, "x", (long)((p->time - current_time) * 1000));
cJSON_AddNumberToObject(pt, "y", strcmp(key, "read_mbps") == 0 ? p->read_mbps : p->write_mbps);
cJSON_AddItemToArray(arr, pt);
}
}
}
return arr;
}
void dashboard_serve_stats_api(connection_t *conn, const char *request_data, size_t request_len) {
if (!conn) return;
if (!check_dashboard_auth(conn, request_data, request_len)) {
return;
}
cJSON *root = cJSON_CreateObject();
if (!root) {
connection_send_error_response(conn, 500, "Internal Server Error", "JSON creation failed");
return;
}
cJSON *current = cJSON_CreateObject();
if (!current) {
cJSON_Delete(root);
connection_send_error_response(conn, 500, "Internal Server Error", "JSON creation failed");
return;
}
cJSON_AddItemToObject(root, "current", current);
char buffer[64];
double last_cpu = 0, last_mem = 0;
double load1 = 0, load5 = 0, load15 = 0;
if (monitor.cpu_history.count > 0) {
int last_idx = (monitor.cpu_history.head - 1 + monitor.cpu_history.capacity) % monitor.cpu_history.capacity;
last_cpu = monitor.cpu_history.points[last_idx].value;
}
if (monitor.memory_history.count > 0) {
int last_idx = (monitor.memory_history.head - 1 + monitor.memory_history.capacity) % monitor.memory_history.capacity;
last_mem = monitor.memory_history.points[last_idx].value;
}
if (monitor.load1_history.count > 0) {
int idx = (monitor.load1_history.head - 1 + monitor.load1_history.capacity) % monitor.load1_history.capacity;
load1 = monitor.load1_history.points[idx].value;
}
if (monitor.load5_history.count > 0) {
int idx = (monitor.load5_history.head - 1 + monitor.load5_history.capacity) % monitor.load5_history.capacity;
load5 = monitor.load5_history.points[idx].value;
}
if (monitor.load15_history.count > 0) {
int idx = (monitor.load15_history.head - 1 + monitor.load15_history.capacity) % monitor.load15_history.capacity;
load15 = monitor.load15_history.points[idx].value;
}
snprintf(buffer, sizeof(buffer), "%.2f", last_cpu);
cJSON_AddStringToObject(current, "cpu_percent", buffer);
snprintf(buffer, sizeof(buffer), "%.2f", last_mem);
cJSON_AddStringToObject(current, "memory_gb", buffer);
cJSON_AddNumberToObject(current, "active_connections", monitor.active_connections);
cJSON_AddNumberToObject(current, "load_1m", load1);
cJSON_AddNumberToObject(current, "load_5m", load5);
cJSON_AddNumberToObject(current, "load_15m", load15);
monitor_compute_health_score();
double current_rps = monitor_get_current_rps();
cJSON_AddNumberToObject(current, "health_score", monitor.health_score);
cJSON_AddNumberToObject(current, "requests_per_second", current_rps);
cJSON_AddNumberToObject(current, "error_rate_1m", monitor.error_rate_1m);
cJSON_AddNumberToObject(current, "uptime_seconds", (double)(time(NULL) - monitor.uptime_start));
cJSON_AddNumberToObject(current, "peak_rps", monitor.peak_rps);
cJSON_AddNumberToObject(current, "total_connections", (double)monitor.total_connections_accepted);
cJSON *latency = cJSON_CreateObject();
if (latency) {
cJSON_AddNumberToObject(latency, "p50_ms", histogram_percentile(&monitor.global_latency, 0.50));
cJSON_AddNumberToObject(latency, "p90_ms", histogram_percentile(&monitor.global_latency, 0.90));
cJSON_AddNumberToObject(latency, "p95_ms", histogram_percentile(&monitor.global_latency, 0.95));
cJSON_AddNumberToObject(latency, "p99_ms", histogram_percentile(&monitor.global_latency, 0.99));
cJSON_AddNumberToObject(latency, "mean_ms", histogram_mean(&monitor.global_latency));
cJSON *histogram_arr = cJSON_CreateArray();
if (histogram_arr) {
for (int i = 0; i < HISTOGRAM_BUCKETS; i++) {
cJSON_AddItemToArray(histogram_arr, cJSON_CreateNumber(monitor.global_latency.buckets[i]));
}
cJSON_AddItemToObject(latency, "histogram", histogram_arr);
}
cJSON *labels = cJSON_CreateArray();
if (labels) {
for (int i = 0; i < HISTOGRAM_BUCKETS; i++) {
cJSON_AddItemToArray(labels, cJSON_CreateString(LATENCY_BUCKET_LABELS[i]));
}
cJSON_AddItemToObject(latency, "bucket_labels", labels);
}
cJSON_AddItemToObject(root, "latency", latency);
}
uint64_t total_2xx = 0, total_3xx = 0, total_4xx = 0, total_5xx = 0;
uint64_t methods[HTTP_METHOD_COUNT] = {0};
uint64_t total_splice = 0, total_buffer = 0;
uint64_t total_splice_bytes = 0, total_buffer_bytes = 0;
for (vhost_stats_t *s = monitor.vhost_stats_head; s; s = s->next) {
total_2xx += s->status_counts.status_2xx;
total_3xx += s->status_counts.status_3xx;
total_4xx += s->status_counts.status_4xx;
total_5xx += s->status_counts.status_5xx;
for (int i = 0; i < HTTP_METHOD_COUNT; i++) {
methods[i] += s->method_counts.counts[i];
}
total_splice += s->splice_transfers;
total_buffer += s->buffered_transfers;
total_splice_bytes += s->bytes_via_splice;
total_buffer_bytes += s->bytes_via_buffer;
}
cJSON *status_codes = cJSON_CreateObject();
if (status_codes) {
cJSON_AddNumberToObject(status_codes, "2xx", (double)total_2xx);
cJSON_AddNumberToObject(status_codes, "3xx", (double)total_3xx);
cJSON_AddNumberToObject(status_codes, "4xx", (double)total_4xx);
cJSON_AddNumberToObject(status_codes, "5xx", (double)total_5xx);
cJSON_AddItemToObject(root, "status_codes", status_codes);
}
cJSON *methods_obj = cJSON_CreateObject();
if (methods_obj) {
cJSON_AddNumberToObject(methods_obj, "GET", (double)methods[HTTP_METHOD_GET]);
cJSON_AddNumberToObject(methods_obj, "POST", (double)methods[HTTP_METHOD_POST]);
cJSON_AddNumberToObject(methods_obj, "PUT", (double)methods[HTTP_METHOD_PUT]);
cJSON_AddNumberToObject(methods_obj, "DELETE", (double)methods[HTTP_METHOD_DELETE]);
cJSON_AddNumberToObject(methods_obj, "PATCH", (double)methods[HTTP_METHOD_PATCH]);
cJSON_AddNumberToObject(methods_obj, "HEAD", (double)methods[HTTP_METHOD_HEAD]);
cJSON_AddNumberToObject(methods_obj, "OPTIONS", (double)methods[HTTP_METHOD_OPTIONS]);
cJSON_AddNumberToObject(methods_obj, "OTHER", (double)methods[HTTP_METHOD_OTHER]);
cJSON_AddItemToObject(root, "methods", methods_obj);
}
cJSON *efficiency = cJSON_CreateObject();
if (efficiency) {
double total_transfers = (double)(total_splice + total_buffer);
double splice_ratio = total_transfers > 0 ? (double)total_splice / total_transfers : 0;
cJSON_AddNumberToObject(efficiency, "zero_copy_ratio", splice_ratio);
cJSON_AddNumberToObject(efficiency, "splice_transfers", (double)total_splice);
cJSON_AddNumberToObject(efficiency, "buffer_transfers", (double)total_buffer);
cJSON_AddNumberToObject(efficiency, "bytes_via_splice", (double)total_splice_bytes);
cJSON_AddNumberToObject(efficiency, "bytes_via_buffer", (double)total_buffer_bytes);
cJSON_AddItemToObject(root, "efficiency", efficiency);
}
cJSON_AddItemToObject(root, "cpu_history", format_history(&monitor.cpu_history, HISTORY_SECONDS));
cJSON_AddItemToObject(root, "memory_history", format_history(&monitor.memory_history, HISTORY_SECONDS));
cJSON_AddItemToObject(root, "network_rx_history", format_network_history(&monitor.network_history, HISTORY_SECONDS, "rx_kbps"));
cJSON_AddItemToObject(root, "network_tx_history", format_network_history(&monitor.network_history, HISTORY_SECONDS, "tx_kbps"));
cJSON_AddItemToObject(root, "disk_read_history", format_disk_history(&monitor.disk_history, HISTORY_SECONDS, "read_mbps"));
cJSON_AddItemToObject(root, "disk_write_history", format_disk_history(&monitor.disk_history, HISTORY_SECONDS, "write_mbps"));
cJSON_AddItemToObject(root, "throughput_history", format_history(&monitor.throughput_history, HISTORY_SECONDS));
cJSON_AddItemToObject(root, "load1_history", format_history(&monitor.load1_history, HISTORY_SECONDS));
cJSON_AddItemToObject(root, "load5_history", format_history(&monitor.load5_history, HISTORY_SECONDS));
cJSON_AddItemToObject(root, "load15_history", format_history(&monitor.load15_history, HISTORY_SECONDS));
cJSON *processes = cJSON_CreateArray();
if (processes) {
cJSON_AddItemToObject(root, "processes", processes);
for (vhost_stats_t *s = monitor.vhost_stats_head; s; s = s->next) {
cJSON *p = cJSON_CreateObject();
if (p) {
cJSON_AddStringToObject(p, "name", s->vhost_name);
cJSON_AddNumberToObject(p, "http_requests", s->http_requests);
cJSON_AddNumberToObject(p, "websocket_requests", s->websocket_requests);
cJSON_AddNumberToObject(p, "total_requests", s->total_requests);
cJSON_AddNumberToObject(p, "avg_request_time_ms", s->avg_request_time_ms);
cJSON_AddNumberToObject(p, "bytes_sent", s->bytes_sent);
cJSON_AddNumberToObject(p, "bytes_recv", s->bytes_recv);
cJSON_AddItemToObject(p, "throughput_history", format_history(&s->throughput_history, 60));
cJSON_AddNumberToObject(p, "latency_p50", histogram_percentile(&s->latency_histogram, 0.50));
cJSON_AddNumberToObject(p, "latency_p90", histogram_percentile(&s->latency_histogram, 0.90));
cJSON_AddNumberToObject(p, "latency_p95", histogram_percentile(&s->latency_histogram, 0.95));
cJSON_AddNumberToObject(p, "latency_p99", histogram_percentile(&s->latency_histogram, 0.99));
cJSON_AddNumberToObject(p, "rps", rate_tracker_get_rps(&s->requests_per_second));
double vhost_total = (double)(s->status_counts.status_2xx + s->status_counts.status_3xx +
s->status_counts.status_4xx + s->status_counts.status_5xx);
double error_rate = vhost_total > 0 ? (double)s->status_counts.status_5xx / vhost_total : 0;
cJSON_AddNumberToObject(p, "error_rate", error_rate);
cJSON_AddNumberToObject(p, "status_2xx", (double)s->status_counts.status_2xx);
cJSON_AddNumberToObject(p, "status_3xx", (double)s->status_counts.status_3xx);
cJSON_AddNumberToObject(p, "status_4xx", (double)s->status_counts.status_4xx);
cJSON_AddNumberToObject(p, "status_5xx", (double)s->status_counts.status_5xx);
cJSON_AddNumberToObject(p, "upstream_success", (double)s->upstream_connect_success);
cJSON_AddNumberToObject(p, "upstream_failures", (double)s->upstream_connect_failures);
cJSON_AddNumberToObject(p, "dns_failures", (double)s->dns_failures);
cJSON_AddNumberToObject(p, "ssl_failures", (double)s->ssl_failures);
cJSON_AddNumberToObject(p, "timeout_errors", (double)s->timeout_errors);
cJSON_AddItemToArray(processes, p);
}
}
}
char *json_string = cJSON_PrintUnformatted(root);
if (!json_string) {
cJSON_Delete(root);
connection_send_error_response(conn, 500, "Internal Server Error", "JSON serialization failed");
return;
}
char header[512];
int hlen = snprintf(header, sizeof(header),
"HTTP/1.1 200 OK\r\n"
"Content-Type: application/json; charset=utf-8\r\n"
"Content-Length: %zu\r\n"
"Connection: %s\r\n"
"Cache-Control: no-cache\r\n"
"\r\n",
strlen(json_string),
conn->request.keep_alive ? "keep-alive" : "close");
if (buffer_ensure_capacity(&conn->write_buf, conn->write_buf.tail + hlen + strlen(json_string)) < 0) {
free(json_string);
cJSON_Delete(root);
connection_send_error_response(conn, 500, "Internal Server Error", "Memory allocation failed");
return;
}
memcpy(conn->write_buf.data + conn->write_buf.tail, header, hlen);
conn->write_buf.tail += hlen;
memcpy(conn->write_buf.data + conn->write_buf.tail, json_string, strlen(json_string));
conn->write_buf.tail += strlen(json_string);
struct epoll_event event = { .data.fd = conn->fd, .events = EPOLLIN | EPOLLOUT };
epoll_ctl(epoll_fd, EPOLL_CTL_MOD, conn->fd, &event);
cJSON_Delete(root);
free(json_string);
}

9
src/dashboard.h Executable file
View File

@ -0,0 +1,9 @@
#ifndef RPROXY_DASHBOARD_H
#define RPROXY_DASHBOARD_H
#include "types.h"
void dashboard_serve(connection_t *conn, const char *request_data, size_t request_len);
void dashboard_serve_stats_api(connection_t *conn, const char *request_data, size_t request_len);
#endif

108
src/deque.c Normal file
View File

@ -0,0 +1,108 @@
// retoor <retoor@molodetz.nl>
#include "deque.h"
#include <stdlib.h>
void history_deque_init(history_deque_t *dq, int capacity) {
if (!dq) return;
dq->points = calloc((size_t)capacity, sizeof(history_point_t));
dq->capacity = capacity;
dq->head = 0;
dq->count = 0;
}
void history_deque_push(history_deque_t *dq, double time, double value) {
if (!dq || !dq->points) return;
dq->points[dq->head] = (history_point_t){ .time = time, .value = value };
dq->head = (dq->head + 1) % dq->capacity;
if (dq->count < dq->capacity) dq->count++;
}
void history_deque_free(history_deque_t *dq) {
if (!dq) return;
if (dq->points) {
free(dq->points);
dq->points = NULL;
}
dq->capacity = 0;
dq->head = 0;
dq->count = 0;
}
void network_history_deque_init(network_history_deque_t *dq, int capacity) {
if (!dq) return;
dq->points = calloc((size_t)capacity, sizeof(network_history_point_t));
dq->capacity = capacity;
dq->head = 0;
dq->count = 0;
}
void network_history_deque_push(network_history_deque_t *dq, double time, double rx, double tx) {
if (!dq || !dq->points) return;
dq->points[dq->head] = (network_history_point_t){ .time = time, .rx_kbps = rx, .tx_kbps = tx };
dq->head = (dq->head + 1) % dq->capacity;
if (dq->count < dq->capacity) dq->count++;
}
void network_history_deque_free(network_history_deque_t *dq) {
if (!dq) return;
if (dq->points) {
free(dq->points);
dq->points = NULL;
}
dq->capacity = 0;
dq->head = 0;
dq->count = 0;
}
void disk_history_deque_init(disk_history_deque_t *dq, int capacity) {
if (!dq) return;
dq->points = calloc((size_t)capacity, sizeof(disk_history_point_t));
dq->capacity = capacity;
dq->head = 0;
dq->count = 0;
}
void disk_history_deque_push(disk_history_deque_t *dq, double time, double read_mbps, double write_mbps) {
if (!dq || !dq->points) return;
dq->points[dq->head] = (disk_history_point_t){ .time = time, .read_mbps = read_mbps, .write_mbps = write_mbps };
dq->head = (dq->head + 1) % dq->capacity;
if (dq->count < dq->capacity) dq->count++;
}
void disk_history_deque_free(disk_history_deque_t *dq) {
if (!dq) return;
if (dq->points) {
free(dq->points);
dq->points = NULL;
}
dq->capacity = 0;
dq->head = 0;
dq->count = 0;
}
void request_time_deque_init(request_time_deque_t *dq, int capacity) {
if (!dq) return;
dq->times = calloc((size_t)capacity, sizeof(double));
dq->capacity = capacity;
dq->head = 0;
dq->count = 0;
}
void request_time_deque_push(request_time_deque_t *dq, double time_ms) {
if (!dq || !dq->times) return;
dq->times[dq->head] = time_ms;
dq->head = (dq->head + 1) % dq->capacity;
if (dq->count < dq->capacity) dq->count++;
}
void request_time_deque_free(request_time_deque_t *dq) {
if (!dq) return;
if (dq->times) {
free(dq->times);
dq->times = NULL;
}
dq->capacity = 0;
dq->head = 0;
dq->count = 0;
}

24
src/deque.h Normal file
View File

@ -0,0 +1,24 @@
// retoor <retoor@molodetz.nl>
#ifndef RPROXY_DEQUE_H
#define RPROXY_DEQUE_H
#include "types.h"
void history_deque_init(history_deque_t *dq, int capacity);
void history_deque_push(history_deque_t *dq, double time, double value);
void history_deque_free(history_deque_t *dq);
void network_history_deque_init(network_history_deque_t *dq, int capacity);
void network_history_deque_push(network_history_deque_t *dq, double time, double rx, double tx);
void network_history_deque_free(network_history_deque_t *dq);
void disk_history_deque_init(disk_history_deque_t *dq, int capacity);
void disk_history_deque_push(disk_history_deque_t *dq, double time, double read_mbps, double write_mbps);
void disk_history_deque_free(disk_history_deque_t *dq);
void request_time_deque_init(request_time_deque_t *dq, int capacity);
void request_time_deque_push(request_time_deque_t *dq, double time_ms);
void request_time_deque_free(request_time_deque_t *dq);
#endif

50
src/epoll_utils.c Normal file
View File

@ -0,0 +1,50 @@
// retoor <retoor@molodetz.nl>
#include "epoll_utils.h"
#include "logging.h"
#include "types.h"
#include <sys/epoll.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
int epoll_fd = -1;
extern connection_t connections[MAX_FDS];
int epoll_utils_create(void) {
epoll_fd = epoll_create1(EPOLL_CLOEXEC);
if (epoll_fd == -1) {
log_error("epoll_create1 failed");
return -1;
}
return epoll_fd;
}
void epoll_utils_add(int fd, uint32_t events) {
struct epoll_event event = { .data.fd = fd, .events = events };
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event) == -1) {
log_error("epoll_ctl_add failed");
close(fd);
} else if (fd >= 0 && fd < MAX_FDS) {
connections[fd].epoll_events = events;
}
}
void epoll_utils_modify(int fd, uint32_t events) {
if (fd >= 0 && fd < MAX_FDS && connections[fd].epoll_events == events) {
return;
}
struct epoll_event event = { .data.fd = fd, .events = events };
if (epoll_ctl(epoll_fd, EPOLL_CTL_MOD, fd, &event) == -1) {
if (errno != EBADF && errno != ENOENT) {
log_debug("epoll_ctl_mod failed for fd %d: %s", fd, strerror(errno));
}
} else if (fd >= 0 && fd < MAX_FDS) {
connections[fd].epoll_events = events;
}
}
void epoll_utils_remove(int fd) {
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
}

15
src/epoll_utils.h Normal file
View File

@ -0,0 +1,15 @@
// retoor <retoor@molodetz.nl>
#ifndef RPROXY_EPOLL_UTILS_H
#define RPROXY_EPOLL_UTILS_H
#include <stdint.h>
extern int epoll_fd;
int epoll_utils_create(void);
void epoll_utils_add(int fd, uint32_t events);
void epoll_utils_modify(int fd, uint32_t events);
void epoll_utils_remove(int fd);
#endif

167
src/forwarding.c Normal file
View File

@ -0,0 +1,167 @@
// retoor <retoor@molodetz.nl>
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "forwarding.h"
#include "buffer.h"
#include "http.h"
#include "monitor.h"
#include "patch.h"
#include "ssl_handler.h"
#include "logging.h"
#include "epoll_utils.h"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>
extern connection_t connections[MAX_FDS];
extern time_t cached_time;
static ssize_t forwarding_write(connection_t *dst, const char *data, size_t len) {
if (dst->ssl && dst->ssl_handshake_done) {
return ssl_write(dst, data, len);
} else if (!dst->ssl) {
return write(dst->fd, data, len);
}
return 0;
}
static ssize_t forwarding_write_all(connection_t *dst, const char *data, size_t len) {
size_t total_written = 0;
while (total_written < len) {
ssize_t n = forwarding_write(dst, data + total_written, len - total_written);
if (n > 0) {
total_written += (size_t)n;
} else if (n == 0) {
break;
} else {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break;
}
return -1;
}
}
return (ssize_t)total_written;
}
#ifdef __linux__
int forwarding_try_splice(connection_t *src, connection_t *dst) {
if (src->splice_pipe[0] < 0 || src->splice_pipe[1] < 0) {
return -1;
}
ssize_t bytes_to_pipe = splice(src->fd, NULL, src->splice_pipe[1], NULL,
CHUNK_SIZE, SPLICE_F_NONBLOCK | SPLICE_F_MOVE);
if (bytes_to_pipe <= 0) {
if (bytes_to_pipe == 0) return 0;
if (errno == EAGAIN || errno == EWOULDBLOCK) return -1;
return -2;
}
ssize_t bytes_from_pipe = splice(src->splice_pipe[0], NULL, dst->fd, NULL,
(size_t)bytes_to_pipe, SPLICE_F_NONBLOCK | SPLICE_F_MOVE);
if (bytes_from_pipe < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
char discard[CHUNK_SIZE];
if (read(src->splice_pipe[0], discard, (size_t)bytes_to_pipe) < 0) {
// Ignore
}
return -1;
}
return -2;
}
src->last_activity = cached_time;
dst->last_activity = cached_time;
if (src->vhost_stats) {
monitor_record_bytes(src->vhost_stats, bytes_from_pipe, 0);
monitor_record_splice_transfer(src->vhost_stats, (size_t)bytes_from_pipe);
}
return (int)bytes_from_pipe;
}
#else
int forwarding_try_splice(connection_t *src, connection_t *dst) {
(void)src;
(void)dst;
return -1;
}
#endif
void forwarding_handle(connection_t *conn, connection_t *pair, int direction) {
if (!conn || !pair) return;
#ifdef __linux__
int is_response = (direction == 1);
if (conn->can_splice &&
buffer_available_read(&conn->read_buf) == 0 &&
buffer_available_read(&pair->write_buf) == 0 &&
(!is_response || conn->response_headers_parsed)) {
int splice_result = forwarding_try_splice(conn, pair);
if (splice_result > 0) return;
if (splice_result == 0) {
conn->state = CLIENT_STATE_CLOSING;
return;
}
if (splice_result == -2) {
conn->state = CLIENT_STATE_CLOSING;
pair->state = CLIENT_STATE_CLOSING;
return;
}
}
#endif
size_t available = conn->read_buf.tail - conn->read_buf.head;
if (available == 0) return;
char *data = conn->read_buf.data + conn->read_buf.head;
if (direction == 1 && conn->type == CONN_TYPE_UPSTREAM && !conn->response_headers_parsed) {
size_t headers_end = 0;
if (http_find_headers_end(data, available, &headers_end)) {
conn->response_headers_parsed = 1;
if (conn->vhost_stats) {
int status = http_extract_status_code(data, available);
if (status > 0) {
monitor_record_status(conn->vhost_stats, status);
}
}
}
}
ssize_t written = forwarding_write_all(pair, data, available);
log_debug("Forward dir=%d src=%d dst=%d avail=%zu written=%zd",
direction, conn->fd, pair->fd, available, written);
if (written > 0) {
conn->read_buf.head += (size_t)written;
if (conn->vhost_stats) {
monitor_record_bytes(conn->vhost_stats, written, 0);
}
}
if (conn->read_buf.head >= conn->read_buf.tail) {
conn->read_buf.head = 0;
conn->read_buf.tail = 0;
} else if (written >= 0) {
epoll_utils_modify(pair->fd, EPOLLIN | EPOLLOUT);
}
if (written < 0) {
conn->state = CLIENT_STATE_CLOSING;
pair->state = CLIENT_STATE_CLOSING;
}
}

13
src/forwarding.h Normal file
View File

@ -0,0 +1,13 @@
// retoor <retoor@molodetz.nl>
#ifndef RPROXY_FORWARDING_H
#define RPROXY_FORWARDING_H
#include "types.h"
#include <stddef.h>
void forwarding_handle(connection_t *conn, connection_t *pair, int direction);
void forwarding_check_pipelining(connection_t *conn);
int forwarding_try_splice(connection_t *src, connection_t *dst);
#endif

171
src/health_check.c Executable file
View File

@ -0,0 +1,171 @@
#include "health_check.h"
#include "logging.h"
#include "config.h"
#include "types.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <fcntl.h>
#include <errno.h>
#include <poll.h>
typedef struct {
char hostname[HOSTNAME_MAX_LEN];
char upstream_host[HOSTNAME_MAX_LEN];
int upstream_port;
int healthy;
int consecutive_failures;
time_t last_check;
} upstream_health_t;
static upstream_health_t *health_states = NULL;
static int health_state_count = 0;
static int g_health_check_enabled = 0;
void health_check_init(void) {
if (health_states) {
free(health_states);
}
health_state_count = config->route_count;
if (health_state_count <= 0) {
health_states = NULL;
return;
}
health_states = calloc(health_state_count, sizeof(upstream_health_t));
if (!health_states) {
health_state_count = 0;
return;
}
for (int i = 0; i < health_state_count; i++) {
snprintf(health_states[i].hostname, sizeof(health_states[i].hostname), "%s", config->routes[i].hostname);
snprintf(health_states[i].upstream_host, sizeof(health_states[i].upstream_host), "%s", config->routes[i].upstream_host);
health_states[i].upstream_port = config->routes[i].upstream_port;
health_states[i].healthy = 1;
health_states[i].consecutive_failures = 0;
health_states[i].last_check = 0;
}
g_health_check_enabled = 1;
log_info("Health check initialized for %d upstreams", health_state_count);
}
void health_check_cleanup(void) {
if (health_states) {
free(health_states);
health_states = NULL;
}
health_state_count = 0;
g_health_check_enabled = 0;
}
static int check_tcp_connection(const char *host, int port, int timeout_ms) {
struct addrinfo hints, *result;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo(host, NULL, &hints, &result) != 0) {
return 0;
}
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
freeaddrinfo(result);
return 0;
}
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
struct sockaddr_in *addr = (struct sockaddr_in *)result->ai_addr;
addr->sin_port = htons(port);
int connect_result = connect(fd, (struct sockaddr *)addr, sizeof(struct sockaddr_in));
freeaddrinfo(result);
if (connect_result == 0) {
close(fd);
return 1;
}
if (errno != EINPROGRESS) {
close(fd);
return 0;
}
struct pollfd pfd;
pfd.fd = fd;
pfd.events = POLLOUT;
int poll_result = poll(&pfd, 1, timeout_ms);
if (poll_result <= 0) {
close(fd);
return 0;
}
int error = 0;
socklen_t len = sizeof(error);
getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &len);
close(fd);
return error == 0;
}
void health_check_run(void) {
if (!g_health_check_enabled) return;
time_t now = time(NULL);
for (int i = 0; i < health_state_count; i++) {
if (now - health_states[i].last_check < HEALTH_CHECK_INTERVAL_SECONDS) {
continue;
}
health_states[i].last_check = now;
int is_healthy = check_tcp_connection(
health_states[i].upstream_host,
health_states[i].upstream_port,
HEALTH_CHECK_TIMEOUT_MS
);
if (is_healthy) {
if (!health_states[i].healthy) {
log_info("Upstream %s:%d is now healthy",
health_states[i].upstream_host,
health_states[i].upstream_port);
}
health_states[i].healthy = 1;
health_states[i].consecutive_failures = 0;
} else {
health_states[i].consecutive_failures++;
if (health_states[i].consecutive_failures >= 3 && health_states[i].healthy) {
log_info("Upstream %s:%d is now unhealthy (failures: %d)",
health_states[i].upstream_host,
health_states[i].upstream_port,
health_states[i].consecutive_failures);
health_states[i].healthy = 0;
}
}
}
}
int health_check_is_healthy(const char *hostname) {
if (!g_health_check_enabled || !hostname) return 1;
for (int i = 0; i < health_state_count; i++) {
if (strcasecmp(health_states[i].hostname, hostname) == 0) {
return health_states[i].healthy;
}
}
return 1;
}

11
src/health_check.h Executable file
View File

@ -0,0 +1,11 @@
#ifndef RPROXY_HEALTH_CHECK_H
#define RPROXY_HEALTH_CHECK_H
#include "types.h"
void health_check_init(void);
void health_check_cleanup(void);
void health_check_run(void);
int health_check_is_healthy(const char *hostname);
#endif

69
src/histogram.c Normal file
View File

@ -0,0 +1,69 @@
// retoor <retoor@molodetz.nl>
#include "histogram.h"
#include <string.h>
const double LATENCY_BUCKET_BOUNDS[HISTOGRAM_BUCKETS] = {
1.0, 2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0,
500.0, 1000.0, 2500.0, 5000.0, 10000.0, 30000.0, 60000.0, 1e9
};
const char *LATENCY_BUCKET_LABELS[HISTOGRAM_BUCKETS] = {
"0-1ms", "1-2ms", "2-5ms", "5-10ms", "10-25ms", "25-50ms", "50-100ms", "100-250ms",
"250-500ms", "500ms-1s", "1-2.5s", "2.5-5s", "5-10s", "10-30s", "30-60s", "60s+"
};
const double SIZE_BUCKET_BOUNDS[HISTOGRAM_BUCKETS] = {
128.0, 512.0, 1024.0, 4096.0, 16384.0, 65536.0, 262144.0, 1048576.0,
4194304.0, 16777216.0, 67108864.0, 268435456.0, 1073741824.0, 4294967296.0, 1e15, 1e18
};
void histogram_init(histogram_t *h) {
if (!h) return;
memset(h, 0, sizeof(histogram_t));
h->min_value = 1e18;
h->max_value = -1e18;
}
static void histogram_add_internal(histogram_t *h, double value, const double *bounds) {
if (!h) return;
h->total_count++;
h->sum += value;
if (value < h->min_value) h->min_value = value;
if (value > h->max_value) h->max_value = value;
for (int i = 0; i < HISTOGRAM_BUCKETS; i++) {
if (value <= bounds[i]) {
h->buckets[i]++;
return;
}
}
h->overflow++;
}
void histogram_add(histogram_t *h, double value) {
histogram_add_internal(h, value, LATENCY_BUCKET_BOUNDS);
}
void histogram_add_with_bounds(histogram_t *h, double value, const double *bounds) {
histogram_add_internal(h, value, bounds);
}
double histogram_percentile(histogram_t *h, double p) {
if (!h || h->total_count == 0) return 0.0;
uint64_t target = (uint64_t)(h->total_count * p);
uint64_t cumulative = 0;
for (int i = 0; i < HISTOGRAM_BUCKETS; i++) {
cumulative += h->buckets[i];
if (cumulative >= target) {
return LATENCY_BUCKET_BOUNDS[i];
}
}
return LATENCY_BUCKET_BOUNDS[HISTOGRAM_BUCKETS - 1];
}
double histogram_mean(histogram_t *h) {
if (!h || h->total_count == 0) return 0.0;
return h->sum / h->total_count;
}

18
src/histogram.h Normal file
View File

@ -0,0 +1,18 @@
// retoor <retoor@molodetz.nl>
#ifndef RPROXY_HISTOGRAM_H
#define RPROXY_HISTOGRAM_H
#include "types.h"
extern const double LATENCY_BUCKET_BOUNDS[HISTOGRAM_BUCKETS];
extern const char *LATENCY_BUCKET_LABELS[HISTOGRAM_BUCKETS];
extern const double SIZE_BUCKET_BOUNDS[HISTOGRAM_BUCKETS];
void histogram_init(histogram_t *h);
void histogram_add(histogram_t *h, double value);
void histogram_add_with_bounds(histogram_t *h, double value, const double *bounds);
double histogram_percentile(histogram_t *h, double p);
double histogram_mean(histogram_t *h);
#endif

378
src/http.c Executable file
View File

@ -0,0 +1,378 @@
#include "http.h"
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
int http_find_header_value(const char* data, size_t len, const char* name, char* value, size_t value_size) {
size_t name_len = strlen(name);
const char* end = data + len;
for (const char* p = data; p < end; ) {
const char* line_end = memchr(p, '\n', end - p);
if (!line_end) break;
if (line_end > p && *(line_end - 1) == '\r') {
line_end--;
}
if ((size_t)(line_end - p) > name_len && strncasecmp(p, name, name_len) == 0 && p[name_len] == ':') {
const char* v_start = p + name_len + 1;
while (v_start < line_end && (*v_start == ' ' || *v_start == '\t')) v_start++;
const char* v_end = line_end;
while (v_end > v_start && (*(v_end - 1) == ' ' || *(v_end - 1) == '\t')) v_end--;
size_t copy_len = v_end - v_start;
if (copy_len >= value_size) copy_len = value_size - 1;
memcpy(value, v_start, copy_len);
value[copy_len] = '\0';
return 1;
}
p = line_end + ( (line_end < end && *(line_end) == '\r') ? 2 : 1 );
}
return 0;
}
static int is_valid_method_char(char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
}
static int is_valid_http_method(const char *method, size_t len) {
if (len == 0 || len > 31) return 0;
for (size_t i = 0; i < len; i++) {
if (!is_valid_method_char(method[i])) return 0;
}
return 1;
}
int http_is_request_start(const char *data, size_t len) {
if (len < 4) return 0;
const char *space = memchr(data, ' ', len > 32 ? 32 : len);
if (!space) return 0;
size_t method_len = space - data;
if (method_len == 0 || method_len > 31) return 0;
for (size_t i = 0; i < method_len; i++) {
if (!is_valid_method_char(data[i])) return 0;
}
return 1;
}
int http_parse_request(const char *data, size_t len, http_request_t *req) {
memset(req, 0, sizeof(http_request_t));
req->content_length = -1;
req->keep_alive = 1;
const char *line_end = memchr(data, '\n', len);
if (!line_end) return -1;
size_t line_len = line_end - data;
if (line_len > 0 && data[line_len - 1] == '\r') {
line_len--;
}
const char *method_end = memchr(data, ' ', line_len);
if (!method_end) return 0;
size_t method_len = method_end - data;
if (method_len >= sizeof(req->method)) return 0;
memcpy(req->method, data, method_len);
req->method[method_len] = '\0';
if (!is_valid_http_method(req->method, method_len)) return 0;
const char *uri_start = method_end + 1;
while (uri_start < data + line_len && *uri_start == ' ') uri_start++;
const char *uri_end = data + line_len;
const char *version_start = NULL;
for (const char *p = uri_end - 1; p > uri_start; p--) {
if (*p == ' ') {
version_start = p + 1;
uri_end = p;
break;
}
}
if (!version_start || version_start == uri_start) return 0;
size_t uri_len = uri_end - uri_start;
if (uri_len >= sizeof(req->uri)) return 0;
memcpy(req->uri, uri_start, uri_len);
req->uri[uri_len] = '\0';
while (version_start < data + line_len && *version_start == ' ') version_start++;
const char *actual_line_end = data + line_len;
size_t version_len = actual_line_end - version_start;
if (version_len >= sizeof(req->version)) return 0;
memcpy(req->version, version_start, version_len);
req->version[version_len] = '\0';
if (strncmp(req->version, "HTTP/1.0", 8) == 0) {
req->keep_alive = 0;
}
const char *headers_start = line_end + 1;
char value[1024];
if (http_find_header_value(headers_start, len - (headers_start - data), "Host", req->host, sizeof(req->host))) {
char *port_colon = strchr(req->host, ':');
if (port_colon) *port_colon = '\0';
}
if (http_find_header_value(headers_start, len - (headers_start - data), "Content-Length", value, sizeof(value))) {
char *endptr;
long parsed = strtol(value, &endptr, 10);
if (endptr != value && *endptr == '\0' && parsed >= 0) {
req->content_length = parsed;
}
}
if (http_find_header_value(headers_start, len - (headers_start - data), "Transfer-Encoding", value, sizeof(value))) {
if (strcasecmp(value, "chunked") == 0) {
req->is_chunked = 1;
}
}
if (http_find_header_value(headers_start, len - (headers_start - data), "Connection", value, sizeof(value))) {
if (strcasecmp(value, "close") == 0) {
req->keep_alive = 0;
req->connection_close = 1;
} else if (strcasecmp(value, "keep-alive") == 0) {
req->keep_alive = 1;
} else if (strcasecmp(value, "upgrade") == 0) {
req->is_websocket = 1;
}
}
if (http_find_header_value(headers_start, len - (headers_start - data), "Upgrade", value, sizeof(value))) {
if (strcasecmp(value, "websocket") == 0) req->is_websocket = 1;
}
return 1;
}
int http_is_textual_content_type(const char *content_type) {
if (!content_type) return 0;
if (strncasecmp(content_type, "text/", 5) == 0) return 1;
if (strcasestr(content_type, "application/json") != NULL) return 1;
if (strcasestr(content_type, "application/xml") != NULL) return 1;
if (strcasestr(content_type, "application/javascript") != NULL) return 1;
if (strcasestr(content_type, "application/x-javascript") != NULL) return 1;
if (strcasestr(content_type, "application/x-www-form-urlencoded") != NULL) return 1;
if (strcasestr(content_type, "application/xhtml") != NULL) return 1;
if (strcasestr(content_type, "application/rss") != NULL) return 1;
if (strcasestr(content_type, "application/atom") != NULL) return 1;
if (strcasestr(content_type, "+xml") != NULL) return 1;
if (strcasestr(content_type, "+json") != NULL) return 1;
return 0;
}
int http_detect_binary_content(const char *data, size_t len) {
if (!data || len == 0) return 0;
size_t check_len = len > 512 ? 512 : len;
for (size_t i = 0; i < check_len; i++) {
unsigned char c = (unsigned char)data[i];
if (c == 0) return 1;
if (c < 0x09) return 1;
if (c > 0x0D && c < 0x20 && c != 0x1B) return 1;
}
return 0;
}
long http_get_content_length(const char *headers, size_t headers_len) {
char value[64];
if (http_find_header_value(headers, headers_len, "Content-Length", value, sizeof(value))) {
char *endptr;
long len = strtol(value, &endptr, 10);
if (endptr != value && len >= 0) {
return len;
}
}
return -1;
}
int http_find_headers_end(const char *data, size_t len, size_t *headers_end) {
if (!data || len < 4 || !headers_end) return 0;
char *end = memmem(data, len, "\r\n\r\n", 4);
if (end) {
*headers_end = (end - data) + 4;
return 1;
}
return 0;
}
int http_rewrite_content_length(char *headers, size_t *headers_len, size_t max_len, long new_length) {
if (!headers || !headers_len || *headers_len == 0) return 0;
const char *cl_start = NULL;
const char *cl_end = NULL;
const char *p = headers;
const char *end = headers + *headers_len;
while (p < end) {
const char *line_end = memchr(p, '\n', end - p);
if (!line_end) break;
if (strncasecmp(p, "Content-Length:", 15) == 0) {
cl_start = p;
cl_end = line_end + 1;
break;
}
p = line_end + 1;
}
if (!cl_start) return 0;
char new_header[64];
int new_header_len = snprintf(new_header, sizeof(new_header), "Content-Length: %ld\r\n", new_length);
if (new_header_len < 0 || (size_t)new_header_len >= sizeof(new_header)) return 0;
size_t old_len = cl_end - cl_start;
size_t new_total_len = *headers_len - old_len + new_header_len;
if (new_total_len > max_len) return 0;
size_t suffix_start = cl_end - headers;
size_t suffix_len = *headers_len - suffix_start;
memmove(headers + (cl_start - headers) + new_header_len, cl_end, suffix_len);
memcpy((char*)cl_start, new_header, new_header_len);
*headers_len = new_total_len;
return 1;
}
int http_find_header_line_bounds(const char* data, size_t len, const char* name, const char** line_start, const char** line_end) {
size_t name_len = strlen(name);
const char* end = data + len;
for (const char* p = data; p < end; ) {
const char* current_line_end = memchr(p, '\n', end - p);
if (!current_line_end) break;
const char* line_actual_end = current_line_end;
if (line_actual_end > p && *(line_actual_end - 1) == '\r') {
line_actual_end--;
}
// Check for header name followed by optional whitespace and a colon
if ((size_t)(line_actual_end - p) > name_len && strncasecmp(p, name, name_len) == 0) {
const char* colon_pos = p + name_len;
while (colon_pos < line_actual_end && (*colon_pos == ' ' || *colon_pos == '\t')) {
colon_pos++;
}
if (colon_pos < line_actual_end && *colon_pos == ':') {
*line_start = p;
*line_end = current_line_end + 1; // Include the \n (and \r if present)
return 1;
}
}
p = current_line_end + 1;
}
*line_start = NULL;
*line_end = NULL;
return 0;
}
int http_extract_status_code(const char *data, size_t len) {
if (!data || len < 12) return 0;
if (strncmp(data, "HTTP/1.", 7) != 0 && strncmp(data, "HTTP/2", 6) != 0) {
return 0;
}
const char *p = data;
while (p < data + len && *p != ' ') p++;
if (p >= data + len) return 0;
while (p < data + len && *p == ' ') p++;
if (p >= data + len) return 0;
int status = 0;
for (int i = 0; i < 3 && p + i < data + len; i++) {
if (p[i] >= '0' && p[i] <= '9') {
status = status * 10 + (p[i] - '0');
} else {
break;
}
}
return (status >= 100 && status < 600) ? status : 0;
}
void http_normalize_uri_path(const char *uri, char *normalized, size_t normalized_size) {
if (!uri || !normalized || normalized_size == 0) return;
size_t uri_len = strlen(uri);
size_t out_pos = 0;
size_t i = 0;
const char *query = strchr(uri, '?');
size_t path_len = query ? (size_t)(query - uri) : uri_len;
while (i < path_len && out_pos < normalized_size - 1) {
if (uri[i] == '/') {
if (out_pos == 0 || normalized[out_pos - 1] != '/') {
normalized[out_pos++] = '/';
}
i++;
if (i < path_len && uri[i] == '.') {
if (i + 1 >= path_len || uri[i + 1] == '/' || uri[i + 1] == '?') {
i++;
continue;
}
if (uri[i + 1] == '.' && (i + 2 >= path_len || uri[i + 2] == '/' || uri[i + 2] == '?')) {
if (out_pos > 1) {
out_pos--;
while (out_pos > 0 && normalized[out_pos - 1] != '/') {
out_pos--;
}
}
i += 2;
continue;
}
}
} else {
normalized[out_pos++] = uri[i++];
}
}
if (query && out_pos < normalized_size - 1) {
size_t query_len = uri_len - (query - uri);
if (out_pos + query_len >= normalized_size) {
query_len = normalized_size - out_pos - 1;
}
memcpy(normalized + out_pos, query, query_len);
out_pos += query_len;
}
normalized[out_pos] = '\0';
if (out_pos == 0 && normalized_size > 1) {
normalized[0] = '/';
normalized[1] = '\0';
}
}
int http_uri_is_internal_route(const char *uri) {
if (!uri) return 0;
char normalized[2048];
http_normalize_uri_path(uri, normalized, sizeof(normalized));
if (strncmp(normalized, "/rproxy/", 8) == 0) {
return 1;
}
return 0;
}

19
src/http.h Executable file
View File

@ -0,0 +1,19 @@
#ifndef RPROXY_HTTP_H
#define RPROXY_HTTP_H
#include "types.h"
int http_parse_request(const char *data, size_t len, http_request_t *req);
int http_find_header_value(const char* data, size_t len, const char* name, char* value, size_t value_size);
int http_is_request_start(const char *data, size_t len);
int http_is_textual_content_type(const char *content_type);
int http_detect_binary_content(const char *data, size_t len);
long http_get_content_length(const char *headers, size_t headers_len);
int http_find_headers_end(const char *data, size_t len, size_t *headers_end);
int http_rewrite_content_length(char *headers, size_t *headers_len, size_t max_len, long new_length);
int http_find_header_line_bounds(const char* data, size_t len, const char* name, const char** line_start, const char** line_end);
int http_extract_status_code(const char *data, size_t len);
int http_uri_is_internal_route(const char *uri);
void http_normalize_uri_path(const char *uri, char *normalized, size_t normalized_size);
#endif

121
src/http_response.c Normal file
View File

@ -0,0 +1,121 @@
// retoor <retoor@molodetz.nl>
#include "http_response.h"
#include "time_utils.h"
#include "epoll_utils.h"
#include "buffer.h"
#include <stdio.h>
#include <string.h>
#include <sys/epoll.h>
int http_response_build(const http_response_params_t *params, char *buf, size_t buf_size) {
if (!params || !buf || buf_size == 0) return -1;
char date_buf[64];
time_format_http_date_now(date_buf, sizeof(date_buf));
const char *content_type = params->content_type ? params->content_type : "text/plain; charset=utf-8";
const char *body = params->body ? params->body : "";
size_t body_len = params->body_len > 0 ? params->body_len : strlen(body);
const char *connection = params->keep_alive ? "keep-alive" : "close";
const char *extra = params->extra_headers ? params->extra_headers : "";
int len = snprintf(buf, buf_size,
"HTTP/1.1 %d %s\r\n"
"Content-Type: %s\r\n"
"Content-Length: %zu\r\n"
"Connection: %s\r\n"
"Date: %s\r\n"
"Server: ReverseProxy/4.0\r\n"
"%s"
"\r\n",
params->code, params->status,
content_type, body_len, connection, date_buf, extra);
if (len < 0 || (size_t)len >= buf_size) return -1;
if (body_len > 0 && (size_t)len + body_len < buf_size) {
memcpy(buf + len, body, body_len);
len += (int)body_len;
}
return len;
}
void http_response_send(connection_t *conn, const http_response_params_t *params) {
if (!conn || !params) return;
char response[ERROR_RESPONSE_SIZE];
int len = http_response_build(params, response, sizeof(response));
if (len <= 0) return;
if (buffer_ensure_capacity(&conn->write_buf, conn->write_buf.tail + (size_t)len) == 0) {
memcpy(conn->write_buf.data + conn->write_buf.tail, response, (size_t)len);
conn->write_buf.tail += (size_t)len;
struct epoll_event event = { .data.fd = conn->fd, .events = EPOLLIN | EPOLLOUT };
epoll_ctl(epoll_fd, EPOLL_CTL_MOD, conn->fd, &event);
}
}
void http_response_send_error(connection_t *conn, int code, const char *status, const char *body) {
if (!conn || !status || !body) return;
http_response_params_t params = {
.code = code,
.status = status,
.body = body,
.keep_alive = 0
};
http_response_send(conn, &params);
conn->state = CLIENT_STATE_ERROR;
conn->request.keep_alive = 0;
}
void http_response_send_auth_required(connection_t *conn, const char *realm) {
if (!conn) return;
char extra_headers[256];
snprintf(extra_headers, sizeof(extra_headers),
"WWW-Authenticate: Basic realm=\"%s\"\r\n",
realm ? realm : "Protected Area");
http_response_params_t params = {
.code = 401,
.status = "Unauthorized",
.body = "401 Unauthorized - Authentication required",
.extra_headers = extra_headers,
.keep_alive = 0
};
http_response_send(conn, &params);
conn->state = CLIENT_STATE_ERROR;
conn->request.keep_alive = 0;
}
void http_response_send_pipeline_rejected(connection_t *conn) {
if (!conn) return;
http_response_params_t params = {
.code = 400,
.status = "Bad Request",
.body = "400 Bad Request - Request pipelining is not supported",
.keep_alive = 0
};
char response[ERROR_RESPONSE_SIZE];
int len = http_response_build(&params, response, sizeof(response));
if (len <= 0) return;
if (buffer_ensure_capacity(&conn->write_buf, conn->write_buf.tail + (size_t)len) == 0) {
memcpy(conn->write_buf.data + conn->write_buf.tail, response, (size_t)len);
conn->write_buf.tail += (size_t)len;
}
conn->state = CLIENT_STATE_CLOSING;
conn->request.keep_alive = 0;
struct epoll_event event = { .data.fd = conn->fd, .events = EPOLLOUT };
epoll_ctl(epoll_fd, EPOLL_CTL_MOD, conn->fd, &event);
}

26
src/http_response.h Normal file
View File

@ -0,0 +1,26 @@
// retoor <retoor@molodetz.nl>
#ifndef RPROXY_HTTP_RESPONSE_H
#define RPROXY_HTTP_RESPONSE_H
#include "types.h"
#include <stddef.h>
typedef struct {
int code;
const char *status;
const char *content_type;
const char *body;
size_t body_len;
const char *extra_headers;
int keep_alive;
} http_response_params_t;
int http_response_build(const http_response_params_t *params, char *buf, size_t buf_size);
void http_response_send_error(connection_t *conn, int code, const char *status, const char *body);
void http_response_send_auth_required(connection_t *conn, const char *realm);
void http_response_send_pipeline_rejected(connection_t *conn);
void http_response_send(connection_t *conn, const http_response_params_t *params);
#endif

160
src/logging.c Executable file
View File

@ -0,0 +1,160 @@
#include "logging.h"
#include <stdio.h>
#include <stdarg.h>
#include <time.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <sys/stat.h>
#include <unistd.h>
#define LOG_MAX_SIZE (10 * 1024 * 1024)
#define LOG_MAX_ROTATIONS 5
static int g_debug_mode = 0;
static FILE *g_log_file = NULL;
static char g_log_path[512] = "";
static pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER;
void logging_set_debug(int enabled) {
g_debug_mode = enabled;
}
int logging_get_debug(void) {
return g_debug_mode;
}
static void rotate_log_file(void) {
if (g_log_path[0] == '\0') return;
if (g_log_file && g_log_file != stdout && g_log_file != stderr) {
fclose(g_log_file);
g_log_file = NULL;
}
char old_path[536], new_path[536];
snprintf(old_path, sizeof(old_path), "%s.%d", g_log_path, LOG_MAX_ROTATIONS);
unlink(old_path);
for (int i = LOG_MAX_ROTATIONS - 1; i >= 1; i--) {
snprintf(old_path, sizeof(old_path), "%s.%d", g_log_path, i);
snprintf(new_path, sizeof(new_path), "%s.%d", g_log_path, i + 1);
rename(old_path, new_path);
}
snprintf(new_path, sizeof(new_path), "%s.1", g_log_path);
rename(g_log_path, new_path);
g_log_file = fopen(g_log_path, "a");
if (!g_log_file) {
g_log_file = stdout;
}
}
static void check_rotation(void) {
if (!g_log_file || g_log_file == stdout || g_log_file == stderr) return;
if (g_log_path[0] == '\0') return;
struct stat st;
if (fstat(fileno(g_log_file), &st) == 0) {
if (st.st_size >= LOG_MAX_SIZE) {
rotate_log_file();
}
}
}
int logging_set_file(const char *path) {
pthread_mutex_lock(&log_mutex);
if (g_log_file && g_log_file != stdout && g_log_file != stderr) {
fclose(g_log_file);
}
g_log_path[0] = '\0';
if (path) {
strncpy(g_log_path, path, sizeof(g_log_path) - 1);
g_log_path[sizeof(g_log_path) - 1] = '\0';
g_log_file = fopen(path, "a");
if (!g_log_file) {
g_log_file = stdout;
g_log_path[0] = '\0';
pthread_mutex_unlock(&log_mutex);
return -1;
}
} else {
g_log_file = stdout;
}
pthread_mutex_unlock(&log_mutex);
return 0;
}
void logging_cleanup(void) {
pthread_mutex_lock(&log_mutex);
if (g_log_file && g_log_file != stdout && g_log_file != stderr) {
fclose(g_log_file);
}
g_log_file = NULL;
g_log_path[0] = '\0';
pthread_mutex_unlock(&log_mutex);
}
static void log_message(const char *level, const char *format, va_list args) {
pthread_mutex_lock(&log_mutex);
check_rotation();
FILE *out = g_log_file ? g_log_file : stdout;
time_t now;
time(&now);
struct tm *local = localtime(&now);
char buf[32];
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", local);
fprintf(out, "%s - %-5s - ", buf, level);
vfprintf(out, format, args);
fprintf(out, "\n");
fflush(out);
pthread_mutex_unlock(&log_mutex);
}
void log_error(const char *format, ...) {
va_list args;
va_start(args, format);
int saved_errno = errno;
char msg[1024];
vsnprintf(msg, sizeof(msg), format, args);
va_end(args);
pthread_mutex_lock(&log_mutex);
check_rotation();
FILE *out = g_log_file ? g_log_file : stderr;
time_t now;
time(&now);
struct tm *local = localtime(&now);
char buf[32];
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", local);
if (saved_errno != 0) {
fprintf(out, "%s - ERROR - %s: %s\n", buf, msg, strerror(saved_errno));
} else {
fprintf(out, "%s - ERROR - %s\n", buf, msg);
}
fflush(out);
pthread_mutex_unlock(&log_mutex);
}
void log_info(const char *format, ...) {
va_list args;
va_start(args, format);
log_message("INFO", format, args);
va_end(args);
}
void log_debug(const char *format, ...) {
if (!g_debug_mode) return;
va_list args;
va_start(args, format);
log_message("DEBUG", format, args);
va_end(args);
}

12
src/logging.h Executable file
View File

@ -0,0 +1,12 @@
#ifndef RPROXY_LOGGING_H
#define RPROXY_LOGGING_H
void log_error(const char *format, ...);
void log_info(const char *format, ...);
void log_debug(const char *format, ...);
void logging_set_debug(int enabled);
int logging_get_debug(void);
int logging_set_file(const char *path);
void logging_cleanup(void);
#endif

239
src/main.c Executable file
View File

@ -0,0 +1,239 @@
// retoor <retoor@molodetz.nl>
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <string.h>
#include "types.h"
#include "logging.h"
#include "config.h"
#include "monitor.h"
#include "ssl_handler.h"
#include "connection.h"
#include "epoll_utils.h"
#include "rate_limit.h"
#include "auth.h"
#include "health_check.h"
#ifndef CONFIG_RELOAD_INTERVAL_SECONDS
#define CONFIG_RELOAD_INTERVAL_SECONDS 5
#endif
static volatile sig_atomic_t g_shutdown = 0;
static volatile sig_atomic_t g_reload_config = 0;
static const char *g_config_file = NULL;
static void signal_handler(int sig) {
if (sig == SIGINT || sig == SIGTERM) {
g_shutdown = 1;
} else if (sig == SIGHUP) {
g_reload_config = 1;
}
}
static void reload_configuration(void) {
if (!g_config_file) return;
config_hot_reload(g_config_file);
}
static void graceful_shutdown(void) {
log_info("Initiating graceful shutdown...");
for (int i = 0; i < MAX_FDS; i++) {
connection_t *conn = &connections[i];
if (conn->type == CONN_TYPE_LISTENER && conn->fd != -1) {
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, conn->fd, NULL);
close(conn->fd);
conn->fd = -1;
conn->type = CONN_TYPE_UNUSED;
log_info("Stopped accepting new connections");
}
}
int active_count = 0;
int drain_timeout = 30;
time_t drain_start = time(NULL);
do {
active_count = 0;
for (int i = 0; i < MAX_FDS; i++) {
if (connections[i].type == CONN_TYPE_CLIENT || connections[i].type == CONN_TYPE_UPSTREAM) {
if (connections[i].fd != -1) {
active_count++;
}
}
}
if (active_count > 0 && (time(NULL) - drain_start) < drain_timeout) {
struct epoll_event events[MAX_EVENTS];
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, 100);
for (int i = 0; i < n; i++) {
connection_handle_event(&events[i]);
}
}
} while (active_count > 0 && (time(NULL) - drain_start) < drain_timeout);
if (active_count > 0) {
log_info("Drain timeout reached, forcing close of %d connections", active_count);
}
}
static void cleanup(void) {
log_info("Cleaning up resources...");
for (int i = 0; i < MAX_FDS; i++) {
if (connections[i].type != CONN_TYPE_UNUSED && connections[i].fd != -1) {
connection_close(i);
}
}
config_free();
monitor_cleanup();
rate_limit_cleanup();
health_check_cleanup();
logging_cleanup();
if (epoll_fd >= 0) {
close(epoll_fd);
epoll_fd = -1;
}
ssl_cleanup();
log_info("Cleanup complete");
}
int main(int argc, char *argv[]) {
signal(SIGPIPE, SIG_IGN);
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
signal(SIGHUP, signal_handler);
if (getenv("DEBUG")) {
logging_set_debug(1);
log_info("Debug mode enabled");
}
const char *log_file = getenv("LOG_FILE");
if (log_file) {
if (logging_set_file(log_file) == 0) {
log_info("Logging to file: %s", log_file);
}
}
g_config_file = (argc > 1) ? argv[1] : "proxy_config.json";
config_create_default(g_config_file);
if (!config_load(g_config_file)) {
fprintf(stderr, "Failed to load configuration\n");
return 1;
}
const char *ssl_verify = getenv("SSL_VERIFY");
if (ssl_verify && strcmp(ssl_verify, "0") == 0) {
ssl_set_verify(0);
}
const char *ca_file = getenv("SSL_CA_FILE");
if (ca_file) {
ssl_set_ca_file(ca_file);
}
const char *ca_path = getenv("SSL_CA_PATH");
if (ca_path) {
ssl_set_ca_path(ca_path);
}
ssl_init();
monitor_init("proxy_stats.db");
const char *rate_limit_str = getenv("RATE_LIMIT");
if (rate_limit_str) {
int rate = atoi(rate_limit_str);
if (rate > 0) {
rate_limit_init(rate, RATE_LIMIT_WINDOW_SECONDS);
}
}
const char *auth_user = getenv("DASHBOARD_USER");
const char *auth_pass = getenv("DASHBOARD_PASS");
if (auth_user && auth_pass) {
auth_init(auth_user, auth_pass);
}
health_check_init();
if (epoll_utils_create() == -1) {
log_error("epoll_create failed");
return 1;
}
connection_init_all();
connection_setup_listener(config->port);
log_info("Port %d", config->port);
log_info("Dashboard: http://localhost:%d/rproxy/dashboard", config->port);
log_info("Stats: http://localhost:%d/rproxy/api/stats", config->port);
log_info("Send SIGHUP to reload configuration");
atexit(cleanup);
struct epoll_event events[MAX_EVENTS];
time_t last_monitor_update = 0;
time_t last_cleanup = 0;
time_t last_config_check = 0;
while (!g_shutdown) {
if (g_reload_config) {
g_reload_config = 0;
reload_configuration();
}
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, 1000);
if (n == -1) {
if (errno == EINTR) continue;
log_error("epoll_wait failed");
break;
}
connection_update_cached_time();
for (int i = 0; i < n; i++) {
connection_handle_event(&events[i]);
}
time_t current_time = cached_time;
if (current_time > last_monitor_update) {
monitor_update();
last_monitor_update = current_time;
}
if (current_time - last_config_check >= CONFIG_RELOAD_INTERVAL_SECONDS) {
if (config_check_file_changed(g_config_file)) {
config_hot_reload(g_config_file);
}
last_config_check = current_time;
}
if (current_time - last_cleanup >= 60) {
connection_cleanup_idle();
rate_limit_purge_expired();
last_cleanup = current_time;
}
health_check_run();
}
log_info("Received shutdown signal");
graceful_shutdown();
log_info("Shutdown complete");
return 0;
}

389
src/monitor.c Normal file
View File

@ -0,0 +1,389 @@
// retoor <retoor@molodetz.nl>
#include "monitor.h"
#include "histogram.h"
#include "deque.h"
#include "rate_tracker.h"
#include "stats_collector.h"
#include "logging.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <math.h>
system_monitor_t monitor = {0};
static double get_current_time_seconds(void) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
}
void monitor_init(const char *db_file) {
memset(&monitor, 0, sizeof(monitor));
monitor.start_time = time(NULL);
monitor.uptime_start = monitor.start_time;
monitor.health_score = 100.0;
history_deque_init(&monitor.cpu_history, HISTORY_SECONDS);
history_deque_init(&monitor.memory_history, HISTORY_SECONDS);
network_history_deque_init(&monitor.network_history, HISTORY_SECONDS);
disk_history_deque_init(&monitor.disk_history, HISTORY_SECONDS);
history_deque_init(&monitor.throughput_history, HISTORY_SECONDS);
history_deque_init(&monitor.load1_history, HISTORY_SECONDS);
history_deque_init(&monitor.load5_history, HISTORY_SECONDS);
history_deque_init(&monitor.load15_history, HISTORY_SECONDS);
histogram_init(&monitor.global_latency);
histogram_init(&monitor.connection_lifetime);
rate_tracker_init(&monitor.global_rps);
stats_get_network(&monitor.last_net_sent, &monitor.last_net_recv);
stats_get_disk(&monitor.last_disk_read, &monitor.last_disk_write);
monitor.last_net_update_time = get_current_time_seconds();
monitor.last_disk_update_time = monitor.last_net_update_time;
if (db_file) {
int rc = sqlite3_open(db_file, &monitor.db);
if (rc != SQLITE_OK) {
log_debug("Cannot open stats database: %s", sqlite3_errmsg(monitor.db));
monitor.db = NULL;
} else {
char *err = NULL;
const char *sql =
"CREATE TABLE IF NOT EXISTS stats_history ("
" timestamp INTEGER PRIMARY KEY,"
" cpu_percent REAL,"
" memory_gb REAL,"
" active_connections INTEGER,"
" requests_per_second REAL"
");";
sqlite3_exec(monitor.db, sql, NULL, NULL, &err);
if (err) {
sqlite3_free(err);
}
}
}
log_info("Monitor initialized");
}
void monitor_cleanup(void) {
history_deque_free(&monitor.cpu_history);
history_deque_free(&monitor.memory_history);
network_history_deque_free(&monitor.network_history);
disk_history_deque_free(&monitor.disk_history);
history_deque_free(&monitor.throughput_history);
history_deque_free(&monitor.load1_history);
history_deque_free(&monitor.load5_history);
history_deque_free(&monitor.load15_history);
vhost_stats_t *current = monitor.vhost_stats_head;
while (current) {
vhost_stats_t *next = current->next;
history_deque_free(&current->throughput_history);
request_time_deque_free(&current->request_times);
free(current);
current = next;
}
monitor.vhost_stats_head = NULL;
if (monitor.db) {
sqlite3_close(monitor.db);
monitor.db = NULL;
}
log_info("Monitor cleanup complete");
}
vhost_stats_t *monitor_get_or_create_vhost_stats(const char *vhost_name) {
if (!vhost_name || vhost_name[0] == '\0') return NULL;
for (vhost_stats_t *s = monitor.vhost_stats_head; s; s = s->next) {
if (strcmp(s->vhost_name, vhost_name) == 0) {
return s;
}
}
vhost_stats_t *stats = calloc(1, sizeof(vhost_stats_t));
if (!stats) return NULL;
strncpy(stats->vhost_name, vhost_name, sizeof(stats->vhost_name) - 1);
stats->vhost_name[sizeof(stats->vhost_name) - 1] = '\0';
history_deque_init(&stats->throughput_history, 60);
request_time_deque_init(&stats->request_times, 1000);
histogram_init(&stats->latency_histogram);
histogram_init(&stats->request_size_histogram);
histogram_init(&stats->response_size_histogram);
histogram_init(&stats->ttfb_histogram);
histogram_init(&stats->upstream_connect_latency);
rate_tracker_init(&stats->requests_per_second);
stats->next = monitor.vhost_stats_head;
monitor.vhost_stats_head = stats;
return stats;
}
void monitor_record_request_start(vhost_stats_t *stats, int is_websocket) {
if (!stats) return;
stats->total_requests++;
if (is_websocket) {
stats->websocket_requests++;
} else {
stats->http_requests++;
}
rate_tracker_increment(&stats->requests_per_second);
rate_tracker_increment(&monitor.global_rps);
}
void monitor_record_request_end(vhost_stats_t *stats, double start_time) {
if (!stats) return;
double now = get_current_time_seconds();
double duration_ms = (now - start_time) * 1000.0;
histogram_add(&stats->latency_histogram, duration_ms);
histogram_add(&monitor.global_latency, duration_ms);
request_time_deque_push(&stats->request_times, duration_ms);
int count = stats->request_times.count;
if (count > 0) {
double sum = 0;
int start_idx = (stats->request_times.head - count + stats->request_times.capacity) % stats->request_times.capacity;
for (int i = 0; i < count; i++) {
int idx = (start_idx + i) % stats->request_times.capacity;
sum += stats->request_times.times[idx];
}
stats->avg_request_time_ms = sum / count;
}
}
void monitor_record_bytes(vhost_stats_t *stats, long long sent, long long recv) {
if (!stats) return;
stats->bytes_sent += sent;
stats->bytes_recv += recv;
}
http_method_t http_method_from_string(const char *method) {
if (!method) return HTTP_METHOD_OTHER;
if (strcmp(method, "GET") == 0) return HTTP_METHOD_GET;
if (strcmp(method, "POST") == 0) return HTTP_METHOD_POST;
if (strcmp(method, "PUT") == 0) return HTTP_METHOD_PUT;
if (strcmp(method, "DELETE") == 0) return HTTP_METHOD_DELETE;
if (strcmp(method, "PATCH") == 0) return HTTP_METHOD_PATCH;
if (strcmp(method, "HEAD") == 0) return HTTP_METHOD_HEAD;
if (strcmp(method, "OPTIONS") == 0) return HTTP_METHOD_OPTIONS;
return HTTP_METHOD_OTHER;
}
void monitor_record_method(vhost_stats_t *stats, http_method_t method) {
if (!stats || method >= HTTP_METHOD_COUNT) return;
stats->method_counts.counts[method]++;
}
void monitor_record_status(vhost_stats_t *stats, int status_code) {
if (!stats) return;
if (status_code >= 100 && status_code < 200) stats->status_counts.status_1xx++;
else if (status_code >= 200 && status_code < 300) stats->status_counts.status_2xx++;
else if (status_code >= 300 && status_code < 400) stats->status_counts.status_3xx++;
else if (status_code >= 400 && status_code < 500) stats->status_counts.status_4xx++;
else if (status_code >= 500 && status_code < 600) stats->status_counts.status_5xx++;
else stats->status_counts.status_unknown++;
}
void monitor_record_request_size(vhost_stats_t *stats, long size) {
if (!stats) return;
histogram_add_with_bounds(&stats->request_size_histogram, (double)size, SIZE_BUCKET_BOUNDS);
}
void monitor_record_response_size(vhost_stats_t *stats, long size) {
if (!stats) return;
histogram_add_with_bounds(&stats->response_size_histogram, (double)size, SIZE_BUCKET_BOUNDS);
stats->response_bytes_total += size;
}
void monitor_record_ttfb(vhost_stats_t *stats, double ttfb_ms) {
if (!stats) return;
histogram_add(&stats->ttfb_histogram, ttfb_ms);
}
void monitor_record_upstream_connect(vhost_stats_t *stats, int success, double latency_ms) {
if (!stats) return;
if (success) {
stats->upstream_connect_success++;
histogram_add(&stats->upstream_connect_latency, latency_ms);
} else {
stats->upstream_connect_failures++;
}
}
void monitor_record_splice_transfer(vhost_stats_t *stats, long long bytes) {
if (!stats) return;
stats->splice_transfers++;
stats->bytes_via_splice += (uint64_t)bytes;
}
void monitor_record_buffer_transfer(vhost_stats_t *stats, long long bytes) {
if (!stats) return;
stats->buffered_transfers++;
stats->bytes_via_buffer += (uint64_t)bytes;
}
void monitor_record_connection_opened(vhost_stats_t *stats) {
if (!stats) return;
stats->connections_opened++;
}
void monitor_record_connection_closed(vhost_stats_t *stats) {
if (!stats) return;
stats->connections_closed++;
}
void monitor_record_keepalive_reuse(vhost_stats_t *stats) {
if (!stats) return;
stats->keep_alive_reused++;
}
void monitor_record_error(vhost_stats_t *stats, int error_type) {
if (!stats) return;
switch (error_type) {
case ERROR_TYPE_DNS:
stats->dns_failures++;
break;
case ERROR_TYPE_SSL:
stats->ssl_failures++;
break;
case ERROR_TYPE_TIMEOUT:
stats->timeout_errors++;
break;
case ERROR_TYPE_CONNECTION:
stats->connection_errors++;
break;
}
}
void monitor_compute_health_score(void) {
double score = 100.0;
if (monitor.cpu_history.count > 0) {
int idx = (monitor.cpu_history.head - 1 + monitor.cpu_history.capacity) % monitor.cpu_history.capacity;
double cpu = monitor.cpu_history.points[idx].value;
if (cpu > 90) score -= 30;
else if (cpu > 80) score -= 15;
else if (cpu > 70) score -= 5;
}
uint64_t total_success = 0, total_errors = 0;
for (vhost_stats_t *s = monitor.vhost_stats_head; s; s = s->next) {
total_success += s->status_counts.status_2xx + s->status_counts.status_3xx;
total_errors += s->status_counts.status_5xx;
}
if (total_success + total_errors > 0) {
double error_rate = (double)total_errors / (double)(total_success + total_errors);
monitor.error_rate_1m = error_rate;
if (error_rate > 0.1) score -= 40;
else if (error_rate > 0.05) score -= 20;
else if (error_rate > 0.01) score -= 10;
}
double p99 = histogram_percentile(&monitor.global_latency, 0.99);
if (p99 > 5000) score -= 20;
else if (p99 > 2000) score -= 10;
else if (p99 > 1000) score -= 5;
monitor.health_score = fmax(0, fmin(100, score));
}
double monitor_get_current_rps(void) {
return (double)rate_tracker_get_rps(&monitor.global_rps);
}
void monitor_update(void) {
double now = get_current_time_seconds();
time_t now_t = (time_t)now;
double cpu = stats_get_cpu_usage();
history_deque_push(&monitor.cpu_history, now, cpu);
double mem_gb = 0;
stats_get_memory_usage(&mem_gb);
history_deque_push(&monitor.memory_history, now, mem_gb);
double load1, load5, load15;
stats_get_load_averages(&load1, &load5, &load15);
history_deque_push(&monitor.load1_history, now, load1);
history_deque_push(&monitor.load5_history, now, load5);
history_deque_push(&monitor.load15_history, now, load15);
long long net_sent, net_recv;
stats_get_network(&net_sent, &net_recv);
double dt = now - monitor.last_net_update_time;
if (dt > 0.5 && monitor.last_net_sent > 0) {
double rx_kbps = (double)(net_recv - monitor.last_net_recv) / dt / 1024.0;
double tx_kbps = (double)(net_sent - monitor.last_net_sent) / dt / 1024.0;
network_history_deque_push(&monitor.network_history, now, rx_kbps, tx_kbps);
}
monitor.last_net_sent = net_sent;
monitor.last_net_recv = net_recv;
monitor.last_net_update_time = now;
long long disk_read, disk_write;
stats_get_disk(&disk_read, &disk_write);
double disk_dt = now - monitor.last_disk_update_time;
if (disk_dt > 0.5 && monitor.last_disk_read > 0) {
double read_mbps = (double)(disk_read - monitor.last_disk_read) * 512.0 / disk_dt / (1024.0 * 1024.0);
double write_mbps = (double)(disk_write - monitor.last_disk_write) * 512.0 / disk_dt / (1024.0 * 1024.0);
disk_history_deque_push(&monitor.disk_history, now, read_mbps, write_mbps);
}
monitor.last_disk_read = disk_read;
monitor.last_disk_write = disk_write;
monitor.last_disk_update_time = now;
double current_rps = monitor_get_current_rps();
if (current_rps > monitor.peak_rps) {
monitor.peak_rps = current_rps;
monitor.peak_rps_time = now_t;
}
for (vhost_stats_t *s = monitor.vhost_stats_head; s; s = s->next) {
if (s->last_update > 0) {
double vhost_dt = now - s->last_update;
if (vhost_dt >= 1.0) {
double throughput = (double)((s->bytes_sent - s->last_bytes_sent) +
(s->bytes_recv - s->last_bytes_recv)) / vhost_dt / 1024.0;
history_deque_push(&s->throughput_history, now, throughput);
s->last_bytes_sent = s->bytes_sent;
s->last_bytes_recv = s->bytes_recv;
s->last_update = now;
}
} else {
s->last_bytes_sent = s->bytes_sent;
s->last_bytes_recv = s->bytes_recv;
s->last_update = now;
}
uint32_t rps = rate_tracker_get_rps(&s->requests_per_second);
if ((double)rps > s->peak_rps) {
s->peak_rps = (double)rps;
s->peak_rps_time = now_t;
}
}
if (monitor.db) {
char sql[512];
snprintf(sql, sizeof(sql),
"INSERT INTO stats_history (timestamp, cpu_percent, memory_gb, active_connections, requests_per_second) "
"VALUES (%ld, %.2f, %.2f, %d, %.2f)",
now_t, cpu, mem_gb, monitor.active_connections, current_rps);
sqlite3_exec(monitor.db, sql, NULL, NULL, NULL);
}
}
void monitor_update_connection_states(void) {
memset(monitor.connections_by_state, 0, sizeof(monitor.connections_by_state));
}

62
src/monitor.h Executable file
View File

@ -0,0 +1,62 @@
#ifndef RPROXY_MONITOR_H
#define RPROXY_MONITOR_H
#include "types.h"
extern system_monitor_t monitor;
void monitor_init(const char *db_file);
void monitor_cleanup(void);
void monitor_update(void);
vhost_stats_t* monitor_get_or_create_vhost_stats(const char *vhost_name);
void monitor_record_request_start(vhost_stats_t *stats, int is_websocket);
void monitor_record_request_end(vhost_stats_t *stats, double start_time);
void monitor_record_bytes(vhost_stats_t *stats, long long sent, long long recv);
void history_deque_init(history_deque_t *dq, int capacity);
void history_deque_push(history_deque_t *dq, double time, double value);
void network_history_deque_init(network_history_deque_t *dq, int capacity);
void network_history_deque_push(network_history_deque_t *dq, double time, double rx, double tx);
void disk_history_deque_init(disk_history_deque_t *dq, int capacity);
void disk_history_deque_push(disk_history_deque_t *dq, double time, double read_mbps, double write_mbps);
void request_time_deque_init(request_time_deque_t *dq, int capacity);
void request_time_deque_push(request_time_deque_t *dq, double time_ms);
void histogram_init(histogram_t *h);
void histogram_add(histogram_t *h, double value);
double histogram_percentile(histogram_t *h, double p);
double histogram_mean(histogram_t *h);
void rate_tracker_init(rate_tracker_t *rt);
void rate_tracker_increment(rate_tracker_t *rt);
uint32_t rate_tracker_get_rps(rate_tracker_t *rt);
uint32_t rate_tracker_get_total_last_minute(rate_tracker_t *rt);
http_method_t http_method_from_string(const char *method);
void monitor_record_method(vhost_stats_t *stats, http_method_t method);
void monitor_record_status(vhost_stats_t *stats, int status_code);
void monitor_record_request_size(vhost_stats_t *stats, long size);
void monitor_record_response_size(vhost_stats_t *stats, long size);
void monitor_record_ttfb(vhost_stats_t *stats, double ttfb_ms);
void monitor_record_upstream_connect(vhost_stats_t *stats, int success, double latency_ms);
void monitor_record_splice_transfer(vhost_stats_t *stats, long long bytes);
void monitor_record_buffer_transfer(vhost_stats_t *stats, long long bytes);
void monitor_record_connection_opened(vhost_stats_t *stats);
void monitor_record_connection_closed(vhost_stats_t *stats);
void monitor_record_keepalive_reuse(vhost_stats_t *stats);
void monitor_record_error(vhost_stats_t *stats, int error_type);
void monitor_compute_health_score(void);
void monitor_update_connection_states(void);
double monitor_get_current_rps(void);
#define ERROR_TYPE_DNS 0
#define ERROR_TYPE_SSL 1
#define ERROR_TYPE_TIMEOUT 2
#define ERROR_TYPE_CONNECTION 3
extern const double LATENCY_BUCKET_BOUNDS[HISTOGRAM_BUCKETS];
extern const char* LATENCY_BUCKET_LABELS[HISTOGRAM_BUCKETS];
extern const double SIZE_BUCKET_BOUNDS[HISTOGRAM_BUCKETS];
#endif

137
src/patch.c Executable file
View File

@ -0,0 +1,137 @@
#include "patch.h"
#include "logging.h"
#include <string.h>
#include <stdlib.h>
int patch_has_rules(const patch_config_t *config) {
return config && config->rule_count > 0;
}
int patch_check_for_block(const patch_config_t *config, const char *data, size_t len) {
if (!config || !data || len == 0) return 0;
for (int i = 0; i < config->rule_count; i++) {
const patch_rule_t *rule = &config->rules[i];
if (!rule->is_null) continue;
if (rule->key_len == 0 || rule->key_len > len) continue;
if (memmem(data, len, rule->key, rule->key_len) != NULL) {
return 1;
}
}
return 0;
}
patch_result_t patch_apply(
const patch_config_t *config,
const char *input,
size_t input_len,
char *output,
size_t output_capacity
) {
patch_result_t result = {0, 0, 0};
if (!config || config->rule_count == 0 || !input || input_len == 0) {
if (output && output_capacity >= input_len) {
memcpy(output, input, input_len);
result.output_len = input_len;
}
return result;
}
if (patch_check_for_block(config, input, input_len)) {
result.should_block = 1;
return result;
}
int has_replacements = 0;
for (int i = 0; i < config->rule_count; i++) {
if (!config->rules[i].is_null && config->rules[i].key_len > 0) {
has_replacements = 1;
break;
}
}
if (!has_replacements) {
if (output && output_capacity >= input_len) {
memcpy(output, input, input_len);
result.output_len = input_len;
}
return result;
}
size_t max_growth = 0;
for (int i = 0; i < config->rule_count; i++) {
const patch_rule_t *rule = &config->rules[i];
if (rule->is_null || rule->key_len == 0) continue;
if (rule->value_len > rule->key_len) {
size_t growth_per_match = rule->value_len - rule->key_len;
size_t max_matches = input_len / rule->key_len + 1;
max_growth += growth_per_match * max_matches;
}
}
size_t work_size = input_len + max_growth + 1;
char *work_buf = malloc(work_size);
if (!work_buf) {
if (output && output_capacity >= input_len) {
memcpy(output, input, input_len);
result.output_len = input_len;
}
return result;
}
memcpy(work_buf, input, input_len);
size_t work_len = input_len;
for (int i = 0; i < config->rule_count; i++) {
const patch_rule_t *rule = &config->rules[i];
if (rule->is_null || rule->key_len == 0) continue;
size_t pos = 0;
while (pos + rule->key_len <= work_len) {
char *found = memmem(work_buf + pos, work_len - pos, rule->key, rule->key_len);
if (!found) break;
size_t match_pos = found - work_buf;
size_t tail_len = work_len - match_pos - rule->key_len;
if (rule->value_len != rule->key_len) {
size_t new_work_len = work_len - rule->key_len + rule->value_len;
if (new_work_len >= work_size) {
size_t new_size = new_work_len + max_growth + 1;
char *new_buf = realloc(work_buf, new_size);
if (!new_buf) {
pos = match_pos + rule->key_len;
continue;
}
work_buf = new_buf;
work_size = new_size;
found = work_buf + match_pos;
}
memmove(found + rule->value_len, found + rule->key_len, tail_len);
work_len = new_work_len;
}
if (rule->value_len > 0) {
memcpy(found, rule->value, rule->value_len);
}
pos = match_pos + rule->value_len;
}
}
result.size_delta = (long)work_len - (long)input_len;
if (output && output_capacity >= work_len) {
memcpy(output, work_buf, work_len);
result.output_len = work_len;
} else if (output && output_capacity > 0) {
memcpy(output, work_buf, output_capacity);
result.output_len = output_capacity;
}
free(work_buf);
return result;
}

28
src/patch.h Executable file
View File

@ -0,0 +1,28 @@
#ifndef RPROXY_PATCH_H
#define RPROXY_PATCH_H
#include "types.h"
typedef struct {
int should_block;
size_t output_len;
long size_delta;
} patch_result_t;
patch_result_t patch_apply(
const patch_config_t *config,
const char *input,
size_t input_len,
char *output,
size_t output_capacity
);
int patch_check_for_block(
const patch_config_t *config,
const char *data,
size_t len
);
int patch_has_rules(const patch_config_t *config);
#endif

113
src/rate_limit.c Executable file
View File

@ -0,0 +1,113 @@
#include "rate_limit.h"
#include "logging.h"
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define MAX_RATE_LIMIT_ENTRIES 10000
typedef struct rate_limit_entry {
char client_ip[64];
int request_count;
time_t window_start;
struct rate_limit_entry *next;
} rate_limit_entry_t;
static rate_limit_entry_t *rate_limit_table[256];
static int g_rate_limit_enabled = 0;
static int g_requests_per_window = DEFAULT_RATE_LIMIT_REQUESTS;
static int g_window_seconds = RATE_LIMIT_WINDOW_SECONDS;
static unsigned int hash_ip(const char *ip) {
unsigned int hash = 0;
while (*ip) {
hash = hash * 31 + (unsigned char)*ip++;
}
return hash % 256;
}
void rate_limit_init(int requests_per_window, int window_seconds) {
g_rate_limit_enabled = 1;
g_requests_per_window = requests_per_window;
g_window_seconds = window_seconds;
memset(rate_limit_table, 0, sizeof(rate_limit_table));
log_info("Rate limiting enabled: %d requests per %d seconds", requests_per_window, window_seconds);
}
void rate_limit_cleanup(void) {
for (int i = 0; i < 256; i++) {
rate_limit_entry_t *entry = rate_limit_table[i];
while (entry) {
rate_limit_entry_t *next = entry->next;
free(entry);
entry = next;
}
rate_limit_table[i] = NULL;
}
g_rate_limit_enabled = 0;
}
int rate_limit_check(const char *client_ip) {
if (!g_rate_limit_enabled || !client_ip) return 1;
time_t now = time(NULL);
unsigned int bucket = hash_ip(client_ip);
rate_limit_entry_t *entry = rate_limit_table[bucket];
while (entry) {
if (strcmp(entry->client_ip, client_ip) == 0) {
if (now - entry->window_start >= g_window_seconds) {
entry->window_start = now;
entry->request_count = 1;
return 1;
}
entry->request_count++;
if (entry->request_count > g_requests_per_window) {
return 0;
}
return 1;
}
entry = entry->next;
}
rate_limit_entry_t *new_entry = calloc(1, sizeof(rate_limit_entry_t));
if (!new_entry) {
log_error("Rate limit entry allocation failed, denying request for safety");
return 0;
}
strncpy(new_entry->client_ip, client_ip, sizeof(new_entry->client_ip) - 1);
new_entry->request_count = 1;
new_entry->window_start = now;
new_entry->next = rate_limit_table[bucket];
rate_limit_table[bucket] = new_entry;
return 1;
}
void rate_limit_purge_expired(void) {
if (!g_rate_limit_enabled) return;
time_t now = time(NULL);
for (int i = 0; i < 256; i++) {
rate_limit_entry_t *entry = rate_limit_table[i];
rate_limit_entry_t *prev = NULL;
while (entry) {
rate_limit_entry_t *next = entry->next;
if (now - entry->window_start >= g_window_seconds * 2) {
if (prev) {
prev->next = next;
} else {
rate_limit_table[i] = next;
}
free(entry);
} else {
prev = entry;
}
entry = next;
}
}
}

11
src/rate_limit.h Executable file
View File

@ -0,0 +1,11 @@
#ifndef RPROXY_RATE_LIMIT_H
#define RPROXY_RATE_LIMIT_H
#include "types.h"
void rate_limit_init(int requests_per_window, int window_seconds);
void rate_limit_cleanup(void);
int rate_limit_check(const char *client_ip);
void rate_limit_purge_expired(void);
#endif

55
src/rate_tracker.c Normal file
View File

@ -0,0 +1,55 @@
// retoor <retoor@molodetz.nl>
#include "rate_tracker.h"
#include <string.h>
#include <time.h>
void rate_tracker_init(rate_tracker_t *rt) {
if (!rt) return;
memset(rt, 0, sizeof(rate_tracker_t));
rt->slot_start = time(NULL);
}
void rate_tracker_increment(rate_tracker_t *rt) {
if (!rt) return;
time_t now = time(NULL);
int slot = (int)(now % RATE_TRACKER_SLOTS);
if (now != rt->slot_start) {
int slots_to_clear = (int)(now - rt->slot_start);
if (slots_to_clear >= RATE_TRACKER_SLOTS) {
memset(rt->counts, 0, sizeof(rt->counts));
} else {
for (int i = 1; i <= slots_to_clear; i++) {
int clear_slot = (rt->current_slot + i) % RATE_TRACKER_SLOTS;
rt->counts[clear_slot] = 0;
}
}
rt->current_slot = slot;
rt->slot_start = now;
}
rt->counts[slot]++;
}
uint32_t rate_tracker_get_rps(rate_tracker_t *rt) {
if (!rt) return 0;
time_t now = time(NULL);
int prev_slot = (int)((now - 1) % RATE_TRACKER_SLOTS);
if ((now - rt->slot_start) > RATE_TRACKER_SLOTS) return 0;
return rt->counts[prev_slot];
}
uint32_t rate_tracker_get_total_last_minute(rate_tracker_t *rt) {
if (!rt) return 0;
time_t now = time(NULL);
if ((now - rt->slot_start) > RATE_TRACKER_SLOTS) {
return 0;
}
uint32_t total = 0;
for (int i = 0; i < RATE_TRACKER_SLOTS; i++) {
total += rt->counts[i];
}
return total;
}

13
src/rate_tracker.h Normal file
View File

@ -0,0 +1,13 @@
// retoor <retoor@molodetz.nl>
#ifndef RPROXY_RATE_TRACKER_H
#define RPROXY_RATE_TRACKER_H
#include "types.h"
void rate_tracker_init(rate_tracker_t *rt);
void rate_tracker_increment(rate_tracker_t *rt);
uint32_t rate_tracker_get_rps(rate_tracker_t *rt);
uint32_t rate_tracker_get_total_last_minute(rate_tracker_t *rt);
#endif

76
src/socket_utils.c Normal file
View File

@ -0,0 +1,76 @@
// retoor <retoor@molodetz.nl>
#include "socket_utils.h"
#include "logging.h"
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
int socket_set_non_blocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0) {
log_error("fcntl F_GETFL failed");
return -1;
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {
log_error("fcntl F_SETFL failed");
return -1;
}
return 0;
}
void socket_set_tcp_keepalive(int fd) {
int yes = 1;
if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &yes, sizeof(yes)) < 0) {
log_debug("setsockopt SO_KEEPALIVE failed for fd %d: %s", fd, strerror(errno));
}
int idle = 60;
if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle)) < 0) {
log_debug("setsockopt TCP_KEEPIDLE failed for fd %d: %s", fd, strerror(errno));
}
int interval = 10;
if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(interval)) < 0) {
log_debug("setsockopt TCP_KEEPINTVL failed for fd %d: %s", fd, strerror(errno));
}
int maxpkt = 6;
if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &maxpkt, sizeof(maxpkt)) < 0) {
log_debug("setsockopt TCP_KEEPCNT failed for fd %d: %s", fd, strerror(errno));
}
}
void socket_set_tcp_nodelay(int fd) {
int yes = 1;
if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &yes, sizeof(yes)) < 0) {
log_debug("setsockopt TCP_NODELAY failed for fd %d: %s", fd, strerror(errno));
}
}
#ifdef TCP_QUICKACK
void socket_set_tcp_quickack(int fd) {
int yes = 1;
if (setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, &yes, sizeof(yes)) < 0) {
log_debug("setsockopt TCP_QUICKACK failed for fd %d: %s", fd, strerror(errno));
}
}
#else
void socket_set_tcp_quickack(int fd) {
(void)fd;
}
#endif
void socket_optimize(int fd, int is_upstream) {
socket_set_tcp_nodelay(fd);
socket_set_tcp_keepalive(fd);
#ifdef TCP_QUICKACK
if (!is_upstream) {
socket_set_tcp_quickack(fd);
}
#endif
int sndbuf = is_upstream ? 262144 : 131072;
int rcvbuf = is_upstream ? 524288 : 131072;
setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
}

12
src/socket_utils.h Normal file
View File

@ -0,0 +1,12 @@
// retoor <retoor@molodetz.nl>
#ifndef RPROXY_SOCKET_UTILS_H
#define RPROXY_SOCKET_UTILS_H
int socket_set_non_blocking(int fd);
void socket_set_tcp_keepalive(int fd);
void socket_set_tcp_nodelay(int fd);
void socket_set_tcp_quickack(int fd);
void socket_optimize(int fd, int is_upstream);
#endif

142
src/ssl_handler.c Executable file
View File

@ -0,0 +1,142 @@
#include "ssl_handler.h"
#include "logging.h"
#include <openssl/err.h>
#include <openssl/x509_vfy.h>
#include <openssl/x509v3.h>
#include <stdlib.h>
#include <string.h>
SSL_CTX *ssl_ctx = NULL;
static int g_ssl_verify_enabled = 1;
static char g_ca_file[512] = "";
static char g_ca_path[512] = "";
void ssl_set_verify(int enabled) {
g_ssl_verify_enabled = enabled;
}
void ssl_set_ca_file(const char *path) {
if (path) {
strncpy(g_ca_file, path, sizeof(g_ca_file) - 1);
g_ca_file[sizeof(g_ca_file) - 1] = '\0';
}
}
void ssl_set_ca_path(const char *path) {
if (path) {
strncpy(g_ca_path, path, sizeof(g_ca_path) - 1);
g_ca_path[sizeof(g_ca_path) - 1] = '\0';
}
}
void ssl_init(void) {
ssl_ctx = SSL_CTX_new(TLS_client_method());
if (!ssl_ctx) {
log_error("Failed to create SSL context");
exit(EXIT_FAILURE);
}
if (g_ssl_verify_enabled) {
SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, NULL);
SSL_CTX_set_verify_depth(ssl_ctx, 4);
int ca_loaded = 0;
if (g_ca_file[0] != '\0' || g_ca_path[0] != '\0') {
const char *file = g_ca_file[0] != '\0' ? g_ca_file : NULL;
const char *path = g_ca_path[0] != '\0' ? g_ca_path : NULL;
if (SSL_CTX_load_verify_locations(ssl_ctx, file, path) == 1) {
ca_loaded = 1;
log_info("Loaded CA certificates from custom location");
}
}
if (!ca_loaded) {
if (SSL_CTX_set_default_verify_paths(ssl_ctx) != 1) {
log_info("Warning: Could not load default CA certificates");
} else {
log_info("Loaded system default CA certificates");
}
}
} else {
SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_NONE, NULL);
log_info("Warning: SSL certificate verification disabled");
}
SSL_CTX_set_options(ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1);
SSL_CTX_set_mode(ssl_ctx, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
if (SSL_CTX_set_cipher_list(ssl_ctx,
"ECDHE+AESGCM:DHE+AESGCM:ECDHE+CHACHA20:DHE+CHACHA20:!aNULL:!MD5:!DSS") != 1) {
log_info("Warning: Could not set preferred cipher list, using defaults");
}
}
int ssl_set_hostname(SSL *ssl, const char *hostname) {
if (!ssl || !hostname || hostname[0] == '\0') return 0;
SSL_set_tlsext_host_name(ssl, hostname);
if (g_ssl_verify_enabled) {
SSL_set_hostflags(ssl, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
if (SSL_set1_host(ssl, hostname) != 1) {
log_debug("Failed to set hostname verification for %s", hostname);
return -1;
}
}
return 0;
}
void ssl_cleanup(void) {
if (ssl_ctx) {
SSL_CTX_free(ssl_ctx);
ssl_ctx = NULL;
}
}
int ssl_do_handshake(connection_t *conn) {
if (!conn || !conn->ssl) return -1;
if (conn->ssl_handshake_done) return 1;
int ret = SSL_do_handshake(conn->ssl);
if (ret == 1) {
conn->ssl_handshake_done = 1;
log_debug("SSL handshake completed for fd %d", conn->fd);
return 1;
}
int ssl_error = SSL_get_error(conn->ssl, ret);
if (ssl_error == SSL_ERROR_WANT_READ || ssl_error == SSL_ERROR_WANT_WRITE) {
return 0;
}
log_debug("SSL handshake failed for fd %d: %d", conn->fd, ssl_error);
return -1;
}
int ssl_read(connection_t *conn, char *buf, size_t len) {
if (!conn || !conn->ssl || !conn->ssl_handshake_done) return -1;
int bytes_read = SSL_read(conn->ssl, buf, len);
if (bytes_read <= 0) {
int ssl_error = SSL_get_error(conn->ssl, bytes_read);
if (ssl_error == SSL_ERROR_WANT_READ || ssl_error == SSL_ERROR_WANT_WRITE) {
return 0;
}
return -1;
}
return bytes_read;
}
int ssl_write(connection_t *conn, const char *buf, size_t len) {
if (!conn || !conn->ssl || !conn->ssl_handshake_done) return -1;
int written = SSL_write(conn->ssl, buf, len);
if (written <= 0) {
int ssl_error = SSL_get_error(conn->ssl, written);
if (ssl_error == SSL_ERROR_WANT_READ || ssl_error == SSL_ERROR_WANT_WRITE) {
return 0;
}
return -1;
}
return written;
}

18
src/ssl_handler.h Executable file
View File

@ -0,0 +1,18 @@
#ifndef RPROXY_SSL_HANDLER_H
#define RPROXY_SSL_HANDLER_H
#include "types.h"
extern SSL_CTX *ssl_ctx;
void ssl_set_verify(int enabled);
void ssl_set_ca_file(const char *path);
void ssl_set_ca_path(const char *path);
void ssl_init(void);
void ssl_cleanup(void);
int ssl_set_hostname(SSL *ssl, const char *hostname);
int ssl_do_handshake(connection_t *conn);
int ssl_read(connection_t *conn, char *buf, size_t len);
int ssl_write(connection_t *conn, const char *buf, size_t len);
#endif

138
src/stats_collector.c Normal file
View File

@ -0,0 +1,138 @@
// retoor <retoor@molodetz.nl>
#include "stats_collector.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/sysinfo.h>
static long long g_prev_user = 0;
static long long g_prev_nice = 0;
static long long g_prev_system = 0;
static long long g_prev_idle = 0;
double stats_get_cpu_usage(void) {
long long user, nice, system, idle, iowait, irq, softirq;
FILE *f = fopen("/proc/stat", "r");
if (!f) return 0.0;
if (fscanf(f, "cpu %lld %lld %lld %lld %lld %lld %lld",
&user, &nice, &system, &idle, &iowait, &irq, &softirq) != 7) {
fclose(f);
return 0.0;
}
fclose(f);
long long prev_total = g_prev_user + g_prev_nice + g_prev_system + g_prev_idle;
long long total = user + nice + system + idle;
long long totald = total - prev_total;
long long idled = idle - g_prev_idle;
g_prev_user = user;
g_prev_nice = nice;
g_prev_system = system;
g_prev_idle = idle;
return totald == 0 ? 0.0 : (double)(totald - idled) * 100.0 / totald;
}
void stats_get_memory_usage(double *used_gb) {
struct sysinfo info;
if (sysinfo(&info) != 0) {
*used_gb = 0;
return;
}
*used_gb = (double)(info.totalram - info.freeram - info.bufferram) * info.mem_unit / (1024.0 * 1024.0 * 1024.0);
}
void stats_get_network(long long *bytes_sent, long long *bytes_recv) {
FILE *f = fopen("/proc/net/dev", "r");
if (!f) {
*bytes_sent = 0;
*bytes_recv = 0;
return;
}
char line[256];
if (!fgets(line, sizeof(line), f) || !fgets(line, sizeof(line), f)) {
fclose(f);
*bytes_sent = 0;
*bytes_recv = 0;
return;
}
long long total_recv = 0, total_sent = 0;
while (fgets(line, sizeof(line), f)) {
char iface[32];
long long r, t;
if (sscanf(line, "%31[^:]: %lld %*d %*d %*d %*d %*d %*d %*d %lld", iface, &r, &t) == 3) {
char *trimmed = iface;
while (*trimmed == ' ') trimmed++;
if (strcmp(trimmed, "lo") != 0) {
total_recv += r;
total_sent += t;
}
}
}
fclose(f);
*bytes_sent = total_sent;
*bytes_recv = total_recv;
}
void stats_get_disk(long long *sectors_read, long long *sectors_written) {
FILE *f = fopen("/proc/diskstats", "r");
if (!f) {
*sectors_read = 0;
*sectors_written = 0;
return;
}
char line[2048];
long long total_read = 0, total_written = 0;
while (fgets(line, sizeof(line), f)) {
char device[64];
long long sr = 0, sw = 0;
char major[16], minor[16], dev[64];
char rc[32], rm[32], srd[32], rtm[32], rtm2[32], wc[32], wm[32], swd[32];
int nfields = sscanf(line, "%15s %15s %63s %31s %31s %31s %31s %31s %31s %31s %31s %31s",
major, minor, dev, rc, rm, srd, rtm, rtm2, wc, wm, swd, swd);
if (nfields >= 11) {
strncpy(device, dev, sizeof(device) - 1);
device[sizeof(device) - 1] = '\0';
char *endptr;
sr = strtoll(srd, &endptr, 10);
if (endptr == srd) sr = 0;
sw = strtoll(swd, &endptr, 10);
if (endptr == swd) sw = 0;
if (strncmp(device, "loop", 4) != 0 && strncmp(device, "ram", 3) != 0) {
size_t len = strlen(device);
if ((strncmp(device, "sd", 2) == 0 && len == 3) ||
(strncmp(device, "nvme", 4) == 0 && strstr(device, "n1p") == NULL) ||
(strncmp(device, "vd", 2) == 0 && len == 3) ||
(strncmp(device, "hd", 2) == 0 && len == 3)) {
total_read += sr;
total_written += sw;
}
}
}
}
fclose(f);
*sectors_read = total_read;
*sectors_written = total_written;
}
void stats_get_load_averages(double *load1, double *load5, double *load15) {
FILE *f = fopen("/proc/loadavg", "r");
if (!f) {
*load1 = *load5 = *load15 = 0.0;
return;
}
if (fscanf(f, "%lf %lf %lf", load1, load5, load15) != 3) {
*load1 = *load5 = *load15 = 0.0;
}
fclose(f);
}

12
src/stats_collector.h Normal file
View File

@ -0,0 +1,12 @@
// retoor <retoor@molodetz.nl>
#ifndef RPROXY_STATS_COLLECTOR_H
#define RPROXY_STATS_COLLECTOR_H
double stats_get_cpu_usage(void);
void stats_get_memory_usage(double *used_gb);
void stats_get_network(long long *bytes_sent, long long *bytes_recv);
void stats_get_disk(long long *sectors_read, long long *sectors_written);
void stats_get_load_averages(double *load1, double *load5, double *load15);
#endif

20
src/time_utils.c Normal file
View File

@ -0,0 +1,20 @@
// retoor <retoor@molodetz.nl>
#include "time_utils.h"
#include <string.h>
void time_format_http_date(time_t t, char *buf, size_t buf_size) {
if (!buf || buf_size == 0) return;
struct tm *gmt = gmtime(&t);
if (gmt) {
strftime(buf, buf_size, "%a, %d %b %Y %H:%M:%S GMT", gmt);
} else {
strncpy(buf, "Thu, 01 Jan 1970 00:00:00 GMT", buf_size - 1);
buf[buf_size - 1] = '\0';
}
}
void time_format_http_date_now(char *buf, size_t buf_size) {
time_format_http_date(time(NULL), buf, buf_size);
}

12
src/time_utils.h Normal file
View File

@ -0,0 +1,12 @@
// retoor <retoor@molodetz.nl>
#ifndef RPROXY_TIME_UTILS_H
#define RPROXY_TIME_UTILS_H
#include <stddef.h>
#include <time.h>
void time_format_http_date(time_t t, char *buf, size_t buf_size);
void time_format_http_date_now(char *buf, size_t buf_size);
#endif

321
src/types.h Executable file
View File

@ -0,0 +1,321 @@
#ifndef RPROXY_TYPES_H
#define RPROXY_TYPES_H
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <time.h>
#include <openssl/ssl.h>
#include <sqlite3.h>
#define MAX_EVENTS 4096
#define MAX_FDS 65536
#define CHUNK_SIZE 65536
#define HISTORY_SECONDS 300
#define MAX_HEADER_SIZE 8192
#define MAX_REQUEST_LINE_SIZE 4096
#define MAX_URI_SIZE 2048
#define CONNECTION_TIMEOUT 300
#define ERROR_RESPONSE_SIZE 4096
#define HOST_HEADER_SIZE 512
#define MAX_BUFFER_SIZE (64 * 1024 * 1024)
#define MIN_DATA_FOR_REQUEST_CHECK 1
#define RATE_LIMIT_WINDOW_SECONDS 60
#define DEFAULT_RATE_LIMIT_REQUESTS 1000
#define HEALTH_CHECK_INTERVAL_SECONDS 30
#define HEALTH_CHECK_TIMEOUT_MS 5000
#define MAX_UPSTREAM_RETRIES 3
#define UPSTREAM_RETRY_DELAY_MS 100
#define MAX_PATCH_RULES 64
#define MAX_PATCH_KEY_SIZE 256
#define MAX_PATCH_VALUE_SIZE 1024
#define SSL_HANDSHAKE_TIMEOUT_SEC 10
#define MAX_CONNECTIONS_PER_IP 100
#define HOSTNAME_MAX_LEN 256
#define HISTOGRAM_BUCKETS 16
#define RATE_TRACKER_SLOTS 60
#define HTTP_METHOD_COUNT 8
typedef enum {
CONN_TYPE_UNUSED,
CONN_TYPE_LISTENER,
CONN_TYPE_CLIENT,
CONN_TYPE_UPSTREAM
} conn_type_t;
typedef enum {
CLIENT_STATE_READING_HEADERS,
CLIENT_STATE_FORWARDING,
CLIENT_STATE_SERVING_INTERNAL,
CLIENT_STATE_ERROR,
CLIENT_STATE_CLOSING
} client_state_t;
typedef enum {
HTTP_METHOD_GET = 0,
HTTP_METHOD_POST,
HTTP_METHOD_PUT,
HTTP_METHOD_DELETE,
HTTP_METHOD_PATCH,
HTTP_METHOD_HEAD,
HTTP_METHOD_OPTIONS,
HTTP_METHOD_OTHER
} http_method_t;
typedef struct {
uint32_t buckets[HISTOGRAM_BUCKETS];
uint32_t overflow;
uint64_t total_count;
double sum;
double min_value;
double max_value;
} histogram_t;
typedef struct {
uint64_t counts[HTTP_METHOD_COUNT];
} method_counter_t;
typedef struct {
uint64_t status_1xx;
uint64_t status_2xx;
uint64_t status_3xx;
uint64_t status_4xx;
uint64_t status_5xx;
uint64_t status_unknown;
} status_counter_t;
typedef struct {
uint32_t counts[RATE_TRACKER_SLOTS];
int current_slot;
time_t slot_start;
} rate_tracker_t;
typedef struct {
char *data;
size_t capacity;
size_t head;
size_t tail;
} buffer_t;
typedef struct {
char method[32];
char uri[MAX_URI_SIZE];
char version[16];
char host[256];
long content_length;
int is_websocket;
int keep_alive;
int connection_close;
bool is_chunked;
} http_request_t;
struct connection_s;
struct vhost_stats_s;
struct route_config_s;
struct app_config_s;
typedef struct connection_s {
conn_type_t type;
client_state_t state;
int fd;
char client_ip[64];
struct connection_s *pair;
struct vhost_stats_s *vhost_stats;
buffer_t read_buf;
buffer_t write_buf;
SSL *ssl;
int ssl_handshake_done;
time_t ssl_handshake_start;
http_request_t request;
double request_start_time;
time_t last_activity;
int half_closed;
int write_shutdown;
struct route_config_s *route;
struct app_config_s *config;
int is_textual_content;
int content_type_checked;
int patch_blocked;
int response_headers_parsed;
long original_content_length;
long content_length_delta;
char *patch_buf;
size_t patch_buf_capacity;
int splice_pipe[2];
int can_splice;
uint32_t epoll_events;
} connection_t;
typedef struct {
char key[MAX_PATCH_KEY_SIZE];
char value[MAX_PATCH_VALUE_SIZE];
int is_null;
size_t key_len;
size_t value_len;
} patch_rule_t;
typedef struct {
patch_rule_t rules[MAX_PATCH_RULES];
int rule_count;
} patch_config_t;
typedef struct route_config_s {
char hostname[256];
char upstream_host[256];
int upstream_port;
int use_ssl;
int rewrite_host;
int use_auth;
char username[128];
char password_hash[256];
patch_config_t patches;
} route_config_t;
typedef struct app_config_s {
int port;
route_config_t *routes;
int route_count;
_Atomic int ref_count;
struct app_config_s *next;
} app_config_t;
typedef struct {
double time;
double value;
} history_point_t;
typedef struct {
history_point_t *points;
int capacity;
int head;
int count;
} history_deque_t;
typedef struct {
double time;
double rx_kbps;
double tx_kbps;
} network_history_point_t;
typedef struct {
network_history_point_t *points;
int capacity;
int head;
int count;
} network_history_deque_t;
typedef struct {
double time;
double read_mbps;
double write_mbps;
} disk_history_point_t;
typedef struct {
disk_history_point_t *points;
int capacity;
int head;
int count;
} disk_history_deque_t;
typedef struct {
double *times;
int capacity;
int head;
int count;
} request_time_deque_t;
typedef struct vhost_stats_s {
char vhost_name[256];
long long http_requests;
long long websocket_requests;
long long total_requests;
long long bytes_sent;
long long bytes_recv;
double avg_request_time_ms;
long long last_bytes_sent;
long long last_bytes_recv;
double last_update;
history_deque_t throughput_history;
request_time_deque_t request_times;
histogram_t latency_histogram;
histogram_t request_size_histogram;
histogram_t response_size_histogram;
histogram_t ttfb_histogram;
histogram_t upstream_connect_latency;
method_counter_t method_counts;
status_counter_t status_counts;
rate_tracker_t requests_per_second;
uint64_t keep_alive_reused;
uint64_t connections_opened;
uint64_t connections_closed;
uint64_t splice_transfers;
uint64_t buffered_transfers;
uint64_t bytes_via_splice;
uint64_t bytes_via_buffer;
uint64_t upstream_connect_success;
uint64_t upstream_connect_failures;
uint64_t upstream_retries;
uint64_t rate_limit_rejections;
uint64_t auth_failures;
uint64_t patch_blocks;
uint64_t dns_failures;
uint64_t ssl_failures;
uint64_t timeout_errors;
uint64_t connection_errors;
double peak_rps;
time_t peak_rps_time;
long long response_bytes_total;
struct vhost_stats_s *next;
} vhost_stats_t;
typedef struct {
time_t start_time;
int active_connections;
history_deque_t cpu_history;
history_deque_t memory_history;
network_history_deque_t network_history;
disk_history_deque_t disk_history;
history_deque_t throughput_history;
history_deque_t load1_history;
history_deque_t load5_history;
history_deque_t load15_history;
long long last_net_sent;
long long last_net_recv;
long long last_disk_read;
long long last_disk_write;
double last_net_update_time;
double last_disk_update_time;
vhost_stats_t *vhost_stats_head;
sqlite3 *db;
histogram_t global_latency;
histogram_t connection_lifetime;
rate_tracker_t global_rps;
uint64_t total_connections_accepted;
uint64_t connections_by_state[5];
uint64_t total_rate_limit_checks;
uint64_t total_rate_limit_blocks;
time_t uptime_start;
double health_score;
double error_rate_1m;
double peak_rps;
time_t peak_rps_time;
} system_monitor_t;
#endif

280
src/upstream.c Normal file
View File

@ -0,0 +1,280 @@
// retoor <retoor@molodetz.nl>
#include "upstream.h"
#include "buffer.h"
#include "logging.h"
#include "config.h"
#include "monitor.h"
#include "http.h"
#include "http_response.h"
#include "ssl_handler.h"
#include "socket_utils.h"
#include "epoll_utils.h"
#include "patch.h"
#include "connection.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/epoll.h>
extern connection_t connections[MAX_FDS];
extern time_t cached_time;
int upstream_try_connect(struct sockaddr_in *addr, int *out_fd) {
int up_fd = socket(AF_INET, SOCK_STREAM, 0);
if (up_fd < 0) {
return -1;
}
if (up_fd >= MAX_FDS) {
close(up_fd);
return -1;
}
socket_set_non_blocking(up_fd);
socket_optimize(up_fd, 1);
int connect_result = connect(up_fd, (struct sockaddr *)addr, sizeof(*addr));
if (connect_result < 0 && errno != EINPROGRESS) {
close(up_fd);
return -1;
}
*out_fd = up_fd;
return 0;
}
void upstream_cleanup_partial(connection_t *up, int up_fd, connection_t *client, int free_read, int free_write) {
if (free_read) buffer_free(&up->read_buf);
if (free_write) buffer_free(&up->write_buf);
if (up->config) config_ref_dec(up->config);
close(up_fd);
memset(up, 0, sizeof(connection_t));
up->type = CONN_TYPE_UNUSED;
up->fd = -1;
if (client) client->pair = NULL;
}
static char *upstream_rewrite_host_header(const char *data, size_t data_len, route_config_t *route, size_t *new_len) {
char new_host_header[512];
const char *old_host_header_start = NULL;
const char *old_host_header_end = NULL;
if (!http_find_header_line_bounds(data, data_len, "Host", &old_host_header_start, &old_host_header_end)) {
return NULL;
}
int is_default_port = (route->use_ssl && route->upstream_port == 443) ||
(!route->use_ssl && route->upstream_port == 80);
if (is_default_port) {
snprintf(new_host_header, sizeof(new_host_header), "Host: %s\r\n", route->upstream_host);
} else {
snprintf(new_host_header, sizeof(new_host_header), "Host: %s:%d\r\n", route->upstream_host, route->upstream_port);
}
size_t new_host_len = strlen(new_host_header);
size_t old_host_len = (size_t)(old_host_header_end - old_host_header_start);
*new_len = data_len - old_host_len + new_host_len;
char *modified = malloc(*new_len + 1);
if (!modified) return NULL;
char *p = modified;
size_t prefix_len = (size_t)(old_host_header_start - data);
memcpy(p, data, prefix_len);
p += prefix_len;
memcpy(p, new_host_header, new_host_len);
p += new_host_len;
size_t suffix_len = data_len - (size_t)(old_host_header_end - data);
memcpy(p, old_host_header_end, suffix_len);
return modified;
}
void upstream_connect(connection_t *client, const char *data, size_t data_len) {
if (!client || !data) return;
app_config_t *current_config = config;
if (!current_config) {
http_response_send_error(client, 503, "Service Unavailable", "Configuration not loaded");
return;
}
route_config_t *route = config_find_route(client->request.host);
if (!route) {
http_response_send_error(client, 502, "Bad Gateway", "No route configured for this host");
return;
}
client->config = current_config;
config_ref_inc(client->config);
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons((uint16_t)route->upstream_port);
if (inet_pton(AF_INET, route->upstream_host, &addr.sin_addr) <= 0) {
struct addrinfo hints, *result;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
int gai_err = getaddrinfo(route->upstream_host, NULL, &hints, &result);
if (gai_err != 0 || !result || !result->ai_addr) {
if (gai_err == 0 && result) freeaddrinfo(result);
log_debug("DNS resolution failed for %s: %s", route->upstream_host,
gai_err ? gai_strerror(gai_err) : "no address returned");
if (client->vhost_stats) {
monitor_record_error(client->vhost_stats, ERROR_TYPE_DNS);
}
http_response_send_error(client, 502, "Bad Gateway", "Cannot resolve upstream hostname");
return;
}
struct sockaddr_in *resolved = (struct sockaddr_in *)result->ai_addr;
addr.sin_addr = resolved->sin_addr;
freeaddrinfo(result);
}
int up_fd = -1;
int retry_count = 0;
while (retry_count < MAX_UPSTREAM_RETRIES) {
if (upstream_try_connect(&addr, &up_fd) == 0) {
break;
}
retry_count++;
if (retry_count < MAX_UPSTREAM_RETRIES) {
log_debug("Upstream connection attempt %d failed for %s:%d, retrying...",
retry_count, route->upstream_host, route->upstream_port);
usleep(UPSTREAM_RETRY_DELAY_MS * 1000);
}
}
if (up_fd < 0) {
log_debug("All %d connection attempts failed for %s:%d",
MAX_UPSTREAM_RETRIES, route->upstream_host, route->upstream_port);
if (client->vhost_stats) {
monitor_record_upstream_connect(client->vhost_stats, 0, 0);
}
http_response_send_error(client, 502, "Bad Gateway", "Failed to connect to upstream");
return;
}
if (client->vhost_stats) {
monitor_record_upstream_connect(client->vhost_stats, 1, 0);
if (retry_count > 0) {
client->vhost_stats->upstream_retries += (uint64_t)retry_count;
}
}
if (client->pair) {
log_debug("Closing existing upstream fd %d before new connection for client fd %d", client->pair->fd, client->fd);
connection_close(client->pair->fd);
}
epoll_utils_add(up_fd, EPOLLIN | EPOLLOUT);
connection_t *up = &connections[up_fd];
memset(up, 0, sizeof(connection_t));
up->type = CONN_TYPE_UPSTREAM;
up->fd = up_fd;
up->last_activity = cached_time;
up->splice_pipe[0] = -1;
up->splice_pipe[1] = -1;
client->pair = up;
up->pair = client;
up->vhost_stats = client->vhost_stats;
up->route = route;
client->route = route;
up->config = client->config;
config_ref_inc(up->config);
int use_splice = !patch_has_rules(&route->patches) && !route->use_ssl;
if (use_splice) {
if (pipe(up->splice_pipe) == 0) {
up->can_splice = 1;
client->can_splice = 1;
client->splice_pipe[0] = up->splice_pipe[0];
client->splice_pipe[1] = up->splice_pipe[1];
} else {
up->splice_pipe[0] = -1;
up->splice_pipe[1] = -1;
}
}
if (buffer_init(&up->read_buf, CHUNK_SIZE) < 0) {
upstream_cleanup_partial(up, up_fd, client, 0, 0);
http_response_send_error(client, 502, "Bad Gateway", "Memory allocation failed");
return;
}
if (buffer_init(&up->write_buf, CHUNK_SIZE) < 0) {
upstream_cleanup_partial(up, up_fd, client, 1, 0);
http_response_send_error(client, 502, "Bad Gateway", "Memory allocation failed");
return;
}
char *data_to_send = (char *)data;
size_t len_to_send = data_len;
char *modified_request = NULL;
if (route->rewrite_host) {
modified_request = upstream_rewrite_host_header(data, data_len, route, &len_to_send);
if (modified_request) {
data_to_send = modified_request;
log_debug("Rewrote Host header to %s for route %s", route->upstream_host, route->hostname);
} else {
log_debug("Failed to rewrite Host header for route %s (Host header not found?)", route->hostname);
}
} else {
log_debug("Host rewrite disabled for route %s", route->hostname);
}
if (buffer_ensure_capacity(&up->write_buf, len_to_send) < 0) {
if (modified_request) free(modified_request);
upstream_cleanup_partial(up, up_fd, client, 1, 1);
http_response_send_error(client, 502, "Bad Gateway", "Memory allocation failed");
return;
}
memcpy(up->write_buf.data, data_to_send, len_to_send);
up->write_buf.tail = len_to_send;
if (modified_request) {
free(modified_request);
}
if (route->use_ssl) {
up->ssl = SSL_new(ssl_ctx);
if (!up->ssl) {
upstream_cleanup_partial(up, up_fd, client, 1, 1);
http_response_send_error(client, 502, "Bad Gateway", "SSL initialization failed");
return;
}
const char *sni_hostname = route->rewrite_host ? route->upstream_host : client->request.host;
ssl_set_hostname(up->ssl, sni_hostname);
SSL_set_fd(up->ssl, up_fd);
SSL_set_connect_state(up->ssl);
up->ssl_handshake_done = 0;
up->ssl_handshake_start = time(NULL);
log_debug("Setting SNI to: %s for upstream %s:%d",
sni_hostname, route->upstream_host, route->upstream_port);
}
up->state = CLIENT_STATE_FORWARDING;
log_debug("Connecting to upstream %s:%d on fd %d (SSL: %s)",
route->upstream_host, route->upstream_port, up_fd, route->use_ssl ? "yes" : "no");
}

13
src/upstream.h Normal file
View File

@ -0,0 +1,13 @@
// retoor <retoor@molodetz.nl>
#ifndef RPROXY_UPSTREAM_H
#define RPROXY_UPSTREAM_H
#include "types.h"
#include <netinet/in.h>
void upstream_connect(connection_t *client, const char *data, size_t data_len);
int upstream_try_connect(struct sockaddr_in *addr, int *out_fd);
void upstream_cleanup_partial(connection_t *up, int up_fd, connection_t *client, int free_read, int free_write);
#endif

182
tests/test_auth.c Executable file
View File

@ -0,0 +1,182 @@
#include "test_framework.h"
#include "../src/types.h"
#include "../src/auth.h"
#include <string.h>
void test_auth_init_and_enabled(void) {
TEST_SUITE_BEGIN("Auth Init and Enabled");
auth_init(NULL, NULL);
TEST_ASSERT_EQ(0, auth_is_enabled(), "Auth disabled with NULL credentials");
auth_init("", "");
TEST_ASSERT_EQ(0, auth_is_enabled(), "Auth disabled with empty credentials");
auth_init("admin", "secret");
TEST_ASSERT_EQ(1, auth_is_enabled(), "Auth enabled with valid credentials");
TEST_SUITE_END();
}
void test_auth_check_credentials(void) {
TEST_SUITE_BEGIN("Auth Check Credentials");
auth_init("testuser", "testpass");
TEST_ASSERT_EQ(1, auth_check_credentials("testuser", "testpass"), "Correct credentials accepted");
TEST_ASSERT_EQ(0, auth_check_credentials("testuser", "wrongpass"), "Wrong password rejected");
TEST_ASSERT_EQ(0, auth_check_credentials("wronguser", "testpass"), "Wrong username rejected");
TEST_ASSERT_EQ(0, auth_check_credentials("wronguser", "wrongpass"), "Wrong both rejected");
TEST_ASSERT_EQ(0, auth_check_credentials(NULL, "testpass"), "NULL username rejected");
TEST_ASSERT_EQ(0, auth_check_credentials("testuser", NULL), "NULL password rejected");
TEST_SUITE_END();
}
void test_auth_check_basic_auth(void) {
TEST_SUITE_BEGIN("Auth Check Basic Auth Header");
auth_init("admin", "password123");
char error[256];
int result = auth_check_basic_auth("Basic YWRtaW46cGFzc3dvcmQxMjM=", error, sizeof(error));
TEST_ASSERT_EQ(1, result, "Valid Basic auth header accepted");
result = auth_check_basic_auth("Basic aW52YWxpZDppbnZhbGlk", error, sizeof(error));
TEST_ASSERT_EQ(0, result, "Invalid credentials rejected");
result = auth_check_basic_auth(NULL, error, sizeof(error));
TEST_ASSERT_EQ(0, result, "NULL auth header rejected");
result = auth_check_basic_auth("Bearer token123", error, sizeof(error));
TEST_ASSERT_EQ(0, result, "Bearer auth rejected");
result = auth_check_basic_auth("Basic !!invalid!!", error, sizeof(error));
TEST_ASSERT_EQ(0, result, "Invalid base64 rejected");
result = auth_check_basic_auth("Basic bm9jb2xvbg==", error, sizeof(error));
TEST_ASSERT_EQ(0, result, "Base64 without colon rejected");
TEST_SUITE_END();
}
void test_auth_route_basic_auth(void) {
TEST_SUITE_BEGIN("Auth Route Basic Auth");
route_config_t route;
memset(&route, 0, sizeof(route));
route.use_auth = 0;
char error[256];
int result = auth_check_route_basic_auth(&route, NULL, error, sizeof(error));
TEST_ASSERT_EQ(1, result, "Route without auth passes");
route.use_auth = 1;
strcpy(route.username, "routeuser");
strcpy(route.password_hash, "5e884898da28047d9166d19d3de276ee6e8b0cb37d63e0c4c7ed6e1b4e5c7fd2");
result = auth_check_route_basic_auth(&route, NULL, error, sizeof(error));
TEST_ASSERT_EQ(0, result, "Route with auth requires header");
result = auth_check_route_basic_auth(NULL, NULL, error, sizeof(error));
TEST_ASSERT_EQ(1, result, "NULL route passes");
TEST_SUITE_END();
}
void test_auth_route_full_flow(void) {
TEST_SUITE_BEGIN("Auth Route Full Flow");
route_config_t route;
memset(&route, 0, sizeof(route));
route.use_auth = 1;
strcpy(route.username, "routeuser");
strcpy(route.password_hash, "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8");
char error[256];
int result = auth_check_route_basic_auth(&route, "Basic cm91dGV1c2VyOnBhc3N3b3Jk", error, sizeof(error));
TEST_ASSERT_EQ(1, result, "Valid route credentials accepted");
result = auth_check_route_basic_auth(&route, "Basic d3Jvbmd1c2VyOnBhc3N3b3Jk", error, sizeof(error));
TEST_ASSERT_EQ(0, result, "Wrong username rejected");
result = auth_check_route_basic_auth(&route, "Basic cm91dGV1c2VyOndyb25ncGFzcw==", error, sizeof(error));
TEST_ASSERT_EQ(0, result, "Wrong password rejected");
result = auth_check_route_basic_auth(&route, "Bearer token123", error, sizeof(error));
TEST_ASSERT_EQ(0, result, "Bearer auth rejected for route");
result = auth_check_route_basic_auth(&route, "Basic !!invalid!!", error, sizeof(error));
TEST_ASSERT_EQ(0, result, "Invalid base64 rejected for route");
result = auth_check_route_basic_auth(&route, "Basic bm9jb2xvbg==", error, sizeof(error));
TEST_ASSERT_EQ(0, result, "Base64 without colon rejected for route");
TEST_SUITE_END();
}
void test_auth_base64_edge_cases(void) {
TEST_SUITE_BEGIN("Auth Base64 Edge Cases");
auth_init("admin", "pass");
char error[256];
int result = auth_check_basic_auth("Basic YWRtaW46cGFzcw==", error, sizeof(error));
TEST_ASSERT_EQ(1, result, "Standard base64 works");
result = auth_check_basic_auth("Basic YTo=", error, sizeof(error));
TEST_ASSERT_EQ(0, result, "Short credentials rejected");
result = auth_check_basic_auth("Basic YTpi", error, sizeof(error));
TEST_ASSERT_EQ(0, result, "Minimal base64 without padding");
TEST_SUITE_END();
}
void test_auth_error_messages(void) {
TEST_SUITE_BEGIN("Auth Error Messages");
auth_init("admin", "secret");
char error[256];
memset(error, 0, sizeof(error));
auth_check_basic_auth(NULL, error, sizeof(error));
TEST_ASSERT(strlen(error) > 0, "Error message set for NULL header");
memset(error, 0, sizeof(error));
auth_check_basic_auth("Invalid", error, sizeof(error));
TEST_ASSERT(strlen(error) > 0, "Error message set for invalid method");
auth_check_basic_auth(NULL, NULL, 0);
TEST_ASSERT(1, "NULL error buffer handled");
TEST_SUITE_END();
}
void test_auth_disabled_passthrough(void) {
TEST_SUITE_BEGIN("Auth Disabled Passthrough");
auth_init(NULL, NULL);
TEST_ASSERT_EQ(1, auth_check_credentials("anyone", "anything"), "Disabled auth passes any credentials");
char error[256];
TEST_ASSERT_EQ(1, auth_check_basic_auth(NULL, error, sizeof(error)), "Disabled auth passes NULL header");
TEST_SUITE_END();
}
void run_auth_tests(void) {
test_auth_init_and_enabled();
test_auth_check_credentials();
test_auth_check_basic_auth();
test_auth_route_basic_auth();
test_auth_route_full_flow();
test_auth_base64_edge_cases();
test_auth_error_messages();
test_auth_disabled_passthrough();
}

282
tests/test_buffer.c Executable file
View File

@ -0,0 +1,282 @@
#include "test_framework.h"
#include "../src/types.h"
#include "../src/buffer.h"
void test_buffer_init(void) {
TEST_SUITE_BEGIN("Buffer Initialization");
buffer_t buf;
int result = buffer_init(&buf, 1024);
TEST_ASSERT_EQ(0, result, "Buffer init returns 0");
TEST_ASSERT(buf.data != NULL, "Buffer data is not NULL");
TEST_ASSERT_EQ(1024, buf.capacity, "Buffer capacity is 1024");
TEST_ASSERT_EQ(0, buf.head, "Buffer head is 0");
TEST_ASSERT_EQ(0, buf.tail, "Buffer tail is 0");
buffer_free(&buf);
TEST_ASSERT(buf.data == NULL, "Buffer data is NULL after free");
TEST_SUITE_END();
}
void test_buffer_read_write(void) {
TEST_SUITE_BEGIN("Buffer Read/Write Operations");
buffer_t buf;
buffer_init(&buf, 1024);
TEST_ASSERT_EQ(1024, buffer_available_write(&buf), "Initial write capacity is 1024");
TEST_ASSERT_EQ(0, buffer_available_read(&buf), "Initial read capacity is 0");
const char *test_data = "Hello, World!";
size_t len = strlen(test_data);
memcpy(buf.data + buf.tail, test_data, len);
buf.tail += len;
TEST_ASSERT_EQ(len, buffer_available_read(&buf), "Read capacity equals written data");
TEST_ASSERT_EQ(1024 - len, buffer_available_write(&buf), "Write capacity reduced");
buffer_free(&buf);
TEST_SUITE_END();
}
void test_buffer_consume(void) {
TEST_SUITE_BEGIN("Buffer Consume Operations");
buffer_t buf;
buffer_init(&buf, 1024);
const char *test_data = "0123456789";
size_t len = strlen(test_data);
memcpy(buf.data + buf.tail, test_data, len);
buf.tail += len;
buffer_consume(&buf, 5);
TEST_ASSERT_EQ(5, buffer_available_read(&buf), "5 bytes remaining after consume");
TEST_ASSERT(memcmp(buf.data + buf.head, "56789", 5) == 0, "Correct data after consume");
buffer_consume(&buf, 5);
TEST_ASSERT_EQ(0, buffer_available_read(&buf), "Buffer empty after full consume");
TEST_ASSERT_EQ(0, buf.head, "Head reset to 0");
TEST_ASSERT_EQ(0, buf.tail, "Tail reset to 0");
buffer_free(&buf);
TEST_SUITE_END();
}
void test_buffer_compact(void) {
TEST_SUITE_BEGIN("Buffer Compact Operations");
buffer_t buf;
buffer_init(&buf, 1024);
const char *test_data = "ABCDEFGHIJ";
size_t len = strlen(test_data);
memcpy(buf.data + buf.tail, test_data, len);
buf.tail += len;
buffer_consume(&buf, 5);
TEST_ASSERT_EQ(5, buf.head, "Head moved to 5");
buffer_compact(&buf);
TEST_ASSERT_EQ(0, buf.head, "Head is 0 after compact");
TEST_ASSERT_EQ(5, buf.tail, "Tail is 5 after compact");
TEST_ASSERT(memcmp(buf.data, "FGHIJ", 5) == 0, "Data moved to beginning");
buffer_free(&buf);
TEST_SUITE_END();
}
void test_buffer_ensure_capacity(void) {
TEST_SUITE_BEGIN("Buffer Ensure Capacity");
buffer_t buf;
buffer_init(&buf, 64);
int result = buffer_ensure_capacity(&buf, 128);
TEST_ASSERT_EQ(0, result, "Capacity increase successful");
TEST_ASSERT(buf.capacity >= 128, "Capacity at least 128");
result = buffer_ensure_capacity(&buf, 512);
TEST_ASSERT_EQ(0, result, "Second capacity increase successful");
TEST_ASSERT(buf.capacity >= 512, "Capacity at least 512");
result = buffer_ensure_capacity(&buf, 64);
TEST_ASSERT_EQ(0, result, "No change when capacity already sufficient");
buffer_free(&buf);
TEST_SUITE_END();
}
void test_buffer_multiple_operations(void) {
TEST_SUITE_BEGIN("Buffer Multiple Operations");
buffer_t buf;
buffer_init(&buf, 128);
for (int i = 0; i < 10; i++) {
char data[10];
snprintf(data, sizeof(data), "MSG%d", i);
size_t len = strlen(data);
if (buffer_available_write(&buf) < len) {
buffer_compact(&buf);
}
memcpy(buf.data + buf.tail, data, len);
buf.tail += len;
}
TEST_ASSERT(buffer_available_read(&buf) > 0, "Buffer has data after multiple writes");
size_t total_read = 0;
while (buffer_available_read(&buf) > 0) {
size_t to_consume = buffer_available_read(&buf) > 5 ? 5 : buffer_available_read(&buf);
buffer_consume(&buf, to_consume);
total_read += to_consume;
}
TEST_ASSERT(total_read > 0, "All data was consumed");
TEST_ASSERT_EQ(0, buffer_available_read(&buf), "Buffer is empty");
buffer_free(&buf);
TEST_SUITE_END();
}
void test_buffer_null_safety(void) {
TEST_SUITE_BEGIN("Buffer NULL Safety");
TEST_ASSERT_EQ(-1, buffer_init(NULL, 1024), "NULL buffer init returns -1");
buffer_free(NULL);
TEST_ASSERT(1, "NULL buffer free doesn't crash");
TEST_ASSERT_EQ(0, buffer_available_read(NULL), "NULL buffer read returns 0");
TEST_ASSERT_EQ(0, buffer_available_write(NULL), "NULL buffer write returns 0");
TEST_ASSERT_EQ(-1, buffer_ensure_capacity(NULL, 1024), "NULL buffer ensure capacity returns -1");
buffer_compact(NULL);
TEST_ASSERT(1, "NULL buffer compact doesn't crash");
buffer_consume(NULL, 100);
TEST_ASSERT(1, "NULL buffer consume doesn't crash");
TEST_SUITE_END();
}
void test_buffer_consume_overflow(void) {
TEST_SUITE_BEGIN("Buffer Consume Overflow");
buffer_t buf;
buffer_init(&buf, 64);
memcpy(buf.data, "TEST", 4);
buf.tail = 4;
buffer_consume(&buf, 100);
TEST_ASSERT_EQ(0, buf.head, "Head reset after over-consume");
TEST_ASSERT_EQ(0, buf.tail, "Tail reset after over-consume");
TEST_ASSERT_EQ(0, buffer_available_read(&buf), "No data after over-consume");
buffer_free(&buf);
TEST_SUITE_END();
}
void test_buffer_compact_edge_cases(void) {
TEST_SUITE_BEGIN("Buffer Compact Edge Cases");
buffer_t buf;
buffer_init(&buf, 64);
buffer_compact(&buf);
TEST_ASSERT_EQ(0, buf.head, "Compact empty buffer - head is 0");
TEST_ASSERT_EQ(0, buf.tail, "Compact empty buffer - tail is 0");
memcpy(buf.data, "TEST", 4);
buf.tail = 4;
buf.head = 0;
buffer_compact(&buf);
TEST_ASSERT_EQ(0, buf.head, "Compact with head=0 unchanged");
buffer_free(&buf);
TEST_SUITE_END();
}
void test_buffer_capacity_limits(void) {
TEST_SUITE_BEGIN("Buffer Capacity Limits");
buffer_t buf;
buffer_init(&buf, 64);
int result = buffer_ensure_capacity(&buf, MAX_BUFFER_SIZE + 1);
TEST_ASSERT_EQ(-1, result, "Exceeding MAX_BUFFER_SIZE returns -1");
buffer_free(&buf);
TEST_SUITE_END();
}
void test_buffer_large_growth(void) {
TEST_SUITE_BEGIN("Buffer Large Growth");
buffer_t buf;
buffer_init(&buf, 64);
int result = buffer_ensure_capacity(&buf, 1024);
TEST_ASSERT_EQ(0, result, "Grow to 1024 succeeds");
TEST_ASSERT(buf.capacity >= 1024, "Capacity at least 1024");
result = buffer_ensure_capacity(&buf, 4096);
TEST_ASSERT_EQ(0, result, "Grow to 4096 succeeds");
TEST_ASSERT(buf.capacity >= 4096, "Capacity at least 4096");
result = buffer_ensure_capacity(&buf, 65536);
TEST_ASSERT_EQ(0, result, "Grow to 65536 succeeds");
TEST_ASSERT(buf.capacity >= 65536, "Capacity at least 65536");
buffer_free(&buf);
TEST_SUITE_END();
}
void test_buffer_near_max_capacity(void) {
TEST_SUITE_BEGIN("Buffer Near Max Capacity");
buffer_t buf;
buffer_init(&buf, 1024);
int result = buffer_ensure_capacity(&buf, MAX_BUFFER_SIZE - 1024);
TEST_ASSERT_EQ(0, result, "Grow to near max succeeds");
result = buffer_ensure_capacity(&buf, MAX_BUFFER_SIZE);
TEST_ASSERT_EQ(0, result, "Grow to exactly max succeeds");
TEST_ASSERT(buf.capacity <= MAX_BUFFER_SIZE, "Capacity at max");
buffer_free(&buf);
TEST_SUITE_END();
}
void run_buffer_tests(void) {
test_buffer_init();
test_buffer_read_write();
test_buffer_consume();
test_buffer_compact();
test_buffer_ensure_capacity();
test_buffer_multiple_operations();
test_buffer_null_safety();
test_buffer_consume_overflow();
test_buffer_compact_edge_cases();
test_buffer_capacity_limits();
test_buffer_large_growth();
test_buffer_near_max_capacity();
}

692
tests/test_config.c Executable file
View File

@ -0,0 +1,692 @@
#include "test_framework.h"
#include "../src/types.h"
#include "../src/config.h"
#include <stdio.h>
#include <unistd.h>
static const char *TEST_CONFIG_FILE = "/tmp/test_proxy_config.json";
static void create_test_config(const char *content) {
FILE *f = fopen(TEST_CONFIG_FILE, "w");
if (f) {
fprintf(f, "%s", content);
fclose(f);
}
}
static void cleanup_test_config(void) {
unlink(TEST_CONFIG_FILE);
}
void test_config_load_valid(void) {
TEST_SUITE_BEGIN("Config Load Valid Configuration");
const char *valid_config =
"{\n"
" \"port\": 9090,\n"
" \"reverse_proxy\": [\n"
" {\n"
" \"hostname\": \"test.example.com\",\n"
" \"upstream_host\": \"127.0.0.1\",\n"
" \"upstream_port\": 3000,\n"
" \"use_ssl\": false,\n"
" \"rewrite_host\": true\n"
" },\n"
" {\n"
" \"hostname\": \"api.example.com\",\n"
" \"upstream_host\": \"192.168.1.100\",\n"
" \"upstream_port\": 443,\n"
" \"use_ssl\": true,\n"
" \"rewrite_host\": false\n"
" }\n"
" ]\n"
"}\n";
create_test_config(valid_config);
int result = config_load(TEST_CONFIG_FILE);
TEST_ASSERT_EQ(1, result, "Config loaded successfully");
TEST_ASSERT_EQ(9090, config->port, "Port is 9090");
TEST_ASSERT_EQ(2, config->route_count, "Two routes configured");
route_config_t *route1 = config_find_route("test.example.com");
TEST_ASSERT(route1 != NULL, "Route for test.example.com found");
if (route1) {
TEST_ASSERT_STR_EQ("127.0.0.1", route1->upstream_host, "First route upstream host");
TEST_ASSERT_EQ(3000, route1->upstream_port, "First route upstream port");
TEST_ASSERT_EQ(0, route1->use_ssl, "First route SSL disabled");
TEST_ASSERT_EQ(1, route1->rewrite_host, "First route host rewrite enabled");
}
route_config_t *route2 = config_find_route("api.example.com");
TEST_ASSERT(route2 != NULL, "Route for api.example.com found");
if (route2) {
TEST_ASSERT_STR_EQ("192.168.1.100", route2->upstream_host, "Second route upstream host");
TEST_ASSERT_EQ(443, route2->upstream_port, "Second route upstream port");
TEST_ASSERT_EQ(1, route2->use_ssl, "Second route SSL enabled");
TEST_ASSERT_EQ(0, route2->rewrite_host, "Second route host rewrite disabled");
}
config_free();
cleanup_test_config();
TEST_SUITE_END();
}
void test_config_find_route_case_insensitive(void) {
TEST_SUITE_BEGIN("Config Find Route Case Insensitive");
const char *config_content =
"{\n"
" \"port\": 8080,\n"
" \"reverse_proxy\": [\n"
" {\n"
" \"hostname\": \"Test.Example.COM\",\n"
" \"upstream_host\": \"localhost\",\n"
" \"upstream_port\": 3000,\n"
" \"use_ssl\": false,\n"
" \"rewrite_host\": false\n"
" }\n"
" ]\n"
"}\n";
create_test_config(config_content);
config_load(TEST_CONFIG_FILE);
route_config_t *route1 = config_find_route("test.example.com");
TEST_ASSERT(route1 != NULL, "Lowercase hostname matches");
route_config_t *route2 = config_find_route("TEST.EXAMPLE.COM");
TEST_ASSERT(route2 != NULL, "Uppercase hostname matches");
route_config_t *route3 = config_find_route("TeSt.ExAmPlE.cOm");
TEST_ASSERT(route3 != NULL, "Mixed case hostname matches");
config_free();
cleanup_test_config();
TEST_SUITE_END();
}
void test_config_find_route_nonexistent(void) {
TEST_SUITE_BEGIN("Config Find Nonexistent Route");
const char *config_content =
"{\n"
" \"port\": 8080,\n"
" \"reverse_proxy\": [\n"
" {\n"
" \"hostname\": \"existing.com\",\n"
" \"upstream_host\": \"localhost\",\n"
" \"upstream_port\": 3000,\n"
" \"use_ssl\": false,\n"
" \"rewrite_host\": false\n"
" }\n"
" ]\n"
"}\n";
create_test_config(config_content);
config_load(TEST_CONFIG_FILE);
route_config_t *route = config_find_route("nonexistent.com");
TEST_ASSERT(route == NULL, "Nonexistent route returns NULL");
route = config_find_route(NULL);
TEST_ASSERT(route == NULL, "NULL hostname returns NULL");
config_free();
cleanup_test_config();
TEST_SUITE_END();
}
void test_config_default_port(void) {
TEST_SUITE_BEGIN("Config Default Port");
const char *config_content =
"{\n"
" \"reverse_proxy\": [\n"
" {\n"
" \"hostname\": \"test.com\",\n"
" \"upstream_host\": \"localhost\",\n"
" \"upstream_port\": 3000,\n"
" \"use_ssl\": false,\n"
" \"rewrite_host\": false\n"
" }\n"
" ]\n"
"}\n";
create_test_config(config_content);
config_load(TEST_CONFIG_FILE);
TEST_ASSERT_EQ(8080, config->port, "Default port is 8080 when not specified");
config_free();
cleanup_test_config();
TEST_SUITE_END();
}
void test_config_invalid_json(void) {
TEST_SUITE_BEGIN("Config Invalid JSON");
const char *invalid_config = "{ invalid json }";
create_test_config(invalid_config);
int result = config_load(TEST_CONFIG_FILE);
TEST_ASSERT_EQ(0, result, "Invalid JSON returns 0");
cleanup_test_config();
TEST_SUITE_END();
}
void test_config_missing_file(void) {
TEST_SUITE_BEGIN("Config Missing File");
unlink("/tmp/nonexistent_config.json");
int result = config_load("/tmp/nonexistent_config.json");
TEST_ASSERT_EQ(0, result, "Missing file returns 0");
TEST_SUITE_END();
}
void test_config_ssl_rewrite_host_options(void) {
TEST_SUITE_BEGIN("Config SSL and Rewrite Host Options");
const char *config_content =
"{\n"
" \"port\": 8080,\n"
" \"reverse_proxy\": [\n"
" {\n"
" \"hostname\": \"https-rewrite.com\",\n"
" \"upstream_host\": \"secure.example.com\",\n"
" \"upstream_port\": 443,\n"
" \"use_ssl\": true,\n"
" \"rewrite_host\": true\n"
" },\n"
" {\n"
" \"hostname\": \"http-norewrite.com\",\n"
" \"upstream_host\": \"plain.example.com\",\n"
" \"upstream_port\": 80,\n"
" \"use_ssl\": false,\n"
" \"rewrite_host\": false\n"
" }\n"
" ]\n"
"}\n";
create_test_config(config_content);
config_load(TEST_CONFIG_FILE);
route_config_t *ssl_route = config_find_route("https-rewrite.com");
TEST_ASSERT(ssl_route != NULL, "SSL route found");
if (ssl_route) {
TEST_ASSERT_EQ(1, ssl_route->use_ssl, "SSL enabled for https route");
TEST_ASSERT_EQ(1, ssl_route->rewrite_host, "Host rewrite enabled for https route");
}
route_config_t *plain_route = config_find_route("http-norewrite.com");
TEST_ASSERT(plain_route != NULL, "Plain route found");
if (plain_route) {
TEST_ASSERT_EQ(0, plain_route->use_ssl, "SSL disabled for http route");
TEST_ASSERT_EQ(0, plain_route->rewrite_host, "Host rewrite disabled for http route");
}
config_free();
cleanup_test_config();
TEST_SUITE_END();
}
void test_config_invalid_port(void) {
TEST_SUITE_BEGIN("Config Invalid Port");
const char *config_content =
"{\n"
" \"port\": 99999,\n"
" \"reverse_proxy\": [\n"
" {\n"
" \"hostname\": \"test.com\",\n"
" \"upstream_host\": \"localhost\",\n"
" \"upstream_port\": 3000,\n"
" \"use_ssl\": false,\n"
" \"rewrite_host\": false\n"
" }\n"
" ]\n"
"}\n";
create_test_config(config_content);
int result = config_load(TEST_CONFIG_FILE);
TEST_ASSERT_EQ(0, result, "Invalid port (99999) rejected");
cleanup_test_config();
const char *config_zero =
"{\n"
" \"port\": 0,\n"
" \"reverse_proxy\": [\n"
" {\n"
" \"hostname\": \"test.com\",\n"
" \"upstream_host\": \"localhost\",\n"
" \"upstream_port\": 3000,\n"
" \"use_ssl\": false,\n"
" \"rewrite_host\": false\n"
" }\n"
" ]\n"
"}\n";
create_test_config(config_zero);
result = config_load(TEST_CONFIG_FILE);
TEST_ASSERT_EQ(0, result, "Invalid port (0) rejected");
cleanup_test_config();
TEST_SUITE_END();
}
void test_config_empty_routes(void) {
TEST_SUITE_BEGIN("Config Empty Routes");
const char *config_content =
"{\n"
" \"port\": 8080,\n"
" \"reverse_proxy\": []\n"
"}\n";
create_test_config(config_content);
int result = config_load(TEST_CONFIG_FILE);
TEST_ASSERT_EQ(0, result, "Empty routes array rejected");
cleanup_test_config();
TEST_SUITE_END();
}
void test_config_with_auth(void) {
TEST_SUITE_BEGIN("Config With Authentication");
const char *config_content =
"{\n"
" \"port\": 8080,\n"
" \"reverse_proxy\": [\n"
" {\n"
" \"hostname\": \"secure.example.com\",\n"
" \"upstream_host\": \"backend.local\",\n"
" \"upstream_port\": 3000,\n"
" \"use_ssl\": false,\n"
" \"rewrite_host\": false,\n"
" \"use_auth\": true,\n"
" \"username\": \"admin\",\n"
" \"password\": \"secret123\"\n"
" }\n"
" ]\n"
"}\n";
create_test_config(config_content);
int result = config_load(TEST_CONFIG_FILE);
TEST_ASSERT_EQ(1, result, "Config with auth loaded");
route_config_t *route = config_find_route("secure.example.com");
TEST_ASSERT(route != NULL, "Auth route found");
if (route) {
TEST_ASSERT_EQ(1, route->use_auth, "Auth is enabled");
TEST_ASSERT_STR_EQ("admin", route->username, "Username is admin");
TEST_ASSERT(strlen(route->password_hash) > 0, "Password hash is set");
}
config_free();
cleanup_test_config();
TEST_SUITE_END();
}
void test_config_with_patches(void) {
TEST_SUITE_BEGIN("Config With Patch Rules");
const char *config_content =
"{\n"
" \"port\": 8080,\n"
" \"reverse_proxy\": [\n"
" {\n"
" \"hostname\": \"patched.example.com\",\n"
" \"upstream_host\": \"backend.local\",\n"
" \"upstream_port\": 3000,\n"
" \"use_ssl\": false,\n"
" \"rewrite_host\": false,\n"
" \"patch\": {\n"
" \"old-text\": \"new-text\",\n"
" \"remove-this\": null\n"
" }\n"
" }\n"
" ]\n"
"}\n";
create_test_config(config_content);
int result = config_load(TEST_CONFIG_FILE);
TEST_ASSERT_EQ(1, result, "Config with patches loaded");
route_config_t *route = config_find_route("patched.example.com");
TEST_ASSERT(route != NULL, "Patched route found");
if (route) {
TEST_ASSERT_EQ(2, route->patches.rule_count, "Two patch rules loaded");
}
config_free();
cleanup_test_config();
TEST_SUITE_END();
}
void test_config_create_default(void) {
TEST_SUITE_BEGIN("Config Create Default");
const char *default_file = "/tmp/test_default_config.json";
unlink(default_file);
config_create_default(default_file);
FILE *f = fopen(default_file, "r");
TEST_ASSERT(f != NULL, "Default config file created");
if (f) {
fclose(f);
}
config_create_default(default_file);
int result = config_load(default_file);
TEST_ASSERT_EQ(1, result, "Default config is valid");
config_free();
unlink(default_file);
TEST_SUITE_END();
}
void test_config_check_file_changed(void) {
TEST_SUITE_BEGIN("Config Check File Changed");
const char *config_content =
"{\n"
" \"port\": 8080,\n"
" \"reverse_proxy\": [\n"
" {\n"
" \"hostname\": \"test.com\",\n"
" \"upstream_host\": \"localhost\",\n"
" \"upstream_port\": 3000,\n"
" \"use_ssl\": false,\n"
" \"rewrite_host\": false\n"
" }\n"
" ]\n"
"}\n";
create_test_config(config_content);
int first_check = config_check_file_changed(TEST_CONFIG_FILE);
TEST_ASSERT_EQ(0, first_check, "First check returns 0 (initializes mtime)");
int second_check = config_check_file_changed(TEST_CONFIG_FILE);
TEST_ASSERT_EQ(0, second_check, "Second check returns 0 (unchanged)");
int missing_check = config_check_file_changed("/tmp/nonexistent_file.json");
TEST_ASSERT_EQ(0, missing_check, "Missing file returns 0");
cleanup_test_config();
TEST_SUITE_END();
}
void test_config_hot_reload(void) {
TEST_SUITE_BEGIN("Config Hot Reload");
const char *initial_config =
"{\n"
" \"port\": 8080,\n"
" \"reverse_proxy\": [\n"
" {\n"
" \"hostname\": \"initial.com\",\n"
" \"upstream_host\": \"localhost\",\n"
" \"upstream_port\": 3000,\n"
" \"use_ssl\": false,\n"
" \"rewrite_host\": false\n"
" }\n"
" ]\n"
"}\n";
create_test_config(initial_config);
config_load(TEST_CONFIG_FILE);
route_config_t *route1 = config_find_route("initial.com");
TEST_ASSERT(route1 != NULL, "Initial route found");
const char *updated_config =
"{\n"
" \"port\": 9090,\n"
" \"reverse_proxy\": [\n"
" {\n"
" \"hostname\": \"updated.com\",\n"
" \"upstream_host\": \"newhost\",\n"
" \"upstream_port\": 4000,\n"
" \"use_ssl\": true,\n"
" \"rewrite_host\": true\n"
" }\n"
" ]\n"
"}\n";
create_test_config(updated_config);
int result = config_hot_reload(TEST_CONFIG_FILE);
TEST_ASSERT_EQ(1, result, "Hot reload succeeded");
route_config_t *route2 = config_find_route("updated.com");
TEST_ASSERT(route2 != NULL, "Updated route found after hot reload");
if (route2) {
TEST_ASSERT_STR_EQ("newhost", route2->upstream_host, "New upstream host");
TEST_ASSERT_EQ(4000, route2->upstream_port, "New upstream port");
TEST_ASSERT_EQ(1, route2->use_ssl, "SSL enabled after reload");
}
config_free();
cleanup_test_config();
TEST_SUITE_END();
}
void test_config_hot_reload_invalid(void) {
TEST_SUITE_BEGIN("Config Hot Reload Invalid");
const char *valid_config =
"{\n"
" \"port\": 8080,\n"
" \"reverse_proxy\": [\n"
" {\n"
" \"hostname\": \"test.com\",\n"
" \"upstream_host\": \"localhost\",\n"
" \"upstream_port\": 3000,\n"
" \"use_ssl\": false,\n"
" \"rewrite_host\": false\n"
" }\n"
" ]\n"
"}\n";
create_test_config(valid_config);
config_load(TEST_CONFIG_FILE);
create_test_config("{ invalid json }");
int result = config_hot_reload(TEST_CONFIG_FILE);
TEST_ASSERT_EQ(0, result, "Hot reload with invalid JSON fails");
route_config_t *route = config_find_route("test.com");
TEST_ASSERT(route != NULL, "Original config preserved after failed reload");
config_free();
cleanup_test_config();
TEST_SUITE_END();
}
void test_config_hot_reload_invalid_port(void) {
TEST_SUITE_BEGIN("Config Hot Reload Invalid Port");
const char *valid_config =
"{\n"
" \"port\": 8080,\n"
" \"reverse_proxy\": [\n"
" {\n"
" \"hostname\": \"test.com\",\n"
" \"upstream_host\": \"localhost\",\n"
" \"upstream_port\": 3000,\n"
" \"use_ssl\": false,\n"
" \"rewrite_host\": false\n"
" }\n"
" ]\n"
"}\n";
create_test_config(valid_config);
config_load(TEST_CONFIG_FILE);
const char *invalid_port_config =
"{\n"
" \"port\": 99999,\n"
" \"reverse_proxy\": [\n"
" {\n"
" \"hostname\": \"new.com\",\n"
" \"upstream_host\": \"localhost\",\n"
" \"upstream_port\": 3000,\n"
" \"use_ssl\": false,\n"
" \"rewrite_host\": false\n"
" }\n"
" ]\n"
"}\n";
create_test_config(invalid_port_config);
int result = config_hot_reload(TEST_CONFIG_FILE);
TEST_ASSERT_EQ(0, result, "Hot reload with invalid port fails");
config_free();
cleanup_test_config();
TEST_SUITE_END();
}
void test_config_ref_counting(void) {
TEST_SUITE_BEGIN("Config Reference Counting");
const char *config_content =
"{\n"
" \"port\": 8080,\n"
" \"reverse_proxy\": [\n"
" {\n"
" \"hostname\": \"test.com\",\n"
" \"upstream_host\": \"localhost\",\n"
" \"upstream_port\": 3000,\n"
" \"use_ssl\": false,\n"
" \"rewrite_host\": false\n"
" }\n"
" ]\n"
"}\n";
create_test_config(config_content);
config_load(TEST_CONFIG_FILE);
TEST_ASSERT(config != NULL, "Config loaded");
TEST_ASSERT_EQ(1, config->ref_count, "Initial ref count is 1");
config_ref_inc(config);
TEST_ASSERT_EQ(2, config->ref_count, "Ref count incremented to 2");
config_ref_inc(config);
TEST_ASSERT_EQ(3, config->ref_count, "Ref count incremented to 3");
config_ref_dec(config);
TEST_ASSERT_EQ(2, config->ref_count, "Ref count decremented to 2");
config_ref_dec(config);
TEST_ASSERT_EQ(1, config->ref_count, "Ref count decremented to 1");
config_ref_inc(NULL);
config_ref_dec(NULL);
TEST_ASSERT(1, "NULL ref operations don't crash");
config_free();
cleanup_test_config();
TEST_SUITE_END();
}
void test_config_hot_reload_with_auth_and_patches(void) {
TEST_SUITE_BEGIN("Config Hot Reload With Auth And Patches");
const char *initial_config =
"{\n"
" \"port\": 8080,\n"
" \"reverse_proxy\": [\n"
" {\n"
" \"hostname\": \"test.com\",\n"
" \"upstream_host\": \"localhost\",\n"
" \"upstream_port\": 3000,\n"
" \"use_ssl\": false,\n"
" \"rewrite_host\": false\n"
" }\n"
" ]\n"
"}\n";
create_test_config(initial_config);
config_load(TEST_CONFIG_FILE);
const char *updated_config =
"{\n"
" \"port\": 8080,\n"
" \"reverse_proxy\": [\n"
" {\n"
" \"hostname\": \"secure.com\",\n"
" \"upstream_host\": \"backend\",\n"
" \"upstream_port\": 443,\n"
" \"use_ssl\": true,\n"
" \"rewrite_host\": true,\n"
" \"use_auth\": true,\n"
" \"username\": \"user\",\n"
" \"password\": \"pass\",\n"
" \"patch\": {\n"
" \"find\": \"replace\"\n"
" }\n"
" }\n"
" ]\n"
"}\n";
create_test_config(updated_config);
int result = config_hot_reload(TEST_CONFIG_FILE);
TEST_ASSERT_EQ(1, result, "Hot reload with auth and patches succeeded");
route_config_t *route = config_find_route("secure.com");
TEST_ASSERT(route != NULL, "Route with auth found");
if (route) {
TEST_ASSERT_EQ(1, route->use_auth, "Auth enabled after reload");
TEST_ASSERT_EQ(1, route->patches.rule_count, "Patch rule loaded after reload");
}
config_free();
cleanup_test_config();
TEST_SUITE_END();
}
void run_config_tests(void) {
test_config_load_valid();
test_config_find_route_case_insensitive();
test_config_find_route_nonexistent();
test_config_default_port();
test_config_invalid_json();
test_config_missing_file();
test_config_ssl_rewrite_host_options();
test_config_invalid_port();
test_config_empty_routes();
test_config_with_auth();
test_config_with_patches();
test_config_create_default();
test_config_check_file_changed();
test_config_hot_reload();
test_config_hot_reload_invalid();
test_config_hot_reload_invalid_port();
test_config_ref_counting();
test_config_hot_reload_with_auth_and_patches();
}

2633
tests/test_connection.c Executable file

File diff suppressed because it is too large Load Diff

273
tests/test_dashboard.c Executable file
View File

@ -0,0 +1,273 @@
#include "test_framework.h"
#include "../src/types.h"
#include "../src/dashboard.h"
#include "../src/monitor.h"
#include "../src/buffer.h"
#include "../src/connection.h"
#include "../src/auth.h"
#include <string.h>
#include <unistd.h>
#include <sys/epoll.h>
extern connection_t connections[MAX_FDS];
extern int epoll_fd;
static connection_t* create_test_connection(void) {
static int test_fd = 100;
int fd = test_fd++;
connection_t *conn = &connections[fd];
memset(conn, 0, sizeof(connection_t));
conn->fd = fd;
conn->type = CONN_TYPE_CLIENT;
conn->state = CLIENT_STATE_SERVING_INTERNAL;
buffer_init(&conn->read_buf, 4096);
buffer_init(&conn->write_buf, 65536);
conn->request.keep_alive = 1;
return conn;
}
static void cleanup_test_connection(connection_t *conn) {
buffer_free(&conn->read_buf);
buffer_free(&conn->write_buf);
memset(conn, 0, sizeof(connection_t));
}
void test_dashboard_serve_null(void) {
TEST_SUITE_BEGIN("Dashboard Serve NULL Safety");
dashboard_serve(NULL, "GET /", 5);
TEST_ASSERT(1, "NULL connection doesn't crash");
dashboard_serve_stats_api(NULL, "GET /", 5);
TEST_ASSERT(1, "NULL connection for stats doesn't crash");
TEST_SUITE_END();
}
void test_dashboard_serve_html(void) {
TEST_SUITE_BEGIN("Dashboard Serve HTML");
epoll_fd = epoll_create1(0);
TEST_ASSERT(epoll_fd >= 0, "Epoll created");
monitor_init(NULL);
auth_init(NULL, NULL);
connection_t *conn = create_test_connection();
struct epoll_event ev = { .events = EPOLLIN, .data.fd = conn->fd };
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn->fd, &ev);
const char *request = "GET /rproxy/dashboard HTTP/1.1\r\nHost: localhost\r\n\r\n";
dashboard_serve(conn, request, strlen(request));
TEST_ASSERT(conn->write_buf.tail > 0, "Response written to buffer");
TEST_ASSERT(strstr((char*)conn->write_buf.data, "HTTP/1.1 200") != NULL, "HTTP 200 response");
TEST_ASSERT(strstr((char*)conn->write_buf.data, "text/html") != NULL, "Content-Type is HTML");
TEST_ASSERT(strstr((char*)conn->write_buf.data, "<!DOCTYPE html>") != NULL, "HTML content present");
cleanup_test_connection(conn);
monitor_cleanup();
close(epoll_fd);
TEST_SUITE_END();
}
void test_dashboard_serve_stats_api(void) {
TEST_SUITE_BEGIN("Dashboard Serve Stats API");
epoll_fd = epoll_create1(0);
monitor_init(NULL);
auth_init(NULL, NULL);
vhost_stats_t *stats = monitor_get_or_create_vhost_stats("test.example.com");
monitor_record_request_start(stats, 0);
monitor_record_bytes(stats, 1000, 500);
connection_t *conn = create_test_connection();
struct epoll_event ev = { .events = EPOLLIN, .data.fd = conn->fd };
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn->fd, &ev);
const char *request = "GET /rproxy/api/stats HTTP/1.1\r\nHost: localhost\r\n\r\n";
dashboard_serve_stats_api(conn, request, strlen(request));
TEST_ASSERT(conn->write_buf.tail > 0, "Stats response written");
TEST_ASSERT(strstr((char*)conn->write_buf.data, "HTTP/1.1 200") != NULL, "HTTP 200 response");
TEST_ASSERT(strstr((char*)conn->write_buf.data, "application/json") != NULL, "Content-Type is JSON");
TEST_ASSERT(strstr((char*)conn->write_buf.data, "cpu_percent") != NULL, "CPU data in response");
TEST_ASSERT(strstr((char*)conn->write_buf.data, "memory_gb") != NULL, "Memory data in response");
TEST_ASSERT(strstr((char*)conn->write_buf.data, "active_connections") != NULL, "Connections data in response");
TEST_ASSERT(strstr((char*)conn->write_buf.data, "processes") != NULL, "Processes array in response");
TEST_ASSERT(strstr((char*)conn->write_buf.data, "test.example.com") != NULL, "Vhost in response");
cleanup_test_connection(conn);
monitor_cleanup();
close(epoll_fd);
TEST_SUITE_END();
}
void test_dashboard_with_auth_disabled(void) {
TEST_SUITE_BEGIN("Dashboard With Auth Disabled");
epoll_fd = epoll_create1(0);
monitor_init(NULL);
auth_init(NULL, NULL);
connection_t *conn = create_test_connection();
struct epoll_event ev = { .events = EPOLLIN, .data.fd = conn->fd };
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn->fd, &ev);
const char *request = "GET /rproxy/dashboard HTTP/1.1\r\nHost: localhost\r\n\r\n";
dashboard_serve(conn, request, strlen(request));
TEST_ASSERT(strstr((char*)conn->write_buf.data, "HTTP/1.1 200") != NULL, "Dashboard accessible without auth when disabled");
cleanup_test_connection(conn);
monitor_cleanup();
close(epoll_fd);
TEST_SUITE_END();
}
void test_dashboard_with_auth_enabled_no_header(void) {
TEST_SUITE_BEGIN("Dashboard With Auth Enabled No Header");
epoll_fd = epoll_create1(0);
monitor_init(NULL);
auth_init("admin", "secret");
connection_t *conn = create_test_connection();
struct epoll_event ev = { .events = EPOLLIN, .data.fd = conn->fd };
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn->fd, &ev);
const char *request = "GET /rproxy/dashboard HTTP/1.1\r\nHost: localhost\r\n\r\n";
dashboard_serve(conn, request, strlen(request));
TEST_ASSERT(strstr((char*)conn->write_buf.data, "HTTP/1.1 401") != NULL, "401 response when auth required");
TEST_ASSERT(strstr((char*)conn->write_buf.data, "WWW-Authenticate") != NULL, "WWW-Authenticate header present");
TEST_ASSERT(conn->state == CLIENT_STATE_ERROR, "Connection state is ERROR");
cleanup_test_connection(conn);
auth_init(NULL, NULL);
monitor_cleanup();
close(epoll_fd);
TEST_SUITE_END();
}
void test_dashboard_with_valid_auth(void) {
TEST_SUITE_BEGIN("Dashboard With Valid Auth");
epoll_fd = epoll_create1(0);
monitor_init(NULL);
auth_init("admin", "secret");
connection_t *conn = create_test_connection();
struct epoll_event ev = { .events = EPOLLIN, .data.fd = conn->fd };
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn->fd, &ev);
const char *request = "GET /rproxy/dashboard HTTP/1.1\r\nHost: localhost\r\nAuthorization: Basic YWRtaW46c2VjcmV0\r\n\r\n";
dashboard_serve(conn, request, strlen(request));
TEST_ASSERT(strstr((char*)conn->write_buf.data, "HTTP/1.1 200") != NULL, "200 response with valid auth");
TEST_ASSERT(strstr((char*)conn->write_buf.data, "<!DOCTYPE html>") != NULL, "HTML content served");
cleanup_test_connection(conn);
auth_init(NULL, NULL);
monitor_cleanup();
close(epoll_fd);
TEST_SUITE_END();
}
void test_dashboard_stats_with_history(void) {
TEST_SUITE_BEGIN("Dashboard Stats With History Data");
epoll_fd = epoll_create1(0);
monitor_init(NULL);
auth_init(NULL, NULL);
for (int i = 0; i < 5; i++) {
monitor_update();
}
vhost_stats_t *stats1 = monitor_get_or_create_vhost_stats("api.example.com");
vhost_stats_t *stats2 = monitor_get_or_create_vhost_stats("web.example.com");
for (int i = 0; i < 10; i++) {
monitor_record_request_start(stats1, 0);
monitor_record_request_start(stats2, 1);
}
monitor_record_bytes(stats1, 50000, 25000);
monitor_record_bytes(stats2, 30000, 15000);
connection_t *conn = create_test_connection();
struct epoll_event ev = { .events = EPOLLIN, .data.fd = conn->fd };
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn->fd, &ev);
const char *request = "GET /rproxy/api/stats HTTP/1.1\r\nHost: localhost\r\n\r\n";
dashboard_serve_stats_api(conn, request, strlen(request));
TEST_ASSERT(strstr((char*)conn->write_buf.data, "api.example.com") != NULL, "First vhost in response");
TEST_ASSERT(strstr((char*)conn->write_buf.data, "web.example.com") != NULL, "Second vhost in response");
TEST_ASSERT(strstr((char*)conn->write_buf.data, "cpu_history") != NULL, "CPU history in response");
TEST_ASSERT(strstr((char*)conn->write_buf.data, "memory_history") != NULL, "Memory history in response");
TEST_ASSERT(strstr((char*)conn->write_buf.data, "network_rx_history") != NULL, "Network RX history in response");
TEST_ASSERT(strstr((char*)conn->write_buf.data, "disk_read_history") != NULL, "Disk read history in response");
cleanup_test_connection(conn);
monitor_cleanup();
close(epoll_fd);
TEST_SUITE_END();
}
void test_dashboard_keep_alive(void) {
TEST_SUITE_BEGIN("Dashboard Keep-Alive Header");
epoll_fd = epoll_create1(0);
monitor_init(NULL);
auth_init(NULL, NULL);
connection_t *conn = create_test_connection();
conn->request.keep_alive = 1;
struct epoll_event ev = { .events = EPOLLIN, .data.fd = conn->fd };
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn->fd, &ev);
const char *request = "GET /rproxy/dashboard HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n";
dashboard_serve(conn, request, strlen(request));
TEST_ASSERT(strstr((char*)conn->write_buf.data, "Connection: keep-alive") != NULL, "Keep-alive in response");
cleanup_test_connection(conn);
conn = create_test_connection();
conn->request.keep_alive = 0;
ev.data.fd = conn->fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn->fd, &ev);
dashboard_serve(conn, request, strlen(request));
TEST_ASSERT(strstr((char*)conn->write_buf.data, "Connection: close") != NULL, "Connection close in response");
cleanup_test_connection(conn);
monitor_cleanup();
close(epoll_fd);
TEST_SUITE_END();
}
void run_dashboard_tests(void) {
test_dashboard_serve_null();
test_dashboard_serve_html();
test_dashboard_serve_stats_api();
test_dashboard_with_auth_disabled();
test_dashboard_with_auth_enabled_no_header();
test_dashboard_with_valid_auth();
test_dashboard_stats_with_history();
test_dashboard_keep_alive();
}

55
tests/test_framework.h Executable file
View File

@ -0,0 +1,55 @@
#ifndef TEST_FRAMEWORK_H
#define TEST_FRAMEWORK_H
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
extern int tests_run;
extern int tests_passed;
extern int tests_failed;
#define TEST_ASSERT(condition, message) do { \
tests_run++; \
if (condition) { \
tests_passed++; \
printf(" [PASS] %s\n", message); \
} else { \
tests_failed++; \
printf(" [FAIL] %s (line %d)\n", message, __LINE__); \
} \
} while(0)
#define TEST_ASSERT_EQ(expected, actual, message) do { \
tests_run++; \
if ((expected) == (actual)) { \
tests_passed++; \
printf(" [PASS] %s\n", message); \
} else { \
tests_failed++; \
printf(" [FAIL] %s (expected %d, got %d, line %d)\n", message, (int)(expected), (int)(actual), __LINE__); \
} \
} while(0)
#define TEST_ASSERT_STR_EQ(expected, actual, message) do { \
tests_run++; \
if (strcmp(expected, actual) == 0) { \
tests_passed++; \
printf(" [PASS] %s\n", message); \
} else { \
tests_failed++; \
printf(" [FAIL] %s (expected '%s', got '%s', line %d)\n", message, expected, actual, __LINE__); \
} \
} while(0)
#define TEST_SUITE_BEGIN(name) do { \
printf("\n=== Test Suite: %s ===\n", name); \
} while(0)
#define TEST_SUITE_END() do { \
printf("\n"); \
} while(0)
void test_summary(void);
#endif

210
tests/test_health_check.c Executable file
View File

@ -0,0 +1,210 @@
#include "test_framework.h"
#include "../src/types.h"
#include "../src/health_check.h"
#include "../src/config.h"
#include <string.h>
#include <stdio.h>
#include <unistd.h>
extern app_config_t *config;
static void setup_test_config(int route_count) {
if (config) {
if (config->routes) free(config->routes);
free(config);
}
config = calloc(1, sizeof(app_config_t));
config->port = 8080;
config->route_count = route_count;
if (route_count > 0) {
config->routes = calloc(route_count, sizeof(route_config_t));
for (int i = 0; i < route_count; i++) {
snprintf(config->routes[i].hostname, sizeof(config->routes[i].hostname), "host%d.example.com", i);
snprintf(config->routes[i].upstream_host, sizeof(config->routes[i].upstream_host), "127.0.0.1");
config->routes[i].upstream_port = 3000 + i;
}
}
}
static void cleanup_test_config(void) {
if (config) {
if (config->routes) free(config->routes);
free(config);
config = NULL;
}
}
void test_health_check_init_no_routes(void) {
TEST_SUITE_BEGIN("Health Check Init No Routes");
setup_test_config(0);
health_check_init();
int healthy = health_check_is_healthy("nonexistent.com");
TEST_ASSERT_EQ(1, healthy, "Nonexistent host returns healthy (fail-open)");
health_check_cleanup();
cleanup_test_config();
TEST_SUITE_END();
}
void test_health_check_init_with_routes(void) {
TEST_SUITE_BEGIN("Health Check Init With Routes");
setup_test_config(3);
health_check_init();
int healthy0 = health_check_is_healthy("host0.example.com");
TEST_ASSERT_EQ(1, healthy0, "Host0 initially healthy");
int healthy1 = health_check_is_healthy("host1.example.com");
TEST_ASSERT_EQ(1, healthy1, "Host1 initially healthy");
int healthy2 = health_check_is_healthy("host2.example.com");
TEST_ASSERT_EQ(1, healthy2, "Host2 initially healthy");
int unknown = health_check_is_healthy("unknown.example.com");
TEST_ASSERT_EQ(1, unknown, "Unknown host returns healthy (fail-open)");
health_check_cleanup();
cleanup_test_config();
TEST_SUITE_END();
}
void test_health_check_null_hostname(void) {
TEST_SUITE_BEGIN("Health Check NULL Hostname");
setup_test_config(1);
health_check_init();
int healthy = health_check_is_healthy(NULL);
TEST_ASSERT_EQ(1, healthy, "NULL hostname returns healthy (fail-open)");
health_check_cleanup();
cleanup_test_config();
TEST_SUITE_END();
}
void test_health_check_case_insensitive(void) {
TEST_SUITE_BEGIN("Health Check Case Insensitive");
setup_test_config(1);
health_check_init();
int lower = health_check_is_healthy("host0.example.com");
TEST_ASSERT_EQ(1, lower, "Lowercase matches");
int upper = health_check_is_healthy("HOST0.EXAMPLE.COM");
TEST_ASSERT_EQ(1, upper, "Uppercase matches");
int mixed = health_check_is_healthy("Host0.Example.COM");
TEST_ASSERT_EQ(1, mixed, "Mixed case matches");
health_check_cleanup();
cleanup_test_config();
TEST_SUITE_END();
}
void test_health_check_cleanup(void) {
TEST_SUITE_BEGIN("Health Check Cleanup");
setup_test_config(2);
health_check_init();
health_check_cleanup();
int healthy = health_check_is_healthy("host0.example.com");
TEST_ASSERT_EQ(1, healthy, "After cleanup returns healthy (disabled)");
cleanup_test_config();
TEST_SUITE_END();
}
void test_health_check_run_no_init(void) {
TEST_SUITE_BEGIN("Health Check Run Without Init");
health_check_run();
TEST_ASSERT(1, "Run without init doesn't crash");
TEST_SUITE_END();
}
void test_health_check_run_with_init(void) {
TEST_SUITE_BEGIN("Health Check Run With Init");
setup_test_config(1);
health_check_init();
health_check_run();
TEST_ASSERT(1, "Health check run completes");
int healthy = health_check_is_healthy("host0.example.com");
TEST_ASSERT(healthy == 0 || healthy == 1, "Health status is valid");
health_check_cleanup();
cleanup_test_config();
TEST_SUITE_END();
}
void test_health_check_multiple_init(void) {
TEST_SUITE_BEGIN("Health Check Multiple Init");
setup_test_config(2);
health_check_init();
int h1 = health_check_is_healthy("host0.example.com");
TEST_ASSERT_EQ(1, h1, "First init: host0 healthy");
setup_test_config(3);
health_check_init();
int h2 = health_check_is_healthy("host2.example.com");
TEST_ASSERT_EQ(1, h2, "Second init: host2 healthy");
health_check_cleanup();
cleanup_test_config();
TEST_SUITE_END();
}
void test_health_check_disabled_state(void) {
TEST_SUITE_BEGIN("Health Check Disabled State");
int healthy_before = health_check_is_healthy("any.host.com");
TEST_ASSERT_EQ(1, healthy_before, "Disabled: any host is healthy");
setup_test_config(1);
health_check_init();
int healthy_enabled = health_check_is_healthy("host0.example.com");
TEST_ASSERT_EQ(1, healthy_enabled, "Enabled: known host is healthy");
health_check_cleanup();
int healthy_after = health_check_is_healthy("any.host.com");
TEST_ASSERT_EQ(1, healthy_after, "After cleanup: any host is healthy");
cleanup_test_config();
TEST_SUITE_END();
}
void run_health_check_tests(void) {
test_health_check_init_no_routes();
test_health_check_init_with_routes();
test_health_check_null_hostname();
test_health_check_case_insensitive();
test_health_check_cleanup();
test_health_check_run_no_init();
test_health_check_run_with_init();
test_health_check_multiple_init();
test_health_check_disabled_state();
}

307
tests/test_host_rewrite.c Executable file
View File

@ -0,0 +1,307 @@
#include "test_framework.h"
#include "../src/types.h"
#include "../src/http.h"
#include "../src/config.h"
#include "../src/buffer.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static const char *TEST_CONFIG_FILE = "/tmp/test_host_rewrite_config.json";
static void create_ssl_rewrite_config(void) {
FILE *f = fopen(TEST_CONFIG_FILE, "w");
if (f) {
fprintf(f,
"{\n"
" \"port\": 9999,\n"
" \"reverse_proxy\": [\n"
" {\n"
" \"hostname\": \"mysite.example.com\",\n"
" \"upstream_host\": \"upstream.internal\",\n"
" \"upstream_port\": 443,\n"
" \"use_ssl\": true,\n"
" \"rewrite_host\": true\n"
" }\n"
" ]\n"
"}\n");
fclose(f);
}
}
static void cleanup_config(void) {
unlink(TEST_CONFIG_FILE);
}
void test_host_header_line_bounds_first_request(void) {
TEST_SUITE_BEGIN("Host Header Line Bounds - First Request");
const char *request =
"GET /page1 HTTP/1.1\r\n"
"Host: mysite.example.com\r\n"
"Connection: keep-alive\r\n"
"\r\n";
size_t len = strlen(request);
const char *line_start = NULL;
const char *line_end = NULL;
int found = http_find_header_line_bounds(request, len, "Host", &line_start, &line_end);
TEST_ASSERT_EQ(1, found, "Host header found in first request");
TEST_ASSERT(line_start != NULL, "Line start is not NULL");
TEST_ASSERT(line_end != NULL, "Line end is not NULL");
if (line_start && line_end) {
size_t host_line_len = line_end - line_start;
char host_line[256];
if (host_line_len < sizeof(host_line)) {
memcpy(host_line, line_start, host_line_len);
host_line[host_line_len] = '\0';
TEST_ASSERT_STR_EQ("Host: mysite.example.com\r\n", host_line, "Host header line extracted correctly");
}
}
TEST_SUITE_END();
}
void test_host_header_line_bounds_second_request(void) {
TEST_SUITE_BEGIN("Host Header Line Bounds - Second Request");
const char *request =
"GET /page2 HTTP/1.1\r\n"
"Host: mysite.example.com\r\n"
"Connection: keep-alive\r\n"
"\r\n";
size_t len = strlen(request);
const char *line_start = NULL;
const char *line_end = NULL;
int found = http_find_header_line_bounds(request, len, "Host", &line_start, &line_end);
TEST_ASSERT_EQ(1, found, "Host header found in second request");
TEST_ASSERT(line_start != NULL, "Line start is not NULL");
TEST_ASSERT(line_end != NULL, "Line end is not NULL");
if (line_start && line_end) {
size_t host_line_len = line_end - line_start;
char host_line[256];
if (host_line_len < sizeof(host_line)) {
memcpy(host_line, line_start, host_line_len);
host_line[host_line_len] = '\0';
TEST_ASSERT_STR_EQ("Host: mysite.example.com\r\n", host_line, "Host header line extracted correctly");
}
}
TEST_SUITE_END();
}
void test_host_rewrite_simulation(void) {
TEST_SUITE_BEGIN("Host Rewrite Simulation - Multiple Requests");
create_ssl_rewrite_config();
config_load(TEST_CONFIG_FILE);
route_config_t *route = config_find_route("mysite.example.com");
TEST_ASSERT(route != NULL, "Route found for mysite.example.com");
if (!route) {
config_free();
cleanup_config();
TEST_SUITE_END();
return;
}
TEST_ASSERT_EQ(1, route->rewrite_host, "Rewrite host is enabled");
TEST_ASSERT_EQ(1, route->use_ssl, "SSL is enabled");
TEST_ASSERT_STR_EQ("upstream.internal", route->upstream_host, "Upstream host is upstream.internal");
const char *request1 =
"GET /page1 HTTP/1.1\r\n"
"Host: mysite.example.com\r\n"
"Connection: keep-alive\r\n"
"\r\n";
const char *request2 =
"GET /page2 HTTP/1.1\r\n"
"Host: mysite.example.com\r\n"
"Connection: keep-alive\r\n"
"\r\n";
for (int req_num = 1; req_num <= 2; req_num++) {
const char *request = (req_num == 1) ? request1 : request2;
size_t request_len = strlen(request);
const char *old_host_start = NULL;
const char *old_host_end = NULL;
int found = http_find_header_line_bounds(request, request_len, "Host", &old_host_start, &old_host_end);
char msg[128];
snprintf(msg, sizeof(msg), "Request %d: Host header found", req_num);
TEST_ASSERT_EQ(1, found, msg);
if (found && old_host_start && old_host_end) {
char new_host_header[512];
int is_default_port = (route->use_ssl && route->upstream_port == 443) ||
(!route->use_ssl && route->upstream_port == 80);
if (is_default_port) {
snprintf(new_host_header, sizeof(new_host_header), "Host: %s\r\n", route->upstream_host);
} else {
snprintf(new_host_header, sizeof(new_host_header), "Host: %s:%d\r\n",
route->upstream_host, route->upstream_port);
}
size_t new_host_len = strlen(new_host_header);
size_t old_host_len = old_host_end - old_host_start;
size_t new_request_len = request_len - old_host_len + new_host_len;
char *modified_request = malloc(new_request_len + 1);
TEST_ASSERT(modified_request != NULL, "Memory allocated for modified request");
if (modified_request) {
char *p = modified_request;
size_t prefix_len = old_host_start - request;
memcpy(p, request, prefix_len);
p += prefix_len;
memcpy(p, new_host_header, new_host_len);
p += new_host_len;
size_t suffix_len = request_len - (old_host_end - request);
memcpy(p, old_host_end, suffix_len);
modified_request[new_request_len] = '\0';
int has_new_host = (strstr(modified_request, "Host: upstream.internal\r\n") != NULL);
snprintf(msg, sizeof(msg), "Request %d: Host rewritten to upstream.internal", req_num);
TEST_ASSERT_EQ(1, has_new_host, msg);
int no_old_host = (strstr(modified_request, "Host: mysite.example.com") == NULL);
snprintf(msg, sizeof(msg), "Request %d: Original host removed", req_num);
TEST_ASSERT_EQ(1, no_old_host, msg);
free(modified_request);
}
}
}
config_free();
cleanup_config();
TEST_SUITE_END();
}
void test_pipelined_request_detection(void) {
TEST_SUITE_BEGIN("Pipelined Request Detection for Host Rewrite");
const char *first_request =
"GET /first HTTP/1.1\r\n"
"Host: mysite.example.com\r\n"
"\r\n";
const char *second_request =
"GET /second HTTP/1.1\r\n"
"Host: mysite.example.com\r\n"
"\r\n";
int is_first_req_start = http_is_request_start(first_request, strlen(first_request));
TEST_ASSERT_EQ(1, is_first_req_start, "First request detected as HTTP request start");
int is_second_req_start = http_is_request_start(second_request, strlen(second_request));
TEST_ASSERT_EQ(1, is_second_req_start, "Second request detected as HTTP request start");
const char *response_data = "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello";
int is_response_req = http_is_request_start(response_data, strlen(response_data));
TEST_ASSERT_EQ(0, is_response_req, "Response data NOT detected as request start");
const char *binary_data = "\x00\x01\x02\x03";
int is_binary_req = http_is_request_start(binary_data, 4);
TEST_ASSERT_EQ(0, is_binary_req, "Binary data NOT detected as request start");
TEST_SUITE_END();
}
void test_ssl_sni_with_host_rewrite(void) {
TEST_SUITE_BEGIN("SSL SNI Hostname with Host Rewrite");
create_ssl_rewrite_config();
config_load(TEST_CONFIG_FILE);
route_config_t *route = config_find_route("mysite.example.com");
TEST_ASSERT(route != NULL, "Route found");
if (route) {
const char *sni_hostname = route->rewrite_host ? route->upstream_host : "mysite.example.com";
TEST_ASSERT_STR_EQ("upstream.internal", sni_hostname, "SNI should be upstream.internal when rewrite_host is true");
}
config_free();
cleanup_config();
TEST_SUITE_END();
}
void test_consecutive_requests_same_connection(void) {
TEST_SUITE_BEGIN("Consecutive Requests on Same Connection");
create_ssl_rewrite_config();
config_load(TEST_CONFIG_FILE);
route_config_t *route = config_find_route("mysite.example.com");
TEST_ASSERT(route != NULL, "Route found");
if (!route) {
config_free();
cleanup_config();
TEST_SUITE_END();
return;
}
const char *requests[] = {
"GET /page1 HTTP/1.1\r\nHost: mysite.example.com\r\nConnection: keep-alive\r\n\r\n",
"GET /page2 HTTP/1.1\r\nHost: mysite.example.com\r\nConnection: keep-alive\r\n\r\n",
"GET /page3 HTTP/1.1\r\nHost: mysite.example.com\r\nConnection: keep-alive\r\n\r\n",
"POST /api HTTP/1.1\r\nHost: mysite.example.com\r\nContent-Length: 0\r\n\r\n",
"GET /page4 HTTP/1.1\r\nHost: mysite.example.com\r\nConnection: close\r\n\r\n"
};
int num_requests = sizeof(requests) / sizeof(requests[0]);
for (int i = 0; i < num_requests; i++) {
const char *request = requests[i];
size_t request_len = strlen(request);
http_request_t parsed_req;
int parse_result = http_parse_request(request, request_len, &parsed_req);
char msg[128];
snprintf(msg, sizeof(msg), "Request %d: Parsed successfully", i + 1);
TEST_ASSERT_EQ(1, parse_result, msg);
route_config_t *found_route = config_find_route(parsed_req.host);
snprintf(msg, sizeof(msg), "Request %d: Route found", i + 1);
TEST_ASSERT(found_route != NULL, msg);
if (found_route) {
snprintf(msg, sizeof(msg), "Request %d: Rewrite host enabled", i + 1);
TEST_ASSERT_EQ(1, found_route->rewrite_host, msg);
const char *old_host_start = NULL;
const char *old_host_end = NULL;
int found = http_find_header_line_bounds(request, request_len, "Host",
&old_host_start, &old_host_end);
snprintf(msg, sizeof(msg), "Request %d: Host header found for rewrite", i + 1);
TEST_ASSERT_EQ(1, found, msg);
}
}
config_free();
cleanup_config();
TEST_SUITE_END();
}
void run_host_rewrite_tests(void) {
test_host_header_line_bounds_first_request();
test_host_header_line_bounds_second_request();
test_host_rewrite_simulation();
test_pipelined_request_detection();
test_ssl_sni_with_host_rewrite();
test_consecutive_requests_same_connection();
}

248
tests/test_http.c Executable file
View File

@ -0,0 +1,248 @@
#include "test_framework.h"
#include "../src/types.h"
#include "../src/http.h"
void test_http_parse_get_request(void) {
TEST_SUITE_BEGIN("HTTP GET Request Parsing");
http_request_t req;
const char *request = "GET /path/to/resource HTTP/1.1\r\nHost: example.com\r\nConnection: keep-alive\r\n\r\n";
int result = http_parse_request(request, strlen(request), &req);
TEST_ASSERT_EQ(1, result, "Parse GET request");
TEST_ASSERT_STR_EQ("GET", req.method, "Method is GET");
TEST_ASSERT_STR_EQ("/path/to/resource", req.uri, "URI parsed correctly");
TEST_ASSERT_STR_EQ("HTTP/1.1", req.version, "Version is HTTP/1.1");
TEST_ASSERT_STR_EQ("example.com", req.host, "Host header parsed");
TEST_ASSERT_EQ(1, req.keep_alive, "Keep-alive is enabled");
TEST_SUITE_END();
}
void test_http_parse_post_request(void) {
TEST_SUITE_BEGIN("HTTP POST Request Parsing");
http_request_t req;
const char *request = "POST /api/data HTTP/1.1\r\nHost: api.example.com\r\nContent-Length: 100\r\nConnection: close\r\n\r\n";
int result = http_parse_request(request, strlen(request), &req);
TEST_ASSERT_EQ(1, result, "Parse POST request");
TEST_ASSERT_STR_EQ("POST", req.method, "Method is POST");
TEST_ASSERT_STR_EQ("/api/data", req.uri, "URI parsed correctly");
TEST_ASSERT_EQ(100, req.content_length, "Content-Length parsed");
TEST_ASSERT_EQ(0, req.keep_alive, "Keep-alive is disabled");
TEST_ASSERT_EQ(1, req.connection_close, "Connection close flag set");
TEST_SUITE_END();
}
void test_http_parse_webdav_methods(void) {
TEST_SUITE_BEGIN("HTTP WebDAV Methods Parsing");
http_request_t req;
const char *propfind = "PROPFIND /folder HTTP/1.1\r\nHost: webdav.example.com\r\n\r\n";
int result = http_parse_request(propfind, strlen(propfind), &req);
TEST_ASSERT_EQ(1, result, "Parse PROPFIND request");
TEST_ASSERT_STR_EQ("PROPFIND", req.method, "Method is PROPFIND");
const char *mkcol = "MKCOL /newfolder HTTP/1.1\r\nHost: webdav.example.com\r\n\r\n";
result = http_parse_request(mkcol, strlen(mkcol), &req);
TEST_ASSERT_EQ(1, result, "Parse MKCOL request");
TEST_ASSERT_STR_EQ("MKCOL", req.method, "Method is MKCOL");
const char *move = "MOVE /source HTTP/1.1\r\nHost: webdav.example.com\r\n\r\n";
result = http_parse_request(move, strlen(move), &req);
TEST_ASSERT_EQ(1, result, "Parse MOVE request");
TEST_ASSERT_STR_EQ("MOVE", req.method, "Method is MOVE");
const char *copy = "COPY /source HTTP/1.1\r\nHost: webdav.example.com\r\n\r\n";
result = http_parse_request(copy, strlen(copy), &req);
TEST_ASSERT_EQ(1, result, "Parse COPY request");
TEST_ASSERT_STR_EQ("COPY", req.method, "Method is COPY");
const char *lock = "LOCK /resource HTTP/1.1\r\nHost: webdav.example.com\r\n\r\n";
result = http_parse_request(lock, strlen(lock), &req);
TEST_ASSERT_EQ(1, result, "Parse LOCK request");
TEST_ASSERT_STR_EQ("LOCK", req.method, "Method is LOCK");
const char *unlock = "UNLOCK /resource HTTP/1.1\r\nHost: webdav.example.com\r\n\r\n";
result = http_parse_request(unlock, strlen(unlock), &req);
TEST_ASSERT_EQ(1, result, "Parse UNLOCK request");
TEST_ASSERT_STR_EQ("UNLOCK", req.method, "Method is UNLOCK");
TEST_SUITE_END();
}
void test_http_parse_custom_methods(void) {
TEST_SUITE_BEGIN("HTTP Custom Methods Parsing");
http_request_t req;
const char *custom1 = "MYMETHOD /path HTTP/1.1\r\nHost: example.com\r\n\r\n";
int result = http_parse_request(custom1, strlen(custom1), &req);
TEST_ASSERT_EQ(1, result, "Parse custom MYMETHOD request");
TEST_ASSERT_STR_EQ("MYMETHOD", req.method, "Method is MYMETHOD");
const char *custom2 = "FOOBAR /path HTTP/1.1\r\nHost: example.com\r\n\r\n";
result = http_parse_request(custom2, strlen(custom2), &req);
TEST_ASSERT_EQ(1, result, "Parse custom FOOBAR request");
TEST_ASSERT_STR_EQ("FOOBAR", req.method, "Method is FOOBAR");
TEST_SUITE_END();
}
void test_http_parse_websocket_upgrade(void) {
TEST_SUITE_BEGIN("HTTP WebSocket Upgrade Parsing");
http_request_t req;
const char *request = "GET /ws HTTP/1.1\r\n"
"Host: example.com\r\n"
"Upgrade: websocket\r\n"
"Connection: upgrade\r\n"
"\r\n";
int result = http_parse_request(request, strlen(request), &req);
TEST_ASSERT_EQ(1, result, "Parse WebSocket upgrade request");
TEST_ASSERT_STR_EQ("GET", req.method, "Method is GET");
TEST_ASSERT_EQ(1, req.is_websocket, "WebSocket flag is set");
TEST_SUITE_END();
}
void test_http_parse_chunked_encoding(void) {
TEST_SUITE_BEGIN("HTTP Chunked Encoding Parsing");
http_request_t req;
const char *request = "POST /api/upload HTTP/1.1\r\n"
"Host: example.com\r\n"
"Transfer-Encoding: chunked\r\n"
"\r\n";
int result = http_parse_request(request, strlen(request), &req);
TEST_ASSERT_EQ(1, result, "Parse chunked request");
TEST_ASSERT_EQ(1, req.is_chunked, "Chunked flag is set");
TEST_SUITE_END();
}
void test_http_parse_http10(void) {
TEST_SUITE_BEGIN("HTTP/1.0 Request Parsing");
http_request_t req;
const char *request = "GET /path HTTP/1.0\r\nHost: example.com\r\n\r\n";
int result = http_parse_request(request, strlen(request), &req);
TEST_ASSERT_EQ(1, result, "Parse HTTP/1.0 request");
TEST_ASSERT_STR_EQ("HTTP/1.0", req.version, "Version is HTTP/1.0");
TEST_ASSERT_EQ(0, req.keep_alive, "Keep-alive is disabled by default for HTTP/1.0");
TEST_SUITE_END();
}
void test_http_parse_host_with_port(void) {
TEST_SUITE_BEGIN("HTTP Host with Port Parsing");
http_request_t req;
const char *request = "GET / HTTP/1.1\r\nHost: example.com:8080\r\n\r\n";
int result = http_parse_request(request, strlen(request), &req);
TEST_ASSERT_EQ(1, result, "Parse request with host:port");
TEST_ASSERT_STR_EQ("example.com", req.host, "Port stripped from host");
TEST_SUITE_END();
}
void test_http_is_request_start(void) {
TEST_SUITE_BEGIN("HTTP Request Start Detection");
TEST_ASSERT_EQ(1, http_is_request_start("GET / HTTP/1.1", 14), "GET is valid request start");
TEST_ASSERT_EQ(1, http_is_request_start("POST /api HTTP/1.1", 18), "POST is valid request start");
TEST_ASSERT_EQ(1, http_is_request_start("PROPFIND / HTTP/1.1", 19), "PROPFIND is valid request start");
TEST_ASSERT_EQ(1, http_is_request_start("CUSTOMMETHOD / HTTP/1.1", 23), "Custom method is valid request start");
TEST_ASSERT_EQ(0, http_is_request_start("123 invalid", 11), "Numbers at start are invalid");
TEST_ASSERT_EQ(0, http_is_request_start("abc", 3), "Too short is invalid");
TEST_SUITE_END();
}
void test_http_malformed_requests(void) {
TEST_SUITE_BEGIN("HTTP Malformed Request Handling");
http_request_t req;
const char *no_space = "GETHTTP/1.1\r\nHost: example.com\r\n\r\n";
int result = http_parse_request(no_space, strlen(no_space), &req);
TEST_ASSERT_EQ(0, result, "Reject request without space after method");
const char *no_version = "GET /path\r\nHost: example.com\r\n\r\n";
result = http_parse_request(no_version, strlen(no_version), &req);
TEST_ASSERT_EQ(0, result, "Reject request without version");
TEST_SUITE_END();
}
void test_http_uri_normalization(void) {
TEST_SUITE_BEGIN("HTTP URI Path Normalization");
char normalized[256];
http_normalize_uri_path("/rproxy/dashboard", normalized, sizeof(normalized));
TEST_ASSERT_STR_EQ("/rproxy/dashboard", normalized, "Normal path unchanged");
http_normalize_uri_path("//rproxy/dashboard", normalized, sizeof(normalized));
TEST_ASSERT_STR_EQ("/rproxy/dashboard", normalized, "Double slash at start collapsed");
http_normalize_uri_path("/rproxy//dashboard", normalized, sizeof(normalized));
TEST_ASSERT_STR_EQ("/rproxy/dashboard", normalized, "Double slash in middle collapsed");
http_normalize_uri_path("/./rproxy/dashboard", normalized, sizeof(normalized));
TEST_ASSERT_STR_EQ("/rproxy/dashboard", normalized, "Dot segment at start removed");
http_normalize_uri_path("/rproxy/./dashboard", normalized, sizeof(normalized));
TEST_ASSERT_STR_EQ("/rproxy/dashboard", normalized, "Dot segment in middle removed");
http_normalize_uri_path("/foo/../rproxy/dashboard", normalized, sizeof(normalized));
TEST_ASSERT_STR_EQ("/rproxy/dashboard", normalized, "Parent reference resolved");
http_normalize_uri_path("/rproxy/dashboard?foo=bar", normalized, sizeof(normalized));
TEST_ASSERT_STR_EQ("/rproxy/dashboard?foo=bar", normalized, "Query string preserved");
http_normalize_uri_path("///rproxy///dashboard///", normalized, sizeof(normalized));
TEST_ASSERT_STR_EQ("/rproxy/dashboard/", normalized, "Multiple slashes collapsed");
TEST_SUITE_END();
}
void test_http_internal_route_detection(void) {
TEST_SUITE_BEGIN("HTTP Internal Route Detection");
TEST_ASSERT_EQ(1, http_uri_is_internal_route("/rproxy/dashboard"), "Normal dashboard path detected");
TEST_ASSERT_EQ(1, http_uri_is_internal_route("/rproxy/api/stats"), "Normal stats path detected");
TEST_ASSERT_EQ(1, http_uri_is_internal_route("//rproxy/dashboard"), "Double slash bypass detected");
TEST_ASSERT_EQ(1, http_uri_is_internal_route("/./rproxy/dashboard"), "Dot segment bypass detected");
TEST_ASSERT_EQ(1, http_uri_is_internal_route("/foo/../rproxy/dashboard"), "Parent reference bypass detected");
TEST_ASSERT_EQ(1, http_uri_is_internal_route("/rproxy//dashboard"), "Slash in path bypass detected");
TEST_ASSERT_EQ(1, http_uri_is_internal_route("/rproxy/./api/stats"), "Dot in internal path detected");
TEST_ASSERT_EQ(0, http_uri_is_internal_route("/api/data"), "Non-internal path rejected");
TEST_ASSERT_EQ(0, http_uri_is_internal_route("/"), "Root path rejected");
TEST_ASSERT_EQ(0, http_uri_is_internal_route("/rproxynotreal"), "Similar prefix rejected");
TEST_ASSERT_EQ(0, http_uri_is_internal_route(NULL), "NULL path rejected");
TEST_SUITE_END();
}
void run_http_tests(void) {
test_http_parse_get_request();
test_http_parse_post_request();
test_http_parse_webdav_methods();
test_http_parse_custom_methods();
test_http_parse_websocket_upgrade();
test_http_parse_chunked_encoding();
test_http_parse_http10();
test_http_parse_host_with_port();
test_http_is_request_start();
test_http_malformed_requests();
test_http_uri_normalization();
test_http_internal_route_detection();
}

200
tests/test_http_helpers.c Executable file
View File

@ -0,0 +1,200 @@
#include "test_framework.h"
#include "../src/types.h"
#include "../src/http.h"
#include <string.h>
void test_http_find_header_value(void) {
TEST_SUITE_BEGIN("HTTP Find Header Value");
const char *headers =
"Host: example.com\r\n"
"Content-Type: application/json\r\n"
"Content-Length: 42\r\n"
"X-Custom-Header: custom-value\r\n"
"Connection: keep-alive\r\n"
"\r\n";
size_t len = strlen(headers);
char value[256];
int found = http_find_header_value(headers, len, "Host", value, sizeof(value));
TEST_ASSERT_EQ(1, found, "Host header found");
TEST_ASSERT_STR_EQ("example.com", value, "Host value correct");
found = http_find_header_value(headers, len, "Content-Type", value, sizeof(value));
TEST_ASSERT_EQ(1, found, "Content-Type header found");
TEST_ASSERT_STR_EQ("application/json", value, "Content-Type value correct");
found = http_find_header_value(headers, len, "Content-Length", value, sizeof(value));
TEST_ASSERT_EQ(1, found, "Content-Length header found");
TEST_ASSERT_STR_EQ("42", value, "Content-Length value correct");
found = http_find_header_value(headers, len, "X-Custom-Header", value, sizeof(value));
TEST_ASSERT_EQ(1, found, "Custom header found");
TEST_ASSERT_STR_EQ("custom-value", value, "Custom header value correct");
found = http_find_header_value(headers, len, "Non-Existent", value, sizeof(value));
TEST_ASSERT_EQ(0, found, "Non-existent header not found");
found = http_find_header_value(headers, len, "host", value, sizeof(value));
TEST_ASSERT_EQ(1, found, "Case-insensitive header search works");
TEST_SUITE_END();
}
void test_http_is_textual_content_type(void) {
TEST_SUITE_BEGIN("HTTP Textual Content Type Detection");
TEST_ASSERT_EQ(1, http_is_textual_content_type("text/html"), "text/html is textual");
TEST_ASSERT_EQ(1, http_is_textual_content_type("text/plain"), "text/plain is textual");
TEST_ASSERT_EQ(1, http_is_textual_content_type("text/css"), "text/css is textual");
TEST_ASSERT_EQ(1, http_is_textual_content_type("text/javascript"), "text/javascript is textual");
TEST_ASSERT_EQ(1, http_is_textual_content_type("application/json"), "application/json is textual");
TEST_ASSERT_EQ(1, http_is_textual_content_type("application/javascript"), "application/javascript is textual");
TEST_ASSERT_EQ(1, http_is_textual_content_type("application/xml"), "application/xml is textual");
TEST_ASSERT_EQ(1, http_is_textual_content_type("application/xhtml+xml"), "application/xhtml+xml is textual");
TEST_ASSERT_EQ(0, http_is_textual_content_type("image/png"), "image/png is not textual");
TEST_ASSERT_EQ(0, http_is_textual_content_type("image/jpeg"), "image/jpeg is not textual");
TEST_ASSERT_EQ(0, http_is_textual_content_type("application/octet-stream"), "octet-stream is not textual");
TEST_ASSERT_EQ(0, http_is_textual_content_type("video/mp4"), "video/mp4 is not textual");
TEST_ASSERT_EQ(0, http_is_textual_content_type("audio/mpeg"), "audio/mpeg is not textual");
TEST_ASSERT_EQ(1, http_is_textual_content_type("text/html; charset=utf-8"), "text/html with charset is textual");
TEST_ASSERT_EQ(1, http_is_textual_content_type("application/json; charset=utf-8"), "json with charset is textual");
TEST_ASSERT_EQ(0, http_is_textual_content_type(NULL), "NULL is not textual");
TEST_SUITE_END();
}
void test_http_detect_binary_content(void) {
TEST_SUITE_BEGIN("HTTP Binary Content Detection");
const char *text_data = "Hello, World! This is plain text content.";
TEST_ASSERT_EQ(0, http_detect_binary_content(text_data, strlen(text_data)), "Plain text is not binary");
const char *html_data = "<html><body><h1>Hello</h1></body></html>";
TEST_ASSERT_EQ(0, http_detect_binary_content(html_data, strlen(html_data)), "HTML is not binary");
const char *json_data = "{\"key\": \"value\", \"number\": 123}";
TEST_ASSERT_EQ(0, http_detect_binary_content(json_data, strlen(json_data)), "JSON is not binary");
const unsigned char binary_data[] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
TEST_ASSERT_EQ(1, http_detect_binary_content((const char*)binary_data, sizeof(binary_data)), "PNG header is binary");
const unsigned char null_bytes[] = {'H', 'e', 'l', 'l', 'o', 0x00, 'W', 'o', 'r', 'l', 'd'};
TEST_ASSERT_EQ(1, http_detect_binary_content((const char*)null_bytes, sizeof(null_bytes)), "Data with null bytes is binary");
TEST_ASSERT_EQ(0, http_detect_binary_content(NULL, 0), "NULL data returns 0");
TEST_ASSERT_EQ(0, http_detect_binary_content("", 0), "Empty data returns 0");
TEST_SUITE_END();
}
void test_http_get_content_length(void) {
TEST_SUITE_BEGIN("HTTP Get Content-Length");
const char *headers1 = "HTTP/1.1 200 OK\r\nContent-Length: 1234\r\nContent-Type: text/html\r\n\r\n";
TEST_ASSERT_EQ(1234, http_get_content_length(headers1, strlen(headers1)), "Content-Length 1234 parsed");
const char *headers2 = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 0\r\n\r\n";
TEST_ASSERT_EQ(0, http_get_content_length(headers2, strlen(headers2)), "Content-Length 0 parsed");
const char *headers3 = "HTTP/1.1 200 OK\r\nContent-Length: 9999999\r\n\r\n";
TEST_ASSERT_EQ(9999999, http_get_content_length(headers3, strlen(headers3)), "Large Content-Length parsed");
const char *headers4 = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n";
TEST_ASSERT_EQ(-1, http_get_content_length(headers4, strlen(headers4)), "Missing Content-Length returns -1");
const char *headers5 = "HTTP/1.1 200 OK\r\ncontent-length: 500\r\n\r\n";
long len5 = http_get_content_length(headers5, strlen(headers5));
TEST_ASSERT(len5 == 500 || len5 == -1, "Lowercase content-length handled");
TEST_SUITE_END();
}
void test_http_find_headers_end(void) {
TEST_SUITE_BEGIN("HTTP Find Headers End");
const char *request1 = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\nBody content here";
size_t end1;
int found1 = http_find_headers_end(request1, strlen(request1), &end1);
TEST_ASSERT_EQ(1, found1, "Headers end found in complete request");
TEST_ASSERT(end1 > 0 && end1 < strlen(request1), "Headers end position is valid");
const char *request2 = "GET / HTTP/1.1\r\nHost: example.com\r\n";
size_t end2;
int found2 = http_find_headers_end(request2, strlen(request2), &end2);
TEST_ASSERT_EQ(0, found2, "Incomplete headers not found");
const char *request3 = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
size_t end3;
int found3 = http_find_headers_end(request3, strlen(request3), &end3);
TEST_ASSERT_EQ(1, found3, "Headers end found with no body");
TEST_ASSERT_EQ(strlen(request3), end3, "Headers end at string end for no body");
TEST_SUITE_END();
}
void test_http_rewrite_content_length(void) {
TEST_SUITE_BEGIN("HTTP Rewrite Content-Length");
char headers1[512] = "HTTP/1.1 200 OK\r\nContent-Length: 100\r\nContent-Type: text/html\r\n\r\n";
size_t len1 = strlen(headers1);
int result1 = http_rewrite_content_length(headers1, &len1, sizeof(headers1), 200);
TEST_ASSERT_EQ(1, result1, "Content-Length rewrite succeeded");
TEST_ASSERT(strstr(headers1, "200") != NULL, "New length present in headers");
char headers2[512] = "HTTP/1.1 200 OK\r\nContent-Length: 99999\r\n\r\n";
size_t len2 = strlen(headers2);
int result2 = http_rewrite_content_length(headers2, &len2, sizeof(headers2), 50);
TEST_ASSERT_EQ(1, result2, "Content-Length rewrite to smaller succeeded");
char headers3[512] = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n";
size_t len3 = strlen(headers3);
int result3 = http_rewrite_content_length(headers3, &len3, sizeof(headers3), 100);
TEST_ASSERT_EQ(0, result3, "Rewrite fails when no Content-Length present");
TEST_SUITE_END();
}
void test_http_find_header_line_bounds_edge_cases(void) {
TEST_SUITE_BEGIN("HTTP Header Line Bounds Edge Cases");
const char *request1 = "GET / HTTP/1.1\r\nHost: example.com\r\nX-Test:no-space\r\n\r\n";
const char *start1, *end1;
int found1 = http_find_header_line_bounds(request1, strlen(request1), "X-Test", &start1, &end1);
TEST_ASSERT_EQ(1, found1, "Header without space after colon found");
const char *request2 = "GET / HTTP/1.1\r\nHost: lots-of-spaces \r\n\r\n";
const char *start2, *end2;
int found2 = http_find_header_line_bounds(request2, strlen(request2), "Host", &start2, &end2);
TEST_ASSERT_EQ(1, found2, "Header with extra spaces found");
const char *request3 = "GET / HTTP/1.1\r\nhost: lowercase.com\r\n\r\n";
const char *start3, *end3;
int found3 = http_find_header_line_bounds(request3, strlen(request3), "Host", &start3, &end3);
TEST_ASSERT_EQ(1, found3, "Lowercase header name found");
const char *request4 = "GET / HTTP/1.1\r\nHOST: UPPERCASE.COM\r\n\r\n";
const char *start4, *end4;
int found4 = http_find_header_line_bounds(request4, strlen(request4), "host", &start4, &end4);
TEST_ASSERT_EQ(1, found4, "Uppercase header with lowercase search found");
const char *request5 = "GET / HTTP/1.1\r\n\r\n";
const char *start5, *end5;
int found5 = http_find_header_line_bounds(request5, strlen(request5), "Host", &start5, &end5);
TEST_ASSERT_EQ(0, found5, "Header not found in request with no headers");
TEST_SUITE_END();
}
void run_http_helper_tests(void) {
test_http_find_header_value();
test_http_is_textual_content_type();
test_http_detect_binary_content();
test_http_get_content_length();
test_http_find_headers_end();
test_http_rewrite_content_length();
test_http_find_header_line_bounds_edge_cases();
}

277
tests/test_logging.c Executable file
View File

@ -0,0 +1,277 @@
#include "test_framework.h"
#include "../src/logging.h"
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/stat.h>
#include <errno.h>
void test_logging_debug_mode(void) {
TEST_SUITE_BEGIN("Logging Debug Mode");
logging_set_debug(0);
TEST_ASSERT_EQ(0, logging_get_debug(), "Debug mode disabled");
logging_set_debug(1);
TEST_ASSERT_EQ(1, logging_get_debug(), "Debug mode enabled");
logging_set_debug(0);
TEST_ASSERT_EQ(0, logging_get_debug(), "Debug mode disabled again");
TEST_SUITE_END();
}
void test_logging_set_file(void) {
TEST_SUITE_BEGIN("Logging Set File");
char tmp_path[] = "/tmp/test_rproxy_log_XXXXXX";
int fd = mkstemp(tmp_path);
TEST_ASSERT(fd >= 0, "Temp file created");
close(fd);
int result = logging_set_file(tmp_path);
TEST_ASSERT_EQ(0, result, "Set log file succeeds");
log_info("Test log message");
struct stat st;
stat(tmp_path, &st);
TEST_ASSERT(st.st_size > 0, "Log file has content");
logging_cleanup();
unlink(tmp_path);
TEST_SUITE_END();
}
void test_logging_set_file_null(void) {
TEST_SUITE_BEGIN("Logging Set File NULL");
int result = logging_set_file(NULL);
TEST_ASSERT_EQ(0, result, "NULL path returns to stdout");
log_info("Test message to stdout");
TEST_ASSERT(1, "Logging to stdout works");
TEST_SUITE_END();
}
void test_logging_set_file_invalid(void) {
TEST_SUITE_BEGIN("Logging Set File Invalid Path");
int result = logging_set_file("/nonexistent/directory/log.txt");
TEST_ASSERT_EQ(-1, result, "Invalid path returns -1");
TEST_SUITE_END();
}
void test_logging_log_functions(void) {
TEST_SUITE_BEGIN("Logging Log Functions");
char tmp_path[] = "/tmp/test_rproxy_log_XXXXXX";
int fd = mkstemp(tmp_path);
close(fd);
logging_set_file(tmp_path);
log_info("Info message: %d", 42);
log_error("Error message: %s", "test error");
logging_set_debug(1);
log_debug("Debug message: %s", "debug info");
logging_set_debug(0);
log_debug("This should not appear");
struct stat st;
stat(tmp_path, &st);
TEST_ASSERT(st.st_size > 0, "Log messages written");
FILE *f = fopen(tmp_path, "r");
char content[4096] = {0};
if (f) {
size_t bytes_read = fread(content, 1, sizeof(content) - 1, f);
(void)bytes_read;
fclose(f);
}
TEST_ASSERT(strstr(content, "INFO") != NULL, "INFO level present");
TEST_ASSERT(strstr(content, "ERROR") != NULL, "ERROR level present");
TEST_ASSERT(strstr(content, "DEBUG") != NULL, "DEBUG level present");
TEST_ASSERT(strstr(content, "42") != NULL, "Info param present");
TEST_ASSERT(strstr(content, "test error") != NULL, "Error param present");
logging_cleanup();
unlink(tmp_path);
TEST_SUITE_END();
}
void test_logging_cleanup(void) {
TEST_SUITE_BEGIN("Logging Cleanup");
char tmp_path[] = "/tmp/test_rproxy_log_XXXXXX";
int fd = mkstemp(tmp_path);
close(fd);
logging_set_file(tmp_path);
log_info("Before cleanup");
logging_cleanup();
log_info("After cleanup to stdout");
TEST_ASSERT(1, "Cleanup completed and logging works");
unlink(tmp_path);
TEST_SUITE_END();
}
void test_logging_multiple_files(void) {
TEST_SUITE_BEGIN("Logging Multiple File Switches");
char tmp1[] = "/tmp/test_rproxy_log1_XXXXXX";
char tmp2[] = "/tmp/test_rproxy_log2_XXXXXX";
int fd1 = mkstemp(tmp1);
int fd2 = mkstemp(tmp2);
close(fd1);
close(fd2);
logging_set_file(tmp1);
log_info("Message to file 1");
logging_set_file(tmp2);
log_info("Message to file 2");
struct stat st1, st2;
stat(tmp1, &st1);
stat(tmp2, &st2);
TEST_ASSERT(st1.st_size > 0, "First file has content");
TEST_ASSERT(st2.st_size > 0, "Second file has content");
logging_cleanup();
unlink(tmp1);
unlink(tmp2);
TEST_SUITE_END();
}
void test_logging_error_with_errno(void) {
TEST_SUITE_BEGIN("Logging Error With Errno");
char tmp_path[] = "/tmp/test_rproxy_log_XXXXXX";
int fd = mkstemp(tmp_path);
close(fd);
logging_set_file(tmp_path);
errno = ENOENT;
log_error("File not found error");
errno = 0;
log_error("Error without errno");
errno = EPERM;
log_error("Permission denied: %s", "/test/file");
errno = 0;
logging_cleanup();
FILE *f = fopen(tmp_path, "r");
char content[4096] = {0};
if (f) {
size_t bytes_read = fread(content, 1, sizeof(content) - 1, f);
(void)bytes_read;
fclose(f);
}
TEST_ASSERT(strstr(content, "File not found") != NULL, "First error logged");
TEST_ASSERT(strstr(content, "without errno") != NULL, "Second error logged");
unlink(tmp_path);
TEST_SUITE_END();
}
void test_logging_debug_disabled(void) {
TEST_SUITE_BEGIN("Logging Debug Disabled");
char tmp_path[] = "/tmp/test_rproxy_log_XXXXXX";
int fd = mkstemp(tmp_path);
close(fd);
logging_set_file(tmp_path);
logging_set_debug(0);
log_debug("This should not appear");
log_info("This should appear");
struct stat st;
stat(tmp_path, &st);
TEST_ASSERT(st.st_size > 0, "File has some content");
FILE *f = fopen(tmp_path, "r");
char content[4096] = {0};
if (f) {
size_t bytes_read = fread(content, 1, sizeof(content) - 1, f);
(void)bytes_read;
fclose(f);
}
TEST_ASSERT(strstr(content, "should appear") != NULL, "Info message present");
logging_cleanup();
unlink(tmp_path);
TEST_SUITE_END();
}
void test_logging_format_strings(void) {
TEST_SUITE_BEGIN("Logging Format Strings");
char tmp_path[] = "/tmp/test_rproxy_log_XXXXXX";
int fd = mkstemp(tmp_path);
close(fd);
logging_set_file(tmp_path);
log_info("Int: %d, String: %s, Float: %.2f", 42, "test", 3.14);
log_error("Code: %d, Msg: %s", 500, "Internal error");
logging_set_debug(1);
log_debug("Debug: %s %d", "value", 123);
logging_set_debug(0);
logging_cleanup();
FILE *f = fopen(tmp_path, "r");
char content[4096] = {0};
if (f) {
size_t bytes_read = fread(content, 1, sizeof(content) - 1, f);
(void)bytes_read;
fclose(f);
}
TEST_ASSERT(strstr(content, "42") != NULL, "Int formatted");
TEST_ASSERT(strstr(content, "test") != NULL, "String formatted");
TEST_ASSERT(strstr(content, "500") != NULL, "Error code formatted");
unlink(tmp_path);
TEST_SUITE_END();
}
void run_logging_tests(void) {
test_logging_debug_mode();
test_logging_set_file();
test_logging_set_file_null();
test_logging_set_file_invalid();
test_logging_log_functions();
test_logging_cleanup();
test_logging_multiple_files();
test_logging_error_with_errno();
test_logging_debug_disabled();
test_logging_format_strings();
}

63
tests/test_main.c Executable file
View File

@ -0,0 +1,63 @@
#include "test_framework.h"
#include <stdio.h>
int tests_run = 0;
int tests_passed = 0;
int tests_failed = 0;
void test_summary(void) {
printf("\n=========================================\n");
printf("Test Results: %d/%d passed\n", tests_passed, tests_run);
if (tests_failed > 0) {
printf("FAILED: %d tests failed\n", tests_failed);
} else {
printf("SUCCESS: All tests passed\n");
}
printf("=========================================\n");
}
extern void run_http_tests(void);
extern void run_buffer_tests(void);
extern void run_config_tests(void);
extern void run_routing_tests(void);
extern void run_host_rewrite_tests(void);
extern void run_http_helper_tests(void);
extern void run_patch_tests(void);
extern void run_auth_tests(void);
extern void run_rate_limit_tests(void);
extern void run_monitor_tests(void);
extern void run_dashboard_tests(void);
extern void run_health_check_tests(void);
extern void run_ssl_handler_tests(void);
extern void run_connection_tests(void);
extern void run_logging_tests(void);
int main(int argc, char *argv[]) {
(void)argc;
(void)argv;
printf("\n");
printf("=========================================\n");
printf(" RProxy Enterprise Unit Tests\n");
printf("=========================================\n");
run_buffer_tests();
run_http_tests();
run_http_helper_tests();
run_config_tests();
run_routing_tests();
run_host_rewrite_tests();
run_patch_tests();
run_auth_tests();
run_rate_limit_tests();
run_monitor_tests();
run_dashboard_tests();
run_health_check_tests();
run_ssl_handler_tests();
run_connection_tests();
run_logging_tests();
test_summary();
return tests_failed > 0 ? 1 : 0;
}

333
tests/test_monitor.c Executable file
View File

@ -0,0 +1,333 @@
#include "test_framework.h"
#include "../src/types.h"
#include "../src/monitor.h"
#include <string.h>
#include <unistd.h>
void test_history_deque_init(void) {
TEST_SUITE_BEGIN("History Deque Initialization");
history_deque_t dq;
history_deque_init(&dq, 10);
TEST_ASSERT(dq.points != NULL, "Deque points allocated");
TEST_ASSERT_EQ(10, dq.capacity, "Capacity is 10");
TEST_ASSERT_EQ(0, dq.count, "Count is 0");
TEST_ASSERT_EQ(0, dq.head, "Head is 0");
free(dq.points);
TEST_SUITE_END();
}
void test_history_deque_push(void) {
TEST_SUITE_BEGIN("History Deque Push");
history_deque_t dq;
history_deque_init(&dq, 5);
history_deque_push(&dq, 1.0, 100.0);
TEST_ASSERT_EQ(1, dq.count, "Count is 1 after first push");
history_deque_push(&dq, 2.0, 200.0);
history_deque_push(&dq, 3.0, 300.0);
TEST_ASSERT_EQ(3, dq.count, "Count is 3 after three pushes");
history_deque_push(&dq, 4.0, 400.0);
history_deque_push(&dq, 5.0, 500.0);
TEST_ASSERT_EQ(5, dq.count, "Count is 5 at capacity");
history_deque_push(&dq, 6.0, 600.0);
TEST_ASSERT_EQ(5, dq.count, "Count stays at capacity after overflow");
free(dq.points);
TEST_SUITE_END();
}
void test_network_history_deque(void) {
TEST_SUITE_BEGIN("Network History Deque");
network_history_deque_t dq;
network_history_deque_init(&dq, 5);
TEST_ASSERT(dq.points != NULL, "Network deque points allocated");
TEST_ASSERT_EQ(5, dq.capacity, "Capacity is 5");
TEST_ASSERT_EQ(0, dq.count, "Count is 0");
network_history_deque_push(&dq, 1.0, 100.0, 50.0);
TEST_ASSERT_EQ(1, dq.count, "Count is 1 after push");
network_history_deque_push(&dq, 2.0, 200.0, 100.0);
network_history_deque_push(&dq, 3.0, 300.0, 150.0);
TEST_ASSERT_EQ(3, dq.count, "Count is 3");
free(dq.points);
TEST_SUITE_END();
}
void test_disk_history_deque(void) {
TEST_SUITE_BEGIN("Disk History Deque");
disk_history_deque_t dq;
disk_history_deque_init(&dq, 5);
TEST_ASSERT(dq.points != NULL, "Disk deque points allocated");
TEST_ASSERT_EQ(5, dq.capacity, "Capacity is 5");
disk_history_deque_push(&dq, 1.0, 10.0, 5.0);
TEST_ASSERT_EQ(1, dq.count, "Count is 1 after push");
disk_history_deque_push(&dq, 2.0, 20.0, 10.0);
TEST_ASSERT_EQ(2, dq.count, "Count is 2");
free(dq.points);
TEST_SUITE_END();
}
void test_request_time_deque(void) {
TEST_SUITE_BEGIN("Request Time Deque");
request_time_deque_t dq;
request_time_deque_init(&dq, 10);
TEST_ASSERT(dq.times != NULL, "Request time deque allocated");
TEST_ASSERT_EQ(10, dq.capacity, "Capacity is 10");
TEST_ASSERT_EQ(0, dq.count, "Count is 0");
request_time_deque_push(&dq, 15.5);
TEST_ASSERT_EQ(1, dq.count, "Count is 1 after push");
request_time_deque_push(&dq, 20.0);
request_time_deque_push(&dq, 25.5);
TEST_ASSERT_EQ(3, dq.count, "Count is 3");
for (int i = 0; i < 10; i++) {
request_time_deque_push(&dq, (double)i);
}
TEST_ASSERT_EQ(10, dq.count, "Count stays at capacity");
free(dq.times);
TEST_SUITE_END();
}
void test_monitor_init_cleanup(void) {
TEST_SUITE_BEGIN("Monitor Init and Cleanup");
monitor_init(NULL);
TEST_ASSERT(monitor.cpu_history.points != NULL, "CPU history initialized");
TEST_ASSERT(monitor.memory_history.points != NULL, "Memory history initialized");
TEST_ASSERT(monitor.network_history.points != NULL, "Network history initialized");
TEST_ASSERT(monitor.disk_history.points != NULL, "Disk history initialized");
monitor_cleanup();
TEST_ASSERT(monitor.cpu_history.points == NULL, "CPU history cleaned up");
TEST_ASSERT(monitor.memory_history.points == NULL, "Memory history cleaned up");
TEST_SUITE_END();
}
void test_monitor_vhost_stats(void) {
TEST_SUITE_BEGIN("Monitor Vhost Stats");
monitor_init(NULL);
vhost_stats_t *stats1 = monitor_get_or_create_vhost_stats("test.example.com");
TEST_ASSERT(stats1 != NULL, "First vhost stats created");
TEST_ASSERT_STR_EQ("test.example.com", stats1->vhost_name, "Vhost name correct");
TEST_ASSERT_EQ(0, stats1->total_requests, "Initial total requests is 0");
vhost_stats_t *stats2 = monitor_get_or_create_vhost_stats("test.example.com");
TEST_ASSERT(stats1 == stats2, "Same vhost returns same stats");
vhost_stats_t *stats3 = monitor_get_or_create_vhost_stats("other.example.com");
TEST_ASSERT(stats3 != NULL, "Second vhost stats created");
TEST_ASSERT(stats1 != stats3, "Different vhosts have different stats");
vhost_stats_t *null_stats = monitor_get_or_create_vhost_stats(NULL);
TEST_ASSERT(null_stats == NULL, "NULL vhost returns NULL");
vhost_stats_t *empty_stats = monitor_get_or_create_vhost_stats("");
TEST_ASSERT(empty_stats == NULL, "Empty vhost returns NULL");
monitor_cleanup();
TEST_SUITE_END();
}
void test_monitor_record_request(void) {
TEST_SUITE_BEGIN("Monitor Record Request");
monitor_init(NULL);
vhost_stats_t *stats = monitor_get_or_create_vhost_stats("api.test.com");
TEST_ASSERT(stats != NULL, "Stats created");
monitor_record_request_start(stats, 0);
TEST_ASSERT_EQ(1, stats->total_requests, "Total requests incremented");
TEST_ASSERT_EQ(1, stats->http_requests, "HTTP requests incremented");
TEST_ASSERT_EQ(0, stats->websocket_requests, "WebSocket requests unchanged");
monitor_record_request_start(stats, 1);
TEST_ASSERT_EQ(2, stats->total_requests, "Total requests is 2");
TEST_ASSERT_EQ(1, stats->http_requests, "HTTP requests still 1");
TEST_ASSERT_EQ(1, stats->websocket_requests, "WebSocket requests is 1");
monitor_record_request_start(NULL, 0);
TEST_ASSERT(1, "NULL stats doesn't crash");
monitor_cleanup();
TEST_SUITE_END();
}
void test_monitor_record_bytes(void) {
TEST_SUITE_BEGIN("Monitor Record Bytes");
monitor_init(NULL);
vhost_stats_t *stats = monitor_get_or_create_vhost_stats("bytes.test.com");
TEST_ASSERT(stats != NULL, "Stats created");
TEST_ASSERT_EQ(0, stats->bytes_sent, "Initial bytes sent is 0");
TEST_ASSERT_EQ(0, stats->bytes_recv, "Initial bytes recv is 0");
monitor_record_bytes(stats, 1000, 500);
TEST_ASSERT_EQ(1000, stats->bytes_sent, "Bytes sent recorded");
TEST_ASSERT_EQ(500, stats->bytes_recv, "Bytes recv recorded");
monitor_record_bytes(stats, 500, 250);
TEST_ASSERT_EQ(1500, stats->bytes_sent, "Bytes sent accumulated");
TEST_ASSERT_EQ(750, stats->bytes_recv, "Bytes recv accumulated");
monitor_record_bytes(NULL, 100, 100);
TEST_ASSERT(1, "NULL stats doesn't crash");
monitor_cleanup();
TEST_SUITE_END();
}
void test_monitor_update(void) {
TEST_SUITE_BEGIN("Monitor Update");
monitor_init(NULL);
monitor_update();
TEST_ASSERT(monitor.cpu_history.count >= 0, "CPU history has data after update");
TEST_ASSERT(monitor.memory_history.count >= 0, "Memory history has data after update");
monitor_update();
monitor_update();
TEST_ASSERT(1, "Multiple updates don't crash");
monitor_cleanup();
TEST_SUITE_END();
}
void test_monitor_record_request_end(void) {
TEST_SUITE_BEGIN("Monitor Record Request End");
monitor_init(NULL);
vhost_stats_t *stats = monitor_get_or_create_vhost_stats("timing.test.com");
TEST_ASSERT(stats != NULL, "Stats created");
double start_time = 1000.0;
monitor_record_request_end(stats, start_time);
TEST_ASSERT(stats->avg_request_time_ms >= 0, "Avg request time recorded");
monitor_record_request_end(stats, start_time);
monitor_record_request_end(stats, start_time);
TEST_ASSERT(1, "Multiple request end calls work");
monitor_record_request_end(NULL, 0);
TEST_ASSERT(1, "NULL stats doesn't crash");
monitor_cleanup();
TEST_SUITE_END();
}
void test_monitor_deque_overflow(void) {
TEST_SUITE_BEGIN("Monitor Deque Overflow Behavior");
history_deque_t dq;
history_deque_init(&dq, 3);
history_deque_push(&dq, 1.0, 10.0);
history_deque_push(&dq, 2.0, 20.0);
history_deque_push(&dq, 3.0, 30.0);
TEST_ASSERT_EQ(3, dq.count, "Count is at capacity");
history_deque_push(&dq, 4.0, 40.0);
TEST_ASSERT_EQ(3, dq.count, "Count stays at capacity");
history_deque_push(&dq, 5.0, 50.0);
history_deque_push(&dq, 6.0, 60.0);
TEST_ASSERT_EQ(3, dq.count, "Count still at capacity after multiple overflows");
free(dq.points);
TEST_SUITE_END();
}
void test_monitor_network_deque_overflow(void) {
TEST_SUITE_BEGIN("Monitor Network Deque Overflow");
network_history_deque_t dq;
network_history_deque_init(&dq, 3);
network_history_deque_push(&dq, 1.0, 100.0, 50.0);
network_history_deque_push(&dq, 2.0, 200.0, 100.0);
network_history_deque_push(&dq, 3.0, 300.0, 150.0);
TEST_ASSERT_EQ(3, dq.count, "Count is at capacity");
network_history_deque_push(&dq, 4.0, 400.0, 200.0);
TEST_ASSERT_EQ(3, dq.count, "Count stays at capacity");
free(dq.points);
TEST_SUITE_END();
}
void test_monitor_disk_deque_overflow(void) {
TEST_SUITE_BEGIN("Monitor Disk Deque Overflow");
disk_history_deque_t dq;
disk_history_deque_init(&dq, 3);
disk_history_deque_push(&dq, 1.0, 10.0, 5.0);
disk_history_deque_push(&dq, 2.0, 20.0, 10.0);
disk_history_deque_push(&dq, 3.0, 30.0, 15.0);
TEST_ASSERT_EQ(3, dq.count, "Count is at capacity");
disk_history_deque_push(&dq, 4.0, 40.0, 20.0);
TEST_ASSERT_EQ(3, dq.count, "Count stays at capacity");
free(dq.points);
TEST_SUITE_END();
}
void run_monitor_tests(void) {
test_history_deque_init();
test_history_deque_push();
test_network_history_deque();
test_disk_history_deque();
test_request_time_deque();
test_monitor_init_cleanup();
test_monitor_vhost_stats();
test_monitor_record_request();
test_monitor_record_bytes();
test_monitor_update();
test_monitor_record_request_end();
test_monitor_deque_overflow();
test_monitor_network_deque_overflow();
test_monitor_disk_deque_overflow();
}

328
tests/test_patch.c Executable file
View File

@ -0,0 +1,328 @@
#include "test_framework.h"
#include "../src/types.h"
#include "../src/patch.h"
#include <string.h>
void test_patch_has_rules(void) {
TEST_SUITE_BEGIN("Patch Has Rules");
patch_config_t empty_config;
memset(&empty_config, 0, sizeof(empty_config));
empty_config.rule_count = 0;
TEST_ASSERT_EQ(0, patch_has_rules(&empty_config), "Empty config has no rules");
patch_config_t config_with_rules;
memset(&config_with_rules, 0, sizeof(config_with_rules));
config_with_rules.rule_count = 1;
strcpy(config_with_rules.rules[0].key, "test");
config_with_rules.rules[0].key_len = 4;
TEST_ASSERT_EQ(1, patch_has_rules(&config_with_rules), "Config with rules returns true");
TEST_ASSERT_EQ(0, patch_has_rules(NULL), "NULL config has no rules");
TEST_SUITE_END();
}
void test_patch_check_for_block(void) {
TEST_SUITE_BEGIN("Patch Check For Block");
patch_config_t config;
memset(&config, 0, sizeof(config));
config.rule_count = 1;
strcpy(config.rules[0].key, "blocked-word");
config.rules[0].key_len = strlen("blocked-word");
config.rules[0].is_null = 1;
const char *data_with_block = "This content contains blocked-word in it";
TEST_ASSERT_EQ(1, patch_check_for_block(&config, data_with_block, strlen(data_with_block)),
"Content with blocked word is blocked");
const char *data_without_block = "This content is clean and allowed";
TEST_ASSERT_EQ(0, patch_check_for_block(&config, data_without_block, strlen(data_without_block)),
"Clean content is not blocked");
patch_config_t empty_config;
memset(&empty_config, 0, sizeof(empty_config));
TEST_ASSERT_EQ(0, patch_check_for_block(&empty_config, data_with_block, strlen(data_with_block)),
"Empty config blocks nothing");
TEST_ASSERT_EQ(0, patch_check_for_block(NULL, data_with_block, strlen(data_with_block)),
"NULL config blocks nothing");
TEST_ASSERT_EQ(0, patch_check_for_block(&config, NULL, 0),
"NULL data is not blocked");
TEST_SUITE_END();
}
void test_patch_apply_simple_replace(void) {
TEST_SUITE_BEGIN("Patch Apply Simple Replacement");
patch_config_t config;
memset(&config, 0, sizeof(config));
config.rule_count = 1;
strcpy(config.rules[0].key, "old");
config.rules[0].key_len = 3;
strcpy(config.rules[0].value, "new");
config.rules[0].value_len = 3;
config.rules[0].is_null = 0;
const char *input = "This is old text with old words";
char output[256];
patch_result_t result = patch_apply(&config, input, strlen(input), output, sizeof(output));
TEST_ASSERT_EQ(0, result.should_block, "Simple replace does not block");
TEST_ASSERT(result.output_len > 0, "Output has content");
TEST_ASSERT(strstr(output, "new") != NULL, "Replacement word present");
TEST_SUITE_END();
}
void test_patch_apply_size_change(void) {
TEST_SUITE_BEGIN("Patch Apply Size Change");
patch_config_t config;
memset(&config, 0, sizeof(config));
config.rule_count = 1;
strcpy(config.rules[0].key, "short");
config.rules[0].key_len = 5;
strcpy(config.rules[0].value, "much-longer-replacement");
config.rules[0].value_len = strlen("much-longer-replacement");
config.rules[0].is_null = 0;
const char *input = "This is short text";
char output[256];
patch_result_t result = patch_apply(&config, input, strlen(input), output, sizeof(output));
TEST_ASSERT_EQ(0, result.should_block, "Size change replace does not block");
TEST_ASSERT(result.output_len > strlen(input), "Output is longer than input");
TEST_ASSERT(result.size_delta > 0, "Size delta is positive");
TEST_SUITE_END();
}
void test_patch_apply_shrink(void) {
TEST_SUITE_BEGIN("Patch Apply Shrink");
patch_config_t config;
memset(&config, 0, sizeof(config));
config.rule_count = 1;
strcpy(config.rules[0].key, "very-long-word");
config.rules[0].key_len = strlen("very-long-word");
strcpy(config.rules[0].value, "tiny");
config.rules[0].value_len = 4;
config.rules[0].is_null = 0;
const char *input = "This has very-long-word in it";
char output[256];
patch_result_t result = patch_apply(&config, input, strlen(input), output, sizeof(output));
TEST_ASSERT_EQ(0, result.should_block, "Shrink replace does not block");
TEST_ASSERT(result.output_len < strlen(input), "Output is shorter than input");
TEST_ASSERT(result.size_delta < 0, "Size delta is negative");
TEST_SUITE_END();
}
void test_patch_apply_multiple_rules(void) {
TEST_SUITE_BEGIN("Patch Apply Multiple Rules");
patch_config_t config;
memset(&config, 0, sizeof(config));
config.rule_count = 3;
strcpy(config.rules[0].key, "foo");
config.rules[0].key_len = 3;
strcpy(config.rules[0].value, "bar");
config.rules[0].value_len = 3;
config.rules[0].is_null = 0;
strcpy(config.rules[1].key, "hello");
config.rules[1].key_len = 5;
strcpy(config.rules[1].value, "world");
config.rules[1].value_len = 5;
config.rules[1].is_null = 0;
strcpy(config.rules[2].key, "test");
config.rules[2].key_len = 4;
strcpy(config.rules[2].value, "demo");
config.rules[2].value_len = 4;
config.rules[2].is_null = 0;
const char *input = "foo says hello during test";
char output[256];
patch_result_t result = patch_apply(&config, input, strlen(input), output, sizeof(output));
TEST_ASSERT_EQ(0, result.should_block, "Multiple rule replace does not block");
TEST_ASSERT(result.output_len > 0, "Output has content");
output[result.output_len] = '\0';
TEST_ASSERT(strstr(output, "bar") != NULL, "First replacement applied");
TEST_ASSERT(strstr(output, "world") != NULL, "Second replacement applied");
TEST_ASSERT(strstr(output, "demo") != NULL, "Third replacement applied");
TEST_SUITE_END();
}
void test_patch_apply_no_match(void) {
TEST_SUITE_BEGIN("Patch Apply No Match");
patch_config_t config;
memset(&config, 0, sizeof(config));
config.rule_count = 1;
strcpy(config.rules[0].key, "nonexistent");
config.rules[0].key_len = strlen("nonexistent");
strcpy(config.rules[0].value, "replacement");
config.rules[0].value_len = strlen("replacement");
config.rules[0].is_null = 0;
const char *input = "This text has no matching patterns";
char output[256];
patch_result_t result = patch_apply(&config, input, strlen(input), output, sizeof(output));
TEST_ASSERT_EQ(0, result.should_block, "No match does not block");
TEST_ASSERT_EQ(strlen(input), result.output_len, "Output length equals input length");
TEST_ASSERT_EQ(0, result.size_delta, "Size delta is zero for no changes");
TEST_SUITE_END();
}
void test_patch_apply_empty_config(void) {
TEST_SUITE_BEGIN("Patch Apply Empty Config");
patch_config_t config;
memset(&config, 0, sizeof(config));
const char *input = "This is some input data";
char output[256];
patch_result_t result = patch_apply(&config, input, strlen(input), output, sizeof(output));
TEST_ASSERT_EQ(0, result.should_block, "Empty config does not block");
TEST_ASSERT_EQ(strlen(input), result.output_len, "Output equals input for empty config");
TEST_SUITE_END();
}
void test_patch_apply_block_rule(void) {
TEST_SUITE_BEGIN("Patch Apply Block Rule");
patch_config_t config;
memset(&config, 0, sizeof(config));
config.rule_count = 1;
strcpy(config.rules[0].key, "malware");
config.rules[0].key_len = strlen("malware");
config.rules[0].is_null = 1;
const char *malicious = "This contains malware content";
char output[256];
patch_result_t result = patch_apply(&config, malicious, strlen(malicious), output, sizeof(output));
TEST_ASSERT_EQ(1, result.should_block, "Block rule triggers block");
const char *clean = "This is clean content";
result = patch_apply(&config, clean, strlen(clean), output, sizeof(output));
TEST_ASSERT_EQ(0, result.should_block, "Clean content not blocked");
TEST_SUITE_END();
}
void test_patch_apply_small_output_buffer(void) {
TEST_SUITE_BEGIN("Patch Apply Small Output Buffer");
patch_config_t config;
memset(&config, 0, sizeof(config));
config.rule_count = 1;
strcpy(config.rules[0].key, "x");
config.rules[0].key_len = 1;
strcpy(config.rules[0].value, "longer");
config.rules[0].value_len = 6;
config.rules[0].is_null = 0;
const char *input = "x x x x x x x x x x";
char output[10];
patch_result_t result = patch_apply(&config, input, strlen(input), output, sizeof(output));
TEST_ASSERT_EQ(0, result.should_block, "Small buffer does not block");
TEST_ASSERT(result.output_len <= sizeof(output), "Output truncated to buffer size");
TEST_SUITE_END();
}
void test_patch_apply_null_input(void) {
TEST_SUITE_BEGIN("Patch Apply NULL Input");
patch_config_t config;
memset(&config, 0, sizeof(config));
config.rule_count = 1;
strcpy(config.rules[0].key, "test");
config.rules[0].key_len = 4;
strcpy(config.rules[0].value, "demo");
config.rules[0].value_len = 4;
char output[256];
patch_result_t result = patch_apply(&config, NULL, 0, output, sizeof(output));
TEST_ASSERT_EQ(0, result.should_block, "NULL input does not block");
result = patch_apply(NULL, "test", 4, output, sizeof(output));
TEST_ASSERT_EQ(0, result.should_block, "NULL config does not block");
TEST_SUITE_END();
}
void test_patch_check_block_edge_cases(void) {
TEST_SUITE_BEGIN("Patch Check Block Edge Cases");
patch_config_t config;
memset(&config, 0, sizeof(config));
config.rule_count = 1;
strcpy(config.rules[0].key, "verylongkeythatwontmatch");
config.rules[0].key_len = strlen("verylongkeythatwontmatch");
config.rules[0].is_null = 1;
const char *short_data = "hi";
TEST_ASSERT_EQ(0, patch_check_for_block(&config, short_data, strlen(short_data)),
"Key longer than data doesn't match");
config.rules[0].key_len = 0;
TEST_ASSERT_EQ(0, patch_check_for_block(&config, "any data", 8),
"Zero length key doesn't match");
TEST_SUITE_END();
}
void test_patch_apply_only_block_rules(void) {
TEST_SUITE_BEGIN("Patch Apply Only Block Rules");
patch_config_t config;
memset(&config, 0, sizeof(config));
config.rule_count = 2;
strcpy(config.rules[0].key, "bad1");
config.rules[0].key_len = 4;
config.rules[0].is_null = 1;
strcpy(config.rules[1].key, "bad2");
config.rules[1].key_len = 4;
config.rules[1].is_null = 1;
const char *input = "good content here";
char output[256];
patch_result_t result = patch_apply(&config, input, strlen(input), output, sizeof(output));
TEST_ASSERT_EQ(0, result.should_block, "No block when content is clean");
TEST_ASSERT_EQ(strlen(input), result.output_len, "Output unchanged with only block rules");
TEST_SUITE_END();
}
void run_patch_tests(void) {
test_patch_has_rules();
test_patch_check_for_block();
test_patch_apply_simple_replace();
test_patch_apply_size_change();
test_patch_apply_shrink();
test_patch_apply_multiple_rules();
test_patch_apply_no_match();
test_patch_apply_empty_config();
test_patch_apply_block_rule();
test_patch_apply_small_output_buffer();
test_patch_apply_null_input();
test_patch_check_block_edge_cases();
test_patch_apply_only_block_rules();
}

152
tests/test_rate_limit.c Executable file
View File

@ -0,0 +1,152 @@
#include "test_framework.h"
#include "../src/types.h"
#include "../src/rate_limit.h"
#include <string.h>
void test_rate_limit_disabled(void) {
TEST_SUITE_BEGIN("Rate Limit Disabled");
rate_limit_cleanup();
TEST_ASSERT_EQ(1, rate_limit_check("192.168.1.1"), "Rate limit check passes when disabled");
TEST_ASSERT_EQ(1, rate_limit_check("10.0.0.1"), "Any IP passes when disabled");
TEST_ASSERT_EQ(1, rate_limit_check(NULL), "NULL IP passes when disabled");
TEST_SUITE_END();
}
void test_rate_limit_init(void) {
TEST_SUITE_BEGIN("Rate Limit Init");
rate_limit_cleanup();
rate_limit_init(100, 60);
TEST_ASSERT_EQ(1, rate_limit_check("192.168.1.100"), "First request passes");
rate_limit_cleanup();
TEST_SUITE_END();
}
void test_rate_limit_within_limit(void) {
TEST_SUITE_BEGIN("Rate Limit Within Limit");
rate_limit_cleanup();
rate_limit_init(10, 60);
const char *ip = "192.168.1.50";
int all_passed = 1;
for (int i = 0; i < 10; i++) {
if (rate_limit_check(ip) != 1) {
all_passed = 0;
break;
}
}
TEST_ASSERT_EQ(1, all_passed, "All requests within limit pass");
rate_limit_cleanup();
TEST_SUITE_END();
}
void test_rate_limit_exceeded(void) {
TEST_SUITE_BEGIN("Rate Limit Exceeded");
rate_limit_cleanup();
rate_limit_init(5, 60);
const char *ip = "192.168.1.200";
for (int i = 0; i < 5; i++) {
rate_limit_check(ip);
}
int exceeded = rate_limit_check(ip);
TEST_ASSERT_EQ(0, exceeded, "Request exceeding limit is blocked");
rate_limit_cleanup();
TEST_SUITE_END();
}
void test_rate_limit_different_ips(void) {
TEST_SUITE_BEGIN("Rate Limit Different IPs");
rate_limit_cleanup();
rate_limit_init(3, 60);
for (int i = 0; i < 3; i++) {
rate_limit_check("10.0.0.1");
}
TEST_ASSERT_EQ(0, rate_limit_check("10.0.0.1"), "First IP blocked after limit");
TEST_ASSERT_EQ(1, rate_limit_check("10.0.0.2"), "Second IP still allowed");
TEST_ASSERT_EQ(1, rate_limit_check("10.0.0.3"), "Third IP still allowed");
rate_limit_cleanup();
TEST_SUITE_END();
}
void test_rate_limit_null_ip(void) {
TEST_SUITE_BEGIN("Rate Limit NULL IP");
rate_limit_cleanup();
rate_limit_init(5, 60);
TEST_ASSERT_EQ(1, rate_limit_check(NULL), "NULL IP passes (fail-open)");
rate_limit_cleanup();
TEST_SUITE_END();
}
void test_rate_limit_purge_expired(void) {
TEST_SUITE_BEGIN("Rate Limit Purge Expired");
rate_limit_cleanup();
rate_limit_init(100, 60);
rate_limit_check("192.168.1.1");
rate_limit_check("192.168.1.2");
rate_limit_check("192.168.1.3");
rate_limit_purge_expired();
TEST_ASSERT_EQ(1, rate_limit_check("192.168.1.1"), "Entry still valid after purge");
rate_limit_cleanup();
TEST_SUITE_END();
}
void test_rate_limit_cleanup(void) {
TEST_SUITE_BEGIN("Rate Limit Cleanup");
rate_limit_init(10, 60);
for (int i = 0; i < 100; i++) {
char ip[32];
snprintf(ip, sizeof(ip), "192.168.%d.%d", i / 256, i % 256);
rate_limit_check(ip);
}
rate_limit_cleanup();
TEST_ASSERT_EQ(1, rate_limit_check("192.168.1.1"), "After cleanup, rate limit disabled");
TEST_SUITE_END();
}
void run_rate_limit_tests(void) {
test_rate_limit_disabled();
test_rate_limit_init();
test_rate_limit_within_limit();
test_rate_limit_exceeded();
test_rate_limit_different_ips();
test_rate_limit_null_ip();
test_rate_limit_purge_expired();
test_rate_limit_cleanup();
}

258
tests/test_routing.c Executable file
View File

@ -0,0 +1,258 @@
#include "test_framework.h"
#include "../src/types.h"
#include "../src/http.h"
#include "../src/config.h"
#include <stdio.h>
#include <unistd.h>
static const char *TEST_CONFIG_FILE = "/tmp/test_routing_config.json";
static void create_routing_config(void) {
FILE *f = fopen(TEST_CONFIG_FILE, "w");
if (f) {
fprintf(f,
"{\n"
" \"port\": 8080,\n"
" \"reverse_proxy\": [\n"
" {\n"
" \"hostname\": \"api.local\",\n"
" \"upstream_host\": \"backend-api\",\n"
" \"upstream_port\": 3000,\n"
" \"use_ssl\": false,\n"
" \"rewrite_host\": true\n"
" },\n"
" {\n"
" \"hostname\": \"secure.local\",\n"
" \"upstream_host\": \"backend-secure\",\n"
" \"upstream_port\": 443,\n"
" \"use_ssl\": true,\n"
" \"rewrite_host\": true\n"
" },\n"
" {\n"
" \"hostname\": \"passthrough.local\",\n"
" \"upstream_host\": \"backend-pass\",\n"
" \"upstream_port\": 8000,\n"
" \"use_ssl\": false,\n"
" \"rewrite_host\": false\n"
" }\n"
" ]\n"
"}\n");
fclose(f);
}
}
static void cleanup_routing_config(void) {
unlink(TEST_CONFIG_FILE);
}
void test_routing_host_rewrite(void) {
TEST_SUITE_BEGIN("Routing Host Rewrite Logic");
create_routing_config();
config_load(TEST_CONFIG_FILE);
route_config_t *route = config_find_route("api.local");
TEST_ASSERT(route != NULL, "Route for api.local found");
if (route) {
TEST_ASSERT_EQ(1, route->rewrite_host, "Host rewrite enabled for api.local");
TEST_ASSERT_STR_EQ("backend-api", route->upstream_host, "Upstream host is backend-api");
TEST_ASSERT_EQ(3000, route->upstream_port, "Upstream port is 3000");
}
route = config_find_route("passthrough.local");
TEST_ASSERT(route != NULL, "Route for passthrough.local found");
if (route) {
TEST_ASSERT_EQ(0, route->rewrite_host, "Host rewrite disabled for passthrough.local");
}
config_free();
cleanup_routing_config();
TEST_SUITE_END();
}
void test_routing_ssl_upstream(void) {
TEST_SUITE_BEGIN("Routing SSL Upstream");
create_routing_config();
config_load(TEST_CONFIG_FILE);
route_config_t *route = config_find_route("secure.local");
TEST_ASSERT(route != NULL, "Route for secure.local found");
if (route) {
TEST_ASSERT_EQ(1, route->use_ssl, "SSL enabled for secure.local");
TEST_ASSERT_EQ(443, route->upstream_port, "SSL port is 443");
}
route = config_find_route("api.local");
TEST_ASSERT(route != NULL, "Route for api.local found");
if (route) {
TEST_ASSERT_EQ(0, route->use_ssl, "SSL disabled for api.local");
}
config_free();
cleanup_routing_config();
TEST_SUITE_END();
}
void test_routing_dashboard_detection(void) {
TEST_SUITE_BEGIN("Routing Dashboard Detection");
http_request_t req;
const char *dashboard_req = "GET /rproxy/dashboard HTTP/1.1\r\nHost: anyhost.com\r\n\r\n";
http_parse_request(dashboard_req, strlen(dashboard_req), &req);
int is_dashboard = (strncmp(req.uri, "/rproxy/dashboard", 17) == 0);
TEST_ASSERT_EQ(1, is_dashboard, "Dashboard URI detected");
const char *api_req = "GET /rproxy/api/stats HTTP/1.1\r\nHost: anyhost.com\r\n\r\n";
http_parse_request(api_req, strlen(api_req), &req);
int is_api = (strncmp(req.uri, "/rproxy/api/stats", 17) == 0);
TEST_ASSERT_EQ(1, is_api, "API stats URI detected");
const char *regular_req = "GET /some/other/path HTTP/1.1\r\nHost: api.local\r\n\r\n";
http_parse_request(regular_req, strlen(regular_req), &req);
int is_regular = (strncmp(req.uri, "/rproxy/", 8) != 0);
TEST_ASSERT_EQ(1, is_regular, "Regular path not matched as internal");
TEST_SUITE_END();
}
void test_routing_keep_alive_handling(void) {
TEST_SUITE_BEGIN("Routing Keep-Alive Handling");
http_request_t req;
const char *ka_req = "GET /api HTTP/1.1\r\nHost: api.local\r\nConnection: keep-alive\r\n\r\n";
http_parse_request(ka_req, strlen(ka_req), &req);
TEST_ASSERT_EQ(1, req.keep_alive, "Keep-alive flag set when Connection: keep-alive");
const char *close_req = "GET /api HTTP/1.1\r\nHost: api.local\r\nConnection: close\r\n\r\n";
http_parse_request(close_req, strlen(close_req), &req);
TEST_ASSERT_EQ(0, req.keep_alive, "Keep-alive flag not set when Connection: close");
const char *default_req = "GET /api HTTP/1.1\r\nHost: api.local\r\n\r\n";
http_parse_request(default_req, strlen(default_req), &req);
TEST_ASSERT_EQ(1, req.keep_alive, "Keep-alive default for HTTP/1.1");
const char *http10_req = "GET /api HTTP/1.0\r\nHost: api.local\r\n\r\n";
http_parse_request(http10_req, strlen(http10_req), &req);
TEST_ASSERT_EQ(0, req.keep_alive, "Keep-alive default off for HTTP/1.0");
TEST_SUITE_END();
}
void test_routing_all_methods_accepted(void) {
TEST_SUITE_BEGIN("Routing All HTTP Methods Accepted");
http_request_t req;
int result;
const char *methods[] = {
"GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH", "TRACE", "CONNECT",
"PROPFIND", "PROPPATCH", "MKCOL", "MOVE", "COPY", "LOCK", "UNLOCK",
"SEARCH", "REPORT", "MKACTIVITY", "CHECKOUT", "MERGE",
"NOTIFY", "SUBSCRIBE", "UNSUBSCRIBE",
"CUSTOMMETHOD", "FOOBAR", "MYPROTO"
};
int num_methods = sizeof(methods) / sizeof(methods[0]);
for (int i = 0; i < num_methods; i++) {
char request[256];
snprintf(request, sizeof(request), "%s /path HTTP/1.1\r\nHost: test.com\r\n\r\n", methods[i]);
result = http_parse_request(request, strlen(request), &req);
char msg[128];
snprintf(msg, sizeof(msg), "Method %s accepted", methods[i]);
TEST_ASSERT_EQ(1, result, msg);
TEST_ASSERT_STR_EQ(methods[i], req.method, msg);
}
TEST_SUITE_END();
}
void test_routing_pipelined_requests(void) {
TEST_SUITE_BEGIN("Routing Pipelined Request Detection");
http_request_t req1, req2;
const char *first_req = "GET /first HTTP/1.1\r\nHost: api.local\r\nConnection: keep-alive\r\n\r\n";
int result1 = http_parse_request(first_req, strlen(first_req), &req1);
TEST_ASSERT_EQ(1, result1, "First request parsed");
TEST_ASSERT_STR_EQ("/first", req1.uri, "First request URI");
TEST_ASSERT_EQ(1, req1.keep_alive, "First request keep-alive");
const char *second_req = "GET /second HTTP/1.1\r\nHost: api.local\r\n\r\n";
int result2 = http_parse_request(second_req, strlen(second_req), &req2);
TEST_ASSERT_EQ(1, result2, "Second request parsed");
TEST_ASSERT_STR_EQ("/second", req2.uri, "Second request URI");
int is_new_request = http_is_request_start(second_req, strlen(second_req));
TEST_ASSERT_EQ(1, is_new_request, "New request detected for pipelining");
TEST_SUITE_END();
}
void test_routing_websocket_upgrade(void) {
TEST_SUITE_BEGIN("Routing WebSocket Upgrade");
http_request_t req;
const char *ws_req =
"GET /ws/chat HTTP/1.1\r\n"
"Host: api.local\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
"Sec-WebSocket-Version: 13\r\n"
"\r\n";
int result = http_parse_request(ws_req, strlen(ws_req), &req);
TEST_ASSERT_EQ(1, result, "WebSocket upgrade request parsed");
TEST_ASSERT_EQ(1, req.is_websocket, "WebSocket flag set");
TEST_ASSERT_STR_EQ("/ws/chat", req.uri, "WebSocket URI parsed");
TEST_SUITE_END();
}
void test_routing_chunked_transfer(void) {
TEST_SUITE_BEGIN("Routing Chunked Transfer Encoding");
http_request_t req;
const char *chunked_req =
"POST /upload HTTP/1.1\r\n"
"Host: api.local\r\n"
"Transfer-Encoding: chunked\r\n"
"\r\n";
int result = http_parse_request(chunked_req, strlen(chunked_req), &req);
TEST_ASSERT_EQ(1, result, "Chunked request parsed");
TEST_ASSERT_EQ(1, req.is_chunked, "Chunked flag set");
const char *non_chunked_req =
"POST /upload HTTP/1.1\r\n"
"Host: api.local\r\n"
"Content-Length: 100\r\n"
"\r\n";
result = http_parse_request(non_chunked_req, strlen(non_chunked_req), &req);
TEST_ASSERT_EQ(1, result, "Non-chunked request parsed");
TEST_ASSERT_EQ(0, req.is_chunked, "Chunked flag not set");
TEST_ASSERT_EQ(100, req.content_length, "Content-Length parsed");
TEST_SUITE_END();
}
void run_routing_tests(void) {
test_routing_host_rewrite();
test_routing_ssl_upstream();
test_routing_dashboard_detection();
test_routing_keep_alive_handling();
test_routing_all_methods_accepted();
test_routing_pipelined_requests();
test_routing_websocket_upgrade();
test_routing_chunked_transfer();
}

288
tests/test_ssl_handler.c Executable file
View File

@ -0,0 +1,288 @@
#include "test_framework.h"
#include "../src/types.h"
#include "../src/ssl_handler.h"
#include <string.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
extern SSL_CTX *ssl_ctx;
void test_ssl_init_cleanup(void) {
TEST_SUITE_BEGIN("SSL Init and Cleanup");
ssl_init();
TEST_ASSERT(ssl_ctx != NULL, "SSL context created");
ssl_cleanup();
TEST_ASSERT(ssl_ctx == NULL, "SSL context cleaned up");
TEST_SUITE_END();
}
void test_ssl_multiple_init(void) {
TEST_SUITE_BEGIN("SSL Multiple Init");
ssl_init();
SSL_CTX *first_ctx = ssl_ctx;
TEST_ASSERT(first_ctx != NULL, "First init creates context");
ssl_cleanup();
ssl_init();
SSL_CTX *second_ctx = ssl_ctx;
TEST_ASSERT(second_ctx != NULL, "Second init creates context");
ssl_cleanup();
TEST_SUITE_END();
}
void test_ssl_set_verify(void) {
TEST_SUITE_BEGIN("SSL Set Verify");
ssl_set_verify(1);
TEST_ASSERT(1, "Set verify enabled doesn't crash");
ssl_set_verify(0);
TEST_ASSERT(1, "Set verify disabled doesn't crash");
ssl_init();
TEST_ASSERT(ssl_ctx != NULL, "Context created after set_verify");
ssl_cleanup();
TEST_SUITE_END();
}
void test_ssl_set_ca_file(void) {
TEST_SUITE_BEGIN("SSL Set CA File");
ssl_set_ca_file(NULL);
TEST_ASSERT(1, "NULL CA file doesn't crash");
ssl_set_ca_file("/nonexistent/path/ca.crt");
TEST_ASSERT(1, "Nonexistent CA file doesn't crash");
ssl_set_ca_file("/etc/ssl/certs/ca-certificates.crt");
TEST_ASSERT(1, "System CA file path set");
ssl_init();
TEST_ASSERT(ssl_ctx != NULL, "Context created after set_ca_file");
ssl_cleanup();
TEST_SUITE_END();
}
void test_ssl_set_ca_path(void) {
TEST_SUITE_BEGIN("SSL Set CA Path");
ssl_set_ca_path(NULL);
TEST_ASSERT(1, "NULL CA path doesn't crash");
ssl_set_ca_path("/nonexistent/path");
TEST_ASSERT(1, "Nonexistent CA path doesn't crash");
ssl_set_ca_path("/etc/ssl/certs");
TEST_ASSERT(1, "System CA path set");
ssl_init();
TEST_ASSERT(ssl_ctx != NULL, "Context created after set_ca_path");
ssl_cleanup();
TEST_SUITE_END();
}
void test_ssl_context_options(void) {
TEST_SUITE_BEGIN("SSL Context Options");
ssl_set_verify(0);
ssl_init();
TEST_ASSERT(ssl_ctx != NULL, "Context created");
if (ssl_ctx) {
long options = SSL_CTX_get_options(ssl_ctx);
TEST_ASSERT((options & SSL_OP_NO_SSLv3) != 0, "SSLv3 disabled");
}
ssl_cleanup();
TEST_SUITE_END();
}
void test_ssl_create_connection(void) {
TEST_SUITE_BEGIN("SSL Create Connection Object");
ssl_set_verify(0);
ssl_init();
if (ssl_ctx) {
SSL *ssl = SSL_new(ssl_ctx);
TEST_ASSERT(ssl != NULL, "SSL object created from context");
if (ssl) {
SSL_free(ssl);
}
} else {
TEST_ASSERT(1, "Skipped - no SSL context");
}
ssl_cleanup();
TEST_SUITE_END();
}
void test_ssl_handshake_null(void) {
TEST_SUITE_BEGIN("SSL Handshake NULL Safety");
int result = ssl_do_handshake(NULL);
TEST_ASSERT_EQ(-1, result, "NULL connection returns -1");
connection_t conn;
memset(&conn, 0, sizeof(conn));
conn.ssl = NULL;
result = ssl_do_handshake(&conn);
TEST_ASSERT_EQ(-1, result, "Connection without SSL returns -1");
TEST_SUITE_END();
}
void test_ssl_read_write_null(void) {
TEST_SUITE_BEGIN("SSL Read/Write NULL Safety");
char buf[100];
int read_result = ssl_read(NULL, buf, sizeof(buf));
TEST_ASSERT_EQ(-1, read_result, "NULL connection read returns -1");
int write_result = ssl_write(NULL, buf, sizeof(buf));
TEST_ASSERT_EQ(-1, write_result, "NULL connection write returns -1");
connection_t conn;
memset(&conn, 0, sizeof(conn));
conn.ssl = NULL;
read_result = ssl_read(&conn, buf, sizeof(buf));
TEST_ASSERT_EQ(-1, read_result, "Connection without SSL read returns -1");
write_result = ssl_write(&conn, buf, sizeof(buf));
TEST_ASSERT_EQ(-1, write_result, "Connection without SSL write returns -1");
TEST_SUITE_END();
}
void test_ssl_reinit(void) {
TEST_SUITE_BEGIN("SSL Reinitialize");
ssl_set_verify(0);
ssl_init();
TEST_ASSERT(ssl_ctx != NULL, "First init with verify=0");
ssl_cleanup();
ssl_set_verify(1);
ssl_set_ca_path("/etc/ssl/certs");
ssl_init();
TEST_ASSERT(ssl_ctx != NULL, "Second init with verify=1");
ssl_cleanup();
TEST_SUITE_END();
}
void test_ssl_handshake_already_done(void) {
TEST_SUITE_BEGIN("SSL Handshake Already Done");
connection_t conn;
memset(&conn, 0, sizeof(conn));
conn.ssl_handshake_done = 1;
ssl_set_verify(0);
ssl_init();
conn.ssl = SSL_new(ssl_ctx);
TEST_ASSERT(conn.ssl != NULL, "SSL object created");
int result = ssl_do_handshake(&conn);
TEST_ASSERT_EQ(1, result, "Already done returns 1");
SSL_free(conn.ssl);
ssl_cleanup();
TEST_SUITE_END();
}
void test_ssl_read_no_handshake(void) {
TEST_SUITE_BEGIN("SSL Read Without Handshake");
connection_t conn;
memset(&conn, 0, sizeof(conn));
conn.ssl_handshake_done = 0;
ssl_set_verify(0);
ssl_init();
conn.ssl = SSL_new(ssl_ctx);
TEST_ASSERT(conn.ssl != NULL, "SSL object created");
char buf[100];
int result = ssl_read(&conn, buf, sizeof(buf));
TEST_ASSERT_EQ(-1, result, "Read without handshake returns -1");
SSL_free(conn.ssl);
ssl_cleanup();
TEST_SUITE_END();
}
void test_ssl_write_no_handshake(void) {
TEST_SUITE_BEGIN("SSL Write Without Handshake");
connection_t conn;
memset(&conn, 0, sizeof(conn));
conn.ssl_handshake_done = 0;
ssl_set_verify(0);
ssl_init();
conn.ssl = SSL_new(ssl_ctx);
TEST_ASSERT(conn.ssl != NULL, "SSL object created");
const char *buf = "test";
int result = ssl_write(&conn, buf, strlen(buf));
TEST_ASSERT_EQ(-1, result, "Write without handshake returns -1");
SSL_free(conn.ssl);
ssl_cleanup();
TEST_SUITE_END();
}
void test_ssl_verify_with_ca(void) {
TEST_SUITE_BEGIN("SSL Verify With CA Settings");
ssl_set_verify(1);
ssl_set_ca_file("/etc/ssl/certs/ca-certificates.crt");
ssl_set_ca_path("/etc/ssl/certs");
ssl_init();
TEST_ASSERT(ssl_ctx != NULL, "Context created with CA settings");
ssl_cleanup();
TEST_SUITE_END();
}
void run_ssl_handler_tests(void) {
test_ssl_init_cleanup();
test_ssl_multiple_init();
test_ssl_set_verify();
test_ssl_set_ca_file();
test_ssl_set_ca_path();
test_ssl_context_options();
test_ssl_create_connection();
test_ssl_handshake_null();
test_ssl_read_write_null();
test_ssl_reinit();
test_ssl_handshake_already_done();
test_ssl_read_no_handshake();
test_ssl_write_no_handshake();
test_ssl_verify_with_ca();
}