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

This commit is contained in:
retoor 2025-12-12 22:20:34 +01:00
parent bd940dd806
commit 9355fcd309
3 changed files with 133 additions and 86 deletions

117
README.md
View File

@ -1,6 +1,8 @@
# rproxy # rproxy
rproxy is a high-performance reverse proxy server written in C. It routes HTTP and WebSocket requests to upstream services based on hostname, with support for SSL/TLS termination and connection pooling. Author: retoor <retoor@molodetz.nl>
A high-performance reverse proxy server written in C. Routes HTTP and WebSocket requests to upstream services based on hostname, with support for SSL/TLS connections and real-time monitoring.
## Features ## Features
@ -14,7 +16,8 @@ rproxy is a high-performance reverse proxy server written in C. It routes HTTP a
- Epoll-based event handling for high concurrency - Epoll-based event handling for high concurrency
- Graceful shutdown with connection draining - Graceful shutdown with connection draining
- Live configuration reload via SIGHUP - Live configuration reload via SIGHUP
- Dashboard authentication (HTTP Basic Auth) - Per-route authentication (HTTP Basic Auth)
- Dashboard authentication
- Rate limiting per client IP - Rate limiting per client IP
- Health checks for upstream servers - Health checks for upstream servers
- Automatic upstream connection retries - Automatic upstream connection retries
@ -27,7 +30,7 @@ rproxy is a high-performance reverse proxy server written in C. It routes HTTP a
- OpenSSL (libssl, libcrypto) - OpenSSL (libssl, libcrypto)
- SQLite3 - SQLite3
- pthreads - pthreads
- cJSON library - cJSON (bundled)
## Build ## Build
@ -35,7 +38,16 @@ rproxy is a high-performance reverse proxy server written in C. It routes HTTP a
make make
``` ```
This compiles the source files in `src/` and produces the `rproxy` executable. Compiles the source files in `src/` and produces the `rproxy` executable.
## Testing
```bash
make test # Run unit tests
make coverage # Run tests with coverage report (minimum 60% required)
make coverage-html # Generate HTML coverage report
make valgrind # Run tests with memory leak detection
```
## Configuration ## Configuration
@ -51,9 +63,11 @@ Configuration is defined in `proxy_config.json`:
"upstream_port": 5000, "upstream_port": 5000,
"use_ssl": false, "use_ssl": false,
"rewrite_host": true, "rewrite_host": true,
"use_auth": true,
"username": "admin",
"password": "secret",
"patch": { "patch": {
"old_string": "new_string", "old_string": "new_string",
"secret_key": "[REDACTED]",
"blocked_content": null "blocked_content": null
} }
} }
@ -61,18 +75,23 @@ Configuration is defined in `proxy_config.json`:
} }
``` ```
- `port`: Listening port for incoming connections ### Route Options
- `reverse_proxy`: Array of routing rules
- `hostname`: Host header to match for routing | Option | Type | Description |
- `upstream_host`: Target server hostname/IP |--------|------|-------------|
- `upstream_port`: Target server port | `hostname` | string | Host header to match for routing |
- `use_ssl`: Enable SSL for upstream connection | `upstream_host` | string | Target server hostname or IP |
- `rewrite_host`: Rewrite Host header to upstream hostname | `upstream_port` | integer | Target server port (1-65535) |
- `patch`: Optional object for stream data patching (see below) | `use_ssl` | boolean | Enable SSL/TLS for upstream connection |
| `rewrite_host` | boolean | Rewrite Host header to upstream hostname |
| `use_auth` | boolean | Enable HTTP Basic Auth for this route |
| `username` | string | Authentication username |
| `password` | string | Authentication password |
| `patch` | object | Stream data patching rules |
### Data Patching ### 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. The `patch` configuration allows rewriting or blocking content in HTTP streams. Patch rules are applied to textual content only. Binary content passes through unmodified.
```json ```json
{ {
@ -86,19 +105,17 @@ The `patch` configuration allows rewriting or blocking content in HTTP streams.
- **String replacement**: Each key-value pair defines a find-replace rule - **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 - **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) - **Bidirectional**: Patches apply to both requests and responses
When content is blocked: Blocked responses return `502 Bad Gateway`. Blocked requests return `403 Forbidden`.
- Blocked responses return `502 Bad Gateway` to the client
- Blocked requests return `403 Forbidden` to the client
Supported textual content types: Supported textual content types:
- `text/*` (text/html, text/plain, text/css, etc.) - `text/*`
- `application/json` - `application/json`
- `application/xml` - `application/xml`
- `application/javascript` - `application/javascript`
- `application/x-www-form-urlencoded` - `application/x-www-form-urlencoded`
- Any content type with `+xml` or `+json` suffix - Content types with `+xml` or `+json` suffix
## Environment Variables ## Environment Variables
@ -121,35 +138,23 @@ Supported textual content types:
If no config file is specified, defaults to `proxy_config.json`. If no config file is specified, defaults to `proxy_config.json`.
Examples:
```bash ```bash
# Basic usage
./rproxy ./rproxy
# With custom config
./rproxy /etc/rproxy/config.json ./rproxy /etc/rproxy/config.json
# With debug logging
DEBUG=1 ./rproxy DEBUG=1 ./rproxy
# With file logging
LOG_FILE=/var/log/rproxy.log ./rproxy LOG_FILE=/var/log/rproxy.log ./rproxy
# With rate limiting (100 requests/minute)
RATE_LIMIT=100 ./rproxy RATE_LIMIT=100 ./rproxy
# With dashboard authentication
DASHBOARD_USER=admin DASHBOARD_PASS=secret ./rproxy DASHBOARD_USER=admin DASHBOARD_PASS=secret ./rproxy
SSL_VERIFY=0 ./rproxy
# Reload configuration kill -HUP $(pidof rproxy) # Reload configuration
kill -HUP $(pidof rproxy)
``` ```
## Endpoints ## Endpoints
- Dashboard: `http://localhost:{port}/rproxy/dashboard` | Path | Description |
- API Stats: `http://localhost:{port}/rproxy/api/stats` |------|-------------|
| `/rproxy/dashboard` | Web-based monitoring dashboard |
| `/rproxy/api/stats` | JSON API for statistics |
## Signals ## Signals
@ -161,24 +166,22 @@ kill -HUP $(pidof rproxy)
## Architecture ## Architecture
- **main.c**: Entry point, event loop, signal handling | Module | Description |
- **connection.c**: Connection management, epoll handling |--------|-------------|
- **http.c**: HTTP request/response parsing | `main.c` | Entry point, event loop, signal handling |
- **ssl_handler.c**: SSL/TLS connection handling | `connection.c` | Connection management, epoll handling |
- **monitor.c**: System and per-vhost statistics collection | `http.c` | HTTP request/response parsing |
- **dashboard.c**: Web dashboard generation | `ssl_handler.c` | SSL/TLS connection handling |
- **config.c**: JSON configuration parsing | `monitor.c` | System and per-vhost statistics collection |
- **buffer.c**: Circular buffer implementation | `dashboard.c` | Web dashboard generation |
- **logging.c**: Logging utilities | `config.c` | JSON configuration parsing with hot-reload |
- **rate_limit.c**: Per-IP rate limiting | `buffer.c` | Circular buffer implementation |
- **auth.c**: Dashboard authentication | `logging.c` | Logging utilities |
- **health_check.c**: Upstream health monitoring | `rate_limit.c` | Per-IP rate limiting with sliding window |
- **patch.c**: Stream data patching engine | `auth.c` | HTTP Basic Auth implementation |
| `health_check.c` | Upstream health monitoring |
| `patch.c` | Stream data patching engine |
## Testing ## License
```bash See LICENSE file for details.
make test
```
Runs unit tests for core components.

View File

@ -86,19 +86,27 @@ static char* read_file_to_string(const char *filename) {
FILE *f = fopen(filename, "rb"); FILE *f = fopen(filename, "rb");
if (!f) return NULL; if (!f) return NULL;
fseek(f, 0, SEEK_END); if (fseek(f, 0, SEEK_END) != 0) {
fclose(f);
return NULL;
}
long length = ftell(f); long length = ftell(f);
if (length < 0 || length > 1024*1024) { if (length < 0 || length > 1024*1024) {
fclose(f); fclose(f);
return NULL; return NULL;
} }
fseek(f, 0, SEEK_SET); if (fseek(f, 0, SEEK_SET) != 0) {
char *buffer = malloc(length + 1); fclose(f);
if (buffer) { return NULL;
size_t read_len = fread(buffer, 1, length, f);
buffer[read_len] = '\0';
} }
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); fclose(f);
return buffer; return buffer;
} }

View File

@ -15,6 +15,7 @@
#include <unistd.h> #include <unistd.h>
#include <errno.h> #include <errno.h>
#include <fcntl.h> #include <fcntl.h>
#include <limits.h>
#include <sys/socket.h> #include <sys/socket.h>
#include <netinet/in.h> #include <netinet/in.h>
#include <netinet/tcp.h> #include <netinet/tcp.h>
@ -46,13 +47,21 @@ int connection_set_non_blocking(int fd) {
void connection_set_tcp_keepalive(int fd) { void connection_set_tcp_keepalive(int fd) {
int yes = 1; int yes = 1;
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &yes, sizeof(yes)); 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; int idle = 60;
setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle)); 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; int interval = 10;
setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(interval)); 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; int maxpkt = 6;
setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &maxpkt, sizeof(maxpkt)); 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 connection_add_to_epoll(int fd, uint32_t events) { void connection_add_to_epoll(int fd, uint32_t events) {
@ -297,7 +306,12 @@ void connection_send_error_response(connection_t *conn, int code, const char* st
time_t now = time(NULL); time_t now = time(NULL);
struct tm *gmt = gmtime(&now); struct tm *gmt = gmtime(&now);
char date_buf[64]; char date_buf[64];
if (gmt) {
strftime(date_buf, sizeof(date_buf), "%a, %d %b %Y %H:%M:%S GMT", gmt); strftime(date_buf, sizeof(date_buf), "%a, %d %b %Y %H:%M:%S GMT", gmt);
} else {
strncpy(date_buf, "Thu, 01 Jan 1970 00:00:00 GMT", sizeof(date_buf) - 1);
date_buf[sizeof(date_buf) - 1] = '\0';
}
char response[ERROR_RESPONSE_SIZE]; char response[ERROR_RESPONSE_SIZE];
int len = snprintf(response, sizeof(response), int len = snprintf(response, sizeof(response),
@ -331,7 +345,12 @@ void connection_send_auth_required(connection_t *conn, const char *realm) {
time_t now = time(NULL); time_t now = time(NULL);
struct tm *gmt = gmtime(&now); struct tm *gmt = gmtime(&now);
char date_buf[64]; char date_buf[64];
if (gmt) {
strftime(date_buf, sizeof(date_buf), "%a, %d %b %Y %H:%M:%S GMT", gmt); strftime(date_buf, sizeof(date_buf), "%a, %d %b %Y %H:%M:%S GMT", gmt);
} else {
strncpy(date_buf, "Thu, 01 Jan 1970 00:00:00 GMT", sizeof(date_buf) - 1);
date_buf[sizeof(date_buf) - 1] = '\0';
}
const char *body = "401 Unauthorized - Authentication required"; const char *body = "401 Unauthorized - Authentication required";
char response[ERROR_RESPONSE_SIZE]; char response[ERROR_RESPONSE_SIZE];
@ -385,6 +404,17 @@ static int try_upstream_connect(struct sockaddr_in *addr, int *out_fd) {
return 0; return 0;
} }
static void cleanup_upstream_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;
}
void connection_connect_to_upstream(connection_t *client, const char *data, size_t data_len) { void connection_connect_to_upstream(connection_t *client, const char *data, size_t data_len) {
if (!client || !data) return; if (!client || !data) return;
@ -466,21 +496,12 @@ void connection_connect_to_upstream(connection_t *client, const char *data, size
config_ref_inc(up->config); config_ref_inc(up->config);
if (buffer_init(&up->read_buf, CHUNK_SIZE) < 0) { if (buffer_init(&up->read_buf, CHUNK_SIZE) < 0) {
close(up_fd); cleanup_upstream_partial(up, up_fd, client, 0, 0);
memset(up, 0, sizeof(connection_t));
up->type = CONN_TYPE_UNUSED;
up->fd = -1;
client->pair = NULL;
connection_send_error_response(client, 502, "Bad Gateway", "Memory allocation failed"); connection_send_error_response(client, 502, "Bad Gateway", "Memory allocation failed");
return; return;
} }
if (buffer_init(&up->write_buf, CHUNK_SIZE) < 0) { if (buffer_init(&up->write_buf, CHUNK_SIZE) < 0) {
buffer_free(&up->read_buf); cleanup_upstream_partial(up, up_fd, client, 1, 0);
close(up_fd);
memset(up, 0, sizeof(connection_t));
up->type = CONN_TYPE_UNUSED;
up->fd = -1;
client->pair = NULL;
connection_send_error_response(client, 502, "Bad Gateway", "Memory allocation failed"); connection_send_error_response(client, 502, "Bad Gateway", "Memory allocation failed");
return; return;
} }
@ -521,10 +542,14 @@ void connection_connect_to_upstream(connection_t *client, const char *data, size
} }
} }
if (buffer_ensure_capacity(&up->write_buf, len_to_send) == 0) { if (buffer_ensure_capacity(&up->write_buf, len_to_send) < 0) {
if (modified_request) free(modified_request);
cleanup_upstream_partial(up, up_fd, client, 1, 1);
connection_send_error_response(client, 502, "Bad Gateway", "Memory allocation failed");
return;
}
memcpy(up->write_buf.data, data_to_send, len_to_send); memcpy(up->write_buf.data, data_to_send, len_to_send);
up->write_buf.tail = len_to_send; up->write_buf.tail = len_to_send;
}
if (modified_request) { if (modified_request) {
free(modified_request); free(modified_request);
@ -533,7 +558,8 @@ void connection_connect_to_upstream(connection_t *client, const char *data, size
if (route->use_ssl) { if (route->use_ssl) {
up->ssl = SSL_new(ssl_ctx); up->ssl = SSL_new(ssl_ctx);
if (!up->ssl) { if (!up->ssl) {
connection_close(client->fd); cleanup_upstream_partial(up, up_fd, client, 1, 1);
connection_send_error_response(client, 502, "Bad Gateway", "SSL initialization failed");
return; return;
} }
@ -665,7 +691,12 @@ static void handle_client_read(connection_t *conn) {
route_config_t *route = config_find_route(conn->request.host); route_config_t *route = config_find_route(conn->request.host);
if (route && route->use_auth) { if (route && route->use_auth) {
char auth_header[1024] = ""; char auth_header[1024] = "";
const char *headers_start = data_start + (strstr(data_start, "\r\n") - data_start + 2); const char *first_crlf = strstr(data_start, "\r\n");
if (!first_crlf || first_crlf >= data_start + headers_len - 2) {
connection_send_error_response(conn, 400, "Bad Request", "Malformed HTTP request headers.");
return;
}
const char *headers_start = first_crlf + 2;
http_find_header_value(headers_start, headers_len - (headers_start - data_start), "Authorization", auth_header, sizeof(auth_header)); http_find_header_value(headers_start, headers_len - (headers_start - data_start), "Authorization", auth_header, sizeof(auth_header));
char error_msg[256] = ""; char error_msg[256] = "";
@ -838,7 +869,12 @@ static void handle_forwarding(connection_t *conn) {
if (result.output_len > 0 && result.size_delta != 0) { if (result.output_len > 0 && result.size_delta != 0) {
output_data = patched_buf; output_data = patched_buf;
output_len = result.output_len; output_len = result.output_len;
if ((result.size_delta > 0 && conn->content_length_delta > LONG_MAX - result.size_delta) ||
(result.size_delta < 0 && conn->content_length_delta < LONG_MIN - result.size_delta)) {
log_debug("Content-length delta overflow detected on fd %d", conn->fd);
} else {
conn->content_length_delta += result.size_delta; conn->content_length_delta += result.size_delta;
}
log_debug("Patched data: %zu -> %zu bytes (delta: %ld)", src_len, output_len, 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) { } else if (result.output_len > 0) {
output_data = patched_buf; output_data = patched_buf;