feat: reject HTTP pipelined requests with 400 error and close upstream connection

Add detection of pipelined requests in handle_client_read by checking if buffered data starts with a new HTTP request. When a pipelined request is detected, the server now sends a 400 Bad Request response with a descriptive body, closes the upstream connection, and transitions the client state to CLIENT_STATE_CLOSING. Previously, the server would silently close the upstream connection and continue reading headers. The change also includes a comprehensive test that validates the rejection behavior, response content, and connection state transitions for pipelined requests.
This commit is contained in:
2026-01-27 16:15:20 +00:00
parent e914b08c29
commit ab70643e1f
3 changed files with 249 additions and 27 deletions
+184
View File
@@ -2303,6 +2303,188 @@ void test_connection_buffered_data_after_upstream_close(void) {
TEST_SUITE_END();
}
void test_connection_pipelined_request_rejected(void) {
TEST_SUITE_BEGIN("Pipelined Request Rejected");
connection_init_all();
int old_epoll = epoll_fd;
epoll_fd = epoll_create1(0);
TEST_ASSERT(epoll_fd >= 0, "Epoll created");
int sockfd[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, sockfd);
connection_set_non_blocking(sockfd[0]);
connection_set_non_blocking(sockfd[1]);
connection_t *client = &connections[sockfd[0]];
memset(client, 0, sizeof(connection_t));
client->fd = sockfd[0];
client->type = CONN_TYPE_CLIENT;
client->state = CLIENT_STATE_FORWARDING;
buffer_init(&client->read_buf, 4096);
buffer_init(&client->write_buf, 4096);
connection_t *upstream = &connections[sockfd[1]];
memset(upstream, 0, sizeof(connection_t));
upstream->fd = sockfd[1];
upstream->type = CONN_TYPE_UPSTREAM;
upstream->state = CLIENT_STATE_FORWARDING;
buffer_init(&upstream->read_buf, 4096);
buffer_init(&upstream->write_buf, 4096);
client->pair = upstream;
upstream->pair = client;
struct epoll_event ev = { .events = EPOLLIN, .data.fd = sockfd[0] };
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd[0], &ev);
client->epoll_events = EPOLLIN;
const char *pipelined_req = "GET /next HTTP/1.1\r\nHost: test.com\r\n\r\n";
IGNORE_RESULT(write(sockfd[1], pipelined_req, strlen(pipelined_req)));
struct epoll_event event = { .events = EPOLLIN, .data.fd = sockfd[0] };
connection_handle_event(&event);
TEST_ASSERT(client->state == CLIENT_STATE_CLOSING || client->type == CONN_TYPE_UNUSED,
"Connection closed on pipelined request");
TEST_ASSERT(strstr((char*)client->write_buf.data, "400") != NULL,
"400 error sent to client");
if (client->type != CONN_TYPE_UNUSED) {
buffer_free(&client->read_buf);
buffer_free(&client->write_buf);
}
buffer_free(&upstream->read_buf);
buffer_free(&upstream->write_buf);
close(sockfd[0]);
close(sockfd[1]);
close(epoll_fd);
epoll_fd = old_epoll;
TEST_SUITE_END();
}
void test_connection_pipelined_dashboard_rejected(void) {
TEST_SUITE_BEGIN("Pipelined Dashboard Request Rejected");
connection_init_all();
int old_epoll = epoll_fd;
epoll_fd = epoll_create1(0);
TEST_ASSERT(epoll_fd >= 0, "Epoll created");
int sockfd[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, sockfd);
connection_set_non_blocking(sockfd[0]);
connection_set_non_blocking(sockfd[1]);
connection_t *client = &connections[sockfd[0]];
memset(client, 0, sizeof(connection_t));
client->fd = sockfd[0];
client->type = CONN_TYPE_CLIENT;
client->state = CLIENT_STATE_FORWARDING;
buffer_init(&client->read_buf, 4096);
buffer_init(&client->write_buf, 4096);
connection_t *upstream = &connections[sockfd[1]];
memset(upstream, 0, sizeof(connection_t));
upstream->fd = sockfd[1];
upstream->type = CONN_TYPE_UPSTREAM;
upstream->state = CLIENT_STATE_FORWARDING;
buffer_init(&upstream->read_buf, 4096);
buffer_init(&upstream->write_buf, 4096);
client->pair = upstream;
upstream->pair = client;
struct epoll_event ev = { .events = EPOLLIN, .data.fd = sockfd[0] };
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd[0], &ev);
client->epoll_events = EPOLLIN;
const char *pipelined_dashboard = "GET /rproxy/dashboard HTTP/1.1\r\nHost: test.com\r\n\r\n";
IGNORE_RESULT(write(sockfd[1], pipelined_dashboard, strlen(pipelined_dashboard)));
struct epoll_event event = { .events = EPOLLIN, .data.fd = sockfd[0] };
connection_handle_event(&event);
TEST_ASSERT(upstream->write_buf.tail == 0, "Dashboard request NOT forwarded to upstream");
TEST_ASSERT(client->state == CLIENT_STATE_CLOSING || client->type == CONN_TYPE_UNUSED,
"Client connection closed");
if (client->type != CONN_TYPE_UNUSED) {
buffer_free(&client->read_buf);
buffer_free(&client->write_buf);
}
buffer_free(&upstream->read_buf);
buffer_free(&upstream->write_buf);
close(sockfd[0]);
close(sockfd[1]);
close(epoll_fd);
epoll_fd = old_epoll;
TEST_SUITE_END();
}
void test_connection_single_request_not_rejected(void) {
TEST_SUITE_BEGIN("Single Request Not Rejected");
connection_init_all();
int old_epoll = epoll_fd;
epoll_fd = epoll_create1(0);
TEST_ASSERT(epoll_fd >= 0, "Epoll created");
int sockfd[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, sockfd);
connection_set_non_blocking(sockfd[0]);
connection_set_non_blocking(sockfd[1]);
connection_t *client = &connections[sockfd[0]];
memset(client, 0, sizeof(connection_t));
client->fd = sockfd[0];
client->type = CONN_TYPE_CLIENT;
client->state = CLIENT_STATE_FORWARDING;
buffer_init(&client->read_buf, 4096);
buffer_init(&client->write_buf, 4096);
connection_t *upstream = &connections[sockfd[1]];
memset(upstream, 0, sizeof(connection_t));
upstream->fd = sockfd[1];
upstream->type = CONN_TYPE_UPSTREAM;
upstream->state = CLIENT_STATE_FORWARDING;
buffer_init(&upstream->read_buf, 4096);
buffer_init(&upstream->write_buf, 4096);
client->pair = upstream;
upstream->pair = client;
struct epoll_event ev = { .events = EPOLLIN, .data.fd = sockfd[0] };
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd[0], &ev);
client->epoll_events = EPOLLIN;
const char *body_data = "body-data-test-x";
IGNORE_RESULT(write(sockfd[1], body_data, strlen(body_data)));
struct epoll_event event = { .events = EPOLLIN, .data.fd = sockfd[0] };
connection_handle_event(&event);
TEST_ASSERT(client->state != CLIENT_STATE_CLOSING && client->type != CONN_TYPE_UNUSED,
"Connection NOT closed on body data");
TEST_ASSERT(strstr((char*)client->write_buf.data, "400") == NULL,
"No 400 error sent");
if (client->type != CONN_TYPE_UNUSED) {
buffer_free(&client->read_buf);
buffer_free(&client->write_buf);
}
buffer_free(&upstream->read_buf);
buffer_free(&upstream->write_buf);
close(sockfd[0]);
close(sockfd[1]);
close(epoll_fd);
epoll_fd = old_epoll;
TEST_SUITE_END();
}
void run_connection_tests(void) {
test_connection_init_all();
test_connection_set_non_blocking();
@@ -2367,4 +2549,6 @@ void run_connection_tests(void) {
test_connection_cleanup_active_conn();
test_connection_keep_alive_internal_route_second_request();
test_connection_buffered_data_after_upstream_close();
test_connection_pipelined_request_rejected();
test_connection_pipelined_dashboard_rejected();
}