Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions e2e/test_http_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,3 +490,103 @@ def test_empty_200(self, shared_r2h):
assert body == b""
finally:
upstream.stop()


# ---------------------------------------------------------------------------
# Connection: close header enforcement
# ---------------------------------------------------------------------------


class TestProxyConnectionClose:
"""Verify that rtp2httpd always sends Connection: close to clients."""

def test_connection_close_in_response(self, shared_r2h):
"""HTTP proxy responses must include Connection: close header."""
upstream = MockHTTPUpstream(
routes={
"/test": {"status": 200, "body": b"test content", "headers": {"Content-Type": "text/plain"}},
}
)
upstream.start()
try:
status, hdrs, body = http_get(
"127.0.0.1",
shared_r2h.port,
"/http/127.0.0.1:%d/test" % upstream.port,
timeout=5.0,
)
assert status == 200
assert body == b"test content"
# Verify Connection: close is present (case-insensitive)
connection_header = None
for k, v in hdrs.items():
if k.lower() == "connection":
connection_header = v.lower()
break
assert connection_header is not None, "Connection header is missing"
assert "close" in connection_header, f"Expected 'close' in Connection header, got '{connection_header}'"
finally:
upstream.stop()

def test_connection_close_with_redirect(self, shared_r2h):
"""HTTP proxy redirect responses must include Connection: close."""
upstream = MockHTTPUpstream(
routes={
"/redirect": {
"status": 302,
"body": b"",
"headers": {"Location": "http://10.0.0.1:8080/new"},
},
}
)
upstream.start()
try:
status, hdrs, _ = http_get(
"127.0.0.1",
shared_r2h.port,
"/http/127.0.0.1:%d/redirect" % upstream.port,
timeout=5.0,
)
assert status == 302
# Verify Connection: close is present
connection_header = None
for k, v in hdrs.items():
if k.lower() == "connection":
connection_header = v.lower()
break
assert connection_header is not None, "Connection header is missing"
assert "close" in connection_header, f"Expected 'close' in Connection header, got '{connection_header}'"
finally:
upstream.stop()

def test_connection_close_with_upstream_keepalive(self, shared_r2h):
"""Even if upstream sends keep-alive, rtp2httpd must send close."""
upstream = MockHTTPUpstream(
routes={
"/keepalive": {
"status": 200,
"body": b"content",
"headers": {"Content-Type": "text/plain", "Connection": "keep-alive"},
},
}
)
upstream.start()
try:
status, hdrs, body = http_get(
"127.0.0.1",
shared_r2h.port,
"/http/127.0.0.1:%d/keepalive" % upstream.port,
timeout=5.0,
)
assert status == 200
assert body == b"content"
# Verify rtp2httpd overrides upstream's keep-alive with close
connection_header = None
for k, v in hdrs.items():
if k.lower() == "connection":
connection_header = v.lower()
break
assert connection_header is not None, "Connection header is missing"
assert "close" in connection_header, f"Expected 'close' in Connection header, got '{connection_header}'"
finally:
upstream.stop()
50 changes: 46 additions & 4 deletions src/http_proxy.c
Original file line number Diff line number Diff line change
Expand Up @@ -1048,6 +1048,10 @@ static int http_proxy_parse_response_headers(http_proxy_session_t *session) {
if (strncasecmp(orig_line, "Location:", 9) == 0) {
/* Replace Location header with rewritten value */
written = snprintf(rebuild_ptr, rebuild_remaining, "Location: %s\r\n", rewritten_location);
} else if (strncasecmp(orig_line, "Connection:", 11) == 0) {
/* Skip Connection header from upstream - will add our own */
orig_line = strtok(NULL, "\r\n");
continue;
} else {
/* Copy other headers as-is */
written = snprintf(rebuild_ptr, rebuild_remaining, "%s\r\n", orig_line);
Expand Down Expand Up @@ -1090,17 +1094,49 @@ static int http_proxy_parse_response_headers(http_proxy_session_t *session) {
session->conn->should_set_r2h_cookie = 0;
}

/* Always add Connection: close header to ensure clients disconnect */
if (connection_queue_output(session->conn, (const uint8_t *)"Connection: close\r\n", 19) < 0) {
logger(LOG_ERROR, "HTTP Proxy: Failed to send Connection header");
return -1;
}

/* Send final CRLF to end headers */
if (connection_queue_output(session->conn, (const uint8_t *)"\r\n", 2) < 0) {
logger(LOG_ERROR, "HTTP Proxy: Failed to send header terminator");
return -1;
}
} else {
/* No Location rewriting needed - use original logic */
size_t headers_without_crlf = header_len - 2; /* Exclude final \r\n */
/* No Location rewriting needed - filter out Connection header from
* upstream and add our own */
char filtered_headers[HTTP_PROXY_RESPONSE_BUFFER_SIZE];
char *filter_ptr = filtered_headers;
size_t filter_remaining = sizeof(filtered_headers);
char *orig_line;
char orig_headers[HTTP_PROXY_RESPONSE_BUFFER_SIZE];

/* Send headers up to (but not including) final \r\n\r\n */
if (connection_queue_output(session->conn, session->response_buffer, headers_without_crlf) < 0) {
/* Copy headers for parsing */
memcpy(orig_headers, session->response_buffer, header_len - 2);
orig_headers[header_len - 2] = '\0';

/* Filter out Connection header */
orig_line = strtok(orig_headers, "\r\n");
while (orig_line != NULL) {
/* Skip Connection header from upstream - will add our own */
if (strncasecmp(orig_line, "Connection:", 11) != 0) {
int written = snprintf(filter_ptr, filter_remaining, "%s\r\n", orig_line);
if (written < 0 || (size_t)written >= filter_remaining) {
logger(LOG_ERROR, "HTTP Proxy: Filtered headers too large");
return -1;
}
filter_ptr += written;
filter_remaining -= written;
}
orig_line = strtok(NULL, "\r\n");
}

/* Send filtered headers */
size_t filtered_len = filter_ptr - filtered_headers;
if (connection_queue_output(session->conn, (const uint8_t *)filtered_headers, filtered_len) < 0) {
logger(LOG_ERROR, "HTTP Proxy: Failed to forward headers to client");
return -1;
}
Expand All @@ -1122,6 +1158,12 @@ static int http_proxy_parse_response_headers(http_proxy_session_t *session) {
session->conn->should_set_r2h_cookie = 0; /* Only set once */
}

/* Always add Connection: close header to ensure clients disconnect */
if (connection_queue_output(session->conn, (const uint8_t *)"Connection: close\r\n", 19) < 0) {
logger(LOG_ERROR, "HTTP Proxy: Failed to send Connection header");
return -1;
}

/* Send final \r\n to end headers */
if (connection_queue_output(session->conn, (const uint8_t *)"\r\n", 2) < 0) {
logger(LOG_ERROR, "HTTP Proxy: Failed to send header terminator");
Expand Down