|
#include "test_framework.h"
|
|
#include "../src/types.h"
|
|
#include "../src/http.h"
|
|
#include "../src/config.h"
|
|
#include <stdio.h>
|
|
#include <unistd.h>
|
|
|
|
static const char *TEST_CONFIG_FILE = "/tmp/test_routing_config.json";
|
|
|
|
static void create_routing_config(void) {
|
|
FILE *f = fopen(TEST_CONFIG_FILE, "w");
|
|
if (f) {
|
|
fprintf(f,
|
|
"{\n"
|
|
" \"port\": 8080,\n"
|
|
" \"reverse_proxy\": [\n"
|
|
" {\n"
|
|
" \"hostname\": \"api.local\",\n"
|
|
" \"upstream_host\": \"backend-api\",\n"
|
|
" \"upstream_port\": 3000,\n"
|
|
" \"use_ssl\": false,\n"
|
|
" \"rewrite_host\": true\n"
|
|
" },\n"
|
|
" {\n"
|
|
" \"hostname\": \"secure.local\",\n"
|
|
" \"upstream_host\": \"backend-secure\",\n"
|
|
" \"upstream_port\": 443,\n"
|
|
" \"use_ssl\": true,\n"
|
|
" \"rewrite_host\": true\n"
|
|
" },\n"
|
|
" {\n"
|
|
" \"hostname\": \"passthrough.local\",\n"
|
|
" \"upstream_host\": \"backend-pass\",\n"
|
|
" \"upstream_port\": 8000,\n"
|
|
" \"use_ssl\": false,\n"
|
|
" \"rewrite_host\": false\n"
|
|
" }\n"
|
|
" ]\n"
|
|
"}\n");
|
|
fclose(f);
|
|
}
|
|
}
|
|
|
|
static void cleanup_routing_config(void) {
|
|
unlink(TEST_CONFIG_FILE);
|
|
}
|
|
|
|
void test_routing_host_rewrite(void) {
|
|
TEST_SUITE_BEGIN("Routing Host Rewrite Logic");
|
|
|
|
create_routing_config();
|
|
config_load(TEST_CONFIG_FILE);
|
|
|
|
route_config_t *route = config_find_route("api.local");
|
|
TEST_ASSERT(route != NULL, "Route for api.local found");
|
|
if (route) {
|
|
TEST_ASSERT_EQ(1, route->rewrite_host, "Host rewrite enabled for api.local");
|
|
TEST_ASSERT_STR_EQ("backend-api", route->upstream_host, "Upstream host is backend-api");
|
|
TEST_ASSERT_EQ(3000, route->upstream_port, "Upstream port is 3000");
|
|
}
|
|
|
|
route = config_find_route("passthrough.local");
|
|
TEST_ASSERT(route != NULL, "Route for passthrough.local found");
|
|
if (route) {
|
|
TEST_ASSERT_EQ(0, route->rewrite_host, "Host rewrite disabled for passthrough.local");
|
|
}
|
|
|
|
config_free();
|
|
cleanup_routing_config();
|
|
|
|
TEST_SUITE_END();
|
|
}
|
|
|
|
void test_routing_ssl_upstream(void) {
|
|
TEST_SUITE_BEGIN("Routing SSL Upstream");
|
|
|
|
create_routing_config();
|
|
config_load(TEST_CONFIG_FILE);
|
|
|
|
route_config_t *route = config_find_route("secure.local");
|
|
TEST_ASSERT(route != NULL, "Route for secure.local found");
|
|
if (route) {
|
|
TEST_ASSERT_EQ(1, route->use_ssl, "SSL enabled for secure.local");
|
|
TEST_ASSERT_EQ(443, route->upstream_port, "SSL port is 443");
|
|
}
|
|
|
|
route = config_find_route("api.local");
|
|
TEST_ASSERT(route != NULL, "Route for api.local found");
|
|
if (route) {
|
|
TEST_ASSERT_EQ(0, route->use_ssl, "SSL disabled for api.local");
|
|
}
|
|
|
|
config_free();
|
|
cleanup_routing_config();
|
|
|
|
TEST_SUITE_END();
|
|
}
|
|
|
|
void test_routing_dashboard_detection(void) {
|
|
TEST_SUITE_BEGIN("Routing Dashboard Detection");
|
|
|
|
http_request_t req;
|
|
|
|
const char *dashboard_req = "GET /rproxy/dashboard HTTP/1.1\r\nHost: anyhost.com\r\n\r\n";
|
|
http_parse_request(dashboard_req, strlen(dashboard_req), &req);
|
|
int is_dashboard = (strncmp(req.uri, "/rproxy/dashboard", 17) == 0);
|
|
TEST_ASSERT_EQ(1, is_dashboard, "Dashboard URI detected");
|
|
|
|
const char *api_req = "GET /rproxy/api/stats HTTP/1.1\r\nHost: anyhost.com\r\n\r\n";
|
|
http_parse_request(api_req, strlen(api_req), &req);
|
|
int is_api = (strncmp(req.uri, "/rproxy/api/stats", 17) == 0);
|
|
TEST_ASSERT_EQ(1, is_api, "API stats URI detected");
|
|
|
|
const char *regular_req = "GET /some/other/path HTTP/1.1\r\nHost: api.local\r\n\r\n";
|
|
http_parse_request(regular_req, strlen(regular_req), &req);
|
|
int is_regular = (strncmp(req.uri, "/rproxy/", 8) != 0);
|
|
TEST_ASSERT_EQ(1, is_regular, "Regular path not matched as internal");
|
|
|
|
TEST_SUITE_END();
|
|
}
|
|
|
|
void test_routing_keep_alive_handling(void) {
|
|
TEST_SUITE_BEGIN("Routing Keep-Alive Handling");
|
|
|
|
http_request_t req;
|
|
|
|
const char *ka_req = "GET /api HTTP/1.1\r\nHost: api.local\r\nConnection: keep-alive\r\n\r\n";
|
|
http_parse_request(ka_req, strlen(ka_req), &req);
|
|
TEST_ASSERT_EQ(1, req.keep_alive, "Keep-alive flag set when Connection: keep-alive");
|
|
|
|
const char *close_req = "GET /api HTTP/1.1\r\nHost: api.local\r\nConnection: close\r\n\r\n";
|
|
http_parse_request(close_req, strlen(close_req), &req);
|
|
TEST_ASSERT_EQ(0, req.keep_alive, "Keep-alive flag not set when Connection: close");
|
|
|
|
const char *default_req = "GET /api HTTP/1.1\r\nHost: api.local\r\n\r\n";
|
|
http_parse_request(default_req, strlen(default_req), &req);
|
|
TEST_ASSERT_EQ(1, req.keep_alive, "Keep-alive default for HTTP/1.1");
|
|
|
|
const char *http10_req = "GET /api HTTP/1.0\r\nHost: api.local\r\n\r\n";
|
|
http_parse_request(http10_req, strlen(http10_req), &req);
|
|
TEST_ASSERT_EQ(0, req.keep_alive, "Keep-alive default off for HTTP/1.0");
|
|
|
|
TEST_SUITE_END();
|
|
}
|
|
|
|
void test_routing_all_methods_accepted(void) {
|
|
TEST_SUITE_BEGIN("Routing All HTTP Methods Accepted");
|
|
|
|
http_request_t req;
|
|
int result;
|
|
|
|
const char *methods[] = {
|
|
"GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH", "TRACE", "CONNECT",
|
|
"PROPFIND", "PROPPATCH", "MKCOL", "MOVE", "COPY", "LOCK", "UNLOCK",
|
|
"SEARCH", "REPORT", "MKACTIVITY", "CHECKOUT", "MERGE",
|
|
"NOTIFY", "SUBSCRIBE", "UNSUBSCRIBE",
|
|
"CUSTOMMETHOD", "FOOBAR", "MYPROTO"
|
|
};
|
|
|
|
int num_methods = sizeof(methods) / sizeof(methods[0]);
|
|
|
|
for (int i = 0; i < num_methods; i++) {
|
|
char request[256];
|
|
snprintf(request, sizeof(request), "%s /path HTTP/1.1\r\nHost: test.com\r\n\r\n", methods[i]);
|
|
result = http_parse_request(request, strlen(request), &req);
|
|
|
|
char msg[128];
|
|
snprintf(msg, sizeof(msg), "Method %s accepted", methods[i]);
|
|
TEST_ASSERT_EQ(1, result, msg);
|
|
TEST_ASSERT_STR_EQ(methods[i], req.method, msg);
|
|
}
|
|
|
|
TEST_SUITE_END();
|
|
}
|
|
|
|
void test_routing_pipelined_requests(void) {
|
|
TEST_SUITE_BEGIN("Routing Pipelined Request Detection");
|
|
|
|
http_request_t req1, req2;
|
|
|
|
const char *first_req = "GET /first HTTP/1.1\r\nHost: api.local\r\nConnection: keep-alive\r\n\r\n";
|
|
int result1 = http_parse_request(first_req, strlen(first_req), &req1);
|
|
TEST_ASSERT_EQ(1, result1, "First request parsed");
|
|
TEST_ASSERT_STR_EQ("/first", req1.uri, "First request URI");
|
|
TEST_ASSERT_EQ(1, req1.keep_alive, "First request keep-alive");
|
|
|
|
const char *second_req = "GET /second HTTP/1.1\r\nHost: api.local\r\n\r\n";
|
|
int result2 = http_parse_request(second_req, strlen(second_req), &req2);
|
|
TEST_ASSERT_EQ(1, result2, "Second request parsed");
|
|
TEST_ASSERT_STR_EQ("/second", req2.uri, "Second request URI");
|
|
|
|
int is_new_request = http_is_request_start(second_req, strlen(second_req));
|
|
TEST_ASSERT_EQ(1, is_new_request, "New request detected for pipelining");
|
|
|
|
TEST_SUITE_END();
|
|
}
|
|
|
|
void test_routing_websocket_upgrade(void) {
|
|
TEST_SUITE_BEGIN("Routing WebSocket Upgrade");
|
|
|
|
http_request_t req;
|
|
|
|
const char *ws_req =
|
|
"GET /ws/chat HTTP/1.1\r\n"
|
|
"Host: api.local\r\n"
|
|
"Upgrade: websocket\r\n"
|
|
"Connection: Upgrade\r\n"
|
|
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
|
|
"Sec-WebSocket-Version: 13\r\n"
|
|
"\r\n";
|
|
|
|
int result = http_parse_request(ws_req, strlen(ws_req), &req);
|
|
TEST_ASSERT_EQ(1, result, "WebSocket upgrade request parsed");
|
|
TEST_ASSERT_EQ(1, req.is_websocket, "WebSocket flag set");
|
|
TEST_ASSERT_STR_EQ("/ws/chat", req.uri, "WebSocket URI parsed");
|
|
|
|
TEST_SUITE_END();
|
|
}
|
|
|
|
void test_routing_chunked_transfer(void) {
|
|
TEST_SUITE_BEGIN("Routing Chunked Transfer Encoding");
|
|
|
|
http_request_t req;
|
|
|
|
const char *chunked_req =
|
|
"POST /upload HTTP/1.1\r\n"
|
|
"Host: api.local\r\n"
|
|
"Transfer-Encoding: chunked\r\n"
|
|
"\r\n";
|
|
|
|
int result = http_parse_request(chunked_req, strlen(chunked_req), &req);
|
|
TEST_ASSERT_EQ(1, result, "Chunked request parsed");
|
|
TEST_ASSERT_EQ(1, req.is_chunked, "Chunked flag set");
|
|
|
|
const char *non_chunked_req =
|
|
"POST /upload HTTP/1.1\r\n"
|
|
"Host: api.local\r\n"
|
|
"Content-Length: 100\r\n"
|
|
"\r\n";
|
|
|
|
result = http_parse_request(non_chunked_req, strlen(non_chunked_req), &req);
|
|
TEST_ASSERT_EQ(1, result, "Non-chunked request parsed");
|
|
TEST_ASSERT_EQ(0, req.is_chunked, "Chunked flag not set");
|
|
TEST_ASSERT_EQ(100, req.content_length, "Content-Length parsed");
|
|
|
|
TEST_SUITE_END();
|
|
}
|
|
|
|
void run_routing_tests(void) {
|
|
test_routing_host_rewrite();
|
|
test_routing_ssl_upstream();
|
|
test_routing_dashboard_detection();
|
|
test_routing_keep_alive_handling();
|
|
test_routing_all_methods_accepted();
|
|
test_routing_pipelined_requests();
|
|
test_routing_websocket_upgrade();
|
|
test_routing_chunked_transfer();
|
|
}
|