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 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
@ -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
- Graceful shutdown with connection draining
- Live configuration reload via SIGHUP
- Dashboard authentication (HTTP Basic Auth)
- Per-route authentication (HTTP Basic Auth)
- Dashboard authentication
- Rate limiting per client IP
- Health checks for upstream servers
- 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)
- SQLite3
- pthreads
- cJSON library
- cJSON (bundled)
## Build
@ -35,7 +38,16 @@ rproxy is a high-performance reverse proxy server written in C. It routes HTTP a
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
@ -51,9 +63,11 @@ Configuration is defined in `proxy_config.json`:
"upstream_port": 5000,
"use_ssl": false,
"rewrite_host": true,
"use_auth": true,
"username": "admin",
"password": "secret",
"patch": {
"old_string": "new_string",
"secret_key": "[REDACTED]",
"blocked_content": null
}
}
@ -61,18 +75,23 @@ Configuration is defined in `proxy_config.json`:
}
```
- `port`: Listening port for incoming connections
- `reverse_proxy`: Array of routing rules
- `hostname`: Host header to match for routing
- `upstream_host`: Target server hostname/IP
- `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)
### Route Options
| Option | Type | Description |
|--------|------|-------------|
| `hostname` | string | Host header to match for routing |
| `upstream_host` | string | Target server hostname or IP |
| `upstream_port` | integer | Target server port (1-65535) |
| `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
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
{
@ -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
- **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` to the client
- Blocked requests return `403 Forbidden` to the client
Blocked responses return `502 Bad Gateway`. Blocked requests return `403 Forbidden`.
Supported textual content types:
- `text/*` (text/html, text/plain, text/css, etc.)
- `text/*`
- `application/json`
- `application/xml`
- `application/javascript`
- `application/x-www-form-urlencoded`
- Any content type with `+xml` or `+json` suffix
- Content types with `+xml` or `+json` suffix
## Environment Variables
@ -121,35 +138,23 @@ Supported textual content types:
If no config file is specified, defaults to `proxy_config.json`.
Examples:
```bash
# Basic usage
./rproxy
# With custom config
./rproxy /etc/rproxy/config.json
# With debug logging
DEBUG=1 ./rproxy
# With file logging
LOG_FILE=/var/log/rproxy.log ./rproxy
# With rate limiting (100 requests/minute)
RATE_LIMIT=100 ./rproxy
# With dashboard authentication
DASHBOARD_USER=admin DASHBOARD_PASS=secret ./rproxy
# Reload configuration
kill -HUP $(pidof rproxy)
SSL_VERIFY=0 ./rproxy
kill -HUP $(pidof rproxy) # Reload configuration
```
## Endpoints
- Dashboard: `http://localhost:{port}/rproxy/dashboard`
- API Stats: `http://localhost:{port}/rproxy/api/stats`
| Path | Description |
|------|-------------|
| `/rproxy/dashboard` | Web-based monitoring dashboard |
| `/rproxy/api/stats` | JSON API for statistics |
## Signals
@ -161,24 +166,22 @@ kill -HUP $(pidof rproxy)
## Architecture
- **main.c**: Entry point, event loop, signal handling
- **connection.c**: Connection management, epoll handling
- **http.c**: HTTP request/response parsing
- **ssl_handler.c**: SSL/TLS connection handling
- **monitor.c**: System and per-vhost statistics collection
- **dashboard.c**: Web dashboard generation
- **config.c**: JSON configuration parsing
- **buffer.c**: Circular buffer implementation
- **logging.c**: Logging utilities
- **rate_limit.c**: Per-IP rate limiting
- **auth.c**: Dashboard authentication
- **health_check.c**: Upstream health monitoring
- **patch.c**: Stream data patching engine
| Module | Description |
|--------|-------------|
| `main.c` | Entry point, event loop, signal handling |
| `connection.c` | Connection management, epoll handling |
| `http.c` | HTTP request/response parsing |
| `ssl_handler.c` | SSL/TLS connection handling |
| `monitor.c` | System and per-vhost statistics collection |
| `dashboard.c` | Web dashboard generation |
| `config.c` | JSON configuration parsing with hot-reload |
| `buffer.c` | Circular buffer implementation |
| `logging.c` | Logging utilities |
| `rate_limit.c` | Per-IP rate limiting with sliding window |
| `auth.c` | HTTP Basic Auth implementation |
| `health_check.c` | Upstream health monitoring |
| `patch.c` | Stream data patching engine |
## Testing
## License
```bash
make test
```
Runs unit tests for core components.
See LICENSE file for details.

View File

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

View File

@ -15,6 +15,7 @@
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
@ -46,13 +47,21 @@ int connection_set_non_blocking(int fd) {
void connection_set_tcp_keepalive(int fd) {
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;
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;
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;
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) {
@ -297,7 +306,12 @@ void connection_send_error_response(connection_t *conn, int code, const char* st
time_t now = time(NULL);
struct tm *gmt = gmtime(&now);
char date_buf[64];
strftime(date_buf, sizeof(date_buf), "%a, %d %b %Y %H:%M:%S GMT", gmt);
if (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];
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);
struct tm *gmt = gmtime(&now);
char date_buf[64];
strftime(date_buf, sizeof(date_buf), "%a, %d %b %Y %H:%M:%S GMT", gmt);
if (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";
char response[ERROR_RESPONSE_SIZE];
@ -385,6 +404,17 @@ static int try_upstream_connect(struct sockaddr_in *addr, int *out_fd) {
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) {
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);
if (buffer_init(&up->read_buf, CHUNK_SIZE) < 0) {
close(up_fd);
memset(up, 0, sizeof(connection_t));
up->type = CONN_TYPE_UNUSED;
up->fd = -1;
client->pair = NULL;
cleanup_upstream_partial(up, up_fd, client, 0, 0);
connection_send_error_response(client, 502, "Bad Gateway", "Memory allocation failed");
return;
}
if (buffer_init(&up->write_buf, CHUNK_SIZE) < 0) {
buffer_free(&up->read_buf);
close(up_fd);
memset(up, 0, sizeof(connection_t));
up->type = CONN_TYPE_UNUSED;
up->fd = -1;
client->pair = NULL;
cleanup_upstream_partial(up, up_fd, client, 1, 0);
connection_send_error_response(client, 502, "Bad Gateway", "Memory allocation failed");
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) {
memcpy(up->write_buf.data, data_to_send, len_to_send);
up->write_buf.tail = len_to_send;
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);
up->write_buf.tail = len_to_send;
if (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) {
up->ssl = SSL_new(ssl_ctx);
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;
}
@ -665,7 +691,12 @@ static void handle_client_read(connection_t *conn) {
route_config_t *route = config_find_route(conn->request.host);
if (route && route->use_auth) {
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));
char error_msg[256] = "";
@ -838,7 +869,12 @@ static void handle_forwarding(connection_t *conn) {
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;
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;
}
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;