Update.
All checks were successful
Build and Test / build (push) Successful in 20s

This commit is contained in:
retoor 2025-11-29 05:56:34 +01:00
parent 23ea7644ce
commit 14ebbbec3f
9 changed files with 504 additions and 9 deletions

View File

@ -18,6 +18,7 @@ SOURCES = $(SRC_DIR)/main.c \
$(SRC_DIR)/rate_limit.c \
$(SRC_DIR)/auth.c \
$(SRC_DIR)/health_check.c \
$(SRC_DIR)/patch.c \
cJSON.c
OBJECTS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(notdir $(SOURCES)))
@ -43,6 +44,7 @@ TEST_LIB_SOURCES = $(SRC_DIR)/buffer.c \
$(SRC_DIR)/rate_limit.c \
$(SRC_DIR)/auth.c \
$(SRC_DIR)/health_check.c \
$(SRC_DIR)/patch.c \
cJSON.c
TEST_LIB_OBJECTS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(notdir $(TEST_LIB_SOURCES)))
@ -95,6 +97,9 @@ $(BUILD_DIR)/auth.o: $(SRC_DIR)/auth.c
$(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 $@

View File

@ -19,6 +19,7 @@ rproxy is a high-performance reverse proxy server written in C. It routes HTTP a
- Health checks for upstream servers
- Automatic upstream connection retries
- File logging support
- Stream data patching/rewriting for textual content
## Dependencies
@ -49,7 +50,12 @@ Configuration is defined in `proxy_config.json`:
"upstream_host": "127.0.0.1",
"upstream_port": 5000,
"use_ssl": false,
"rewrite_host": true
"rewrite_host": true,
"patch": {
"old_string": "new_string",
"secret_key": "[REDACTED]",
"blocked_content": null
}
}
]
}
@ -62,6 +68,37 @@ Configuration is defined in `proxy_config.json`:
- `upstream_port`: Target server port
- `use_ssl`: Enable SSL for upstream connection
- `rewrite_host`: Rewrite Host header to upstream hostname
- `patch`: Optional object for stream data patching (see below)
### Data Patching
The `patch` configuration allows rewriting or blocking content in HTTP streams. Patch rules are applied to textual content only (text/*, application/json, application/xml, etc.). Binary content passes through unmodified.
```json
{
"patch": {
"find_this": "replace_with_this",
"another_string": "replacement",
"blocked_term": null
}
}
```
- **String replacement**: Each key-value pair defines a find-replace rule
- **Content blocking**: Setting value to `null` blocks the entire response/request when the key is found
- **Bidirectional**: Patches apply to both requests (client → upstream) and responses (upstream → client)
When content is blocked:
- Blocked responses return `502 Bad Gateway` to the client
- Blocked requests return `403 Forbidden` to the client
Supported textual content types:
- `text/*` (text/html, text/plain, text/css, etc.)
- `application/json`
- `application/xml`
- `application/javascript`
- `application/x-www-form-urlencoded`
- Any content type with `+xml` or `+json` suffix
## Environment Variables
@ -136,6 +173,7 @@ kill -HUP $(pidof rproxy)
- **rate_limit.c**: Per-IP rate limiting
- **auth.c**: Dashboard authentication
- **health_check.c**: Upstream health monitoring
- **patch.c**: Stream data patching engine
## Testing

View File

@ -197,6 +197,45 @@ int config_load(const char *filename) {
}
}
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",
@ -373,9 +412,43 @@ int config_hot_reload(const char *filename) {
}
}
log_info("Hot-reload route: %s -> %s:%d (SSL: %s, Auth: %s)",
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->use_ssl ? "yes" : "no", route->use_auth ? "yes" : "no",
route->patches.rule_count);
i++;
}
new_config.route_count = i;

View File

@ -7,6 +7,7 @@
#include "ssl_handler.h"
#include "dashboard.h"
#include "auth.h"
#include "patch.h"
#include <stdio.h>
#include <stdlib.h>
@ -437,6 +438,8 @@ void connection_connect_to_upstream(connection_t *client, const char *data, size
client->pair = up;
up->pair = client;
up->vhost_stats = client->vhost_stats;
up->route = route;
client->route = route;
if (buffer_init(&up->read_buf, CHUNK_SIZE) < 0) {
close(up_fd);
@ -724,21 +727,104 @@ static void handle_forwarding(connection_t *conn) {
connection_do_write(pair);
}
size_t space_needed = pair->write_buf.tail + data_to_forward;
char *src_data = conn->read_buf.data + conn->read_buf.head;
size_t src_len = data_to_forward;
route_config_t *route = conn->route;
int should_patch = 0;
int is_response = (conn->type == CONN_TYPE_UPSTREAM);
if (route && patch_has_rules(&route->patches)) {
if (is_response && !conn->content_type_checked) {
size_t headers_end = 0;
if (http_find_headers_end(src_data, src_len, &headers_end)) {
conn->content_type_checked = 1;
char content_type[256] = "";
http_find_header_value(src_data, headers_end, "Content-Type", content_type, sizeof(content_type));
conn->is_textual_content = http_is_textual_content_type(content_type);
conn->original_content_length = http_get_content_length(src_data, headers_end);
conn->response_headers_parsed = 1;
log_debug("Response Content-Type: %s, textual: %d, content-length: %ld",
content_type[0] ? content_type : "(none)",
conn->is_textual_content,
conn->original_content_length);
}
}
if (conn->content_type_checked && conn->is_textual_content) {
should_patch = 1;
}
if (!is_response) {
should_patch = 1;
}
}
if (should_patch && patch_check_for_block(&route->patches, src_data, src_len)) {
log_info("Blocked content due to patch rule match on fd %d", conn->fd);
conn->patch_blocked = 1;
if (is_response) {
connection_send_error_response(pair, 502, "Bad Gateway", "Content blocked by policy");
} else {
connection_send_error_response(pair, 403, "Forbidden", "Request blocked by policy");
}
connection_close(conn->fd);
return;
}
char *output_data = src_data;
size_t output_len = src_len;
char *patched_buf = NULL;
if (should_patch) {
size_t max_output = src_len * 4;
if (max_output < CHUNK_SIZE) max_output = CHUNK_SIZE;
patched_buf = malloc(max_output);
if (patched_buf) {
patch_result_t result = patch_apply(&route->patches, src_data, src_len, patched_buf, max_output);
if (result.should_block) {
free(patched_buf);
log_info("Blocked content during patching on fd %d", conn->fd);
conn->patch_blocked = 1;
if (is_response) {
connection_send_error_response(pair, 502, "Bad Gateway", "Content blocked by policy");
} else {
connection_send_error_response(pair, 403, "Forbidden", "Request blocked by policy");
}
connection_close(conn->fd);
return;
}
if (result.output_len > 0 && result.size_delta != 0) {
output_data = patched_buf;
output_len = result.output_len;
conn->content_length_delta += result.size_delta;
log_debug("Patched data: %zu -> %zu bytes (delta: %ld)", src_len, output_len, result.size_delta);
} else if (result.output_len > 0) {
output_data = patched_buf;
output_len = result.output_len;
}
}
}
size_t space_needed = pair->write_buf.tail + output_len;
if (buffer_ensure_capacity(&pair->write_buf, space_needed) < 0) {
if (patched_buf) free(patched_buf);
log_debug("Failed to buffer data for fd %d, closing connection", conn->fd);
connection_close(conn->fd);
return;
}
memcpy(pair->write_buf.data + pair->write_buf.tail,
conn->read_buf.data + conn->read_buf.head,
data_to_forward);
pair->write_buf.tail += data_to_forward;
memcpy(pair->write_buf.data + pair->write_buf.tail, output_data, output_len);
pair->write_buf.tail += output_len;
buffer_consume(&conn->read_buf, data_to_forward);
connection_do_write(pair);
if (patched_buf) free(patched_buf);
connection_do_write(pair);
connection_modify_epoll(pair->fd, EPOLLIN | EPOLLOUT);
}
}

View File

@ -154,3 +154,100 @@ int http_parse_request(const char *data, size_t len, http_request_t *req) {
return 1;
}
int http_is_textual_content_type(const char *content_type) {
if (!content_type) return 0;
if (strncasecmp(content_type, "text/", 5) == 0) return 1;
if (strcasestr(content_type, "application/json") != NULL) return 1;
if (strcasestr(content_type, "application/xml") != NULL) return 1;
if (strcasestr(content_type, "application/javascript") != NULL) return 1;
if (strcasestr(content_type, "application/x-javascript") != NULL) return 1;
if (strcasestr(content_type, "application/x-www-form-urlencoded") != NULL) return 1;
if (strcasestr(content_type, "application/xhtml") != NULL) return 1;
if (strcasestr(content_type, "application/rss") != NULL) return 1;
if (strcasestr(content_type, "application/atom") != NULL) return 1;
if (strcasestr(content_type, "+xml") != NULL) return 1;
if (strcasestr(content_type, "+json") != NULL) return 1;
return 0;
}
int http_detect_binary_content(const char *data, size_t len) {
if (!data || len == 0) return 0;
size_t check_len = len > 512 ? 512 : len;
for (size_t i = 0; i < check_len; i++) {
unsigned char c = (unsigned char)data[i];
if (c == 0) return 1;
if (c < 0x09) return 1;
if (c > 0x0D && c < 0x20 && c != 0x1B) return 1;
}
return 0;
}
long http_get_content_length(const char *headers, size_t headers_len) {
char value[64];
if (http_find_header_value(headers, headers_len, "Content-Length", value, sizeof(value))) {
char *endptr;
long len = strtol(value, &endptr, 10);
if (endptr != value && len >= 0) {
return len;
}
}
return -1;
}
int http_find_headers_end(const char *data, size_t len, size_t *headers_end) {
if (!data || len < 4 || !headers_end) return 0;
char *end = memmem(data, len, "\r\n\r\n", 4);
if (end) {
*headers_end = (end - data) + 4;
return 1;
}
return 0;
}
int http_rewrite_content_length(char *headers, size_t *headers_len, size_t max_len, long new_length) {
if (!headers || !headers_len || *headers_len == 0) return 0;
const char *cl_start = NULL;
const char *cl_end = NULL;
const char *p = headers;
const char *end = headers + *headers_len;
while (p < end) {
const char *line_end = memchr(p, '\n', end - p);
if (!line_end) break;
if (strncasecmp(p, "Content-Length:", 15) == 0) {
cl_start = p;
cl_end = line_end + 1;
break;
}
p = line_end + 1;
}
if (!cl_start) return 0;
char new_header[64];
int new_header_len = snprintf(new_header, sizeof(new_header), "Content-Length: %ld\r\n", new_length);
if (new_header_len < 0 || (size_t)new_header_len >= sizeof(new_header)) return 0;
size_t old_len = cl_end - cl_start;
size_t new_total_len = *headers_len - old_len + new_header_len;
if (new_total_len > max_len) return 0;
size_t suffix_start = cl_end - headers;
size_t suffix_len = *headers_len - suffix_start;
memmove(headers + (cl_start - headers) + new_header_len, cl_end, suffix_len);
memcpy((char*)cl_start, new_header, new_header_len);
*headers_len = new_total_len;
return 1;
}

View File

@ -6,5 +6,10 @@
int http_parse_request(const char *data, size_t len, http_request_t *req);
int http_find_header_value(const char* data, size_t len, const char* name, char* value, size_t value_size);
int http_is_request_start(const char *data, size_t len);
int http_is_textual_content_type(const char *content_type);
int http_detect_binary_content(const char *data, size_t len);
long http_get_content_length(const char *headers, size_t headers_len);
int http_find_headers_end(const char *data, size_t len, size_t *headers_end);
int http_rewrite_content_length(char *headers, size_t *headers_len, size_t max_len, long new_length);
#endif

137
src/patch.c Normal file
View File

@ -0,0 +1,137 @@
#include "patch.h"
#include "logging.h"
#include <string.h>
#include <stdlib.h>
int patch_has_rules(const patch_config_t *config) {
return config && config->rule_count > 0;
}
int patch_check_for_block(const patch_config_t *config, const char *data, size_t len) {
if (!config || !data || len == 0) return 0;
for (int i = 0; i < config->rule_count; i++) {
const patch_rule_t *rule = &config->rules[i];
if (!rule->is_null) continue;
if (rule->key_len == 0 || rule->key_len > len) continue;
if (memmem(data, len, rule->key, rule->key_len) != NULL) {
return 1;
}
}
return 0;
}
patch_result_t patch_apply(
const patch_config_t *config,
const char *input,
size_t input_len,
char *output,
size_t output_capacity
) {
patch_result_t result = {0, 0, 0};
if (!config || config->rule_count == 0 || !input || input_len == 0) {
if (output && output_capacity >= input_len) {
memcpy(output, input, input_len);
result.output_len = input_len;
}
return result;
}
if (patch_check_for_block(config, input, input_len)) {
result.should_block = 1;
return result;
}
int has_replacements = 0;
for (int i = 0; i < config->rule_count; i++) {
if (!config->rules[i].is_null && config->rules[i].key_len > 0) {
has_replacements = 1;
break;
}
}
if (!has_replacements) {
if (output && output_capacity >= input_len) {
memcpy(output, input, input_len);
result.output_len = input_len;
}
return result;
}
size_t max_growth = 0;
for (int i = 0; i < config->rule_count; i++) {
const patch_rule_t *rule = &config->rules[i];
if (rule->is_null || rule->key_len == 0) continue;
if (rule->value_len > rule->key_len) {
size_t growth_per_match = rule->value_len - rule->key_len;
size_t max_matches = input_len / rule->key_len + 1;
max_growth += growth_per_match * max_matches;
}
}
size_t work_size = input_len + max_growth + 1;
char *work_buf = malloc(work_size);
if (!work_buf) {
if (output && output_capacity >= input_len) {
memcpy(output, input, input_len);
result.output_len = input_len;
}
return result;
}
memcpy(work_buf, input, input_len);
size_t work_len = input_len;
for (int i = 0; i < config->rule_count; i++) {
const patch_rule_t *rule = &config->rules[i];
if (rule->is_null || rule->key_len == 0) continue;
size_t pos = 0;
while (pos + rule->key_len <= work_len) {
char *found = memmem(work_buf + pos, work_len - pos, rule->key, rule->key_len);
if (!found) break;
size_t match_pos = found - work_buf;
size_t tail_len = work_len - match_pos - rule->key_len;
if (rule->value_len != rule->key_len) {
size_t new_work_len = work_len - rule->key_len + rule->value_len;
if (new_work_len >= work_size) {
size_t new_size = new_work_len + max_growth + 1;
char *new_buf = realloc(work_buf, new_size);
if (!new_buf) {
pos = match_pos + rule->key_len;
continue;
}
work_buf = new_buf;
work_size = new_size;
found = work_buf + match_pos;
}
memmove(found + rule->value_len, found + rule->key_len, tail_len);
work_len = new_work_len;
}
if (rule->value_len > 0) {
memcpy(found, rule->value, rule->value_len);
}
pos = match_pos + rule->value_len;
}
}
result.size_delta = (long)work_len - (long)input_len;
if (output && output_capacity >= work_len) {
memcpy(output, work_buf, work_len);
result.output_len = work_len;
} else if (output && output_capacity > 0) {
memcpy(output, work_buf, output_capacity);
result.output_len = output_capacity;
}
free(work_buf);
return result;
}

28
src/patch.h Normal file
View File

@ -0,0 +1,28 @@
#ifndef RPROXY_PATCH_H
#define RPROXY_PATCH_H
#include "types.h"
typedef struct {
int should_block;
size_t output_len;
long size_delta;
} patch_result_t;
patch_result_t patch_apply(
const patch_config_t *config,
const char *input,
size_t input_len,
char *output,
size_t output_capacity
);
int patch_check_for_block(
const patch_config_t *config,
const char *data,
size_t len
);
int patch_has_rules(const patch_config_t *config);
#endif

View File

@ -28,6 +28,9 @@
#define HEALTH_CHECK_TIMEOUT_MS 5000
#define MAX_UPSTREAM_RETRIES 3
#define UPSTREAM_RETRY_DELAY_MS 100
#define MAX_PATCH_RULES 64
#define MAX_PATCH_KEY_SIZE 256
#define MAX_PATCH_VALUE_SIZE 1024
typedef enum {
CONN_TYPE_UNUSED,
@ -66,6 +69,8 @@ typedef struct {
struct connection_s;
struct vhost_stats_s;
struct route_config_s;
typedef struct connection_s {
conn_type_t type;
client_state_t state;
@ -81,9 +86,29 @@ typedef struct connection_s {
time_t last_activity;
int half_closed;
int write_shutdown;
struct route_config_s *route;
int is_textual_content;
int content_type_checked;
int patch_blocked;
int response_headers_parsed;
long original_content_length;
long content_length_delta;
} connection_t;
typedef struct {
char key[MAX_PATCH_KEY_SIZE];
char value[MAX_PATCH_VALUE_SIZE];
int is_null;
size_t key_len;
size_t value_len;
} patch_rule_t;
typedef struct {
patch_rule_t rules[MAX_PATCH_RULES];
int rule_count;
} patch_config_t;
typedef struct route_config_s {
char hostname[256];
char upstream_host[256];
int upstream_port;
@ -92,6 +117,7 @@ typedef struct {
int use_auth;
char username[128];
char password_hash[256];
patch_config_t patches;
} route_config_t;
typedef struct {