This commit is contained in:
parent
23ea7644ce
commit
14ebbbec3f
5
Makefile
5
Makefile
@ -18,6 +18,7 @@ SOURCES = $(SRC_DIR)/main.c \
|
|||||||
$(SRC_DIR)/rate_limit.c \
|
$(SRC_DIR)/rate_limit.c \
|
||||||
$(SRC_DIR)/auth.c \
|
$(SRC_DIR)/auth.c \
|
||||||
$(SRC_DIR)/health_check.c \
|
$(SRC_DIR)/health_check.c \
|
||||||
|
$(SRC_DIR)/patch.c \
|
||||||
cJSON.c
|
cJSON.c
|
||||||
|
|
||||||
OBJECTS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(notdir $(SOURCES)))
|
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)/rate_limit.c \
|
||||||
$(SRC_DIR)/auth.c \
|
$(SRC_DIR)/auth.c \
|
||||||
$(SRC_DIR)/health_check.c \
|
$(SRC_DIR)/health_check.c \
|
||||||
|
$(SRC_DIR)/patch.c \
|
||||||
cJSON.c
|
cJSON.c
|
||||||
|
|
||||||
TEST_LIB_OBJECTS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(notdir $(TEST_LIB_SOURCES)))
|
TEST_LIB_OBJECTS = $(patsubst %.c,$(BUILD_DIR)/%.o,$(notdir $(TEST_LIB_SOURCES)))
|
||||||
@ -95,6 +97,9 @@ $(BUILD_DIR)/auth.o: $(SRC_DIR)/auth.c
|
|||||||
$(BUILD_DIR)/health_check.o: $(SRC_DIR)/health_check.c
|
$(BUILD_DIR)/health_check.o: $(SRC_DIR)/health_check.c
|
||||||
$(CC) $(CFLAGS) -c $< -o $@
|
$(CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
$(BUILD_DIR)/patch.o: $(SRC_DIR)/patch.c
|
||||||
|
$(CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
||||||
$(BUILD_DIR)/cJSON.o: cJSON.c
|
$(BUILD_DIR)/cJSON.o: cJSON.c
|
||||||
$(CC) $(CFLAGS) -c $< -o $@
|
$(CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
|||||||
40
README.md
40
README.md
@ -19,6 +19,7 @@ rproxy is a high-performance reverse proxy server written in C. It routes HTTP a
|
|||||||
- Health checks for upstream servers
|
- Health checks for upstream servers
|
||||||
- Automatic upstream connection retries
|
- Automatic upstream connection retries
|
||||||
- File logging support
|
- File logging support
|
||||||
|
- Stream data patching/rewriting for textual content
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
@ -49,7 +50,12 @@ Configuration is defined in `proxy_config.json`:
|
|||||||
"upstream_host": "127.0.0.1",
|
"upstream_host": "127.0.0.1",
|
||||||
"upstream_port": 5000,
|
"upstream_port": 5000,
|
||||||
"use_ssl": false,
|
"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
|
- `upstream_port`: Target server port
|
||||||
- `use_ssl`: Enable SSL for upstream connection
|
- `use_ssl`: Enable SSL for upstream connection
|
||||||
- `rewrite_host`: Rewrite Host header to upstream hostname
|
- `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
|
## Environment Variables
|
||||||
|
|
||||||
@ -136,6 +173,7 @@ kill -HUP $(pidof rproxy)
|
|||||||
- **rate_limit.c**: Per-IP rate limiting
|
- **rate_limit.c**: Per-IP rate limiting
|
||||||
- **auth.c**: Dashboard authentication
|
- **auth.c**: Dashboard authentication
|
||||||
- **health_check.c**: Upstream health monitoring
|
- **health_check.c**: Upstream health monitoring
|
||||||
|
- **patch.c**: Stream data patching engine
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
|
|||||||
77
src/config.c
77
src/config.c
@ -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)",
|
log_info("Route configured: %s -> %s:%d (SSL: %s, Rewrite Host: %s, Auth: %s)",
|
||||||
route->hostname, route->upstream_host, route->upstream_port,
|
route->hostname, route->upstream_host, route->upstream_port,
|
||||||
route->use_ssl ? "yes" : "no", route->rewrite_host ? "yes" : "no",
|
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->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++;
|
i++;
|
||||||
}
|
}
|
||||||
new_config.route_count = i;
|
new_config.route_count = i;
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
#include "ssl_handler.h"
|
#include "ssl_handler.h"
|
||||||
#include "dashboard.h"
|
#include "dashboard.h"
|
||||||
#include "auth.h"
|
#include "auth.h"
|
||||||
|
#include "patch.h"
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@ -437,6 +438,8 @@ void connection_connect_to_upstream(connection_t *client, const char *data, size
|
|||||||
client->pair = up;
|
client->pair = up;
|
||||||
up->pair = client;
|
up->pair = client;
|
||||||
up->vhost_stats = client->vhost_stats;
|
up->vhost_stats = client->vhost_stats;
|
||||||
|
up->route = route;
|
||||||
|
client->route = route;
|
||||||
|
|
||||||
if (buffer_init(&up->read_buf, CHUNK_SIZE) < 0) {
|
if (buffer_init(&up->read_buf, CHUNK_SIZE) < 0) {
|
||||||
close(up_fd);
|
close(up_fd);
|
||||||
@ -724,21 +727,104 @@ static void handle_forwarding(connection_t *conn) {
|
|||||||
connection_do_write(pair);
|
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 (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);
|
log_debug("Failed to buffer data for fd %d, closing connection", conn->fd);
|
||||||
connection_close(conn->fd);
|
connection_close(conn->fd);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
memcpy(pair->write_buf.data + pair->write_buf.tail,
|
memcpy(pair->write_buf.data + pair->write_buf.tail, output_data, output_len);
|
||||||
conn->read_buf.data + conn->read_buf.head,
|
pair->write_buf.tail += output_len;
|
||||||
data_to_forward);
|
|
||||||
pair->write_buf.tail += data_to_forward;
|
|
||||||
buffer_consume(&conn->read_buf, data_to_forward);
|
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);
|
connection_modify_epoll(pair->fd, EPOLLIN | EPOLLOUT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
97
src/http.c
97
src/http.c
@ -154,3 +154,100 @@ int http_parse_request(const char *data, size_t len, http_request_t *req) {
|
|||||||
|
|
||||||
return 1;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -6,5 +6,10 @@
|
|||||||
int http_parse_request(const char *data, size_t len, http_request_t *req);
|
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_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_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
|
#endif
|
||||||
|
|||||||
137
src/patch.c
Normal file
137
src/patch.c
Normal 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
28
src/patch.h
Normal 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
|
||||||
26
src/types.h
26
src/types.h
@ -28,6 +28,9 @@
|
|||||||
#define HEALTH_CHECK_TIMEOUT_MS 5000
|
#define HEALTH_CHECK_TIMEOUT_MS 5000
|
||||||
#define MAX_UPSTREAM_RETRIES 3
|
#define MAX_UPSTREAM_RETRIES 3
|
||||||
#define UPSTREAM_RETRY_DELAY_MS 100
|
#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 {
|
typedef enum {
|
||||||
CONN_TYPE_UNUSED,
|
CONN_TYPE_UNUSED,
|
||||||
@ -66,6 +69,8 @@ typedef struct {
|
|||||||
struct connection_s;
|
struct connection_s;
|
||||||
struct vhost_stats_s;
|
struct vhost_stats_s;
|
||||||
|
|
||||||
|
struct route_config_s;
|
||||||
|
|
||||||
typedef struct connection_s {
|
typedef struct connection_s {
|
||||||
conn_type_t type;
|
conn_type_t type;
|
||||||
client_state_t state;
|
client_state_t state;
|
||||||
@ -81,9 +86,29 @@ typedef struct connection_s {
|
|||||||
time_t last_activity;
|
time_t last_activity;
|
||||||
int half_closed;
|
int half_closed;
|
||||||
int write_shutdown;
|
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;
|
} connection_t;
|
||||||
|
|
||||||
typedef struct {
|
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 hostname[256];
|
||||||
char upstream_host[256];
|
char upstream_host[256];
|
||||||
int upstream_port;
|
int upstream_port;
|
||||||
@ -92,6 +117,7 @@ typedef struct {
|
|||||||
int use_auth;
|
int use_auth;
|
||||||
char username[128];
|
char username[128];
|
||||||
char password_hash[256];
|
char password_hash[256];
|
||||||
|
patch_config_t patches;
|
||||||
} route_config_t;
|
} route_config_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user