refactor: extract base64 decoding into separate module
build: simplify Makefile with pattern rules and loops
This commit is contained in:
parent
c6aca9d83d
commit
3abeb0e226
@ -13,6 +13,14 @@
|
||||
|
||||
|
||||
|
||||
|
||||
## 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
191
Makefile
@ -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
137
src/auth.c
Executable file → Normal 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
235
src/backup/auth.c
Executable 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
534
src/backup/config.c
Executable 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 ¤t_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
1381
src/backup/connection.c
Executable file
File diff suppressed because it is too large
Load Diff
813
src/backup/monitor.c
Executable file
813
src/backup/monitor.c
Executable 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
42
src/base64.c
Normal 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
10
src/base64.h
Normal 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
141
src/client_handler.c
Normal 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
13
src/client_handler.h
Normal 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
|
||||
421
src/config.c
Executable file → Normal file
421
src/config.c
Executable file → Normal file
@ -1,119 +1,89 @@
|
||||
// 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 +92,19 @@ 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;
|
||||
}
|
||||
|
||||
if (config) {
|
||||
config_ref_dec(config);
|
||||
}
|
||||
@ -379,17 +218,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,129 +229,17 @@ 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;
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
|
||||
app_config_t *old_config = config;
|
||||
config = new_config;
|
||||
|
||||
|
||||
198
src/config_parser.c
Normal file
198
src/config_parser.c
Normal 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
16
src/config_parser.h
Normal 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
|
||||
1490
src/connection.c
Executable file → Normal file
1490
src/connection.c
Executable file → Normal file
File diff suppressed because it is too large
Load Diff
108
src/deque.c
Normal file
108
src/deque.c
Normal 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
24
src/deque.h
Normal 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
50
src/epoll_utils.c
Normal 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
15
src/epoll_utils.h
Normal 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
|
||||
105
src/forwarding.c
Normal file
105
src/forwarding.c
Normal file
@ -0,0 +1,105 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
int forwarding_try_splice(connection_t *src, connection_t *dst) {
|
||||
(void)src;
|
||||
(void)dst;
|
||||
return -1;
|
||||
}
|
||||
|
||||
void forwarding_handle(connection_t *conn, connection_t *pair, int direction) {
|
||||
if (!conn || !pair) return;
|
||||
|
||||
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
13
src/forwarding.h
Normal 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
69
src/histogram.c
Normal 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
18
src/histogram.h
Normal 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
121
src/http_response.c
Normal 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, ¶ms);
|
||||
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, ¶ms);
|
||||
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(¶ms, 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
26
src/http_response.h
Normal 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
|
||||
13
src/main.c
13
src/main.c
@ -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
858
src/monitor.c
Executable file → Normal 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(¤t->throughput_history);
|
||||
request_time_deque_free(¤t->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
55
src/rate_tracker.c
Normal 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
13
src/rate_tracker.h
Normal 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
76
src/socket_utils.c
Normal 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
12
src/socket_utils.h
Normal 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
138
src/stats_collector.c
Normal 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
12
src/stats_collector.h
Normal 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
20
src/time_utils.c
Normal 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
12
src/time_utils.h
Normal 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
|
||||
274
src/upstream.c
Normal file
274
src/upstream.c
Normal file
@ -0,0 +1,274 @@
|
||||
// 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 <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;
|
||||
}
|
||||
}
|
||||
|
||||
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
13
src/upstream.h
Normal 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
|
||||
@ -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();
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user