feat: enhance authentication with constant-time comparisons and memory clearing
Some checks failed
Build and Test / build (push) Failing after 31s
Build and Test / coverage (push) Failing after 41s

feat: implement rate limiting for client requests
feat: enable SSL hostname verification and set preferred cipher suites
fix: deny requests on rate limit allocation failure
test: suppress unused result warnings in connection tests
docs: update test results and coverage requirements in README
This commit is contained in:
retoor 2025-12-28 05:16:15 +01:00
parent 248c5647e2
commit 6ad8586770
49 changed files with 114 additions and 32 deletions

0
.gitea/workflows/build.yaml Normal file → Executable file
View File

0
.gitignore vendored Normal file → Executable file
View File

8
CHANGELOG.md Normal file → Executable file
View File

@ -5,6 +5,14 @@
## Version 0.5.0 - 2025-12-28
Enhances authentication security by preventing timing attacks and clearing sensitive memory, while adding rate limiting to protect against abusive client requests. Enables SSL hostname verification and preferred cipher suites for improved connection security, and fixes request denial when rate limit allocation fails.
**Changes:** 49 files, 138 lines
**Languages:** C (134 lines), Markdown (4 lines)
## Version 0.4.0 - 2025-12-15
Add comprehensive tests for the auth, buffer, and connection modules. Enhance the build process with logging test support and an increased minimum coverage threshold.

0
Makefile Normal file → Executable file
View File

4
README.md Normal file → Executable file
View File

@ -44,7 +44,7 @@ Compiles the source files in `src/` and produces the `rproxy` executable.
```bash
make test # Run unit tests
make coverage # Run tests with coverage report (minimum 60% required)
make coverage # Run tests with coverage report (minimum 69% required)
make coverage-html # Generate HTML coverage report
make valgrind # Run tests with memory leak detection
```
@ -52,7 +52,7 @@ make valgrind # Run tests with memory leak detection
### Test Results
```
Test Results: 559/559 passed
Test Results: 741/741 passed
HEAP SUMMARY:
in use at exit: 0 bytes in 0 blocks

0
cJSON.c Normal file → Executable file
View File

0
cJSON.h Normal file → Executable file
View File

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

@ -4,11 +4,16 @@
#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;
@ -48,12 +53,19 @@ int auth_check_credentials(const char *username, const char *password) {
if (!g_auth_enabled) return 1;
if (!username || !password) return 0;
if (strcmp(username, g_dashboard_username) != 0) return 0;
char password_hash[256];
compute_sha256(password, password_hash, sizeof(password_hash));
return strcmp(password_hash, g_dashboard_password_hash) == 0;
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) {
@ -108,8 +120,18 @@ int auth_check_basic_auth(const char *auth_header, char *error_msg, size_t error
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];
if (base64_decode(auth_header + 6, decoded, sizeof(decoded)) < 0) {
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);
}
@ -128,7 +150,10 @@ int auth_check_basic_auth(const char *auth_header, char *error_msg, size_t error
const char *username = decoded;
const char *password = colon + 1;
if (!auth_check_credentials(username, password)) {
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);
}
@ -155,8 +180,18 @@ int auth_check_route_basic_auth(const route_config_t *route, const char *auth_he
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];
if (base64_decode(auth_header + 6, decoded, sizeof(decoded)) < 0) {
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);
}
@ -165,6 +200,7 @@ int auth_check_route_basic_auth(const route_config_t *route, const char *auth_he
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);
}
@ -175,17 +211,20 @@ int auth_check_route_basic_auth(const route_config_t *route, const char *auth_he
const char *username = decoded;
const char *password = colon + 1;
if (strcmp(username, route->username) != 0) {
if (error_msg && error_size > 0) {
strncpy(error_msg, "Invalid username or password", error_size - 1);
}
return 0;
}
char password_hash[256];
compute_sha256(password, password_hash, sizeof(password_hash));
if (strcmp(password_hash, route->password_hash) != 0) {
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);
}

0
src/auth.h Normal file → Executable file
View File

0
src/buffer.c Normal file → Executable file
View File

0
src/buffer.h Normal file → Executable file
View File

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

0
src/config.h Normal file → Executable file
View File

13
src/connection.c Normal file → Executable file
View File

@ -8,6 +8,7 @@
#include "dashboard.h"
#include "auth.h"
#include "patch.h"
#include "rate_limit.h"
#include <stdio.h>
#include <stdlib.h>
@ -204,6 +205,8 @@ void connection_accept(int listener_fd) {
conn->state = CLIENT_STATE_READING_HEADERS;
conn->fd = client_fd;
conn->last_activity = cached_time;
strncpy(conn->client_ip, inet_ntoa(client_addr.sin_addr), sizeof(conn->client_ip) - 1);
conn->client_ip[sizeof(conn->client_ip) - 1] = '\0';
if (buffer_init(&conn->read_buf, CHUNK_SIZE) < 0 ||
buffer_init(&conn->write_buf, CHUNK_SIZE) < 0) {
@ -213,7 +216,7 @@ void connection_accept(int listener_fd) {
monitor.active_connections++;
log_debug("New connection on fd %d from %s, total: %d",
client_fd, inet_ntoa(client_addr.sin_addr), monitor.active_connections);
client_fd, conn->client_ip, monitor.active_connections);
}
}
@ -656,7 +659,7 @@ void connection_connect_to_upstream(connection_t *client, const char *data, size
}
const char *sni_hostname = route->rewrite_host ? route->upstream_host : client->request.host;
SSL_set_tlsext_host_name(up->ssl, sni_hostname);
ssl_set_hostname(up->ssl, sni_hostname);
SSL_set_fd(up->ssl, up_fd);
SSL_set_connect_state(up->ssl);
@ -739,6 +742,12 @@ static void handle_client_read(connection_t *conn) {
return;
}
if (!rate_limit_check(conn->client_ip)) {
log_info("[RATE-LIMIT] Request blocked for %s from %s", conn->request.host, conn->client_ip);
connection_send_error_response(conn, 429, "Too Many Requests", "Rate limit exceeded. Please try again later.");
return;
}
long long body_len = (conn->request.content_length > 0) ? conn->request.content_length : 0;
size_t total_request_len = headers_len + body_len;

0
src/connection.h Normal file → Executable file
View File

0
src/dashboard.c Normal file → Executable file
View File

0
src/dashboard.h Normal file → Executable file
View File

0
src/health_check.c Normal file → Executable file
View File

0
src/health_check.h Normal file → Executable file
View File

0
src/http.c Normal file → Executable file
View File

0
src/http.h Normal file → Executable file
View File

0
src/logging.c Normal file → Executable file
View File

0
src/logging.h Normal file → Executable file
View File

0
src/main.c Normal file → Executable file
View File

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

0
src/monitor.h Normal file → Executable file
View File

0
src/patch.c Normal file → Executable file
View File

0
src/patch.h Normal file → Executable file
View File

3
src/rate_limit.c Normal file → Executable file
View File

@ -74,7 +74,8 @@ int rate_limit_check(const char *client_ip) {
rate_limit_entry_t *new_entry = calloc(1, sizeof(rate_limit_entry_t));
if (!new_entry) {
return 1;
log_error("Rate limit entry allocation failed, denying request for safety");
return 0;
}
strncpy(new_entry->client_ip, client_ip, sizeof(new_entry->client_ip) - 1);

0
src/rate_limit.h Normal file → Executable file
View File

23
src/ssl_handler.c Normal file → Executable file
View File

@ -2,11 +2,12 @@
#include "logging.h"
#include <openssl/err.h>
#include <openssl/x509_vfy.h>
#include <openssl/x509v3.h>
#include <stdlib.h>
#include <string.h>
SSL_CTX *ssl_ctx = NULL;
static int g_ssl_verify_enabled = 0;
static int g_ssl_verify_enabled = 1;
static char g_ca_file[512] = "";
static char g_ca_path[512] = "";
@ -63,6 +64,26 @@ void ssl_init(void) {
SSL_CTX_set_options(ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1);
SSL_CTX_set_mode(ssl_ctx, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
if (SSL_CTX_set_cipher_list(ssl_ctx,
"ECDHE+AESGCM:DHE+AESGCM:ECDHE+CHACHA20:DHE+CHACHA20:!aNULL:!MD5:!DSS") != 1) {
log_info("Warning: Could not set preferred cipher list, using defaults");
}
}
int ssl_set_hostname(SSL *ssl, const char *hostname) {
if (!ssl || !hostname || hostname[0] == '\0') return 0;
SSL_set_tlsext_host_name(ssl, hostname);
if (g_ssl_verify_enabled) {
SSL_set_hostflags(ssl, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
if (SSL_set1_host(ssl, hostname) != 1) {
log_debug("Failed to set hostname verification for %s", hostname);
return -1;
}
}
return 0;
}
void ssl_cleanup(void) {

1
src/ssl_handler.h Normal file → Executable file
View File

@ -10,6 +10,7 @@ void ssl_set_ca_file(const char *path);
void ssl_set_ca_path(const char *path);
void ssl_init(void);
void ssl_cleanup(void);
int ssl_set_hostname(SSL *ssl, const char *hostname);
int ssl_do_handshake(connection_t *conn);
int ssl_read(connection_t *conn, char *buf, size_t len);
int ssl_write(connection_t *conn, const char *buf, size_t len);

1
src/types.h Normal file → Executable file
View File

@ -80,6 +80,7 @@ typedef struct connection_s {
conn_type_t type;
client_state_t state;
int fd;
char client_ip[64];
struct connection_s *pair;
struct vhost_stats_s *vhost_stats;
buffer_t read_buf;

0
tests/test_auth.c Normal file → Executable file
View File

0
tests/test_buffer.c Normal file → Executable file
View File

0
tests/test_config.c Normal file → Executable file
View File

26
tests/test_connection.c Normal file → Executable file
View File

@ -15,6 +15,8 @@
#include <time.h>
#include <errno.h>
#define IGNORE_RESULT(x) do { if (x) {} } while(0)
extern connection_t connections[MAX_FDS];
extern int epoll_fd;
@ -938,7 +940,7 @@ void test_connection_buffer_compaction(void) {
conn.read_buf.tail = 2500;
const char *test_data = "Test data for compaction";
write(sockfd[1], test_data, strlen(test_data));
IGNORE_RESULT(write(sockfd[1], test_data, strlen(test_data)));
int bytes = connection_do_read(&conn);
TEST_ASSERT(bytes > 0 || (bytes < 0 && conn.read_buf.head < 2000), "Buffer compacted or data read");
@ -1266,7 +1268,7 @@ void test_connection_do_read_grow_buffer(void) {
char large_data[128];
memset(large_data, 'A', sizeof(large_data));
write(sockfd[1], large_data, sizeof(large_data));
IGNORE_RESULT(write(sockfd[1], large_data, sizeof(large_data)));
int bytes = connection_do_read(&conn);
TEST_ASSERT(bytes > 0 || conn.read_buf.capacity > 64, "Buffer grew or data read");
@ -1297,7 +1299,7 @@ void test_connection_close_with_splice_pipes(void) {
buffer_init(&conn->write_buf, 4096);
int pipefd[2];
pipe(pipefd);
IGNORE_RESULT(pipe(pipefd));
conn->splice_pipe[0] = pipefd[0];
conn->splice_pipe[1] = pipefd[1];
conn->can_splice = 1;
@ -1439,7 +1441,7 @@ void test_connection_handle_event_client_read(void) {
conn->epoll_events = EPOLLIN;
const char *partial_request = "GET / HTTP/1.1\r\n";
write(sockfd[1], partial_request, strlen(partial_request));
IGNORE_RESULT(write(sockfd[1], partial_request, strlen(partial_request)));
struct epoll_event event = { .events = EPOLLIN, .data.fd = sockfd[0] };
connection_handle_event(&event);
@ -1485,7 +1487,7 @@ void test_connection_handle_complete_request(void) {
conn->epoll_events = EPOLLIN;
const char *request = "GET / HTTP/1.1\r\nHost: test.example.com\r\n\r\n";
write(sockfd[1], request, strlen(request));
IGNORE_RESULT(write(sockfd[1], request, strlen(request)));
struct epoll_event event = { .events = EPOLLIN, .data.fd = sockfd[0] };
connection_handle_event(&event);
@ -1531,7 +1533,7 @@ void test_connection_handle_dashboard_request(void) {
conn->epoll_events = EPOLLIN;
const char *request = "GET /rproxy/dashboard HTTP/1.1\r\nHost: localhost\r\n\r\n";
write(sockfd[1], request, strlen(request));
IGNORE_RESULT(write(sockfd[1], request, strlen(request)));
struct epoll_event event = { .events = EPOLLIN, .data.fd = sockfd[0] };
connection_handle_event(&event);
@ -1577,7 +1579,7 @@ void test_connection_handle_stats_api(void) {
conn->epoll_events = EPOLLIN;
const char *request = "GET /rproxy/api/stats HTTP/1.1\r\nHost: localhost\r\n\r\n";
write(sockfd[1], request, strlen(request));
IGNORE_RESULT(write(sockfd[1], request, strlen(request)));
struct epoll_event event = { .events = EPOLLIN, .data.fd = sockfd[0] };
connection_handle_event(&event);
@ -1629,7 +1631,7 @@ void test_connection_handle_oversized_header(void) {
huge_header[2] = 'T';
huge_header[3] = ' ';
huge_header[sizeof(huge_header)-1] = '\0';
write(sockfd[1], huge_header, sizeof(huge_header) - 1);
IGNORE_RESULT(write(sockfd[1], huge_header, sizeof(huge_header) - 1));
struct epoll_event event = { .events = EPOLLIN, .data.fd = sockfd[0] };
connection_handle_event(&event);
@ -1675,7 +1677,7 @@ void test_connection_handle_empty_host(void) {
conn->epoll_events = EPOLLIN;
const char *request = "GET / HTTP/1.1\r\n\r\n";
write(sockfd[1], request, strlen(request));
IGNORE_RESULT(write(sockfd[1], request, strlen(request)));
struct epoll_event event = { .events = EPOLLIN, .data.fd = sockfd[0] };
connection_handle_event(&event);
@ -1721,7 +1723,7 @@ void test_connection_handle_malformed_request(void) {
conn->epoll_events = EPOLLIN;
const char *request = "INVALID REQUEST\r\n\r\n";
write(sockfd[1], request, strlen(request));
IGNORE_RESULT(write(sockfd[1], request, strlen(request)));
struct epoll_event event = { .events = EPOLLIN, .data.fd = sockfd[0] };
connection_handle_event(&event);
@ -1839,7 +1841,7 @@ void test_connection_handle_forwarding_state(void) {
upstream->epoll_events = EPOLLIN;
const char *response = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK";
write(sockfd2[1], response, strlen(response));
IGNORE_RESULT(write(sockfd2[1], response, strlen(response)));
struct epoll_event event = { .events = EPOLLIN, .data.fd = sockfd2[0] };
connection_handle_event(&event);
@ -1976,7 +1978,7 @@ void test_connection_handle_client_forwarding_read(void) {
upstream->epoll_events = EPOLLIN;
const char *data = "More client data\r\n";
write(sockfd1[1], data, strlen(data));
IGNORE_RESULT(write(sockfd1[1], data, strlen(data)));
struct epoll_event event = { .events = EPOLLIN, .data.fd = sockfd1[0] };
connection_handle_event(&event);

0
tests/test_dashboard.c Normal file → Executable file
View File

0
tests/test_framework.h Normal file → Executable file
View File

0
tests/test_health_check.c Normal file → Executable file
View File

0
tests/test_host_rewrite.c Normal file → Executable file
View File

0
tests/test_http.c Normal file → Executable file
View File

0
tests/test_http_helpers.c Normal file → Executable file
View File

0
tests/test_main.c Normal file → Executable file
View File

0
tests/test_monitor.c Normal file → Executable file
View File

0
tests/test_patch.c Normal file → Executable file
View File

0
tests/test_rate_limit.c Normal file → Executable file
View File

0
tests/test_routing.c Normal file → Executable file
View File

0
tests/test_ssl_handler.c Normal file → Executable file
View File