From 726515c0a990ab944ab4f6031a8eab20c9a4ad26 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:23:38 +0000 Subject: [PATCH 1/2] Initial plan From a953e5522110da360f44b34aa9f983206f25a3c6 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:28:45 +0000 Subject: [PATCH 2/2] fix: ensure Connection: close header in HTTP proxy responses When using an HTTP proxy (like v2ray), rtp2httpd was forwarding upstream response headers without ensuring Connection: close was present. This caused HTTP proxies to keep connections alive even when clients disconnected or switched channels. Changes: - Filter out Connection header from upstream responses - Always inject Connection: close in responses to clients - Add e2e tests to verify Connection: close header presence Fixes issue where connections cannot be terminated when switching channels or closing browser while using system proxy. Co-authored-by: stackia <5107241+stackia@users.noreply.github.com> Agent-Logs-Url: https://github.com/stackia/rtp2httpd/sessions/492e1c66-6b45-40a3-9efa-2f9757bb72a4 --- e2e/test_http_proxy.py | 100 +++++++++++++++++++++++++++++++++++++++++ src/http_proxy.c | 50 +++++++++++++++++++-- 2 files changed, 146 insertions(+), 4 deletions(-) diff --git a/e2e/test_http_proxy.py b/e2e/test_http_proxy.py index 532887c..68a24c1 100644 --- a/e2e/test_http_proxy.py +++ b/e2e/test_http_proxy.py @@ -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() diff --git a/src/http_proxy.c b/src/http_proxy.c index 84e78b4..85c1463 100644 --- a/src/http_proxy.c +++ b/src/http_proxy.c @@ -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); @@ -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; } @@ -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");