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
38 changed files with 5456 additions and 2427 deletions
+16
View File
@@ -13,6 +13,22 @@
## 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 ## Version 0.12.0 - 2026-01-27
The server now rejects HTTP pipelined requests with a 400 Bad Request error. The server now rejects HTTP pipelined requests with a 400 Bad Request error.
+48 -143
View File
@@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
CC = gcc CC = gcc
CFLAGS = -Wall -Wextra -Werror -O3 -march=native -flto=auto -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
@@ -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)))
@@ -50,22 +65,35 @@ 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)))
TEST_TARGET = rproxy_test TEST_TARGET = rproxy_test
MIN_COVERAGE = 69 MIN_COVERAGE = 60
COVERAGE_MODULES = auth.c buffer.c config.c http.c logging.c patch.c rate_limit.c monitor.c dashboard.c health_check.c ssl_handler.c connection.c COVERAGE_MODULES = auth.c buffer.c config.c http.c logging.c patch.c rate_limit.c monitor.c dashboard.c health_check.c ssl_handler.c connection.c
.PHONY: all clean test legacy run coverage coverage-html valgrind .PHONY: all clean test legacy run coverage coverage-html valgrind
@@ -78,94 +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 $@
$(BUILD_DIR)/test_logging.o: $(TESTS_DIR)/test_logging.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)
@@ -182,35 +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) -Isrc -c tests/test_logging.c -o build/test_logging.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 ""
@@ -266,35 +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) -Isrc -c tests/test_logging.c -o build/test_logging.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=$$?; \
Executable → Regular
+36 -101
View File
@@ -1,9 +1,12 @@
// 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> #include <openssl/crypto.h>
static char g_dashboard_username[128] = ""; static char g_dashboard_username[128] = "";
@@ -68,104 +71,13 @@ int auth_check_credentials(const char *username, const char *password) {
return username_match && hash_match; return username_match && hash_match;
} }
static int base64_decode_char(char c) { typedef struct {
if (c >= 'A' && c <= 'Z') return c - 'A'; const char *expected_username;
if (c >= 'a' && c <= 'z') return c - 'a' + 26; const char *expected_password_hash;
if (c >= '0' && c <= '9') return c - '0' + 52; } auth_credentials_t;
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;
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 (!auth_header) {
if (error_msg && error_size > 0) { if (error_msg && error_size > 0) {
strncpy(error_msg, "Authentication required", error_size - 1); strncpy(error_msg, "Authentication required", error_size - 1);
@@ -215,11 +127,11 @@ int auth_check_route_basic_auth(const route_config_t *route, const char *auth_he
compute_sha256(password, password_hash, sizeof(password_hash)); compute_sha256(password, password_hash, sizeof(password_hash));
size_t username_len = strlen(username); size_t username_len = strlen(username);
size_t expected_username_len = strlen(route->username); size_t expected_username_len = strlen(creds->expected_username);
int username_match = (username_len == expected_username_len) && int username_match = (username_len == expected_username_len) &&
constant_time_compare(username, route->username, username_len); constant_time_compare(username, creds->expected_username, username_len);
int hash_match = constant_time_compare(password_hash, route->password_hash, 64); int hash_match = constant_time_compare(password_hash, creds->expected_password_hash, 64);
memset(decoded, 0, sizeof(decoded)); memset(decoded, 0, sizeof(decoded));
memset(password_hash, 0, sizeof(password_hash)); memset(password_hash, 0, sizeof(password_hash));
@@ -233,3 +145,26 @@ int auth_check_route_basic_auth(const route_config_t *route, const char *auth_he
return 1; return 1;
} }
int auth_check_basic_auth(const char *auth_header, char *error_msg, size_t error_size) {
if (!g_auth_enabled) return 1;
auth_credentials_t creds = {
.expected_username = g_dashboard_username,
.expected_password_hash = g_dashboard_password_hash
};
return auth_parse_and_verify(auth_header, &creds, error_msg, error_size);
}
int auth_check_route_basic_auth(const route_config_t *route, const char *auth_header,
char *error_msg, size_t error_size) {
if (!route || !route->use_auth) return 1;
auth_credentials_t creds = {
.expected_username = route->username,
.expected_password_hash = route->password_hash
};
return auth_parse_and_verify(auth_header, &creds, error_msg, error_size);
}
+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
+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
Executable → Regular
+75 -358
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) {
const char *p = hostname; log_error("Failed to allocate memory for config");
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; return NULL;
} }
long length = ftell(f); new_config->ref_count = 1;
if (length < 0 || length > 1024*1024) {
fclose(f); cJSON *port_item = cJSON_GetObjectItem(root, "port");
new_config->port = cJSON_IsNumber(port_item) ? port_item->valueint : 8080;
if (new_config->port < 1 || new_config->port > 65535) {
log_error("Invalid port number: %d", new_config->port);
free(new_config);
return NULL; return NULL;
} }
if (fseek(f, 0, SEEK_SET) != 0) { cJSON *proxy_array = cJSON_GetObjectItem(root, "reverse_proxy");
fclose(f); if (!cJSON_IsArray(proxy_array)) {
log_error("No reverse_proxy array found");
free(new_config);
return NULL; return NULL;
} }
char *buffer = malloc((size_t)length + 1);
if (!buffer) { int total_routes = cJSON_GetArraySize(proxy_array);
fclose(f); if (total_routes <= 0) {
log_error("Empty reverse_proxy array");
free(new_config);
return NULL; return NULL;
} }
size_t read_len = fread(buffer, 1, (size_t)length, f);
buffer[read_len] = '\0'; new_config->routes = calloc((size_t)total_routes, sizeof(route_config_t));
fclose(f); if (!new_config->routes) {
return buffer; log_error("Failed to allocate memory for routes");
free(new_config);
return NULL;
}
int valid_routes = 0;
int index = 0;
cJSON *route_item;
cJSON_ArrayForEach(route_item, proxy_array) {
route_config_t *route = &new_config->routes[valid_routes];
if (config_parse_route(route_item, route, index)) {
log_info("Route: %s -> %s:%d (SSL: %s, Auth: %s, Patches: %d)",
route->hostname, route->upstream_host, route->upstream_port,
route->use_ssl ? "yes" : "no", route->use_auth ? "yes" : "no",
route->patches.rule_count);
valid_routes++;
}
index++;
}
if (valid_routes == 0) {
log_error("No valid routes parsed");
free(new_config->routes);
free(new_config);
return NULL;
}
new_config->route_count = valid_routes;
return new_config;
} }
int config_load(const char *filename) { 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);
cJSON_Delete(root);
if (!new_config) { 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; 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) {
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) { 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;
if (new_config->port < 1 || new_config->port > 65535) {
log_error("Hot-reload: Invalid port number: %d", new_config->port);
cJSON_Delete(root); cJSON_Delete(root);
free(new_config);
if (!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 (config) {
if (!new_config->routes) { config_ref_dec(config);
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; 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;
} }
+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
Executable → Regular
+319 -1150
View File
File diff suppressed because it is too large Load Diff
+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
+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
+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
+10 -3
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;
} }
Executable → Regular
+214 -638
View File
@@ -1,157 +1,30 @@
// 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 = time(NULL); monitor.uptime_start = monitor.start_time;
monitor.health_score = 100.0; monitor.health_score = 100.0;
history_deque_init(&monitor.cpu_history, HISTORY_SECONDS); history_deque_init(&monitor.cpu_history, HISTORY_SECONDS);
@@ -167,367 +40,125 @@ void monitor_init(const char *db_file) {
histogram_init(&monitor.connection_lifetime); histogram_init(&monitor.connection_lifetime);
rate_tracker_init(&monitor.global_rps); rate_tracker_init(&monitor.global_rps);
if (sqlite3_open(db_file, &monitor.db) != SQLITE_OK) { stats_get_network(&monitor.last_net_sent, &monitor.last_net_recv);
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(monitor.db)); stats_get_disk(&monitor.last_disk_read, &monitor.last_disk_write);
if (monitor.db) { monitor.last_net_update_time = get_current_time_seconds();
sqlite3_close(monitor.db); 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 { } else {
init_db(); char *err = NULL;
load_stats_from_db(); 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);
} }
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);
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) { 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(&stats->requests_per_second);
rate_tracker_increment(&monitor.global_rps); 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) {
request_time_deque_push(&stats->request_times, duration_ms);
histogram_add(&stats->latency_histogram, duration_ms); histogram_add(&stats->latency_histogram, duration_ms);
histogram_add(&monitor.global_latency, 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;
} }
} }
@@ -537,129 +168,6 @@ void monitor_record_bytes(vhost_stats_t *stats, long long sent, long long recv)
stats->bytes_recv += recv; 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) { http_method_t http_method_from_string(const char *method) {
if (!method) return HTTP_METHOD_OTHER; if (!method) return HTTP_METHOD_OTHER;
if (strcmp(method, "GET") == 0) return HTTP_METHOD_GET; if (strcmp(method, "GET") == 0) return HTTP_METHOD_GET;
@@ -679,34 +187,27 @@ 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_status(vhost_stats_t *stats, int status_code) {
if (!stats) return; if (!stats) return;
if (status_code >= 100 && status_code < 200) { if (status_code >= 100 && status_code < 200) stats->status_counts.status_1xx++;
stats->status_counts.status_1xx++; else if (status_code >= 200 && status_code < 300) stats->status_counts.status_2xx++;
} else if (status_code >= 200 && status_code < 300) { else if (status_code >= 300 && status_code < 400) stats->status_counts.status_3xx++;
stats->status_counts.status_2xx++; else if (status_code >= 400 && status_code < 500) stats->status_counts.status_4xx++;
} else if (status_code >= 300 && status_code < 400) { else if (status_code >= 500 && status_code < 600) stats->status_counts.status_5xx++;
stats->status_counts.status_3xx++; else stats->status_counts.status_unknown++;
} 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) { void monitor_record_request_size(vhost_stats_t *stats, long size) {
if (!stats || size < 0) return; if (!stats) return;
histogram_add_with_bounds(&stats->request_size_histogram, (double)size, SIZE_BUCKET_BOUNDS); histogram_add_with_bounds(&stats->request_size_histogram, (double)size, SIZE_BUCKET_BOUNDS);
} }
void monitor_record_response_size(vhost_stats_t *stats, long size) { void monitor_record_response_size(vhost_stats_t *stats, long size) {
if (!stats || size < 0) return; if (!stats) return;
histogram_add_with_bounds(&stats->response_size_histogram, (double)size, SIZE_BUCKET_BOUNDS); histogram_add_with_bounds(&stats->response_size_histogram, (double)size, SIZE_BUCKET_BOUNDS);
stats->response_bytes_total += size; stats->response_bytes_total += size;
} }
void monitor_record_ttfb(vhost_stats_t *stats, double ttfb_ms) { void monitor_record_ttfb(vhost_stats_t *stats, double ttfb_ms) {
if (!stats || ttfb_ms < 0) return; if (!stats) return;
histogram_add(&stats->ttfb_histogram, ttfb_ms); histogram_add(&stats->ttfb_histogram, ttfb_ms);
} }
@@ -714,9 +215,7 @@ void monitor_record_upstream_connect(vhost_stats_t *stats, int success, double l
if (!stats) return; if (!stats) return;
if (success) { if (success) {
stats->upstream_connect_success++; stats->upstream_connect_success++;
if (latency_ms >= 0) {
histogram_add(&stats->upstream_connect_latency, latency_ms); histogram_add(&stats->upstream_connect_latency, latency_ms);
}
} else { } else {
stats->upstream_connect_failures++; stats->upstream_connect_failures++;
} }
@@ -725,19 +224,18 @@ void monitor_record_upstream_connect(vhost_stats_t *stats, int success, double l
void monitor_record_splice_transfer(vhost_stats_t *stats, long long bytes) { void monitor_record_splice_transfer(vhost_stats_t *stats, long long bytes) {
if (!stats) return; if (!stats) return;
stats->splice_transfers++; stats->splice_transfers++;
stats->bytes_via_splice += bytes; stats->bytes_via_splice += (uint64_t)bytes;
} }
void monitor_record_buffer_transfer(vhost_stats_t *stats, long long bytes) { void monitor_record_buffer_transfer(vhost_stats_t *stats, long long bytes) {
if (!stats) return; if (!stats) return;
stats->buffered_transfers++; stats->buffered_transfers++;
stats->bytes_via_buffer += bytes; stats->bytes_via_buffer += (uint64_t)bytes;
} }
void monitor_record_connection_opened(vhost_stats_t *stats) { void monitor_record_connection_opened(vhost_stats_t *stats) {
if (!stats) return; if (!stats) return;
stats->connections_opened++; stats->connections_opened++;
monitor.total_connections_accepted++;
} }
void monitor_record_connection_closed(vhost_stats_t *stats) { void monitor_record_connection_closed(vhost_stats_t *stats) {
@@ -769,45 +267,123 @@ void monitor_record_error(vhost_stats_t *stats, int error_type) {
} }
void monitor_compute_health_score(void) { void monitor_compute_health_score(void) {
uint64_t total_requests = 0; double score = 100.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) { if (monitor.cpu_history.count > 0) {
total_requests += s->total_requests; int idx = (monitor.cpu_history.head - 1 + monitor.cpu_history.capacity) % monitor.cpu_history.capacity;
total_errors += s->status_counts.status_5xx; double cpu = monitor.cpu_history.points[idx].value;
total_upstream_failures += s->upstream_connect_failures; if (cpu > 90) score -= 30;
total_upstream_attempts += s->upstream_connect_success + s->upstream_connect_failures; else if (cpu > 80) score -= 15;
total_timeouts += s->timeout_errors; else if (cpu > 70) score -= 5;
} }
double error_rate = total_requests > 0 ? (double)total_errors / total_requests : 0; uint64_t total_success = 0, total_errors = 0;
double upstream_fail_rate = total_upstream_attempts > 0 ? (double)total_upstream_failures / total_upstream_attempts : 0; for (vhost_stats_t *s = monitor.vhost_stats_head; s; s = s->next) {
double timeout_rate = total_requests > 0 ? (double)total_timeouts / total_requests : 0; total_success += s->status_counts.status_2xx + s->status_counts.status_3xx;
total_errors += s->status_counts.status_5xx;
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;
if (total_success + total_errors > 0) {
double error_rate = (double)total_errors / (double)(total_success + total_errors);
monitor.error_rate_1m = error_rate; 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) { void monitor_update_connection_states(void) {
memset(monitor.connections_by_state, 0, sizeof(monitor.connections_by_state)); 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;
}
+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
+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
+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
+94 -15
View File
@@ -2303,8 +2303,8 @@ void test_connection_buffered_data_after_upstream_close(void) {
TEST_SUITE_END(); TEST_SUITE_END();
} }
void test_connection_pipelined_request_rejected(void) { void test_connection_pipelined_request_forwarded(void) {
TEST_SUITE_BEGIN("Pipelined Request Rejected"); TEST_SUITE_BEGIN("Pipelined Request Forwarded");
connection_init_all(); connection_init_all();
int old_epoll = epoll_fd; int old_epoll = epoll_fd;
@@ -2345,10 +2345,10 @@ void test_connection_pipelined_request_rejected(void) {
struct epoll_event event = { .events = EPOLLIN, .data.fd = sockfd[0] }; struct epoll_event event = { .events = EPOLLIN, .data.fd = sockfd[0] };
connection_handle_event(&event); connection_handle_event(&event);
TEST_ASSERT(client->state == CLIENT_STATE_CLOSING || client->type == CONN_TYPE_UNUSED, TEST_ASSERT(client->state == CLIENT_STATE_FORWARDING,
"Connection closed on pipelined request"); "Client still in forwarding state");
TEST_ASSERT(strstr((char*)client->write_buf.data, "400") != NULL, TEST_ASSERT(client->type == CONN_TYPE_CLIENT,
"400 error sent to client"); "Client connection still active");
if (client->type != CONN_TYPE_UNUSED) { if (client->type != CONN_TYPE_UNUSED) {
buffer_free(&client->read_buf); buffer_free(&client->read_buf);
@@ -2364,8 +2364,8 @@ void test_connection_pipelined_request_rejected(void) {
TEST_SUITE_END(); TEST_SUITE_END();
} }
void test_connection_pipelined_dashboard_rejected(void) { void test_connection_pipelined_request_allowed(void) {
TEST_SUITE_BEGIN("Pipelined Dashboard Request Rejected"); TEST_SUITE_BEGIN("Pipelined Request Allowed");
connection_init_all(); connection_init_all();
int old_epoll = epoll_fd; int old_epoll = epoll_fd;
@@ -2400,15 +2400,16 @@ void test_connection_pipelined_dashboard_rejected(void) {
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd[0], &ev); epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd[0], &ev);
client->epoll_events = EPOLLIN; client->epoll_events = EPOLLIN;
const char *pipelined_dashboard = "GET /rproxy/dashboard HTTP/1.1\r\nHost: test.com\r\n\r\n"; const char *pipelined_req = "GET /api/data HTTP/1.1\r\nHost: test.com\r\n\r\n";
IGNORE_RESULT(write(sockfd[1], pipelined_dashboard, strlen(pipelined_dashboard))); IGNORE_RESULT(write(sockfd[1], pipelined_req, strlen(pipelined_req)));
struct epoll_event event = { .events = EPOLLIN, .data.fd = sockfd[0] }; struct epoll_event event = { .events = EPOLLIN, .data.fd = sockfd[0] };
connection_handle_event(&event); connection_handle_event(&event);
TEST_ASSERT(upstream->write_buf.tail == 0, "Dashboard request NOT forwarded to upstream"); TEST_ASSERT(client->state == CLIENT_STATE_FORWARDING,
TEST_ASSERT(client->state == CLIENT_STATE_CLOSING || client->type == CONN_TYPE_UNUSED, "Client remains in forwarding state");
"Client connection closed"); TEST_ASSERT(client->type == CONN_TYPE_CLIENT,
"Client connection not closed");
if (client->type != CONN_TYPE_UNUSED) { if (client->type != CONN_TYPE_UNUSED) {
buffer_free(&client->read_buf); buffer_free(&client->read_buf);
@@ -2485,6 +2486,83 @@ void test_connection_single_request_not_rejected(void) {
TEST_SUITE_END(); TEST_SUITE_END();
} }
void test_connection_pipelined_host_rewrite(void) {
TEST_SUITE_BEGIN("Pipelined Host Rewrite");
connection_init_all();
int old_epoll = epoll_fd;
epoll_fd = epoll_create1(0);
TEST_ASSERT(epoll_fd >= 0, "Epoll created");
static route_config_t test_route;
memset(&test_route, 0, sizeof(test_route));
strncpy(test_route.hostname, "test.com", sizeof(test_route.hostname) - 1);
strncpy(test_route.upstream_host, "backend.internal", sizeof(test_route.upstream_host) - 1);
test_route.upstream_port = 8080;
test_route.rewrite_host = 1;
int sockfd[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, sockfd);
connection_set_non_blocking(sockfd[0]);
connection_set_non_blocking(sockfd[1]);
connection_t *client = &connections[sockfd[0]];
memset(client, 0, sizeof(connection_t));
client->fd = sockfd[0];
client->type = CONN_TYPE_CLIENT;
client->state = CLIENT_STATE_FORWARDING;
buffer_init(&client->read_buf, 4096);
buffer_init(&client->write_buf, 4096);
connection_t *upstream = &connections[sockfd[1]];
memset(upstream, 0, sizeof(connection_t));
upstream->fd = sockfd[1];
upstream->type = CONN_TYPE_UPSTREAM;
upstream->state = CLIENT_STATE_FORWARDING;
upstream->route = &test_route;
buffer_init(&upstream->read_buf, 4096);
buffer_init(&upstream->write_buf, 4096);
client->pair = upstream;
upstream->pair = client;
struct epoll_event ev = { .events = EPOLLIN, .data.fd = sockfd[0] };
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd[0], &ev);
client->epoll_events = EPOLLIN;
const char *pipelined_req = "GET /api HTTP/1.1\r\nHost: test.com\r\n\r\n";
IGNORE_RESULT(write(sockfd[1], pipelined_req, strlen(pipelined_req)));
struct epoll_event event = { .events = EPOLLIN, .data.fd = sockfd[0] };
connection_handle_event(&event);
TEST_ASSERT(client->state == CLIENT_STATE_FORWARDING,
"Client still in forwarding state");
char recv_buf[4096];
ssize_t n = read(sockfd[1], recv_buf, sizeof(recv_buf) - 1);
if (n > 0) {
recv_buf[n] = '\0';
TEST_ASSERT(strstr(recv_buf, "Host: backend.internal:8080") != NULL,
"Host header rewritten to upstream");
TEST_ASSERT(strstr(recv_buf, "Host: test.com") == NULL,
"Original Host header removed");
}
if (client->type != CONN_TYPE_UNUSED) {
buffer_free(&client->read_buf);
buffer_free(&client->write_buf);
}
buffer_free(&upstream->read_buf);
buffer_free(&upstream->write_buf);
close(sockfd[0]);
close(sockfd[1]);
close(epoll_fd);
epoll_fd = old_epoll;
TEST_SUITE_END();
}
void run_connection_tests(void) { void run_connection_tests(void) {
test_connection_init_all(); test_connection_init_all();
test_connection_set_non_blocking(); test_connection_set_non_blocking();
@@ -2549,6 +2627,7 @@ void run_connection_tests(void) {
test_connection_cleanup_active_conn(); test_connection_cleanup_active_conn();
test_connection_keep_alive_internal_route_second_request(); test_connection_keep_alive_internal_route_second_request();
test_connection_buffered_data_after_upstream_close(); test_connection_buffered_data_after_upstream_close();
test_connection_pipelined_request_rejected(); test_connection_pipelined_request_forwarded();
test_connection_pipelined_dashboard_rejected(); test_connection_pipelined_request_allowed();
test_connection_pipelined_host_rewrite();
} }