Compare commits

..
52 Commits
Author SHA1 Message Date
retoor 8ae8ccdfe0 feat: implement zero-copy splice forwarding on Linux and fix upstream connection leak
Build and Test / coverage (push) Successful in 38s
Build and Test / build (push) Failing after 27s
- 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
retoor 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
retoor 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
retoor e914b08c29 chore: switch from -flto to -flto=auto for automatic job count in Makefile 2026-01-27 15:24:10 +00:00
retoor 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
retoor 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
retoor a4438c6b60 chore: remove unused stdio.h include from main.c and fix typo in comment 2026-01-01 20:55:23 +00:00
retoor 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
retoor 45005f904b feat: add formatNum and formatMs helpers for dashboard metric display 2025-12-29 00:50:08 +00:00
retoor 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
retoor 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
retoor 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
retoor 353e6b08b6 feat: add TCP_NODELAY socket optimization and upstream handling with connection caching tests 2025-12-15 00:31:27 +00:00
retoor 9e7c5940dc perf: cache epoll events in connection struct and add socket optimization routines 2025-12-15 00:28:34 +00:00
retoor c84d8c9d21 feat: add splice pipe fields and patch buffer to connection struct for zero-copy forwarding 2025-12-15 00:12:09 +00:00
retoor 521bd1eb5b chore: add bc dependency and coverage job with gcovr/lcov to build pipeline 2025-12-12 23:21:55 +00:00
retoor 87ede0b4c9 chore: add trailing whitespace to author line in readme for placeholder formatting 2025-12-12 21:37:32 +00:00
retoor 5cea9db29e chore: add test results section to README and tag routing logs with hostname 2025-12-12 21:33:27 +00:00
retoor 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
retoor 0461f21d5a chore: update README with author, testing section, and per-route auth config 2025-12-12 21:20:34 +00:00
retoor 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
retoor ef3cb0fc54 chore: remove entire original rproxy.c source tree from repository history 2025-12-12 19:43:24 +00:00
retoor 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
retoor 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
retoor 5a07aeffba refactor: convert global config struct to heap-allocated ref-counted pointer with hot-reload support 2025-11-29 12:18:07 +00:00
retoor 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
retoor 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
retoor 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
retoor 949034bf74 fix: correct SNI hostname field name and SSL API call in connection setup 2025-11-29 03:18:39 +00:00
retoor 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
retoor 988dff1b20 fix: correct default port detection to respect SSL context in connection routing 2025-11-29 02:15:45 +00:00
retoor 8436787928 fix: correct SSL read error handling and replace magic strings with macros in connection.c 2025-11-29 01:51:08 +00:00
retoor 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
retoor 50a682c865 chore: remove trailing whitespace from README.md formatting and update project name to lowercase 2025-11-29 00:57:12 +00:00
retoor feb2fb8a05 chore: add Gitea CI workflow for build and test on main/master pushes 2025-11-29 00:53:11 +00:00
retoor 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
retoor 2a492dd934 fix: prefix internal dashboard and stats routes with /rproxy/ in rproxy.c 2025-11-29 00:04:25 +00:00
retoor 94df53367b chore: remove trailing whitespace and clean up comment blocks in rproxy.c 2025-11-20 05:29:28 +00:00
retoor 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
retoor 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
retoor 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
retoor 9485b7a618 chore: remove extraneous blank lines between function definitions in rproxy.c 2025-09-25 22:07:06 +00:00
retoor 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
retoor d2eabd26a3 chore: remove trailing whitespace from README.md line 42 2025-09-25 17:36:47 +00:00
retoor 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
retoor a47849b005 chore: remove trailing whitespace from README.md line 42 2025-09-25 17:23:20 +00:00
retoor 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
retoor 5c4c357bc3 fix: reset orphaned forwarding state to reading headers and add routing debug logging 2025-09-25 04:37:51 +00:00
retoor 33b230f722 fix: correct SSL upstream configuration for dr endpoint to resolve connection failure 2025-09-25 03:43:10 +00:00
retoor 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
retoor 0fbf53e0bf fix: simplify client state machine and fix buffer management in rproxy.c 2025-09-25 03:11:52 +00:00
retoor 4128f7b597 feat: scaffold initial project structure with empty source directories and config stubs 2025-09-25 00:35:00 +00:00
80 changed files with 9112 additions and 2220 deletions
Regular → Executable
+19 -1
View File
@@ -1,3 +1,4 @@
# retoor <retoor@molodetz.nl>
name: Build and Test name: Build and Test
on: on:
@@ -20,7 +21,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y gcc make libssl-dev libsqlite3-dev sudo apt-get install -y gcc make libssl-dev libsqlite3-dev bc
- name: Build - name: Build
run: make all run: make all
@@ -28,5 +29,22 @@ jobs:
- name: Run tests - name: Run tests
run: make test run: make test
- name: Build legacy
run: make legacy
- name: Clean - name: Clean
run: make 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
Regular → Executable
View File
Executable
+114
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)
Regular → Executable
+50 -139
View File
@@ -1,5 +1,7 @@
# retoor <retoor@molodetz.nl>
CC = gcc CC = gcc
CFLAGS = -Wall -Wextra -Werror -O3 -march=native -flto -fomit-frame-pointer -D_GNU_SOURCE 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_DEBUG = -Wall -Wextra -Werror -O0 -g -D_GNU_SOURCE
CFLAGS_COV = -Wall -Wextra -Werror -O0 -g -D_GNU_SOURCE --coverage -fprofile-arcs -ftest-coverage CFLAGS_COV = -Wall -Wextra -Werror -O0 -g -D_GNU_SOURCE --coverage -fprofile-arcs -ftest-coverage
LDFLAGS = -flto -lssl -lcrypto -lsqlite3 -lm -lpthread LDFLAGS = -flto -lssl -lcrypto -lsqlite3 -lm -lpthread
@@ -13,15 +15,28 @@ SOURCES = $(SRC_DIR)/main.c \
$(SRC_DIR)/buffer.c \ $(SRC_DIR)/buffer.c \
$(SRC_DIR)/logging.c \ $(SRC_DIR)/logging.c \
$(SRC_DIR)/config.c \ $(SRC_DIR)/config.c \
$(SRC_DIR)/config_parser.c \
$(SRC_DIR)/monitor.c \ $(SRC_DIR)/monitor.c \
$(SRC_DIR)/http.c \ $(SRC_DIR)/http.c \
$(SRC_DIR)/http_response.c \
$(SRC_DIR)/ssl_handler.c \ $(SRC_DIR)/ssl_handler.c \
$(SRC_DIR)/connection.c \ $(SRC_DIR)/connection.c \
$(SRC_DIR)/client_handler.c \
$(SRC_DIR)/upstream.c \
$(SRC_DIR)/forwarding.c \
$(SRC_DIR)/dashboard.c \ $(SRC_DIR)/dashboard.c \
$(SRC_DIR)/rate_limit.c \ $(SRC_DIR)/rate_limit.c \
$(SRC_DIR)/auth.c \ $(SRC_DIR)/auth.c \
$(SRC_DIR)/base64.c \
$(SRC_DIR)/health_check.c \ $(SRC_DIR)/health_check.c \
$(SRC_DIR)/patch.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 cJSON.c
OBJECTS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(notdir $(SOURCES))) OBJECTS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(notdir $(SOURCES)))
@@ -42,22 +57,36 @@ TEST_SOURCES = $(TESTS_DIR)/test_main.c \
$(TESTS_DIR)/test_dashboard.c \ $(TESTS_DIR)/test_dashboard.c \
$(TESTS_DIR)/test_health_check.c \ $(TESTS_DIR)/test_health_check.c \
$(TESTS_DIR)/test_ssl_handler.c \ $(TESTS_DIR)/test_ssl_handler.c \
$(TESTS_DIR)/test_connection.c $(TESTS_DIR)/test_connection.c \
$(TESTS_DIR)/test_logging.c
TEST_OBJECTS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(notdir $(TEST_SOURCES))) TEST_OBJECTS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(notdir $(TEST_SOURCES)))
TEST_LIB_SOURCES = $(SRC_DIR)/buffer.c \ TEST_LIB_SOURCES = $(SRC_DIR)/buffer.c \
$(SRC_DIR)/logging.c \ $(SRC_DIR)/logging.c \
$(SRC_DIR)/config.c \ $(SRC_DIR)/config.c \
$(SRC_DIR)/config_parser.c \
$(SRC_DIR)/monitor.c \ $(SRC_DIR)/monitor.c \
$(SRC_DIR)/http.c \ $(SRC_DIR)/http.c \
$(SRC_DIR)/http_response.c \
$(SRC_DIR)/ssl_handler.c \ $(SRC_DIR)/ssl_handler.c \
$(SRC_DIR)/connection.c \ $(SRC_DIR)/connection.c \
$(SRC_DIR)/client_handler.c \
$(SRC_DIR)/upstream.c \
$(SRC_DIR)/forwarding.c \
$(SRC_DIR)/dashboard.c \ $(SRC_DIR)/dashboard.c \
$(SRC_DIR)/rate_limit.c \ $(SRC_DIR)/rate_limit.c \
$(SRC_DIR)/auth.c \ $(SRC_DIR)/auth.c \
$(SRC_DIR)/base64.c \
$(SRC_DIR)/health_check.c \ $(SRC_DIR)/health_check.c \
$(SRC_DIR)/patch.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 cJSON.c
TEST_LIB_OBJECTS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(notdir $(TEST_LIB_SOURCES))) TEST_LIB_OBJECTS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(notdir $(TEST_LIB_SOURCES)))
@@ -77,91 +106,13 @@ $(BUILD_DIR):
$(TARGET): $(OBJECTS) $(TARGET): $(OBJECTS)
$(CC) $(OBJECTS) -o $@ $(LDFLAGS) $(CC) $(OBJECTS) -o $@ $(LDFLAGS)
$(BUILD_DIR)/main.o: $(SRC_DIR)/main.c $(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR)
$(CC) $(CFLAGS) -c $< -o $@ $(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR)/buffer.o: $(SRC_DIR)/buffer.c $(BUILD_DIR)/cJSON.o: cJSON.c | $(BUILD_DIR)
$(CC) $(CFLAGS) -c $< -o $@ $(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR)/logging.o: $(SRC_DIR)/logging.c $(BUILD_DIR)/test_%.o: $(TESTS_DIR)/test_%.c | $(BUILD_DIR)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR)/config.o: $(SRC_DIR)/config.c
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR)/monitor.o: $(SRC_DIR)/monitor.c
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR)/http.o: $(SRC_DIR)/http.c
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR)/ssl_handler.o: $(SRC_DIR)/ssl_handler.c
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR)/connection.o: $(SRC_DIR)/connection.c
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR)/dashboard.o: $(SRC_DIR)/dashboard.c
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR)/rate_limit.o: $(SRC_DIR)/rate_limit.c
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR)/auth.o: $(SRC_DIR)/auth.c
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR)/health_check.o: $(SRC_DIR)/health_check.c
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR)/patch.o: $(SRC_DIR)/patch.c
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR)/cJSON.o: cJSON.c
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR)/test_main.o: $(TESTS_DIR)/test_main.c
$(CC) $(CFLAGS) -I$(SRC_DIR) -c $< -o $@
$(BUILD_DIR)/test_http.o: $(TESTS_DIR)/test_http.c
$(CC) $(CFLAGS) -I$(SRC_DIR) -c $< -o $@
$(BUILD_DIR)/test_buffer.o: $(TESTS_DIR)/test_buffer.c
$(CC) $(CFLAGS) -I$(SRC_DIR) -c $< -o $@
$(BUILD_DIR)/test_config.o: $(TESTS_DIR)/test_config.c
$(CC) $(CFLAGS) -I$(SRC_DIR) -c $< -o $@
$(BUILD_DIR)/test_routing.o: $(TESTS_DIR)/test_routing.c
$(CC) $(CFLAGS) -I$(SRC_DIR) -c $< -o $@
$(BUILD_DIR)/test_host_rewrite.o: $(TESTS_DIR)/test_host_rewrite.c
$(CC) $(CFLAGS) -I$(SRC_DIR) -c $< -o $@
$(BUILD_DIR)/test_http_helpers.o: $(TESTS_DIR)/test_http_helpers.c
$(CC) $(CFLAGS) -I$(SRC_DIR) -c $< -o $@
$(BUILD_DIR)/test_patch.o: $(TESTS_DIR)/test_patch.c
$(CC) $(CFLAGS) -I$(SRC_DIR) -c $< -o $@
$(BUILD_DIR)/test_auth.o: $(TESTS_DIR)/test_auth.c
$(CC) $(CFLAGS) -I$(SRC_DIR) -c $< -o $@
$(BUILD_DIR)/test_rate_limit.o: $(TESTS_DIR)/test_rate_limit.c
$(CC) $(CFLAGS) -I$(SRC_DIR) -c $< -o $@
$(BUILD_DIR)/test_monitor.o: $(TESTS_DIR)/test_monitor.c
$(CC) $(CFLAGS) -I$(SRC_DIR) -c $< -o $@
$(BUILD_DIR)/test_dashboard.o: $(TESTS_DIR)/test_dashboard.c
$(CC) $(CFLAGS) -I$(SRC_DIR) -c $< -o $@
$(BUILD_DIR)/test_health_check.o: $(TESTS_DIR)/test_health_check.c
$(CC) $(CFLAGS) -I$(SRC_DIR) -c $< -o $@
$(BUILD_DIR)/test_ssl_handler.o: $(TESTS_DIR)/test_ssl_handler.c
$(CC) $(CFLAGS) -I$(SRC_DIR) -c $< -o $@
$(BUILD_DIR)/test_connection.o: $(TESTS_DIR)/test_connection.c
$(CC) $(CFLAGS) -I$(SRC_DIR) -c $< -o $@ $(CC) $(CFLAGS) -I$(SRC_DIR) -c $< -o $@
$(TEST_TARGET): $(BUILD_DIR) $(TEST_OBJECTS) $(TEST_LIB_OBJECTS) $(TEST_TARGET): $(BUILD_DIR) $(TEST_OBJECTS) $(TEST_LIB_OBJECTS)
@@ -178,34 +129,14 @@ run: $(TARGET)
coverage: clean coverage: clean
mkdir -p $(BUILD_DIR) mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_main.c -o build/test_main.o @for src in $(TEST_LIB_SOURCES); do \
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_http.c -o build/test_http.o obj=$(BUILD_DIR)/$$(basename $${src%.c}.o); \
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_buffer.c -o build/test_buffer.o $(CC) $(CFLAGS_COV) -c $$src -o $$obj; \
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_config.c -o build/test_config.o done
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_routing.c -o build/test_routing.o @for src in $(TEST_SOURCES); do \
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_host_rewrite.c -o build/test_host_rewrite.o obj=$(BUILD_DIR)/$$(basename $${src%.c}.o); \
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_http_helpers.c -o build/test_http_helpers.o $(CC) $(CFLAGS_COV) -I$(SRC_DIR) -c $$src -o $$obj; \
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_patch.c -o build/test_patch.o done
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_auth.c -o build/test_auth.o
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_rate_limit.c -o build/test_rate_limit.o
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_monitor.c -o build/test_monitor.o
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_dashboard.c -o build/test_dashboard.o
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_health_check.c -o build/test_health_check.o
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_ssl_handler.c -o build/test_ssl_handler.o
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_connection.c -o build/test_connection.o
$(CC) $(CFLAGS_COV) -c src/buffer.c -o build/buffer.o
$(CC) $(CFLAGS_COV) -c src/logging.c -o build/logging.o
$(CC) $(CFLAGS_COV) -c src/config.c -o build/config.o
$(CC) $(CFLAGS_COV) -c src/monitor.c -o build/monitor.o
$(CC) $(CFLAGS_COV) -c src/http.c -o build/http.o
$(CC) $(CFLAGS_COV) -c src/ssl_handler.c -o build/ssl_handler.o
$(CC) $(CFLAGS_COV) -c src/connection.c -o build/connection.o
$(CC) $(CFLAGS_COV) -c src/dashboard.c -o build/dashboard.o
$(CC) $(CFLAGS_COV) -c src/rate_limit.c -o build/rate_limit.o
$(CC) $(CFLAGS_COV) -c src/auth.c -o build/auth.o
$(CC) $(CFLAGS_COV) -c src/health_check.c -o build/health_check.o
$(CC) $(CFLAGS_COV) -c src/patch.c -o build/patch.o
$(CC) $(CFLAGS_COV) -c cJSON.c -o build/cJSON.o
$(CC) $(TEST_OBJECTS) $(TEST_LIB_OBJECTS) -o $(TEST_TARGET) $(LDFLAGS_COV) $(CC) $(TEST_OBJECTS) $(TEST_LIB_OBJECTS) -o $(TEST_TARGET) $(LDFLAGS_COV)
./$(TEST_TARGET) ./$(TEST_TARGET)
@echo "" @echo ""
@@ -261,34 +192,14 @@ coverage-html: coverage
valgrind: clean valgrind: clean
mkdir -p $(BUILD_DIR) mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_main.c -o build/test_main.o @for src in $(TEST_LIB_SOURCES); do \
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_http.c -o build/test_http.o obj=$(BUILD_DIR)/$$(basename $${src%.c}.o); \
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_buffer.c -o build/test_buffer.o $(CC) $(CFLAGS_DEBUG) -c $$src -o $$obj; \
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_config.c -o build/test_config.o done
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_routing.c -o build/test_routing.o @for src in $(TEST_SOURCES); do \
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_host_rewrite.c -o build/test_host_rewrite.o obj=$(BUILD_DIR)/$$(basename $${src%.c}.o); \
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_http_helpers.c -o build/test_http_helpers.o $(CC) $(CFLAGS_DEBUG) -I$(SRC_DIR) -c $$src -o $$obj; \
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_patch.c -o build/test_patch.o done
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_auth.c -o build/test_auth.o
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_rate_limit.c -o build/test_rate_limit.o
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_monitor.c -o build/test_monitor.o
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_dashboard.c -o build/test_dashboard.o
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_health_check.c -o build/test_health_check.o
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_ssl_handler.c -o build/test_ssl_handler.o
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_connection.c -o build/test_connection.o
$(CC) $(CFLAGS_DEBUG) -c src/buffer.c -o build/buffer.o
$(CC) $(CFLAGS_DEBUG) -c src/logging.c -o build/logging.o
$(CC) $(CFLAGS_DEBUG) -c src/config.c -o build/config.o
$(CC) $(CFLAGS_DEBUG) -c src/monitor.c -o build/monitor.o
$(CC) $(CFLAGS_DEBUG) -c src/http.c -o build/http.o
$(CC) $(CFLAGS_DEBUG) -c src/ssl_handler.c -o build/ssl_handler.o
$(CC) $(CFLAGS_DEBUG) -c src/connection.c -o build/connection.o
$(CC) $(CFLAGS_DEBUG) -c src/dashboard.c -o build/dashboard.o
$(CC) $(CFLAGS_DEBUG) -c src/rate_limit.c -o build/rate_limit.o
$(CC) $(CFLAGS_DEBUG) -c src/auth.c -o build/auth.o
$(CC) $(CFLAGS_DEBUG) -c src/health_check.c -o build/health_check.o
$(CC) $(CFLAGS_DEBUG) -c src/patch.c -o build/patch.o
$(CC) $(CFLAGS_DEBUG) -c cJSON.c -o build/cJSON.o
$(CC) $(TEST_OBJECTS) $(TEST_LIB_OBJECTS) -o $(TEST_TARGET) -lssl -lcrypto -lsqlite3 -lm -lpthread $(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 --leak-check=full --show-leak-kinds=definite,indirect --error-exitcode=1 ./$(TEST_TARGET) 2>&1 | tee valgrind.log; \
VALGRIND_EXIT=$$?; \ VALGRIND_EXIT=$$?; \
Regular → Executable
+15 -1
View File
@@ -44,11 +44,25 @@ Compiles the source files in `src/` and produces the `rproxy` executable.
```bash ```bash
make test # Run unit tests make test # Run unit tests
make coverage # Run tests with coverage report (minimum 60% required) make coverage # Run tests with coverage report (minimum 69% required)
make coverage-html # Generate HTML coverage report make coverage-html # Generate HTML coverage report
make valgrind # Run tests with memory leak detection 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
Configuration is defined in `proxy_config.json`: Configuration is defined in `proxy_config.json`:
Regular → Executable
View File
Regular → Executable
View File
+99 -125
View File
@@ -1,14 +1,22 @@
// retoor <retoor@molodetz.nl>
#include "auth.h" #include "auth.h"
#include "base64.h"
#include "logging.h" #include "logging.h"
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <openssl/evp.h> #include <openssl/evp.h>
#include <openssl/rand.h> #include <openssl/crypto.h>
static char g_dashboard_username[128] = ""; static char g_dashboard_username[128] = "";
static char g_dashboard_password_hash[256] = ""; static char g_dashboard_password_hash[256] = "";
static int g_auth_enabled = 0; 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) { static void compute_sha256(const char *input, char *output, size_t output_size) {
EVP_MD_CTX *ctx = EVP_MD_CTX_new(); EVP_MD_CTX *ctx = EVP_MD_CTX_new();
if (!ctx) return; if (!ctx) return;
@@ -48,149 +56,115 @@ int auth_check_credentials(const char *username, const char *password) {
if (!g_auth_enabled) return 1; if (!g_auth_enabled) return 1;
if (!username || !password) return 0; if (!username || !password) return 0;
if (strcmp(username, g_dashboard_username) != 0) 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]; char password_hash[256];
compute_sha256(password, password_hash, sizeof(password_hash)); compute_sha256(password, password_hash, sizeof(password_hash));
return strcmp(password_hash, g_dashboard_password_hash) == 0; 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);
static int base64_decode_char(char c) { int hash_match = constant_time_compare(password_hash, creds->expected_password_hash, 64);
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) { memset(decoded, 0, sizeof(decoded));
size_t input_len = strlen(input); memset(password_hash, 0, sizeof(password_hash));
size_t output_idx = 0;
for (size_t i = 0; i < input_len && output_idx < output_size - 1; i += 4) { if (!username_match || !hash_match) {
int v[4] = {0, 0, 0, 0}; if (error_msg && error_size > 0) {
int pad = 0; strncpy(error_msg, "Invalid username or password", error_size - 1);
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;
}
} }
return 0;
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 1;
return output_idx;
} }
int auth_check_basic_auth(const char *auth_header, char *error_msg, size_t error_size) { int auth_check_basic_auth(const char *auth_header, char *error_msg, size_t error_size) {
if (!g_auth_enabled) return 1; 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) { auth_credentials_t creds = {
if (error_msg && error_size > 0) { .expected_username = g_dashboard_username,
strncpy(error_msg, "Invalid authentication method", error_size - 1); .expected_password_hash = g_dashboard_password_hash
} };
return 0;
}
char decoded[512]; return auth_parse_and_verify(auth_header, &creds, error_msg, error_size);
if (base64_decode(auth_header + 6, decoded, sizeof(decoded)) < 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;
if (!auth_check_credentials(username, password)) {
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) { 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 (!route || !route->use_auth) return 1;
if (!auth_header) { auth_credentials_t creds = {
if (error_msg && error_size > 0) { .expected_username = route->username,
strncpy(error_msg, "Authentication required", error_size - 1); .expected_password_hash = route->password_hash
} };
return 0;
}
if (strncmp(auth_header, "Basic ", 6) != 0) { return auth_parse_and_verify(auth_header, &creds, error_msg, error_size);
if (error_msg && error_size > 0) {
strncpy(error_msg, "Invalid authentication method", error_size - 1);
}
return 0;
}
char decoded[512];
if (base64_decode(auth_header + 6, decoded, sizeof(decoded)) < 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;
if (strcmp(username, route->username) != 0) {
if (error_msg && error_size > 0) {
strncpy(error_msg, "Invalid username or password", error_size - 1);
}
return 0;
}
char password_hash[256];
compute_sha256(password, password_hash, sizeof(password_hash));
if (strcmp(password_hash, route->password_hash) != 0) {
if (error_msg && error_size > 0) {
strncpy(error_msg, "Invalid username or password", error_size - 1);
}
return 0;
}
return 1;
} }
Regular → Executable
View File
+235
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
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
View File
File diff suppressed because it is too large Load Diff
+813
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
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
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
Regular → Executable
View File
Regular → Executable
View File
+141
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
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
+81 -364
View File
@@ -1,119 +1,88 @@
// retoor <retoor@molodetz.nl>
#include "config.h" #include "config.h"
#include "config_parser.h"
#include "logging.h" #include "logging.h"
#include "../cJSON.h" #include "../cJSON.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <ctype.h>
#include <openssl/evp.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <stdatomic.h> #include <stdatomic.h>
static time_t config_file_mtime = 0; 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; app_config_t *config = NULL;
static app_config_t *stale_configs_head = NULL;
static int is_valid_hostname(const char *hostname) { static app_config_t *config_create_from_json(cJSON *root) {
if (!hostname || strlen(hostname) == 0 || strlen(hostname) > 253) return 0; 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;
const char *p = hostname; cJSON *port_item = cJSON_GetObjectItem(root, "port");
int label_len = 0; new_config->port = cJSON_IsNumber(port_item) ? port_item->valueint : 8080;
while (*p) { if (new_config->port < 1 || new_config->port > 65535) {
char c = *p; log_error("Invalid port number: %d", new_config->port);
if (c == '.') { free(new_config);
if (label_len == 0) return 0; return NULL;
label_len = 0; }
} else if (isalnum((unsigned char)c) || c == '-' || c == '_') {
label_len++; cJSON *proxy_array = cJSON_GetObjectItem(root, "reverse_proxy");
if (label_len > 63) return 0; if (!cJSON_IsArray(proxy_array)) {
} else { log_error("No reverse_proxy array found");
return 0; 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++;
} }
p++; index++;
} }
return 1; if (valid_routes == 0) {
} log_error("No valid routes parsed");
free(new_config->routes);
static int is_valid_ip(const char *ip) { free(new_config);
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; return NULL;
} }
if (fseek(f, 0, SEEK_SET) != 0) { new_config->route_count = valid_routes;
fclose(f); return new_config;
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) { int config_load(const char *filename) {
log_info("Loading configuration from %s", filename); log_info("Loading configuration from %s", filename);
char *json_string = read_file_to_string(filename);
char *json_string = config_read_file(filename);
if (!json_string) { if (!json_string) {
log_error("Could not read config file"); log_error("Could not read config file");
return 0; return 0;
@@ -122,150 +91,24 @@ int config_load(const char *filename) {
cJSON *root = cJSON_Parse(json_string); cJSON *root = cJSON_Parse(json_string);
if (!root) { if (!root) {
const char *error_ptr = cJSON_GetErrorPtr(); const char *error_ptr = cJSON_GetErrorPtr();
fprintf(stderr, "JSON parse error: %s\n", error_ptr ? error_ptr : "unknown"); log_error("JSON parse error: %s", error_ptr ? error_ptr : "unknown");
free(json_string); free(json_string);
return 0; return 0;
} }
free(json_string); free(json_string);
app_config_t *new_config = calloc(1, sizeof(app_config_t)); app_config_t *new_config = config_create_from_json(root);
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); cJSON_Delete(root);
if (!new_config) {
return 0;
}
struct stat st;
if (stat(filename, &st) == 0) {
config_file_mtime = st.st_mtime;
}
if (config) { if (config) {
config_ref_dec(config); config_ref_dec(config);
} }
@@ -298,14 +141,6 @@ void config_free(void) {
config_ref_dec(config); config_ref_dec(config);
config = NULL; 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) { void config_create_default(const char *filename) {
@@ -379,17 +214,9 @@ int config_check_file_changed(const char *filename) {
int config_hot_reload(const char *filename) { int config_hot_reload(const char *filename) {
log_info("Hot-reloading configuration from %s", filename); log_info("Hot-reloading configuration from %s", filename);
app_config_t *new_config = calloc(1, sizeof(app_config_t)); char *json_string = config_read_file(filename);
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) { if (!json_string) {
log_error("Hot-reload: Could not read config file"); log_error("Hot-reload: Could not read config file");
free(new_config);
return 0; return 0;
} }
@@ -398,137 +225,27 @@ int config_hot_reload(const char *filename) {
const char *error_ptr = cJSON_GetErrorPtr(); const char *error_ptr = cJSON_GetErrorPtr();
log_error("Hot-reload: JSON parse error: %s", error_ptr ? error_ptr : "unknown"); log_error("Hot-reload: JSON parse error: %s", error_ptr ? error_ptr : "unknown");
free(json_string); free(json_string);
free(new_config);
return 0; return 0;
} }
free(json_string); free(json_string);
cJSON *port_item = cJSON_GetObjectItem(root, "port"); app_config_t *new_config = config_create_from_json(root);
new_config->port = cJSON_IsNumber(port_item) ? port_item->valueint : 8080; cJSON_Delete(root);
if (new_config->port < 1 || new_config->port > 65535) { if (!new_config) {
log_error("Hot-reload: Invalid port number: %d", new_config->port);
cJSON_Delete(root);
free(new_config);
return 0; return 0;
} }
cJSON *proxy_array = cJSON_GetObjectItem(root, "reverse_proxy"); struct stat st;
if (cJSON_IsArray(proxy_array)) { if (stat(filename, &st) == 0) {
new_config->route_count = cJSON_GetArraySize(proxy_array); config_file_mtime = st.st_mtime;
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); if (config) {
config_ref_dec(config);
app_config_t *old_config = config; }
config = new_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); log_info("Hot-reload complete: %d routes loaded", new_config->route_count);
return 1; return 1;
} }
Regular → Executable
View File
+198
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
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
+353 -872
View File
File diff suppressed because it is too large Load Diff
Regular → Executable
+4
View File
@@ -6,7 +6,9 @@
extern connection_t connections[MAX_FDS]; extern connection_t connections[MAX_FDS];
extern int epoll_fd; extern int epoll_fd;
extern time_t cached_time;
void connection_update_cached_time(void);
void connection_init_all(void); void connection_init_all(void);
void connection_setup_listener(int port); void connection_setup_listener(int port);
void connection_accept(int listener_fd); void connection_accept(int listener_fd);
@@ -16,6 +18,8 @@ void connection_cleanup_idle(void);
int connection_set_non_blocking(int fd); int connection_set_non_blocking(int fd);
void connection_set_tcp_keepalive(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_add_to_epoll(int fd, uint32_t events);
void connection_modify_epoll(int fd, uint32_t events); void connection_modify_epoll(int fd, uint32_t events);
Regular → Executable
+212 -257
View File
@@ -19,275 +19,122 @@ static const char *DASHBOARD_HTML =
" <style>\n" " <style>\n"
" * { margin: 0; padding: 0; box-sizing: border-box; }\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" " 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(150px, 1fr)); margin-bottom: 30px; gap: 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: #000; border-radius: 8px; padding: 15px; }\n" " .metric { text-align: center; background: #111; border-radius: 8px; padding: 12px; }\n"
" .metric-value { font-size: 36px; font-weight: 300; }\n" " .metric-value { font-size: 28px; font-weight: 300; }\n"
" .metric-label { font-size: 14px; opacity: 0.7; text-transform: uppercase; margin-top: 5px; }\n" " .metric-label { font-size: 11px; opacity: 0.7; text-transform: uppercase; margin-top: 4px; }\n"
" .chart-container { background: #000; border-radius: 8px; padding: 20px; margin-bottom: 20px; height: 250px; position: relative; }\n" " .metric-value.health { color: #2ecc71; }\n"
" .chart-title { position: absolute; top: 10px; left: 20px; font-size: 14px; opacity: 0.7; z-index: 10; }\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" " canvas { width: 100% !important; height: 100% !important; }\n"
" .process-table { background: #000; border-radius: 8px; padding: 20px; }\n" " .process-table { background: #111; border-radius: 8px; padding: 20px; overflow-x: auto; }\n"
" table { width: 100%; border-collapse: collapse; }\n" " table { width: 100%; border-collapse: collapse; font-size: 13px; }\n"
" th, td { padding: 10px; text-align: left; border-bottom: 1px solid #2a2e3e; }\n" " th, td { padding: 8px 10px; text-align: left; border-bottom: 1px solid #2a2e3e; }\n"
" th { font-weight: 500; opacity: 0.7; }\n" " th { font-weight: 500; opacity: 0.7; white-space: nowrap; }\n"
" .legend { position: absolute; top: 10px; right: 20px; display: flex; gap: 20px; font-size: 12px; z-index: 10; }\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: 5px; }\n" " .legend-item { display: flex; align-items: center; gap: 4px; }\n"
" .legend-color { width: 12px; height: 12px; border-radius: 2px; }\n" " .legend-color { width: 10px; height: 10px; border-radius: 2px; }\n"
" .vhost-chart-container { height: 200px; background: #000; border-radius: 8px; position: relative; padding: 20px; margin-bottom: 10px; }\n" " .vhost-chart-container { height: 180px; background: #111; border-radius: 8px; position: relative; padding: 15px; margin-bottom: 10px; }\n"
" .load-values { display: flex; gap: 10px; font-size: 12px; opacity: 0.8; }\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" " </style>\n"
" <script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>\n" " <script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>\n"
"</head>\n" "</head>\n"
"<body>\n" "<body>\n"
" <div class=\"header\">\n" " <div class=\"header\">\n"
" <div class=\"metric\">\n" " <div class=\"metric\"><div class=\"metric-value health\" id=\"healthScore\">100</div><div class=\"metric-label\">Health</div></div>\n"
" <div class=\"metric-value\" id=\"connections\">0</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-label\">Connections</div>\n" " <div class=\"metric\"><div class=\"metric-value\" id=\"p50\">0</div><div class=\"metric-label\">p50 ms</div></div>\n"
" </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\">\n" " <div class=\"metric\"><div class=\"metric-value\" id=\"errorRate\">0%</div><div class=\"metric-label\">Errors</div></div>\n"
" <div class=\"metric-value\" id=\"memory\">0</div>\n" " <div class=\"metric\"><div class=\"metric-value\" id=\"connections\">0</div><div class=\"metric-label\">Conns</div></div>\n"
" <div class=\"metric-label\">Memory</div>\n" " <div class=\"metric\"><div class=\"metric-value\" id=\"cpu\">0</div><div class=\"metric-label\">CPU</div></div>\n"
" </div>\n" " <div class=\"metric\"><div class=\"metric-value\" id=\"memory\">0</div><div class=\"metric-label\">Memory</div></div>\n"
" <div class=\"metric\">\n" " <div class=\"metric\"><div class=\"metric-value\" id=\"uptime\">0s</div><div class=\"metric-label\">Uptime</div></div>\n"
" <div class=\"metric-value\" id=\"cpu\">0</div>\n"
" <div class=\"metric-label\">CPU %</div>\n"
" </div>\n"
" <div class=\"metric\">\n"
" <div class=\"metric-value\" id=\"load1\">0.00</div>\n"
" <div class=\"metric-label\">Load 1m</div>\n"
" </div>\n"
" <div class=\"metric\">\n"
" <div class=\"metric-value\" id=\"load5\">0.00</div>\n"
" <div class=\"metric-label\">Load 5m</div>\n"
" </div>\n"
" <div class=\"metric\">\n"
" <div class=\"metric-value\" id=\"load15\">0.00</div>\n"
" <div class=\"metric-label\">Load 15m</div>\n"
" </div>\n"
" </div>\n" " </div>\n"
"\n" " <div class=\"charts-grid\">\n"
" <div class=\"chart-container\">\n" " <div class=\"chart-container\"><div class=\"chart-title\">Latency Distribution</div><canvas id=\"latencyHistChart\"></canvas></div>\n"
" <div class=\"chart-title\">CPU Usage</div>\n" " <div class=\"chart-container\"><div class=\"chart-title\">Status Codes</div><canvas id=\"statusChart\"></canvas></div>\n"
" <canvas id=\"cpuChart\"></canvas>\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>\n"
"\n" " <div class=\"charts-grid\">\n"
" <div class=\"chart-container\">\n" " <div class=\"chart-container\"><div class=\"chart-title\">CPU Usage</div><canvas id=\"cpuChart\"></canvas></div>\n"
" <div class=\"chart-title\">Memory Usage</div>\n" " <div class=\"chart-container\"><div class=\"chart-title\">Memory Usage</div><canvas id=\"memChart\"></canvas></div>\n"
" <canvas id=\"memChart\"></canvas>\n"
" </div>\n" " </div>\n"
"\n" " <div class=\"charts-grid\">\n"
" <div class=\"chart-container\">\n" " <div class=\"chart-container\"><div class=\"chart-title\">Network I/O</div>\n"
" <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"
" <div class=\"legend\">\n" " <canvas id=\"netChart\"></canvas></div>\n"
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #3498db\"></div><span>RX</span></div>\n" " <div class=\"chart-container\"><div class=\"chart-title\">Disk I/O</div>\n"
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #2ecc71\"></div><span>TX</span></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"
" </div>\n" " <canvas id=\"diskChart\"></canvas></div>\n"
" <canvas id=\"netChart\"></canvas>\n"
" </div>\n" " </div>\n"
"\n" " <div class=\"chart-container tall\" style=\"margin-bottom:20px;\"><div class=\"chart-title\">Load Average</div>\n"
" <div class=\"chart-container\">\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"
" <div class=\"chart-title\">Disk I/O</div>\n" " <canvas id=\"loadChart\"></canvas></div>\n"
" <div class=\"legend\">\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"
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #9b59b6\"></div><span>Read</span></div>\n"
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #e67e22\"></div><span>Write</span></div>\n"
" </div>\n"
" <canvas id=\"diskChart\"></canvas>\n"
" </div>\n"
"\n"
" <div class=\"chart-container\">\n"
" <div class=\"chart-title\">Load Average</div>\n"
" <div class=\"legend\">\n"
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #e74c3c\"></div><span>1 min</span></div>\n"
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #f39c12\"></div><span>5 min</span></div>\n"
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #3498db\"></div><span>15 min</span></div>\n"
" </div>\n"
" <canvas id=\"loadChart\"></canvas>\n"
" </div>\n"
"\n"
" <div class=\"process-table\">\n"
" <table>\n"
" <thead>\n"
" <tr>\n"
" <th>Virtual Host</th>\n"
" <th>HTTP Req</th>\n"
" <th>WS Req</th>\n"
" <th>Total Req</th>\n"
" <th>Avg Resp (ms)</th>\n"
" <th>Sent</th>\n"
" <th>Received</th>\n"
" </tr>\n"
" </thead>\n"
" <tbody id=\"processTable\"></tbody>\n"
" </table>\n"
" </div>\n"
"\n"
" <script>\n" " <script>\n"
" const formatTimeTick = (value) => {\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 seconds = Math.abs(Math.round(value / 1000));\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"
" if (seconds === 0) return 'now';\n" " const formatDisk = v => v>=1024?(v/1024).toFixed(1)+' GB/s':v.toFixed(1)+' MB/s';\n"
" const m = Math.floor(seconds / 60);\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 s = seconds % 60;\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"
" return `-${m > 0 ? `${m}m ` : ''}${s}s`;\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"
" };\n" " const formatMs = v => { if(v>=1000)return(v/1000).toFixed(1)+'s'; return v.toFixed(0)+'ms'; };\n"
"\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 formatSizeTick = (value) => {\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"
" if (value >= 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(1)} GB/s`;\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"
" if (value >= 1024) return `${(value / 1024).toFixed(1)} MB/s`;\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"
" return `${value.toFixed(0)} KB/s`;\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"
" };\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"
"\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 formatDiskTick = (value) => {\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"
" if (value >= 1024) return `${(value / 1024).toFixed(1)} GB/s`;\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"
" return `${value.toFixed(1)} MB/s`;\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"
" };\n" " window.vhostCharts={}; let prevNames=[];\n"
"\n" " async function updateStats(){\n"
" const formatBytes = (bytes) => {\n" " try{\n"
" if (bytes === 0) return '0 B';\n" " const r=await fetch('/rproxy/api/stats'),d=await r.json();\n"
" const k = 1024;\n" " const hs=d.current.health_score||100; document.getElementById('healthScore').textContent=hs.toFixed(0);\n"
" const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];\n" " const hEl=document.getElementById('healthScore'); hEl.className='metric-value '+(hs>=80?'health':hs>=50?'warning':'danger');\n"
" const i = Math.floor(Math.log(bytes) / Math.log(k));\n" " document.getElementById('rps').textContent=formatNum(d.current.requests_per_second||0);\n"
" return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];\n" " document.getElementById('p50').textContent=formatMs(d.latency?.p50_ms||0);\n"
" };\n" " document.getElementById('p99').textContent=formatMs(d.latency?.p99_ms||0);\n"
"\n" " document.getElementById('errorRate').textContent=((d.current.error_rate_1m||0)*100).toFixed(1)+'%';\n"
" const createBaseChartOptions = (historySeconds, yTickCallback) => ({\n" " document.getElementById('connections').textContent=formatNum(d.current.active_connections);\n"
" responsive: true,\n" " document.getElementById('cpu').textContent=d.current.cpu_percent+'%';\n"
" maintainAspectRatio: false,\n" " document.getElementById('memory').textContent=d.current.memory_gb+'GB';\n"
" animation: false,\n" " document.getElementById('uptime').textContent=formatUptime(d.current.uptime_seconds||0);\n"
" layout: { padding: { top: 30 } },\n" " cpuChart.data.datasets[0].data=d.cpu_history; memChart.data.datasets[0].data=d.memory_history;\n"
" interaction: { mode: 'nearest', axis: 'x', intersect: false },\n" " netChart.data.datasets[0].data=d.network_rx_history; netChart.data.datasets[1].data=d.network_tx_history;\n"
" scales: {\n" " diskChart.data.datasets[0].data=d.disk_read_history; diskChart.data.datasets[1].data=d.disk_write_history;\n"
" x: { type: 'linear', display: true, grid: { color: '#2a2e3e' }, ticks: { color: '#666', maxTicksLimit: 7, callback: formatTimeTick }, min: -historySeconds * 1000, max: 0 },\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"
" y: { display: true, grid: { color: '#2a2e3e' }, ticks: { color: '#666', beginAtZero: true, callback: yTickCallback } }\n" " [cpuChart,memChart,netChart,diskChart,loadChart].forEach(c=>c.update('none'));\n"
" },\n" " if(d.latency?.histogram){latencyHistChart.data.labels=d.latency.bucket_labels||[]; latencyHistChart.data.datasets[0].data=d.latency.histogram; latencyHistChart.update('none');}\n"
" plugins: { legend: { display: false }, tooltip: { displayColors: false } },\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"
" elements: { point: { radius: 0 }, line: { borderWidth: 2, tension: 0.4, fill: true } }\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"
" });\n" " if(d.efficiency){efficiencyChart.data.datasets[0].data=[d.efficiency.splice_transfers||0,d.efficiency.buffer_transfers||0]; efficiencyChart.update('none');}\n"
"\n" " const tbody=document.getElementById('processTable'),names=d.processes.map(p=>p.name).join(',');\n"
" const cpuChart = new Chart(document.getElementById('cpuChart'), {\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"
" type: 'line',\n" " d.processes.forEach((p,i)=>{\n"
" data: { datasets: [{ label: 'CPU %', data: [], borderColor: '#f39c12', backgroundColor: 'rgba(243, 156, 18, 0.1)' }] },\n" " let row=tbody.children[i*2]; if(!row){row=document.createElement('tr'); tbody.appendChild(row);}\n"
" options: { ...createBaseChartOptions(300, v => `${v}%`), scales: { ...createBaseChartOptions(300).scales, y: { ...createBaseChartOptions(300).scales.y, max: 100, ticks: { ...createBaseChartOptions(300).scales.y.ticks, callback: v => `${v}%` } } } }\n" " const errP=(p.error_rate*100).toFixed(1); const cls=parseFloat(errP)>5?'status-error':parseFloat(errP)>1?'status-warn':'status-ok';\n"
" });\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"
"\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 memChart = new Chart(document.getElementById('memChart'), {\n" " const cid='vc'+i,cv=document.getElementById(cid); if(!cv)return;\n"
" type: 'line',\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"
" data: { datasets: [{ label: 'Memory GB', data: [], borderColor: '#e74c3c', backgroundColor: 'rgba(231, 76, 60, 0.1)' }] },\n" " else{window.vhostCharts[cid].data.datasets[0].data=p.throughput_history||[]; window.vhostCharts[cid].update('none');}\n"
" options: createBaseChartOptions(300, v => `${v.toFixed(2)} GiB`)\n" " });\n"
" });\n" " }catch(e){console.error('Stats error:',e);}\n"
"\n"
" const netChart = new Chart(document.getElementById('netChart'), {\n"
" type: 'line',\n"
" data: {\n"
" datasets: [\n"
" { label: 'RX KB/s', data: [], borderColor: '#3498db', backgroundColor: 'rgba(52, 152, 219, 0.1)' },\n"
" { label: 'TX KB/s', data: [], borderColor: '#2ecc71', backgroundColor: 'rgba(46, 204, 113, 0.1)' }\n"
" ]\n"
" },\n"
" options: createBaseChartOptions(300, formatSizeTick)\n"
" });\n"
"\n"
" const diskChart = new Chart(document.getElementById('diskChart'), {\n"
" type: 'line',\n"
" data: {\n"
" datasets: [\n"
" { label: 'Read MB/s', data: [], borderColor: '#9b59b6', backgroundColor: 'rgba(155, 89, 182, 0.1)' },\n"
" { label: 'Write MB/s', data: [], borderColor: '#e67e22', backgroundColor: 'rgba(230, 126, 34, 0.1)' }\n"
" ]\n"
" },\n"
" options: createBaseChartOptions(300, formatDiskTick)\n"
" });\n"
"\n"
" const loadChart = new Chart(document.getElementById('loadChart'), {\n"
" type: 'line',\n"
" data: {\n"
" datasets: [\n"
" { label: 'Load 1m', data: [], borderColor: '#e74c3c', backgroundColor: 'rgba(231, 76, 60, 0.1)' },\n"
" { label: 'Load 5m', data: [], borderColor: '#f39c12', backgroundColor: 'rgba(243, 156, 18, 0.1)' },\n"
" { label: 'Load 15m', data: [], borderColor: '#3498db', backgroundColor: 'rgba(52, 152, 219, 0.1)' }\n"
" ]\n"
" },\n"
" options: createBaseChartOptions(300, v => v.toFixed(2))\n"
" });\n"
"\n"
" window.vhostCharts = {};\n"
" let prevProcessNames = [];\n"
"\n"
" async function updateStats() {\n"
" try {\n"
" const response = await fetch('/rproxy/api/stats');\n"
" const data = await response.json();\n"
"\n"
" document.getElementById('connections').textContent = data.current.active_connections;\n"
" document.getElementById('memory').textContent = data.current.memory_gb + ' GiB';\n"
" document.getElementById('cpu').textContent = data.current.cpu_percent + '%';\n"
" document.getElementById('load1').textContent = data.current.load_1m.toFixed(2);\n"
" document.getElementById('load5').textContent = data.current.load_5m.toFixed(2);\n"
" document.getElementById('load15').textContent = data.current.load_15m.toFixed(2);\n"
"\n"
" cpuChart.data.datasets[0].data = data.cpu_history;\n"
" memChart.data.datasets[0].data = data.memory_history;\n"
" netChart.data.datasets[0].data = data.network_rx_history;\n"
" netChart.data.datasets[1].data = data.network_tx_history;\n"
" diskChart.data.datasets[0].data = data.disk_read_history;\n"
" diskChart.data.datasets[1].data = data.disk_write_history;\n"
" loadChart.data.datasets[0].data = data.load1_history;\n"
" loadChart.data.datasets[1].data = data.load5_history;\n"
" loadChart.data.datasets[2].data = data.load15_history;\n"
"\n"
" cpuChart.update('none');\n"
" memChart.update('none');\n"
" netChart.update('none');\n"
" diskChart.update('none');\n"
" loadChart.update('none');\n"
"\n"
" const tbody = document.getElementById('processTable');\n"
" const processNames = data.processes.map(p => p.name).join(',');\n"
" if (processNames !== prevProcessNames.join(',')) {\n"
" Object.values(window.vhostCharts).forEach(chart => chart.destroy());\n"
" window.vhostCharts = {};\n"
" tbody.innerHTML = '';\n"
" data.processes.forEach((p, index) => {\n"
" const colors = ['#3498db', '#2ecc71', '#f39c12', '#e74c3c', '#9b59b6', '#1abc9c'];\n"
" const chartColor = colors[index % colors.length];\n"
" const mainRow = document.createElement('tr');\n"
" mainRow.innerHTML = `<td style='color: ${chartColor}'>${p.name}</td><td>${p.http_requests}</td><td>${p.websocket_requests}</td><td>${p.total_requests}</td><td>${p.avg_request_time_ms.toFixed(2)}</td><td>${formatBytes(p.bytes_sent)}</td><td>${formatBytes(p.bytes_recv)}</td>`;\n"
" tbody.appendChild(mainRow);\n"
" \n"
" const chartRow = document.createElement('tr');\n"
" chartRow.innerHTML = `<td colspan='7' style='padding: 10px 0; border: none;'><div class='vhost-chart-container'><div class='chart-title'>Live Throughput - ${p.name}</div><canvas id='vhostChart${index}'></canvas></div></td>`;\n"
" tbody.appendChild(chartRow);\n"
" });\n"
" prevProcessNames = data.processes.map(p => p.name);\n"
" }\n"
"\n"
" data.processes.forEach((p, index) => {\n"
" const chartId = `vhostChart${index}`;\n"
" const canvas = document.getElementById(chartId);\n"
" if (!canvas) return;\n"
" if (!window.vhostCharts[chartId]) {\n"
" const colors = [\n"
" { border: '#3498db', bg: 'rgba(52, 152, 219, 0.1)' }, { border: '#2ecc71', bg: 'rgba(46, 204, 113, 0.1)' },\n"
" { border: '#f39c12', bg: 'rgba(243, 156, 18, 0.1)' }, { border: '#e74c3c', bg: 'rgba(231, 76, 60, 0.1)' },\n"
" { border: '#9b59b6', bg: 'rgba(155, 89, 182, 0.1)' }, { border: '#1abc9c', bg: 'rgba(26, 188, 156, 0.1)' }\n"
" ];\n"
" const color = colors[index % colors.length];\n"
" window.vhostCharts[chartId] = new Chart(canvas.getContext('2d'), {\n"
" type: 'line',\n"
" data: { datasets: [{ label: 'Throughput KB/s', data: p.throughput_history || [], borderColor: color.border, backgroundColor: color.bg }] },\n"
" options: createBaseChartOptions(60, formatSizeTick)\n"
" });\n"
" } else {\n"
" window.vhostCharts[chartId].data.datasets[0].data = p.throughput_history || [];\n"
" window.vhostCharts[chartId].update('none');\n"
" }\n"
" });\n"
" } catch (e) {\n"
" console.error('Failed to fetch stats:', e);\n"
" }\n"
" }\n" " }\n"
"\n" " updateStats(); setInterval(updateStats,1000);\n"
" updateStats();\n"
" setInterval(updateStats, 1000);\n"
" </script>\n" " </script>\n"
"</body>\n" "</body>\n"
"</html>\n"; "</html>\n";
@@ -497,6 +344,91 @@ void dashboard_serve_stats_api(connection_t *conn, const char *request_data, siz
cJSON_AddNumberToObject(current, "load_5m", load5); cJSON_AddNumberToObject(current, "load_5m", load5);
cJSON_AddNumberToObject(current, "load_15m", load15); 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, "cpu_history", format_history(&monitor.cpu_history, HISTORY_SECONDS));
cJSON_AddItemToObject(root, "memory_history", format_history(&monitor.memory_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_rx_history", format_network_history(&monitor.network_history, HISTORY_SECONDS, "rx_kbps"));
@@ -522,6 +454,29 @@ void dashboard_serve_stats_api(connection_t *conn, const char *request_data, siz
cJSON_AddNumberToObject(p, "bytes_sent", s->bytes_sent); cJSON_AddNumberToObject(p, "bytes_sent", s->bytes_sent);
cJSON_AddNumberToObject(p, "bytes_recv", s->bytes_recv); cJSON_AddNumberToObject(p, "bytes_recv", s->bytes_recv);
cJSON_AddItemToObject(p, "throughput_history", format_history(&s->throughput_history, 60)); 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); cJSON_AddItemToArray(processes, p);
} }
} }
Regular → Executable
View File
+108
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
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
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
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
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
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
Regular → Executable
View File
Regular → Executable
View File
+69
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
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
Regular → Executable
+93
View File
@@ -283,3 +283,96 @@ int http_find_header_line_bounds(const char* data, size_t len, const char* name,
*line_end = NULL; *line_end = NULL;
return 0; 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;
}
Regular → Executable
+3
View File
@@ -12,5 +12,8 @@ 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_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_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_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 #endif
+121
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
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
Regular → Executable
+1 -1
View File
@@ -32,7 +32,7 @@ static void rotate_log_file(void) {
g_log_file = NULL; g_log_file = NULL;
} }
char old_path[520], new_path[520]; char old_path[536], new_path[536];
snprintf(old_path, sizeof(old_path), "%s.%d", g_log_path, LOG_MAX_ROTATIONS); snprintf(old_path, sizeof(old_path), "%s.%d", g_log_path, LOG_MAX_ROTATIONS);
unlink(old_path); unlink(old_path);
Regular → Executable
View File
Regular → Executable
+13 -4
View File
@@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
#ifndef _GNU_SOURCE #ifndef _GNU_SOURCE
#define _GNU_SOURCE #define _GNU_SOURCE
#endif #endif
@@ -7,6 +9,7 @@
#include <errno.h> #include <errno.h>
#include <sys/epoll.h> #include <sys/epoll.h>
#include <unistd.h> #include <unistd.h>
#include <string.h>
#include "types.h" #include "types.h"
#include "logging.h" #include "logging.h"
@@ -14,10 +17,15 @@
#include "monitor.h" #include "monitor.h"
#include "ssl_handler.h" #include "ssl_handler.h"
#include "connection.h" #include "connection.h"
#include "epoll_utils.h"
#include "rate_limit.h" #include "rate_limit.h"
#include "auth.h" #include "auth.h"
#include "health_check.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_shutdown = 0;
static volatile sig_atomic_t g_reload_config = 0; static volatile sig_atomic_t g_reload_config = 0;
static const char *g_config_file = NULL; static const char *g_config_file = NULL;
@@ -162,9 +170,8 @@ int main(int argc, char *argv[]) {
health_check_init(); health_check_init();
epoll_fd = epoll_create1(EPOLL_CLOEXEC); if (epoll_utils_create() == -1) {
if (epoll_fd == -1) { log_error("epoll_create failed");
log_error("epoll_create1 failed");
return 1; return 1;
} }
@@ -196,11 +203,13 @@ int main(int argc, char *argv[]) {
break; break;
} }
connection_update_cached_time();
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
connection_handle_event(&events[i]); connection_handle_event(&events[i]);
} }
time_t current_time = time(NULL); time_t current_time = cached_time;
if (current_time > last_monitor_update) { if (current_time > last_monitor_update) {
monitor_update(); monitor_update();
+322 -453
View File
@@ -1,156 +1,31 @@
// retoor <retoor@molodetz.nl>
#include "monitor.h" #include "monitor.h"
#include "histogram.h"
#include "deque.h"
#include "rate_tracker.h"
#include "stats_collector.h"
#include "logging.h" #include "logging.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/sysinfo.h> #include <time.h>
#include <math.h> #include <math.h>
system_monitor_t monitor; system_monitor_t monitor = {0};
void history_deque_init(history_deque_t *dq, int capacity) { static double get_current_time_seconds(void) {
dq->points = calloc(capacity, sizeof(history_point_t)); struct timespec ts;
dq->capacity = capacity; clock_gettime(CLOCK_REALTIME, &ts);
dq->head = 0; return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
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) { void monitor_init(const char *db_file) {
memset(&monitor, 0, sizeof(system_monitor_t)); memset(&monitor, 0, sizeof(monitor));
monitor.start_time = time(NULL); 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.cpu_history, HISTORY_SECONDS);
history_deque_init(&monitor.memory_history, HISTORY_SECONDS); history_deque_init(&monitor.memory_history, HISTORY_SECONDS);
@@ -161,355 +36,129 @@ void monitor_init(const char *db_file) {
history_deque_init(&monitor.load5_history, HISTORY_SECONDS); history_deque_init(&monitor.load5_history, HISTORY_SECONDS);
history_deque_init(&monitor.load15_history, HISTORY_SECONDS); history_deque_init(&monitor.load15_history, HISTORY_SECONDS);
if (sqlite3_open(db_file, &monitor.db) != SQLITE_OK) { histogram_init(&monitor.global_latency);
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(monitor.db)); histogram_init(&monitor.connection_lifetime);
if (monitor.db) { rate_tracker_init(&monitor.global_rps);
sqlite3_close(monitor.db);
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; 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);
}
} }
} else {
init_db();
load_stats_from_db();
} }
monitor_update();
log_info("Monitor initialized");
} }
void monitor_cleanup(void) { void monitor_cleanup(void) {
if (monitor.db) { history_deque_free(&monitor.cpu_history);
sqlite3_close(monitor.db); history_deque_free(&monitor.memory_history);
monitor.db = NULL; 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; vhost_stats_t *current = monitor.vhost_stats_head;
while (current) { while (current) {
vhost_stats_t *next = current->next; vhost_stats_t *next = current->next;
if (current->throughput_history.points) free(current->throughput_history.points); history_deque_free(&current->throughput_history);
if (current->request_times.times) free(current->request_times.times); request_time_deque_free(&current->request_times);
free(current); free(current);
current = next; current = next;
} }
monitor.vhost_stats_head = NULL; monitor.vhost_stats_head = NULL;
if (monitor.cpu_history.points) { free(monitor.cpu_history.points); monitor.cpu_history.points = NULL; } if (monitor.db) {
if (monitor.memory_history.points) { free(monitor.memory_history.points); monitor.memory_history.points = NULL; } sqlite3_close(monitor.db);
if (monitor.network_history.points) { free(monitor.network_history.points); monitor.network_history.points = NULL; } monitor.db = 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; } log_info("Monitor cleanup complete");
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) { vhost_stats_t *monitor_get_or_create_vhost_stats(const char *vhost_name) {
if (!monitor.db) return; if (!vhost_name || vhost_name[0] == '\0') return NULL;
double cutoff = (double)time(NULL) - DATA_RETENTION_SECONDS; for (vhost_stats_t *s = monitor.vhost_stats_head; s; s = s->next) {
sqlite3_stmt *stmt; if (strcmp(s->vhost_name, vhost_name) == 0) {
const char *sql = "DELETE FROM vhost_stats WHERE timestamp < ?;"; return s;
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; vhost_stats_t *stats = calloc(1, sizeof(vhost_stats_t));
if (current_time - last_db_save >= 10) { if (!stats) return NULL;
save_stats_to_db();
last_db_save = current_time;
}
}
vhost_stats_t* monitor_get_or_create_vhost_stats(const char *vhost_name) { strncpy(stats->vhost_name, vhost_name, sizeof(stats->vhost_name) - 1);
if (!vhost_name || strlen(vhost_name) == 0) return NULL; stats->vhost_name[sizeof(stats->vhost_name) - 1] = '\0';
for (vhost_stats_t *curr = monitor.vhost_stats_head; curr; curr = curr->next) { history_deque_init(&stats->throughput_history, 60);
if (strcmp(curr->vhost_name, vhost_name) == 0) { request_time_deque_init(&stats->request_times, 1000);
return curr; 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);
vhost_stats_t *new_stats = calloc(1, sizeof(vhost_stats_t)); stats->next = monitor.vhost_stats_head;
if (!new_stats) { monitor.vhost_stats_head = stats;
return NULL;
}
strncpy(new_stats->vhost_name, vhost_name, sizeof(new_stats->vhost_name) - 1); return stats;
new_stats->last_update = time(NULL);
history_deque_init(&new_stats->throughput_history, 60);
request_time_deque_init(&new_stats->request_times, 100);
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) { void monitor_record_request_start(vhost_stats_t *stats, int is_websocket) {
if (!stats) return; if (!stats) return;
stats->total_requests++;
if (is_websocket) { if (is_websocket) {
stats->websocket_requests++; stats->websocket_requests++;
} else { } else {
stats->http_requests++; 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) { void monitor_record_request_end(vhost_stats_t *stats, double start_time) {
if (!stats || start_time <= 0) return; if (!stats) return;
struct timespec end_time; double now = get_current_time_seconds();
clock_gettime(CLOCK_MONOTONIC, &end_time); double duration_ms = (now - start_time) * 1000.0;
double duration_ms = ((end_time.tv_sec + end_time.tv_nsec / 1e9) - start_time) * 1000.0;
if (duration_ms >= 0 && duration_ms < 60000) { histogram_add(&stats->latency_histogram, duration_ms);
request_time_deque_push(&stats->request_times, 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;
} }
} }
@@ -518,3 +167,223 @@ void monitor_record_bytes(vhost_stats_t *stats, long long sent, long long recv)
stats->bytes_sent += sent; stats->bytes_sent += sent;
stats->bytes_recv += recv; 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));
}
Regular → Executable
+37
View File
@@ -22,4 +22,41 @@ void disk_history_deque_push(disk_history_deque_t *dq, double time, double read_
void request_time_deque_init(request_time_deque_t *dq, int capacity); 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_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 #endif
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
+2 -1
View File
@@ -74,7 +74,8 @@ int rate_limit_check(const char *client_ip) {
rate_limit_entry_t *new_entry = calloc(1, sizeof(rate_limit_entry_t)); rate_limit_entry_t *new_entry = calloc(1, sizeof(rate_limit_entry_t));
if (!new_entry) { if (!new_entry) {
return 1; 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); strncpy(new_entry->client_ip, client_ip, sizeof(new_entry->client_ip) - 1);
Regular → Executable
View File
+55
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
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
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
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
Regular → Executable
+22 -1
View File
@@ -2,11 +2,12 @@
#include "logging.h" #include "logging.h"
#include <openssl/err.h> #include <openssl/err.h>
#include <openssl/x509_vfy.h> #include <openssl/x509_vfy.h>
#include <openssl/x509v3.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
SSL_CTX *ssl_ctx = NULL; SSL_CTX *ssl_ctx = NULL;
static int g_ssl_verify_enabled = 0; static int g_ssl_verify_enabled = 1;
static char g_ca_file[512] = ""; static char g_ca_file[512] = "";
static char g_ca_path[512] = ""; static char g_ca_path[512] = "";
@@ -63,6 +64,26 @@ void ssl_init(void) {
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_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); 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) { void ssl_cleanup(void) {
Regular → Executable
+1
View File
@@ -10,6 +10,7 @@ void ssl_set_ca_file(const char *path);
void ssl_set_ca_path(const char *path); void ssl_set_ca_path(const char *path);
void ssl_init(void); void ssl_init(void);
void ssl_cleanup(void); void ssl_cleanup(void);
int ssl_set_hostname(SSL *ssl, const char *hostname);
int ssl_do_handshake(connection_t *conn); int ssl_do_handshake(connection_t *conn);
int ssl_read(connection_t *conn, char *buf, size_t len); int ssl_read(connection_t *conn, char *buf, size_t len);
int ssl_write(connection_t *conn, const char *buf, size_t len); int ssl_write(connection_t *conn, const char *buf, size_t len);
+138
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
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
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
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
Regular → Executable
+102
View File
@@ -6,6 +6,7 @@
#endif #endif
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h> #include <stddef.h>
#include <stdint.h>
#include <time.h> #include <time.h>
#include <openssl/ssl.h> #include <openssl/ssl.h>
#include <sqlite3.h> #include <sqlite3.h>
@@ -34,6 +35,9 @@
#define SSL_HANDSHAKE_TIMEOUT_SEC 10 #define SSL_HANDSHAKE_TIMEOUT_SEC 10
#define MAX_CONNECTIONS_PER_IP 100 #define MAX_CONNECTIONS_PER_IP 100
#define HOSTNAME_MAX_LEN 256 #define HOSTNAME_MAX_LEN 256
#define HISTOGRAM_BUCKETS 16
#define RATE_TRACKER_SLOTS 60
#define HTTP_METHOD_COUNT 8
typedef enum { typedef enum {
CONN_TYPE_UNUSED, CONN_TYPE_UNUSED,
@@ -50,6 +54,45 @@ typedef enum {
CLIENT_STATE_CLOSING CLIENT_STATE_CLOSING
} client_state_t; } 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 { typedef struct {
char *data; char *data;
size_t capacity; size_t capacity;
@@ -79,6 +122,7 @@ typedef struct connection_s {
conn_type_t type; conn_type_t type;
client_state_t state; client_state_t state;
int fd; int fd;
char client_ip[64];
struct connection_s *pair; struct connection_s *pair;
struct vhost_stats_s *vhost_stats; struct vhost_stats_s *vhost_stats;
buffer_t read_buf; buffer_t read_buf;
@@ -99,6 +143,11 @@ typedef struct connection_s {
int response_headers_parsed; int response_headers_parsed;
long original_content_length; long original_content_length;
long content_length_delta; 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; } connection_t;
typedef struct { typedef struct {
@@ -192,6 +241,43 @@ typedef struct vhost_stats_s {
double last_update; double last_update;
history_deque_t throughput_history; history_deque_t throughput_history;
request_time_deque_t request_times; 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; struct vhost_stats_s *next;
} vhost_stats_t; } vhost_stats_t;
@@ -214,6 +300,22 @@ typedef struct {
double last_disk_update_time; double last_disk_update_time;
vhost_stats_t *vhost_stats_head; vhost_stats_t *vhost_stats_head;
sqlite3 *db; 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; } system_monitor_t;
#endif #endif
+280
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
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
Regular → Executable
+75
View File
@@ -85,6 +85,78 @@ void test_auth_route_basic_auth(void) {
TEST_SUITE_END(); 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) { void test_auth_disabled_passthrough(void) {
TEST_SUITE_BEGIN("Auth Disabled Passthrough"); TEST_SUITE_BEGIN("Auth Disabled Passthrough");
@@ -103,5 +175,8 @@ void run_auth_tests(void) {
test_auth_check_credentials(); test_auth_check_credentials();
test_auth_check_basic_auth(); test_auth_check_basic_auth();
test_auth_route_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(); test_auth_disabled_passthrough();
} }
Regular → Executable
+123
View File
@@ -149,6 +149,123 @@ void test_buffer_multiple_operations(void) {
TEST_SUITE_END(); 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) { void run_buffer_tests(void) {
test_buffer_init(); test_buffer_init();
test_buffer_read_write(); test_buffer_read_write();
@@ -156,4 +273,10 @@ void run_buffer_tests(void) {
test_buffer_compact(); test_buffer_compact();
test_buffer_ensure_capacity(); test_buffer_ensure_capacity();
test_buffer_multiple_operations(); 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();
} }
Regular → Executable
View File
Regular → Executable
+2151
View File
File diff suppressed because it is too large Load Diff
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
+52
View File
@@ -182,6 +182,56 @@ void test_http_malformed_requests(void) {
TEST_SUITE_END(); 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) { void run_http_tests(void) {
test_http_parse_get_request(); test_http_parse_get_request();
test_http_parse_post_request(); test_http_parse_post_request();
@@ -193,4 +243,6 @@ void run_http_tests(void) {
test_http_parse_host_with_port(); test_http_parse_host_with_port();
test_http_is_request_start(); test_http_is_request_start();
test_http_malformed_requests(); test_http_malformed_requests();
test_http_uri_normalization();
test_http_internal_route_detection();
} }
Regular → Executable
View File
+277
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();
}
Regular → Executable
+2
View File
@@ -30,6 +30,7 @@ extern void run_dashboard_tests(void);
extern void run_health_check_tests(void); extern void run_health_check_tests(void);
extern void run_ssl_handler_tests(void); extern void run_ssl_handler_tests(void);
extern void run_connection_tests(void); extern void run_connection_tests(void);
extern void run_logging_tests(void);
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
(void)argc; (void)argc;
@@ -54,6 +55,7 @@ int main(int argc, char *argv[]) {
run_health_check_tests(); run_health_check_tests();
run_ssl_handler_tests(); run_ssl_handler_tests();
run_connection_tests(); run_connection_tests();
run_logging_tests();
test_summary(); test_summary();
Regular → Executable
+89
View File
@@ -230,6 +230,91 @@ void test_monitor_update(void) {
TEST_SUITE_END(); 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) { void run_monitor_tests(void) {
test_history_deque_init(); test_history_deque_init();
test_history_deque_push(); test_history_deque_push();
@@ -241,4 +326,8 @@ void run_monitor_tests(void) {
test_monitor_record_request(); test_monitor_record_request();
test_monitor_record_bytes(); test_monitor_record_bytes();
test_monitor_update(); test_monitor_update();
test_monitor_record_request_end();
test_monitor_deque_overflow();
test_monitor_network_deque_overflow();
test_monitor_disk_deque_overflow();
} }
Regular → Executable
+91
View File
@@ -224,6 +224,93 @@ void test_patch_apply_block_rule(void) {
TEST_SUITE_END(); 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) { void run_patch_tests(void) {
test_patch_has_rules(); test_patch_has_rules();
test_patch_check_for_block(); test_patch_check_for_block();
@@ -234,4 +321,8 @@ void run_patch_tests(void) {
test_patch_apply_no_match(); test_patch_apply_no_match();
test_patch_apply_empty_config(); test_patch_apply_empty_config();
test_patch_apply_block_rule(); 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();
} }
Regular → Executable
View File
Regular → Executable
View File
Regular → Executable
+83
View File
@@ -191,6 +191,85 @@ void test_ssl_reinit(void) {
TEST_SUITE_END(); 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) { void run_ssl_handler_tests(void) {
test_ssl_init_cleanup(); test_ssl_init_cleanup();
test_ssl_multiple_init(); test_ssl_multiple_init();
@@ -202,4 +281,8 @@ void run_ssl_handler_tests(void) {
test_ssl_handshake_null(); test_ssl_handshake_null();
test_ssl_read_write_null(); test_ssl_read_write_null();
test_ssl_reinit(); test_ssl_reinit();
test_ssl_handshake_already_done();
test_ssl_read_no_handshake();
test_ssl_write_no_handshake();
test_ssl_verify_with_ca();
} }