Compare commits

...

2 Commits

Author SHA1 Message Date
5f60edd4ce perf: add zero-copy forwarding using splice syscall on linux
Some checks failed
Build and Test / coverage (push) Successful in 42s
Build and Test / build (push) Failing after 11m59s
refactor: simplify config hot reload by removing stale config list
fix: improve connection event handling and buffering logic
fix: prevent multiple upstream connections by closing existing pairs
2026-01-27 20:53:29 +01:00
3abeb0e226 refactor: extract base64 decoding into separate module
build: simplify Makefile with pattern rules and loops
2026-01-27 20:34:43 +01:00
38 changed files with 5456 additions and 2427 deletions

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
The server now rejects HTTP pipelined requests with a 400 Bad Request error.

191
Makefile
View File

@ -1,3 +1,5 @@
# retoor <retoor@molodetz.nl>
CC = gcc
CFLAGS = -Wall -Wextra -Werror -O3 -march=native -flto=auto -fomit-frame-pointer -D_GNU_SOURCE
CFLAGS_DEBUG = -Wall -Wextra -Werror -O0 -g -D_GNU_SOURCE
@ -13,15 +15,28 @@ SOURCES = $(SRC_DIR)/main.c \
$(SRC_DIR)/buffer.c \
$(SRC_DIR)/logging.c \
$(SRC_DIR)/config.c \
$(SRC_DIR)/config_parser.c \
$(SRC_DIR)/monitor.c \
$(SRC_DIR)/http.c \
$(SRC_DIR)/http_response.c \
$(SRC_DIR)/ssl_handler.c \
$(SRC_DIR)/connection.c \
$(SRC_DIR)/client_handler.c \
$(SRC_DIR)/upstream.c \
$(SRC_DIR)/forwarding.c \
$(SRC_DIR)/dashboard.c \
$(SRC_DIR)/rate_limit.c \
$(SRC_DIR)/auth.c \
$(SRC_DIR)/base64.c \
$(SRC_DIR)/health_check.c \
$(SRC_DIR)/patch.c \
$(SRC_DIR)/socket_utils.c \
$(SRC_DIR)/epoll_utils.c \
$(SRC_DIR)/time_utils.c \
$(SRC_DIR)/histogram.c \
$(SRC_DIR)/deque.c \
$(SRC_DIR)/rate_tracker.c \
$(SRC_DIR)/stats_collector.c \
cJSON.c
OBJECTS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(notdir $(SOURCES)))
@ -50,22 +65,35 @@ TEST_OBJECTS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(notdir $(TEST_SOURCES)))
TEST_LIB_SOURCES = $(SRC_DIR)/buffer.c \
$(SRC_DIR)/logging.c \
$(SRC_DIR)/config.c \
$(SRC_DIR)/config_parser.c \
$(SRC_DIR)/monitor.c \
$(SRC_DIR)/http.c \
$(SRC_DIR)/http_response.c \
$(SRC_DIR)/ssl_handler.c \
$(SRC_DIR)/connection.c \
$(SRC_DIR)/client_handler.c \
$(SRC_DIR)/upstream.c \
$(SRC_DIR)/forwarding.c \
$(SRC_DIR)/dashboard.c \
$(SRC_DIR)/rate_limit.c \
$(SRC_DIR)/auth.c \
$(SRC_DIR)/base64.c \
$(SRC_DIR)/health_check.c \
$(SRC_DIR)/patch.c \
$(SRC_DIR)/socket_utils.c \
$(SRC_DIR)/epoll_utils.c \
$(SRC_DIR)/time_utils.c \
$(SRC_DIR)/histogram.c \
$(SRC_DIR)/deque.c \
$(SRC_DIR)/rate_tracker.c \
$(SRC_DIR)/stats_collector.c \
cJSON.c
TEST_LIB_OBJECTS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(notdir $(TEST_LIB_SOURCES)))
TEST_TARGET = rproxy_test
MIN_COVERAGE = 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
.PHONY: all clean test legacy run coverage coverage-html valgrind
@ -78,94 +106,13 @@ $(BUILD_DIR):
$(TARGET): $(OBJECTS)
$(CC) $(OBJECTS) -o $@ $(LDFLAGS)
$(BUILD_DIR)/main.o: $(SRC_DIR)/main.c
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR)/buffer.o: $(SRC_DIR)/buffer.c
$(BUILD_DIR)/cJSON.o: cJSON.c | $(BUILD_DIR)
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR)/logging.o: $(SRC_DIR)/logging.c
$(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
$(BUILD_DIR)/test_%.o: $(TESTS_DIR)/test_%.c | $(BUILD_DIR)
$(CC) $(CFLAGS) -I$(SRC_DIR) -c $< -o $@
$(TEST_TARGET): $(BUILD_DIR) $(TEST_OBJECTS) $(TEST_LIB_OBJECTS)
@ -182,35 +129,14 @@ run: $(TARGET)
coverage: clean
mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_main.c -o build/test_main.o
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_http.c -o build/test_http.o
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_buffer.c -o build/test_buffer.o
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_config.c -o build/test_config.o
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_routing.c -o build/test_routing.o
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_host_rewrite.c -o build/test_host_rewrite.o
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_http_helpers.c -o build/test_http_helpers.o
$(CC) $(CFLAGS_COV) -Isrc -c tests/test_patch.c -o build/test_patch.o
$(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
@for src in $(TEST_LIB_SOURCES); do \
obj=$(BUILD_DIR)/$$(basename $${src%.c}.o); \
$(CC) $(CFLAGS_COV) -c $$src -o $$obj; \
done
@for src in $(TEST_SOURCES); do \
obj=$(BUILD_DIR)/$$(basename $${src%.c}.o); \
$(CC) $(CFLAGS_COV) -I$(SRC_DIR) -c $$src -o $$obj; \
done
$(CC) $(TEST_OBJECTS) $(TEST_LIB_OBJECTS) -o $(TEST_TARGET) $(LDFLAGS_COV)
./$(TEST_TARGET)
@echo ""
@ -266,35 +192,14 @@ coverage-html: coverage
valgrind: clean
mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_main.c -o build/test_main.o
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_http.c -o build/test_http.o
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_buffer.c -o build/test_buffer.o
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_config.c -o build/test_config.o
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_routing.c -o build/test_routing.o
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_host_rewrite.c -o build/test_host_rewrite.o
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_http_helpers.c -o build/test_http_helpers.o
$(CC) $(CFLAGS_DEBUG) -Isrc -c tests/test_patch.c -o build/test_patch.o
$(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
@for src in $(TEST_LIB_SOURCES); do \
obj=$(BUILD_DIR)/$$(basename $${src%.c}.o); \
$(CC) $(CFLAGS_DEBUG) -c $$src -o $$obj; \
done
@for src in $(TEST_SOURCES); do \
obj=$(BUILD_DIR)/$$(basename $${src%.c}.o); \
$(CC) $(CFLAGS_DEBUG) -I$(SRC_DIR) -c $$src -o $$obj; \
done
$(CC) $(TEST_OBJECTS) $(TEST_LIB_OBJECTS) -o $(TEST_TARGET) -lssl -lcrypto -lsqlite3 -lm -lpthread
valgrind --leak-check=full --show-leak-kinds=definite,indirect --error-exitcode=1 ./$(TEST_TARGET) 2>&1 | tee valgrind.log; \
VALGRIND_EXIT=$$?; \

137
src/auth.c Executable file → Normal file
View File

@ -1,9 +1,12 @@
// retoor <retoor@molodetz.nl>
#include "auth.h"
#include "base64.h"
#include "logging.h"
#include <stdlib.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <openssl/crypto.h>
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;
}
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;
typedef struct {
const char *expected_username;
const char *expected_password_hash;
} auth_credentials_t;
static int auth_parse_and_verify(const char *auth_header, const auth_credentials_t *creds,
char *error_msg, size_t error_size) {
if (!auth_header) {
if (error_msg && error_size > 0) {
strncpy(error_msg, "Authentication required", error_size - 1);
@ -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));
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) &&
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(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;
}
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
src/backup/auth.c Executable file
View File

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

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

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

1381
src/backup/connection.c Executable file

File diff suppressed because it is too large Load Diff

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

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

42
src/base64.c Normal file
View File

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

10
src/base64.h Normal file
View File

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

141
src/client_handler.c Normal file
View File

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

13
src/client_handler.h Normal file
View File

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

445
src/config.c Executable file → Normal file
View File

@ -1,119 +1,88 @@
// retoor <retoor@molodetz.nl>
#include "config.h"
#include "config_parser.h"
#include "logging.h"
#include "../cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <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;
static app_config_t *config_create_from_json(cJSON *root) {
app_config_t *new_config = calloc(1, sizeof(app_config_t));
if (!new_config) {
log_error("Failed to allocate memory for config");
return NULL;
}
new_config->ref_count = 1;
const char *p = hostname;
int label_len = 0;
cJSON *port_item = cJSON_GetObjectItem(root, "port");
new_config->port = cJSON_IsNumber(port_item) ? port_item->valueint : 8080;
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;
if (new_config->port < 1 || new_config->port > 65535) {
log_error("Invalid port number: %d", new_config->port);
free(new_config);
return NULL;
}
cJSON *proxy_array = cJSON_GetObjectItem(root, "reverse_proxy");
if (!cJSON_IsArray(proxy_array)) {
log_error("No reverse_proxy array found");
free(new_config);
return NULL;
}
int total_routes = cJSON_GetArraySize(proxy_array);
if (total_routes <= 0) {
log_error("Empty reverse_proxy array");
free(new_config);
return NULL;
}
new_config->routes = calloc((size_t)total_routes, sizeof(route_config_t));
if (!new_config->routes) {
log_error("Failed to allocate memory for routes");
free(new_config);
return NULL;
}
int valid_routes = 0;
int index = 0;
cJSON *route_item;
cJSON_ArrayForEach(route_item, proxy_array) {
route_config_t *route = &new_config->routes[valid_routes];
if (config_parse_route(route_item, route, index)) {
log_info("Route: %s -> %s:%d (SSL: %s, Auth: %s, Patches: %d)",
route->hostname, route->upstream_host, route->upstream_port,
route->use_ssl ? "yes" : "no", route->use_auth ? "yes" : "no",
route->patches.rule_count);
valid_routes++;
}
p++;
index++;
}
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);
if (valid_routes == 0) {
log_error("No valid routes parsed");
free(new_config->routes);
free(new_config);
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;
new_config->route_count = valid_routes;
return new_config;
}
int config_load(const char *filename) {
log_info("Loading configuration from %s", filename);
char *json_string = read_file_to_string(filename);
char *json_string = config_read_file(filename);
if (!json_string) {
log_error("Could not read config file");
return 0;
@ -122,150 +91,24 @@ int config_load(const char *filename) {
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");
log_error("JSON parse error: %s", 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++;
}
}
app_config_t *new_config = config_create_from_json(root);
cJSON_Delete(root);
if (!new_config) {
return 0;
}
struct stat st;
if (stat(filename, &st) == 0) {
config_file_mtime = st.st_mtime;
}
if (config) {
config_ref_dec(config);
}
@ -298,14 +141,6 @@ void config_free(void) {
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) {
@ -379,17 +214,9 @@ int config_check_file_changed(const char *filename) {
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);
char *json_string = config_read_file(filename);
if (!json_string) {
log_error("Hot-reload: Could not read config file");
free(new_config);
return 0;
}
@ -398,137 +225,27 @@ int config_hot_reload(const char *filename) {
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;
app_config_t *new_config = config_create_from_json(root);
cJSON_Delete(root);
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);
if (!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;
struct stat st;
if (stat(filename, &st) == 0) {
config_file_mtime = st.st_mtime;
}
cJSON_Delete(root);
app_config_t *old_config = config;
if (config) {
config_ref_dec(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;
}

198
src/config_parser.c Normal file
View File

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

16
src/config_parser.h Normal file
View File

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

1489
src/connection.c Executable file → Normal file

File diff suppressed because it is too large Load Diff

108
src/deque.c Normal file
View File

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

24
src/deque.h Normal file
View File

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

50
src/epoll_utils.c Normal file
View File

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

15
src/epoll_utils.h Normal file
View File

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

167
src/forwarding.c Normal file
View File

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

13
src/forwarding.h Normal file
View File

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

69
src/histogram.c Normal file
View File

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

18
src/histogram.h Normal file
View File

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

121
src/http_response.c Normal file
View File

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

26
src/http_response.h Normal file
View File

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

View File

@ -1,3 +1,5 @@
// retoor <retoor@molodetz.nl>
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
@ -7,6 +9,7 @@
#include <errno.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <string.h>
#include "types.h"
#include "logging.h"
@ -14,10 +17,15 @@
#include "monitor.h"
#include "ssl_handler.h"
#include "connection.h"
#include "epoll_utils.h"
#include "rate_limit.h"
#include "auth.h"
#include "health_check.h"
#ifndef CONFIG_RELOAD_INTERVAL_SECONDS
#define CONFIG_RELOAD_INTERVAL_SECONDS 5
#endif
static volatile sig_atomic_t g_shutdown = 0;
static volatile sig_atomic_t g_reload_config = 0;
static const char *g_config_file = NULL;
@ -162,9 +170,8 @@ int main(int argc, char *argv[]) {
health_check_init();
epoll_fd = epoll_create1(EPOLL_CLOEXEC);
if (epoll_fd == -1) {
log_error("epoll_create1 failed");
if (epoll_utils_create() == -1) {
log_error("epoll_create failed");
return 1;
}

858
src/monitor.c Executable file → Normal file
View File

@ -1,157 +1,30 @@
// retoor <retoor@molodetz.nl>
#include "monitor.h"
#include "histogram.h"
#include "deque.h"
#include "rate_tracker.h"
#include "stats_collector.h"
#include "logging.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/sysinfo.h>
#include <time.h>
#include <math.h>
system_monitor_t monitor;
system_monitor_t monitor = {0};
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);
static double get_current_time_seconds(void) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
}
void monitor_init(const char *db_file) {
memset(&monitor, 0, sizeof(system_monitor_t));
memset(&monitor, 0, sizeof(monitor));
monitor.start_time = time(NULL);
monitor.uptime_start = time(NULL);
monitor.uptime_start = monitor.start_time;
monitor.health_score = 100.0;
history_deque_init(&monitor.cpu_history, HISTORY_SECONDS);
@ -167,367 +40,125 @@ void monitor_init(const char *db_file) {
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);
stats_get_network(&monitor.last_net_sent, &monitor.last_net_recv);
stats_get_disk(&monitor.last_disk_read, &monitor.last_disk_write);
monitor.last_net_update_time = get_current_time_seconds();
monitor.last_disk_update_time = monitor.last_net_update_time;
if (db_file) {
int rc = sqlite3_open(db_file, &monitor.db);
if (rc != SQLITE_OK) {
log_debug("Cannot open stats database: %s", sqlite3_errmsg(monitor.db));
monitor.db = NULL;
} else {
char *err = NULL;
const char *sql =
"CREATE TABLE IF NOT EXISTS stats_history ("
" timestamp INTEGER PRIMARY KEY,"
" cpu_percent REAL,"
" memory_gb REAL,"
" active_connections INTEGER,"
" requests_per_second REAL"
");";
sqlite3_exec(monitor.db, sql, NULL, NULL, &err);
if (err) {
sqlite3_free(err);
}
}
} else {
init_db();
load_stats_from_db();
}
monitor_update();
log_info("Monitor initialized");
}
void monitor_cleanup(void) {
if (monitor.db) {
sqlite3_close(monitor.db);
monitor.db = NULL;
}
history_deque_free(&monitor.cpu_history);
history_deque_free(&monitor.memory_history);
network_history_deque_free(&monitor.network_history);
disk_history_deque_free(&monitor.disk_history);
history_deque_free(&monitor.throughput_history);
history_deque_free(&monitor.load1_history);
history_deque_free(&monitor.load5_history);
history_deque_free(&monitor.load15_history);
vhost_stats_t *current = monitor.vhost_stats_head;
while (current) {
vhost_stats_t *next = current->next;
if (current->throughput_history.points) free(current->throughput_history.points);
if (current->request_times.times) free(current->request_times.times);
history_deque_free(&current->throughput_history);
request_time_deque_free(&current->request_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; }
if (monitor.db) {
sqlite3_close(monitor.db);
monitor.db = NULL;
}
log_info("Monitor cleanup complete");
}
static void cleanup_old_stats(void) {
if (!monitor.db) return;
vhost_stats_t *monitor_get_or_create_vhost_stats(const char *vhost_name) {
if (!vhost_name || vhost_name[0] == '\0') return NULL;
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;
for (vhost_stats_t *s = monitor.vhost_stats_head; s; s = s->next) {
if (strcmp(s->vhost_name, vhost_name) == 0) {
return s;
}
}
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 *stats = calloc(1, sizeof(vhost_stats_t));
if (!stats) return NULL;
vhost_stats_t* monitor_get_or_create_vhost_stats(const char *vhost_name) {
if (!vhost_name || strlen(vhost_name) == 0) return NULL;
strncpy(stats->vhost_name, vhost_name, sizeof(stats->vhost_name) - 1);
stats->vhost_name[sizeof(stats->vhost_name) - 1] = '\0';
for (vhost_stats_t *curr = monitor.vhost_stats_head; curr; curr = curr->next) {
if (strcmp(curr->vhost_name, vhost_name) == 0) {
return curr;
}
}
history_deque_init(&stats->throughput_history, 60);
request_time_deque_init(&stats->request_times, 1000);
histogram_init(&stats->latency_histogram);
histogram_init(&stats->request_size_histogram);
histogram_init(&stats->response_size_histogram);
histogram_init(&stats->ttfb_histogram);
histogram_init(&stats->upstream_connect_latency);
rate_tracker_init(&stats->requests_per_second);
vhost_stats_t *new_stats = calloc(1, sizeof(vhost_stats_t));
if (!new_stats) {
return NULL;
}
stats->next = monitor.vhost_stats_head;
monitor.vhost_stats_head = stats;
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;
return stats;
}
void monitor_record_request_start(vhost_stats_t *stats, int is_websocket) {
if (!stats) return;
stats->total_requests++;
if (is_websocket) {
stats->websocket_requests++;
} else {
stats->http_requests++;
}
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);
if (!stats) return;
double now = get_current_time_seconds();
double duration_ms = (now - start_time) * 1000.0;
histogram_add(&stats->latency_histogram, duration_ms);
histogram_add(&monitor.global_latency, duration_ms);
request_time_deque_push(&stats->request_times, duration_ms);
int count = stats->request_times.count;
if (count > 0) {
double sum = 0;
int start_idx = (stats->request_times.head - count + stats->request_times.capacity) % stats->request_times.capacity;
for (int i = 0; i < count; i++) {
int idx = (start_idx + i) % stats->request_times.capacity;
sum += stats->request_times.times[idx];
}
stats->avg_request_time_ms = sum / count;
}
}
@ -537,129 +168,6 @@ void monitor_record_bytes(vhost_stats_t *stats, long long sent, long long 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) {
if (!method) return HTTP_METHOD_OTHER;
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) {
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++;
}
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;
if (!stats) return;
histogram_add_with_bounds(&stats->request_size_histogram, (double)size, SIZE_BUCKET_BOUNDS);
}
void monitor_record_response_size(vhost_stats_t *stats, long size) {
if (!stats || size < 0) return;
if (!stats) return;
histogram_add_with_bounds(&stats->response_size_histogram, (double)size, SIZE_BUCKET_BOUNDS);
stats->response_bytes_total += size;
}
void monitor_record_ttfb(vhost_stats_t *stats, double ttfb_ms) {
if (!stats || ttfb_ms < 0) return;
if (!stats) return;
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 (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 {
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) {
if (!stats) return;
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) {
if (!stats) return;
stats->buffered_transfers++;
stats->bytes_via_buffer += bytes;
stats->bytes_via_buffer += (uint64_t)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) {
@ -769,45 +267,123 @@ void monitor_record_error(vhost_stats_t *stats, int error_type) {
}
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;
double score = 100.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;
if (monitor.cpu_history.count > 0) {
int idx = (monitor.cpu_history.head - 1 + monitor.cpu_history.capacity) % monitor.cpu_history.capacity;
double cpu = monitor.cpu_history.points[idx].value;
if (cpu > 90) score -= 30;
else if (cpu > 80) score -= 15;
else if (cpu > 70) score -= 5;
}
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;
uint64_t total_success = 0, total_errors = 0;
for (vhost_stats_t *s = monitor.vhost_stats_head; s; s = s->next) {
total_success += s->status_counts.status_2xx + s->status_counts.status_3xx;
total_errors += s->status_counts.status_5xx;
}
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;
if (error_rate > 0.1) score -= 40;
else if (error_rate > 0.05) score -= 20;
else if (error_rate > 0.01) score -= 10;
}
monitor.error_rate_1m = error_rate;
double p99 = histogram_percentile(&monitor.global_latency, 0.99);
if (p99 > 5000) score -= 20;
else if (p99 > 2000) score -= 10;
else if (p99 > 1000) score -= 5;
monitor.health_score = fmax(0, fmin(100, score));
}
double monitor_get_current_rps(void) {
return (double)rate_tracker_get_rps(&monitor.global_rps);
}
void monitor_update(void) {
double now = get_current_time_seconds();
time_t now_t = (time_t)now;
double cpu = stats_get_cpu_usage();
history_deque_push(&monitor.cpu_history, now, cpu);
double mem_gb = 0;
stats_get_memory_usage(&mem_gb);
history_deque_push(&monitor.memory_history, now, mem_gb);
double load1, load5, load15;
stats_get_load_averages(&load1, &load5, &load15);
history_deque_push(&monitor.load1_history, now, load1);
history_deque_push(&monitor.load5_history, now, load5);
history_deque_push(&monitor.load15_history, now, load15);
long long net_sent, net_recv;
stats_get_network(&net_sent, &net_recv);
double dt = now - monitor.last_net_update_time;
if (dt > 0.5 && monitor.last_net_sent > 0) {
double rx_kbps = (double)(net_recv - monitor.last_net_recv) / dt / 1024.0;
double tx_kbps = (double)(net_sent - monitor.last_net_sent) / dt / 1024.0;
network_history_deque_push(&monitor.network_history, now, rx_kbps, tx_kbps);
}
monitor.last_net_sent = net_sent;
monitor.last_net_recv = net_recv;
monitor.last_net_update_time = now;
long long disk_read, disk_write;
stats_get_disk(&disk_read, &disk_write);
double disk_dt = now - monitor.last_disk_update_time;
if (disk_dt > 0.5 && monitor.last_disk_read > 0) {
double read_mbps = (double)(disk_read - monitor.last_disk_read) * 512.0 / disk_dt / (1024.0 * 1024.0);
double write_mbps = (double)(disk_write - monitor.last_disk_write) * 512.0 / disk_dt / (1024.0 * 1024.0);
disk_history_deque_push(&monitor.disk_history, now, read_mbps, write_mbps);
}
monitor.last_disk_read = disk_read;
monitor.last_disk_write = disk_write;
monitor.last_disk_update_time = now;
double current_rps = monitor_get_current_rps();
if (current_rps > monitor.peak_rps) {
monitor.peak_rps = current_rps;
monitor.peak_rps_time = now_t;
}
for (vhost_stats_t *s = monitor.vhost_stats_head; s; s = s->next) {
if (s->last_update > 0) {
double vhost_dt = now - s->last_update;
if (vhost_dt >= 1.0) {
double throughput = (double)((s->bytes_sent - s->last_bytes_sent) +
(s->bytes_recv - s->last_bytes_recv)) / vhost_dt / 1024.0;
history_deque_push(&s->throughput_history, now, throughput);
s->last_bytes_sent = s->bytes_sent;
s->last_bytes_recv = s->bytes_recv;
s->last_update = now;
}
} else {
s->last_bytes_sent = s->bytes_sent;
s->last_bytes_recv = s->bytes_recv;
s->last_update = now;
}
uint32_t rps = rate_tracker_get_rps(&s->requests_per_second);
if ((double)rps > s->peak_rps) {
s->peak_rps = (double)rps;
s->peak_rps_time = now_t;
}
}
if (monitor.db) {
char sql[512];
snprintf(sql, sizeof(sql),
"INSERT INTO stats_history (timestamp, cpu_percent, memory_gb, active_connections, requests_per_second) "
"VALUES (%ld, %.2f, %.2f, %d, %.2f)",
now_t, cpu, mem_gb, monitor.active_connections, current_rps);
sqlite3_exec(monitor.db, sql, NULL, NULL, NULL);
}
}
void monitor_update_connection_states(void) {
memset(monitor.connections_by_state, 0, sizeof(monitor.connections_by_state));
}
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
src/rate_tracker.c Normal file
View File

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

13
src/rate_tracker.h Normal file
View File

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

76
src/socket_utils.c Normal file
View File

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

12
src/socket_utils.h Normal file
View File

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

138
src/stats_collector.c Normal file
View File

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

12
src/stats_collector.h Normal file
View File

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

20
src/time_utils.c Normal file
View File

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

12
src/time_utils.h Normal file
View File

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

280
src/upstream.c Normal file
View File

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

13
src/upstream.h Normal file
View File

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

View File

@ -2303,8 +2303,8 @@ void test_connection_buffered_data_after_upstream_close(void) {
TEST_SUITE_END();
}
void test_connection_pipelined_request_rejected(void) {
TEST_SUITE_BEGIN("Pipelined Request Rejected");
void test_connection_pipelined_request_forwarded(void) {
TEST_SUITE_BEGIN("Pipelined Request Forwarded");
connection_init_all();
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] };
connection_handle_event(&event);
TEST_ASSERT(client->state == CLIENT_STATE_CLOSING || client->type == CONN_TYPE_UNUSED,
"Connection closed on pipelined request");
TEST_ASSERT(strstr((char*)client->write_buf.data, "400") != NULL,
"400 error sent to client");
TEST_ASSERT(client->state == CLIENT_STATE_FORWARDING,
"Client still in forwarding state");
TEST_ASSERT(client->type == CONN_TYPE_CLIENT,
"Client connection still active");
if (client->type != CONN_TYPE_UNUSED) {
buffer_free(&client->read_buf);
@ -2364,8 +2364,8 @@ void test_connection_pipelined_request_rejected(void) {
TEST_SUITE_END();
}
void test_connection_pipelined_dashboard_rejected(void) {
TEST_SUITE_BEGIN("Pipelined Dashboard Request Rejected");
void test_connection_pipelined_request_allowed(void) {
TEST_SUITE_BEGIN("Pipelined Request Allowed");
connection_init_all();
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);
client->epoll_events = EPOLLIN;
const char *pipelined_dashboard = "GET /rproxy/dashboard HTTP/1.1\r\nHost: test.com\r\n\r\n";
IGNORE_RESULT(write(sockfd[1], pipelined_dashboard, strlen(pipelined_dashboard)));
const char *pipelined_req = "GET /api/data 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(upstream->write_buf.tail == 0, "Dashboard request NOT forwarded to upstream");
TEST_ASSERT(client->state == CLIENT_STATE_CLOSING || client->type == CONN_TYPE_UNUSED,
"Client connection closed");
TEST_ASSERT(client->state == CLIENT_STATE_FORWARDING,
"Client remains in forwarding state");
TEST_ASSERT(client->type == CONN_TYPE_CLIENT,
"Client connection not closed");
if (client->type != CONN_TYPE_UNUSED) {
buffer_free(&client->read_buf);
@ -2485,6 +2486,83 @@ void test_connection_single_request_not_rejected(void) {
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) {
test_connection_init_all();
test_connection_set_non_blocking();
@ -2549,6 +2627,7 @@ void run_connection_tests(void) {
test_connection_cleanup_active_conn();
test_connection_keep_alive_internal_route_second_request();
test_connection_buffered_data_after_upstream_close();
test_connection_pipelined_request_rejected();
test_connection_pipelined_dashboard_rejected();
test_connection_pipelined_request_forwarded();
test_connection_pipelined_request_allowed();
test_connection_pipelined_host_rewrite();
}