From 9355fcd309373b4139ec550dfe2c8120dce7fb90 Mon Sep 17 00:00:00 2001 From: retoor Date: Fri, 12 Dec 2025 22:20:34 +0100 Subject: [PATCH] Update. --- README.md | 117 ++++++++++++++++++++++++----------------------- src/config.c | 20 +++++--- src/connection.c | 82 +++++++++++++++++++++++---------- 3 files changed, 133 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index 27365b4..861ee97 100644 --- a/README.md +++ b/README.md @@ -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 + +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. \ No newline at end of file +See LICENSE file for details. diff --git a/src/config.c b/src/config.c index 5f3c34d..ab8934f 100644 --- a/src/config.c +++ b/src/config.c @@ -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; } diff --git a/src/connection.c b/src/connection.c index ccc8293..44021eb 100644 --- a/src/connection.c +++ b/src/connection.c @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -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;