diff --git a/CHANGELOG.md b/CHANGELOG.md index e7e5c88..8e56dc0 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,14 @@ + +## Version 0.10.0 - 2026-01-06 + +update c, h files + +**Changes:** 4 files, 145 lines +**Languages:** C (145 lines) + ## Version 0.9.0 - 2026-01-01 update c files diff --git a/src/connection.c b/src/connection.c index 6fde003..b4e3429 100755 --- a/src/connection.c +++ b/src/connection.c @@ -776,14 +776,12 @@ static void handle_client_read(connection_t *conn) { clock_gettime(CLOCK_MONOTONIC, &ts); conn->request_start_time = ts.tv_sec + ts.tv_nsec / 1e9; - #define DASHBOARD_PATH "/rproxy/dashboard" - #define STATS_PATH "/rproxy/api/stats" + if (http_uri_is_internal_route(conn->request.uri)) { + char normalized_uri[2048]; + http_normalize_uri_path(conn->request.uri, normalized_uri, sizeof(normalized_uri)); - if (strncmp(conn->request.uri, DASHBOARD_PATH, sizeof(DASHBOARD_PATH) - 1) == 0 || - strncmp(conn->request.uri, STATS_PATH, sizeof(STATS_PATH) - 1) == 0) { - - log_info("[ROUTING-INTERNAL] Serving internal route %s for fd=%d", - conn->request.uri, conn->fd); + log_info("[ROUTING-INTERNAL] Serving internal route %s (normalized: %s) for fd=%d", + conn->request.uri, normalized_uri, conn->fd); if (conn->pair) { connection_close(conn->pair->fd); @@ -791,10 +789,14 @@ static void handle_client_read(connection_t *conn) { } conn->state = CLIENT_STATE_SERVING_INTERNAL; - if (strncmp(conn->request.uri, DASHBOARD_PATH, sizeof(DASHBOARD_PATH) - 1) == 0) { + if (strncmp(normalized_uri, "/rproxy/dashboard", 17) == 0) { dashboard_serve(conn, data_start, headers_len); - } else { + } else if (strncmp(normalized_uri, "/rproxy/api/stats", 17) == 0) { dashboard_serve_stats_api(conn, data_start, headers_len); + } else { + connection_send_error_response(conn, 404, "Not Found", "Internal route not found."); + buffer_consume(buf, total_request_len); + return; } buffer_consume(buf, total_request_len); @@ -805,9 +807,6 @@ static void handle_client_read(connection_t *conn) { return; } - #undef DASHBOARD_PATH - #undef STATS_PATH - route_config_t *route = config_find_route(conn->request.host); if (route && route->use_auth) { char auth_header[1024] = ""; diff --git a/src/http.c b/src/http.c index 9c67147..4058355 100755 --- a/src/http.c +++ b/src/http.c @@ -308,3 +308,71 @@ int http_extract_status_code(const char *data, size_t len) { return (status >= 100 && status < 600) ? status : 0; } + +void http_normalize_uri_path(const char *uri, char *normalized, size_t normalized_size) { + if (!uri || !normalized || normalized_size == 0) return; + + size_t uri_len = strlen(uri); + size_t out_pos = 0; + size_t i = 0; + + const char *query = strchr(uri, '?'); + size_t path_len = query ? (size_t)(query - uri) : uri_len; + + while (i < path_len && out_pos < normalized_size - 1) { + if (uri[i] == '/') { + if (out_pos == 0 || normalized[out_pos - 1] != '/') { + normalized[out_pos++] = '/'; + } + i++; + + if (i < path_len && uri[i] == '.') { + if (i + 1 >= path_len || uri[i + 1] == '/' || uri[i + 1] == '?') { + i++; + continue; + } + if (uri[i + 1] == '.' && (i + 2 >= path_len || uri[i + 2] == '/' || uri[i + 2] == '?')) { + if (out_pos > 1) { + out_pos--; + while (out_pos > 0 && normalized[out_pos - 1] != '/') { + out_pos--; + } + } + i += 2; + continue; + } + } + } else { + normalized[out_pos++] = uri[i++]; + } + } + + if (query && out_pos < normalized_size - 1) { + size_t query_len = uri_len - (query - uri); + if (out_pos + query_len >= normalized_size) { + query_len = normalized_size - out_pos - 1; + } + memcpy(normalized + out_pos, query, query_len); + out_pos += query_len; + } + + normalized[out_pos] = '\0'; + + if (out_pos == 0 && normalized_size > 1) { + normalized[0] = '/'; + normalized[1] = '\0'; + } +} + +int http_uri_is_internal_route(const char *uri) { + if (!uri) return 0; + + char normalized[2048]; + http_normalize_uri_path(uri, normalized, sizeof(normalized)); + + if (strncmp(normalized, "/rproxy/", 8) == 0) { + return 1; + } + + return 0; +} diff --git a/src/http.h b/src/http.h index 3fbbb48..985685b 100755 --- a/src/http.h +++ b/src/http.h @@ -13,5 +13,7 @@ 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); int http_find_header_line_bounds(const char* data, size_t len, const char* name, const char** line_start, const char** line_end); int http_extract_status_code(const char *data, size_t len); +int http_uri_is_internal_route(const char *uri); +void http_normalize_uri_path(const char *uri, char *normalized, size_t normalized_size); #endif diff --git a/tests/test_http.c b/tests/test_http.c index 2cee81e..e1c74e2 100755 --- a/tests/test_http.c +++ b/tests/test_http.c @@ -182,6 +182,56 @@ void test_http_malformed_requests(void) { TEST_SUITE_END(); } +void test_http_uri_normalization(void) { + TEST_SUITE_BEGIN("HTTP URI Path Normalization"); + + char normalized[256]; + + http_normalize_uri_path("/rproxy/dashboard", normalized, sizeof(normalized)); + TEST_ASSERT_STR_EQ("/rproxy/dashboard", normalized, "Normal path unchanged"); + + http_normalize_uri_path("//rproxy/dashboard", normalized, sizeof(normalized)); + TEST_ASSERT_STR_EQ("/rproxy/dashboard", normalized, "Double slash at start collapsed"); + + http_normalize_uri_path("/rproxy//dashboard", normalized, sizeof(normalized)); + TEST_ASSERT_STR_EQ("/rproxy/dashboard", normalized, "Double slash in middle collapsed"); + + http_normalize_uri_path("/./rproxy/dashboard", normalized, sizeof(normalized)); + TEST_ASSERT_STR_EQ("/rproxy/dashboard", normalized, "Dot segment at start removed"); + + http_normalize_uri_path("/rproxy/./dashboard", normalized, sizeof(normalized)); + TEST_ASSERT_STR_EQ("/rproxy/dashboard", normalized, "Dot segment in middle removed"); + + http_normalize_uri_path("/foo/../rproxy/dashboard", normalized, sizeof(normalized)); + TEST_ASSERT_STR_EQ("/rproxy/dashboard", normalized, "Parent reference resolved"); + + http_normalize_uri_path("/rproxy/dashboard?foo=bar", normalized, sizeof(normalized)); + TEST_ASSERT_STR_EQ("/rproxy/dashboard?foo=bar", normalized, "Query string preserved"); + + http_normalize_uri_path("///rproxy///dashboard///", normalized, sizeof(normalized)); + TEST_ASSERT_STR_EQ("/rproxy/dashboard/", normalized, "Multiple slashes collapsed"); + + TEST_SUITE_END(); +} + +void test_http_internal_route_detection(void) { + TEST_SUITE_BEGIN("HTTP Internal Route Detection"); + + TEST_ASSERT_EQ(1, http_uri_is_internal_route("/rproxy/dashboard"), "Normal dashboard path detected"); + TEST_ASSERT_EQ(1, http_uri_is_internal_route("/rproxy/api/stats"), "Normal stats path detected"); + TEST_ASSERT_EQ(1, http_uri_is_internal_route("//rproxy/dashboard"), "Double slash bypass detected"); + TEST_ASSERT_EQ(1, http_uri_is_internal_route("/./rproxy/dashboard"), "Dot segment bypass detected"); + TEST_ASSERT_EQ(1, http_uri_is_internal_route("/foo/../rproxy/dashboard"), "Parent reference bypass detected"); + TEST_ASSERT_EQ(1, http_uri_is_internal_route("/rproxy//dashboard"), "Slash in path bypass detected"); + TEST_ASSERT_EQ(1, http_uri_is_internal_route("/rproxy/./api/stats"), "Dot in internal path detected"); + TEST_ASSERT_EQ(0, http_uri_is_internal_route("/api/data"), "Non-internal path rejected"); + TEST_ASSERT_EQ(0, http_uri_is_internal_route("/"), "Root path rejected"); + TEST_ASSERT_EQ(0, http_uri_is_internal_route("/rproxynotreal"), "Similar prefix rejected"); + TEST_ASSERT_EQ(0, http_uri_is_internal_route(NULL), "NULL path rejected"); + + TEST_SUITE_END(); +} + void run_http_tests(void) { test_http_parse_get_request(); test_http_parse_post_request(); @@ -193,4 +243,6 @@ void run_http_tests(void) { test_http_parse_host_with_port(); test_http_is_request_start(); test_http_malformed_requests(); + test_http_uri_normalization(); + test_http_internal_route_detection(); }