feat: add rejection of HTTP pipelined requests with 400 error
test: add tests for pipelined request rejection behavior
This commit is contained in:
parent
ee412956f9
commit
c6aca9d83d
@ -12,6 +12,14 @@
|
||||
|
||||
|
||||
|
||||
|
||||
## Version 0.12.0 - 2026-01-27
|
||||
|
||||
The server now rejects HTTP pipelined requests with a 400 Bad Request error.
|
||||
|
||||
**Changes:** 2 files, 268 lines
|
||||
**Languages:** C (268 lines)
|
||||
|
||||
## Version 0.11.0 - 2026-01-27
|
||||
|
||||
The system now processes buffered client data after an upstream connection closes, ensuring no data loss in connection scenarios. A corresponding test validates this behavior.
|
||||
|
||||
@ -717,20 +717,36 @@ static void handle_client_read(connection_t *conn) {
|
||||
char *data_start = buf->data + buf->head;
|
||||
size_t data_len = buffer_available_read(buf);
|
||||
|
||||
if (data_len >= 1) {
|
||||
if (data_len >= 4) {
|
||||
int looks_like_new_request = http_is_request_start(data_start, data_len);
|
||||
|
||||
if (!looks_like_new_request) {
|
||||
if (looks_like_new_request) {
|
||||
log_info("[ROUTING-PIPELINE] Pipelined request detected on fd=%d, rejecting with 400", conn->fd);
|
||||
if (conn->pair) {
|
||||
connection_close(conn->pair->fd);
|
||||
conn->pair = NULL;
|
||||
}
|
||||
|
||||
char *body = "400 Bad Request - Request pipelining is not supported";
|
||||
char header[512];
|
||||
int len = snprintf(header, sizeof(header),
|
||||
"HTTP/1.1 400 Bad Request\r\n"
|
||||
"Content-Type: text/plain; charset=utf-8\r\n"
|
||||
"Content-Length: %zu\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n"
|
||||
"%s",
|
||||
strlen(body), body);
|
||||
|
||||
if (buffer_ensure_capacity(&conn->write_buf, len) == 0) {
|
||||
memcpy(conn->write_buf.data, header, len);
|
||||
conn->write_buf.tail = len;
|
||||
}
|
||||
|
||||
conn->state = CLIENT_STATE_CLOSING;
|
||||
conn->request.keep_alive = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
log_debug("Pipelined request detected on fd %d, closing upstream fd %d",
|
||||
conn->fd, conn->pair->fd);
|
||||
connection_close(conn->pair->fd);
|
||||
conn->pair = NULL;
|
||||
conn->state = CLIENT_STATE_READING_HEADERS;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -845,10 +861,16 @@ static void handle_client_read(connection_t *conn) {
|
||||
}
|
||||
}
|
||||
|
||||
char routing_tag[16];
|
||||
snprintf(routing_tag, sizeof(routing_tag), "%-15.15s", conn->request.host);
|
||||
log_info("[%s] Forwarding request for fd=%d: %s %s",
|
||||
routing_tag, conn->fd, conn->request.method, conn->request.uri);
|
||||
if (route) {
|
||||
log_info("[ROUTING] fd=%d method=%s uri=%s host=%s -> FORWARDING to %s:%d",
|
||||
conn->fd, conn->request.method, conn->request.uri, conn->request.host,
|
||||
route->upstream_host, route->upstream_port);
|
||||
} else {
|
||||
char routing_tag[16];
|
||||
snprintf(routing_tag, sizeof(routing_tag), "%-15.15s", conn->request.host);
|
||||
log_info("[%s] Forwarding request for fd=%d: %s %s",
|
||||
routing_tag, conn->fd, conn->request.method, conn->request.uri);
|
||||
}
|
||||
|
||||
conn->vhost_stats = monitor_get_or_create_vhost_stats(conn->request.host);
|
||||
monitor_record_request_start(conn->vhost_stats, conn->request.is_websocket);
|
||||
@ -968,22 +990,28 @@ static void handle_forwarding(connection_t *conn) {
|
||||
size_t data_len = buffer_available_read(&conn->read_buf);
|
||||
|
||||
if (data_len >= 4 && http_is_request_start(data_start, data_len)) {
|
||||
log_debug("Pipelined request detected in handle_forwarding on fd %d, closing upstream fd %d",
|
||||
conn->fd, pair->fd);
|
||||
log_info("[ROUTING-PIPELINE] Pipelined request detected on fd=%d, rejecting with 400", conn->fd);
|
||||
connection_close(pair->fd);
|
||||
conn->pair = NULL;
|
||||
conn->state = CLIENT_STATE_READING_HEADERS;
|
||||
|
||||
conn->content_type_checked = 0;
|
||||
conn->is_textual_content = 0;
|
||||
conn->response_headers_parsed = 0;
|
||||
conn->original_content_length = 0;
|
||||
conn->content_length_delta = 0;
|
||||
conn->patch_blocked = 0;
|
||||
conn->half_closed = 0;
|
||||
conn->write_shutdown = 0;
|
||||
char *body = "400 Bad Request - Request pipelining is not supported";
|
||||
char header[512];
|
||||
int len = snprintf(header, sizeof(header),
|
||||
"HTTP/1.1 400 Bad Request\r\n"
|
||||
"Content-Type: text/plain; charset=utf-8\r\n"
|
||||
"Content-Length: %zu\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n"
|
||||
"%s",
|
||||
strlen(body), body);
|
||||
|
||||
handle_client_read(conn);
|
||||
if (buffer_ensure_capacity(&conn->write_buf, len) == 0) {
|
||||
memcpy(conn->write_buf.data, header, len);
|
||||
conn->write_buf.tail = len;
|
||||
}
|
||||
|
||||
conn->state = CLIENT_STATE_CLOSING;
|
||||
conn->request.keep_alive = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -1267,7 +1295,9 @@ static void handle_write_event(connection_t *conn) {
|
||||
|
||||
connection_modify_epoll(conn->fd, EPOLLIN);
|
||||
|
||||
if (buffer_available_read(&conn->read_buf) > 0) {
|
||||
if (conn->state == CLIENT_STATE_CLOSING) {
|
||||
connection_close(conn->fd);
|
||||
} else if (buffer_available_read(&conn->read_buf) > 0) {
|
||||
handle_client_read(conn);
|
||||
}
|
||||
} else {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user