From de48f0c2d60849c95a3dedcc4cf72e88e5a590d3 Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Tue, 14 Jul 2026 19:17:54 +0800 Subject: [PATCH 1/3] fix(http-proxy): decode chunked m3u responses --- CMakeLists.txt | 1 + e2e/test_http_proxy_m3u_rewrite.py | 198 ++++++++++++- src/http_chunked_decoder.c | 195 +++++++++++++ src/http_chunked_decoder.h | 50 ++++ src/http_proxy.c | 437 ++++++++++++++++++----------- src/http_proxy.h | 5 + 6 files changed, 718 insertions(+), 168 deletions(-) create mode 100644 src/http_chunked_decoder.c create mode 100644 src/http_chunked_decoder.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 6fc5a6e8..44de69da 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,7 @@ set(COMMON_SOURCES src/fcc_huawei.c src/stream.c src/rtsp.c + src/http_chunked_decoder.c src/http_proxy.c src/http_proxy_rewrite.c src/stun.c diff --git a/e2e/test_http_proxy_m3u_rewrite.py b/e2e/test_http_proxy_m3u_rewrite.py index fc6a4d2f..27225d46 100644 --- a/e2e/test_http_proxy_m3u_rewrite.py +++ b/e2e/test_http_proxy_m3u_rewrite.py @@ -7,6 +7,7 @@ """ import socket +import struct import threading import pytest @@ -15,6 +16,7 @@ MockHTTPUpstream, R2HProcess, find_free_port, + get_header, http_get, stream_get, ) @@ -114,9 +116,12 @@ def _make_m3u_upstream(path, body, content_type="application/vnd.apple.mpegurl") class _RawHTTPResponseUpstream: """Serve a prebuilt raw HTTP response and keep the connection open.""" - def __init__(self, response): + def __init__(self, response, *, part_delay=0.0, keep_open=True, reset_after_send=False): self.port = find_free_port() - self.response = response + self.response_parts = [response] if isinstance(response, bytes) else list(response) + self.part_delay = part_delay + self.keep_open = keep_open + self.reset_after_send = reset_after_send self._server_sock = None self._thread = None self._stop = threading.Event() @@ -162,11 +167,20 @@ def _handle(self, conn): if not chunk: return request += chunk - conn.sendall(self.response) - self._stop.wait(_TIMEOUT * 2) + for part in self.response_parts: + conn.sendall(part) + if self.part_delay > 0 and self._stop.wait(self.part_delay): + return + if self.keep_open: + self._stop.wait(_TIMEOUT * 2) except OSError: pass finally: + if self.reset_after_send: + try: + conn.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0)) + except OSError: + pass conn.close() @@ -186,6 +200,20 @@ def _make_padded_header_m3u_upstream(body, content_type="application/vnd.apple.m return upstream +def _raw_chunked_headers(content_type="text/plain", transfer_encoding="chunked", trailer=None, content_length=None): + headers = ( + "HTTP/1.1 200 OK\r\n" + f"Content-Type: {content_type}\r\n" + f"Transfer-Encoding: {transfer_encoding}\r\n" + "Connection: keep-alive\r\n" + ) + if trailer: + headers += f"Trailer: {trailer}\r\n" + if content_length is not None: + headers += f"Content-Length: {content_length}\r\n" + return (headers + "\r\n").encode() + + # --------------------------------------------------------------------------- # Basic absolute http:// URL rewriting # --------------------------------------------------------------------------- @@ -702,6 +730,168 @@ def test_full_hls_media_playlist(self, shared_r2h): finally: upstream.stop() + +# --------------------------------------------------------------------------- +# Chunked transfer decoding +# --------------------------------------------------------------------------- + + +class TestM3URewriteChunked: + """Chunk framing should be removed only for M3U rewrite responses.""" + + def test_chunked_text_plain_m3u_is_decoded_and_rewritten(self, shared_r2h): + """Regression: chunk sizes and the zero chunk must not become playlist URLs.""" + first = b"#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:8\n" + second = b"#EXTINF:8.000,\n321124334400000.jpeg\n" + headers = _raw_chunked_headers(trailer="X-Playlist-Checksum", content_length=1) + response_parts = [ + headers, + f"{len(first):X};source=test\r".encode(), + b"\n", + first[:7], + first[7:], + b"\r", + b"\n", + f"{len(second):x}\r\n".encode(), + second[:-1], + second[-1:], + b"\r\n0\r", + b"\nX-Playlist-Checksum: ok\r\n", + b"\r", + b"\n", + ] + upstream = _RawHTTPResponseUpstream(response_parts, part_delay=0.005) + upstream.start() + try: + status, hdrs, body = http_get( + "127.0.0.1", + shared_r2h.port, + f"/http/127.0.0.1:{upstream.port}/video/index.m3u8", + timeout=2.0, + ) + text = body.decode() + assert status == 200 + assert text.startswith("#EXTM3U\n") + assert f"/http/127.0.0.1:{upstream.port}/video/321124334400000.jpeg" in text + assert not any(line.endswith("/0") for line in text.splitlines()) + assert get_header(hdrs, "Transfer-Encoding") == "" + assert get_header(hdrs, "Trailer") == "" + assert int(get_header(hdrs, "Content-Length")) == len(body) + finally: + upstream.stop() + + def test_empty_chunked_m3u_returns_empty_content_length_body(self, shared_r2h): + upstream = _RawHTTPResponseUpstream(_raw_chunked_headers() + b"0\r\n\r\n") + upstream.start() + try: + status, hdrs, body = http_get( + "127.0.0.1", + shared_r2h.port, + f"/http/127.0.0.1:{upstream.port}/empty.m3u8", + timeout=2.0, + ) + assert status == 200 + assert body == b"" + assert get_header(hdrs, "Content-Length") == "0" + assert get_header(hdrs, "Transfer-Encoding") == "" + finally: + upstream.stop() + + def test_complete_chunked_m3u_survives_immediate_upstream_reset(self, shared_r2h): + playlist = b"#EXTM3U\n#EXTINF:8.000,\nsegment.ts\n" + response = ( + _raw_chunked_headers() + + f"{len(playlist):x}\r\n".encode() + + playlist + + b"\r\n0\r\n\r\n" + ) + upstream = _RawHTTPResponseUpstream(response, keep_open=False, reset_after_send=True) + upstream.start() + try: + status, hdrs, body = http_get( + "127.0.0.1", + shared_r2h.port, + f"/http/127.0.0.1:{upstream.port}/reset.m3u8", + timeout=2.0, + ) + assert status == 200 + assert f"/http/127.0.0.1:{upstream.port}/segment.ts".encode() in body + assert int(get_header(hdrs, "Content-Length")) == len(body) + finally: + upstream.stop() + + def test_non_m3u_chunked_response_remains_passthrough(self, shared_r2h): + response = _raw_chunked_headers(content_type="application/octet-stream") + b"5\r\nhello\r\n0\r\n\r\n" + upstream = _RawHTTPResponseUpstream(response) + upstream.start() + try: + status, hdrs, body = http_get( + "127.0.0.1", + shared_r2h.port, + f"/http/127.0.0.1:{upstream.port}/data.bin", + timeout=2.0, + ) + assert status == 200 + assert get_header(hdrs, "Transfer-Encoding").lower() == "chunked" + assert body == b"hello" + finally: + upstream.stop() + + @pytest.mark.parametrize( + ("chunked_body", "keep_open"), + [ + (b"Z\r\n", True), + (b"10000000000000000\r\n", True), + (b"1;" + b"a" * 4095 + b"\r\nx\r\n0\r\n\r\n", True), + (b"3\r\nabcX\n0\r\n\r\n", True), + (b"0\r\nX: " + b"a" * 8192 + b"\r\n\r\n", True), + (b"0\r\nX: invalid\n\r\n", True), + (b"5\r\nhello\r\n", False), + ], + ids=[ + "invalid-size", + "size-overflow", + "oversized-size-line", + "invalid-data-crlf", + "oversized-trailer", + "invalid-trailer-crlf", + "missing-zero-chunk", + ], + ) + def test_malformed_chunked_m3u_returns_503(self, shared_r2h, chunked_body, keep_open): + upstream = _RawHTTPResponseUpstream(_raw_chunked_headers() + chunked_body, keep_open=keep_open) + upstream.start() + try: + status, _, body = http_get( + "127.0.0.1", + shared_r2h.port, + f"/http/127.0.0.1:{upstream.port}/invalid.m3u8", + timeout=2.0, + ) + assert status == 503 + assert b"Service Unavailable" in body + finally: + upstream.stop() + + def test_unsupported_transfer_coding_returns_503(self, shared_r2h): + response = _raw_chunked_headers(transfer_encoding="gzip, chunked") + b"0\r\n\r\n" + upstream = _RawHTTPResponseUpstream(response) + upstream.start() + try: + status, _, _ = http_get( + "127.0.0.1", + shared_r2h.port, + f"/http/127.0.0.1:{upstream.port}/encoded.m3u8", + timeout=2.0, + ) + assert status == 503 + finally: + upstream.stop() + + +class TestM3URewritePlaylistVariants: + """Master, mixed-source, and large playlist scenarios.""" + def test_master_playlist_with_audio(self, shared_r2h): """A master playlist with #EXT-X-MEDIA and URI for audio renditions.""" m3u = ( diff --git a/src/http_chunked_decoder.c b/src/http_chunked_decoder.c new file mode 100644 index 00000000..aea90eac --- /dev/null +++ b/src/http_chunked_decoder.c @@ -0,0 +1,195 @@ +#include "http_chunked_decoder.h" +#include +#include + +static int http_chunked_hex_value(uint8_t ch) { + if (ch >= '0' && ch <= '9') + return ch - '0'; + if (ch >= 'a' && ch <= 'f') + return ch - 'a' + 10; + if (ch >= 'A' && ch <= 'F') + return ch - 'A' + 10; + return -1; +} + +static http_chunked_decode_result_t http_chunked_fail(http_chunked_decoder_t *decoder, size_t offset, + size_t *consumed) { + decoder->state = HTTP_CHUNKED_STATE_ERROR; + if (consumed) + *consumed = offset; + return HTTP_CHUNKED_DECODE_ERROR; +} + +static int http_chunked_count_size_byte(http_chunked_decoder_t *decoder) { + decoder->size_line_length++; + return decoder->size_line_length <= HTTP_CHUNKED_MAX_SIZE_LINE ? 0 : -1; +} + +static int http_chunked_count_trailer_byte(http_chunked_decoder_t *decoder) { + decoder->trailer_size++; + return decoder->trailer_size <= HTTP_CHUNKED_MAX_TRAILER_SIZE ? 0 : -1; +} + +void http_chunked_decoder_init(http_chunked_decoder_t *decoder) { + if (!decoder) + return; + memset(decoder, 0, sizeof(*decoder)); + decoder->state = HTTP_CHUNKED_STATE_SIZE; +} + +http_chunked_decode_result_t http_chunked_decoder_feed(http_chunked_decoder_t *decoder, const uint8_t *input, + size_t input_len, http_chunked_emit_cb emit, void *opaque, + size_t *consumed) { + size_t offset = 0; + + if (consumed) + *consumed = 0; + if (!decoder || (!input && input_len > 0)) + return HTTP_CHUNKED_DECODE_ERROR; + if (decoder->state == HTTP_CHUNKED_STATE_ERROR) + return HTTP_CHUNKED_DECODE_ERROR; + if (decoder->state == HTTP_CHUNKED_STATE_DONE) + return input_len == 0 ? HTTP_CHUNKED_DECODE_DONE : http_chunked_fail(decoder, 0, consumed); + + while (offset < input_len) { + uint8_t ch = input[offset]; + + switch (decoder->state) { + case HTTP_CHUNKED_STATE_SIZE: { + int digit; + if (http_chunked_count_size_byte(decoder) < 0) + return http_chunked_fail(decoder, offset, consumed); + + digit = http_chunked_hex_value(ch); + if (digit >= 0) { + if (decoder->chunk_size > (UINT64_MAX - (uint64_t)digit) / 16) + return http_chunked_fail(decoder, offset, consumed); + decoder->chunk_size = decoder->chunk_size * 16 + (uint64_t)digit; + decoder->saw_size_digit = 1; + offset++; + } else if (ch == ';' && decoder->saw_size_digit) { + decoder->state = HTTP_CHUNKED_STATE_EXTENSION; + offset++; + } else if (ch == '\r' && decoder->saw_size_digit) { + decoder->state = HTTP_CHUNKED_STATE_SIZE_LF; + offset++; + } else { + return http_chunked_fail(decoder, offset, consumed); + } + break; + } + + case HTTP_CHUNKED_STATE_EXTENSION: + if (http_chunked_count_size_byte(decoder) < 0) + return http_chunked_fail(decoder, offset, consumed); + if (ch == '\r') { + decoder->state = HTTP_CHUNKED_STATE_SIZE_LF; + offset++; + } else if (ch == '\n' || (ch < 0x20 && ch != '\t') || ch == 0x7f) { + return http_chunked_fail(decoder, offset, consumed); + } else { + offset++; + } + break; + + case HTTP_CHUNKED_STATE_SIZE_LF: + if (ch != '\n') + return http_chunked_fail(decoder, offset, consumed); + offset++; + decoder->chunk_remaining = decoder->chunk_size; + decoder->state = decoder->chunk_size == 0 ? HTTP_CHUNKED_STATE_TRAILER_START : HTTP_CHUNKED_STATE_DATA; + break; + + case HTTP_CHUNKED_STATE_DATA: { + size_t available = input_len - offset; + size_t emit_len = decoder->chunk_remaining < (uint64_t)available ? (size_t)decoder->chunk_remaining : available; + if (emit_len > 0 && (!emit || emit(opaque, input + offset, emit_len) < 0)) + return http_chunked_fail(decoder, offset, consumed); + offset += emit_len; + decoder->chunk_remaining -= emit_len; + if (decoder->chunk_remaining == 0) + decoder->state = HTTP_CHUNKED_STATE_DATA_CR; + break; + } + + case HTTP_CHUNKED_STATE_DATA_CR: + if (ch != '\r') + return http_chunked_fail(decoder, offset, consumed); + decoder->state = HTTP_CHUNKED_STATE_DATA_LF; + offset++; + break; + + case HTTP_CHUNKED_STATE_DATA_LF: + if (ch != '\n') + return http_chunked_fail(decoder, offset, consumed); + decoder->state = HTTP_CHUNKED_STATE_SIZE; + decoder->chunk_size = 0; + decoder->size_line_length = 0; + decoder->saw_size_digit = 0; + offset++; + break; + + case HTTP_CHUNKED_STATE_TRAILER_START: + if (http_chunked_count_trailer_byte(decoder) < 0) + return http_chunked_fail(decoder, offset, consumed); + if (ch == '\r') { + decoder->state = HTTP_CHUNKED_STATE_TRAILER_END_LF; + offset++; + } else if (ch == '\n') { + return http_chunked_fail(decoder, offset, consumed); + } else { + decoder->state = HTTP_CHUNKED_STATE_TRAILER; + offset++; + } + break; + + case HTTP_CHUNKED_STATE_TRAILER: + if (http_chunked_count_trailer_byte(decoder) < 0) + return http_chunked_fail(decoder, offset, consumed); + if (ch == '\r') { + decoder->state = HTTP_CHUNKED_STATE_TRAILER_LF; + offset++; + } else if (ch == '\n') { + return http_chunked_fail(decoder, offset, consumed); + } else { + offset++; + } + break; + + case HTTP_CHUNKED_STATE_TRAILER_LF: + if (http_chunked_count_trailer_byte(decoder) < 0 || ch != '\n') + return http_chunked_fail(decoder, offset, consumed); + decoder->state = HTTP_CHUNKED_STATE_TRAILER_START; + offset++; + break; + + case HTTP_CHUNKED_STATE_TRAILER_END_LF: + if (http_chunked_count_trailer_byte(decoder) < 0 || ch != '\n') + return http_chunked_fail(decoder, offset, consumed); + decoder->state = HTTP_CHUNKED_STATE_DONE; + offset++; + if (offset != input_len) + return http_chunked_fail(decoder, offset, consumed); + if (consumed) + *consumed = offset; + return HTTP_CHUNKED_DECODE_DONE; + + case HTTP_CHUNKED_STATE_DONE: + case HTTP_CHUNKED_STATE_ERROR: + return http_chunked_fail(decoder, offset, consumed); + } + } + + if (consumed) + *consumed = offset; + return decoder->state == HTTP_CHUNKED_STATE_DONE ? HTTP_CHUNKED_DECODE_DONE : HTTP_CHUNKED_DECODE_NEED_MORE; +} + +http_chunked_decode_result_t http_chunked_decoder_finish(http_chunked_decoder_t *decoder) { + if (!decoder || decoder->state == HTTP_CHUNKED_STATE_ERROR) + return HTTP_CHUNKED_DECODE_ERROR; + if (decoder->state == HTTP_CHUNKED_STATE_DONE) + return HTTP_CHUNKED_DECODE_DONE; + decoder->state = HTTP_CHUNKED_STATE_ERROR; + return HTTP_CHUNKED_DECODE_ERROR; +} diff --git a/src/http_chunked_decoder.h b/src/http_chunked_decoder.h new file mode 100644 index 00000000..edec052a --- /dev/null +++ b/src/http_chunked_decoder.h @@ -0,0 +1,50 @@ +#ifndef __HTTP_CHUNKED_DECODER_H__ +#define __HTTP_CHUNKED_DECODER_H__ + +#include +#include + +#define HTTP_CHUNKED_MAX_SIZE_LINE 4096 +#define HTTP_CHUNKED_MAX_TRAILER_SIZE 8192 + +typedef enum { + HTTP_CHUNKED_STATE_SIZE = 0, + HTTP_CHUNKED_STATE_EXTENSION, + HTTP_CHUNKED_STATE_SIZE_LF, + HTTP_CHUNKED_STATE_DATA, + HTTP_CHUNKED_STATE_DATA_CR, + HTTP_CHUNKED_STATE_DATA_LF, + HTTP_CHUNKED_STATE_TRAILER_START, + HTTP_CHUNKED_STATE_TRAILER, + HTTP_CHUNKED_STATE_TRAILER_LF, + HTTP_CHUNKED_STATE_TRAILER_END_LF, + HTTP_CHUNKED_STATE_DONE, + HTTP_CHUNKED_STATE_ERROR +} http_chunked_state_t; + +typedef enum { + HTTP_CHUNKED_DECODE_ERROR = -1, + HTTP_CHUNKED_DECODE_NEED_MORE = 0, + HTTP_CHUNKED_DECODE_DONE = 1 +} http_chunked_decode_result_t; + +typedef int (*http_chunked_emit_cb)(void *opaque, const uint8_t *data, size_t len); + +typedef struct http_chunked_decoder_s { + http_chunked_state_t state; + uint64_t chunk_size; + uint64_t chunk_remaining; + size_t size_line_length; + size_t trailer_size; + int saw_size_digit; +} http_chunked_decoder_t; + +void http_chunked_decoder_init(http_chunked_decoder_t *decoder); + +http_chunked_decode_result_t http_chunked_decoder_feed(http_chunked_decoder_t *decoder, const uint8_t *input, + size_t input_len, http_chunked_emit_cb emit, void *opaque, + size_t *consumed); + +http_chunked_decode_result_t http_chunked_decoder_finish(http_chunked_decoder_t *decoder); + +#endif /* __HTTP_CHUNKED_DECODER_H__ */ diff --git a/src/http_proxy.c b/src/http_proxy.c index abf196db..73e84573 100644 --- a/src/http_proxy.c +++ b/src/http_proxy.c @@ -37,6 +37,53 @@ static int http_proxy_append_raw_headers(http_proxy_session_t *session, char **d int filter_user_agent); static void http_proxy_pause_upstream(http_proxy_session_t *session); +static void http_proxy_parse_transfer_encoding(http_proxy_session_t *session, const char *value) { + const char *cursor = value; + int saw_token = 0; + + session->transfer_encoding_seen = 1; + + while (*cursor) { + const char *token_start; + const char *token_end; + const char *element_end; + + while (*cursor == ' ' || *cursor == '\t' || *cursor == ',') + cursor++; + if (*cursor == '\0') + break; + + token_start = cursor; + while (*cursor && *cursor != ',' && *cursor != ';' && *cursor != ' ' && *cursor != '\t') + cursor++; + token_end = cursor; + while (*cursor && *cursor != ',') + cursor++; + element_end = cursor; + while (element_end > token_end && (element_end[-1] == ' ' || element_end[-1] == '\t')) + element_end--; + + if (token_end == token_start) { + session->unsupported_transfer_coding = 1; + } else if ((size_t)(token_end - token_start) == 7 && strncasecmp(token_start, "chunked", 7) == 0 && + element_end == token_end && !session->response_is_chunked) { + session->response_is_chunked = 1; + } else { + session->unsupported_transfer_coding = 1; + } + saw_token = 1; + + if (*cursor == ',') { + if (session->response_is_chunked) + session->unsupported_transfer_coding = 1; + cursor++; + } + } + + if (!saw_token) + session->unsupported_transfer_coding = 1; +} + static int http_proxy_get_app_base_path(char *output, size_t output_size) { int written; @@ -94,6 +141,7 @@ void http_proxy_session_init(http_proxy_session_t *session) { session->bytes_received = 0; session->headers_received = 0; session->headers_forwarded = 0; + http_chunked_decoder_init(&session->chunked_decoder); session->upstream_paused = 0; session->cleanup_done = 0; } @@ -633,6 +681,42 @@ static int http_proxy_try_send_pending(http_proxy_session_t *session) { return total_sent; } +static int http_proxy_append_rewrite_body(http_proxy_session_t *session, const uint8_t *data, size_t len) { + size_t new_size; + + if (len == 0) + return 0; + if (!session || !data || len > REWRITE_MAX_BODY_SIZE - session->rewrite_body_buffer_used) { + logger(LOG_ERROR, "HTTP Proxy: Rewrite body exceeds max size"); + return -1; + } + + new_size = session->rewrite_body_buffer_used + len; + if (new_size > session->rewrite_body_buffer_size) { + size_t new_alloc = session->rewrite_body_buffer_size == 0 ? 16384 : session->rewrite_body_buffer_size * 2; + while (new_alloc < new_size) + new_alloc *= 2; + if (new_alloc > REWRITE_MAX_BODY_SIZE) + new_alloc = REWRITE_MAX_BODY_SIZE; + + char *new_buf = realloc(session->rewrite_body_buffer, new_alloc); + if (!new_buf) { + logger(LOG_ERROR, "HTTP Proxy: Failed to grow rewrite buffer"); + return -1; + } + session->rewrite_body_buffer = new_buf; + session->rewrite_body_buffer_size = new_alloc; + } + + memcpy(session->rewrite_body_buffer + session->rewrite_body_buffer_used, data, len); + session->rewrite_body_buffer_used = new_size; + return 0; +} + +static int http_proxy_emit_decoded_body(void *opaque, const uint8_t *data, size_t len) { + return http_proxy_append_rewrite_body((http_proxy_session_t *)opaque, data, len); +} + /** * Finalize M3U body rewriting: rewrite the buffered body and send the * response (headers + rewritten body) to the client. @@ -641,6 +725,8 @@ static int http_proxy_try_send_pending(http_proxy_session_t *session) { */ static int http_proxy_finalize_rewrite(http_proxy_session_t *session) { int bytes_forwarded = 0; + char *rewritten = NULL; + size_t rewritten_size = 0; if (session->rewrite_body_buffer_used > 0) { /* Null-terminate for rewriting */ @@ -667,9 +753,6 @@ static int http_proxy_finalize_rewrite(http_proxy_session_t *session) { .upstream_path = session->target_path, .base_url = base_url}; - char *rewritten = NULL; - size_t rewritten_size = 0; - int rewrite_result = rewrite_m3u_content(&ctx, session->rewrite_body_buffer, &rewritten, &rewritten_size); free(base_url); @@ -677,103 +760,145 @@ static int http_proxy_finalize_rewrite(http_proxy_session_t *session) { logger(LOG_ERROR, "HTTP Proxy: M3U rewrite failed"); return -1; } + } else { + rewritten = strdup(""); + if (!rewritten) { + logger(LOG_ERROR, "HTTP Proxy: Failed to allocate empty rewrite body"); + return -1; + } + } - /* Build and send response headers with new Content-Length - * Passthrough original headers except Content-Length and Transfer-Encoding */ - char headers[HTTP_PROXY_RESPONSE_BUFFER_SIZE]; - char *hdr_ptr = headers; - size_t hdr_remaining = sizeof(headers); - int headers_len = 0; - - if (session->saved_response_headers && session->saved_response_headers_len > 0) { - /* Parse and rebuild headers from saved original headers */ - char *saved_copy = strdup(session->saved_response_headers); - if (saved_copy) { - char *line = strtok(saved_copy, "\r\n"); - while (line != NULL) { - /* Skip headers that need to be modified */ - if (strncasecmp(line, "Content-Length:", 15) == 0 || strncasecmp(line, "Transfer-Encoding:", 18) == 0) { - /* Skip - will add correct Content-Length later */ - } else { - /* Pass through this header */ - int written = snprintf(hdr_ptr, hdr_remaining, "%s\r\n", line); - if (written > 0 && (size_t)written < hdr_remaining) { - hdr_ptr += written; - hdr_remaining -= written; - headers_len += written; - } + /* Build and send response headers with new Content-Length + * Passthrough original headers except Content-Length, Transfer-Encoding, and Trailer */ + char headers[HTTP_PROXY_RESPONSE_BUFFER_SIZE]; + char *hdr_ptr = headers; + size_t hdr_remaining = sizeof(headers); + int headers_len = 0; + + if (session->saved_response_headers && session->saved_response_headers_len > 0) { + /* Parse and rebuild headers from saved original headers */ + char *saved_copy = strdup(session->saved_response_headers); + if (saved_copy) { + char *line = strtok(saved_copy, "\r\n"); + while (line != NULL) { + /* Skip headers that need to be modified */ + if (strncasecmp(line, "Content-Length:", 15) == 0 || strncasecmp(line, "Transfer-Encoding:", 18) == 0 || + strncasecmp(line, "Trailer:", 8) == 0) { + /* Skip - will add correct Content-Length later */ + } else { + /* Pass through this header */ + int written = snprintf(hdr_ptr, hdr_remaining, "%s\r\n", line); + if (written > 0 && (size_t)written < hdr_remaining) { + hdr_ptr += written; + hdr_remaining -= written; + headers_len += written; } - line = strtok(NULL, "\r\n"); } - free(saved_copy); - } - - /* Add correct Content-Length */ - int cl_written = snprintf(hdr_ptr, hdr_remaining, "Content-Length: %zu\r\n", rewritten_size); - if (cl_written > 0 && (size_t)cl_written < hdr_remaining) { - hdr_ptr += cl_written; - hdr_remaining -= cl_written; - headers_len += cl_written; + line = strtok(NULL, "\r\n"); } - } else { - /* Fallback: build minimal headers */ - headers_len = snprintf(headers, sizeof(headers), - "HTTP/1.1 %d OK\r\n" - "Content-Type: %s\r\n" - "Content-Length: %zu\r\n" - "Connection: close\r\n", - session->response_status_code, session->response_content_type, rewritten_size); - hdr_ptr = headers + headers_len; - hdr_remaining = sizeof(headers) - headers_len; + free(saved_copy); } - /* Inject Set-Cookie header if needed */ - if (session->conn && session->conn->should_set_r2h_cookie && config.r2h_token && config.r2h_token[0] != '\0') { - int cookie_written = http_build_r2h_token_cookie_header(hdr_ptr, hdr_remaining, http_proxy_get_cookie_path()); - if (cookie_written > 0 && (size_t)cookie_written < hdr_remaining) { - hdr_ptr += cookie_written; - hdr_remaining -= cookie_written; - headers_len += cookie_written; - } else if (cookie_written < 0) { - logger(LOG_ERROR, "HTTP Proxy: Failed to build Set-Cookie header for r2h-token"); - } - session->conn->should_set_r2h_cookie = 0; - } - - /* Add final CRLF to end headers */ - int final_written = snprintf(hdr_ptr, hdr_remaining, "\r\n"); - if (final_written > 0) { - headers_len += final_written; + /* Add correct Content-Length */ + int cl_written = snprintf(hdr_ptr, hdr_remaining, "Content-Length: %zu\r\n", rewritten_size); + if (cl_written > 0 && (size_t)cl_written < hdr_remaining) { + hdr_ptr += cl_written; + hdr_remaining -= cl_written; + headers_len += cl_written; } + } else { + /* Fallback: build minimal headers */ + headers_len = snprintf(headers, sizeof(headers), + "HTTP/1.1 %d OK\r\n" + "Content-Type: %s\r\n" + "Content-Length: %zu\r\n" + "Connection: close\r\n", + session->response_status_code, session->response_content_type, rewritten_size); + hdr_ptr = headers + headers_len; + hdr_remaining = sizeof(headers) - headers_len; + } - if (connection_queue_output(session->conn, (const uint8_t *)headers, headers_len) < 0) { - free(rewritten); - logger(LOG_ERROR, "HTTP Proxy: Failed to send rewritten headers"); - return -1; + /* Inject Set-Cookie header if needed */ + if (session->conn && session->conn->should_set_r2h_cookie && config.r2h_token && config.r2h_token[0] != '\0') { + int cookie_written = http_build_r2h_token_cookie_header(hdr_ptr, hdr_remaining, http_proxy_get_cookie_path()); + if (cookie_written > 0 && (size_t)cookie_written < hdr_remaining) { + hdr_ptr += cookie_written; + hdr_remaining -= cookie_written; + headers_len += cookie_written; + } else if (cookie_written < 0) { + logger(LOG_ERROR, "HTTP Proxy: Failed to build Set-Cookie header for r2h-token"); } + session->conn->should_set_r2h_cookie = 0; + } - /* Send rewritten body */ - if (connection_queue_output(session->conn, (const uint8_t *)rewritten, rewritten_size) < 0) { - free(rewritten); - logger(LOG_ERROR, "HTTP Proxy: Failed to send rewritten body"); - return -1; - } + /* Add final CRLF to end headers */ + int final_written = snprintf(hdr_ptr, hdr_remaining, "\r\n"); + if (final_written > 0) { + headers_len += final_written; + } - session->headers_forwarded = 1; - if (session->conn) { - session->conn->headers_sent = 1; - } + if (connection_queue_output(session->conn, (const uint8_t *)headers, headers_len) < 0) { + free(rewritten); + logger(LOG_ERROR, "HTTP Proxy: Failed to send rewritten headers"); + return -1; + } - bytes_forwarded = (int)(headers_len + rewritten_size); + /* Send rewritten body */ + if (connection_queue_output(session->conn, (const uint8_t *)rewritten, rewritten_size) < 0) { free(rewritten); + logger(LOG_ERROR, "HTTP Proxy: Failed to send rewritten body"); + return -1; + } - logger(LOG_DEBUG, "HTTP Proxy: Sent rewritten M3U (%zu bytes body)", rewritten_size); + session->headers_forwarded = 1; + if (session->conn) { + session->conn->headers_sent = 1; } + bytes_forwarded = (int)(headers_len + rewritten_size); + free(rewritten); + + logger(LOG_DEBUG, "HTTP Proxy: Sent rewritten M3U (%zu bytes body)", rewritten_size); + http_proxy_set_state(session, HTTP_PROXY_STATE_COMPLETE); return bytes_forwarded; } +static int http_proxy_consume_rewrite_body(http_proxy_session_t *session, const uint8_t *data, size_t len) { + if (session->response_is_chunked) { + size_t consumed = 0; + http_chunked_decode_result_t result = http_chunked_decoder_feed(&session->chunked_decoder, data, len, + http_proxy_emit_decoded_body, session, &consumed); + if (result == HTTP_CHUNKED_DECODE_ERROR || consumed != len) { + logger(LOG_ERROR, "HTTP Proxy: Invalid chunked M3U response body"); + return -1; + } + if (result == HTTP_CHUNKED_DECODE_DONE) + return http_proxy_finalize_rewrite(session); + return (int)len; + } + + if (http_proxy_append_rewrite_body(session, data, len) < 0) + return -1; + if (session->content_length >= 0 && session->bytes_received >= session->content_length) + return http_proxy_finalize_rewrite(session); + return (int)len; +} + +static int http_proxy_handle_upstream_close(http_proxy_session_t *session) { + if (session->needs_body_rewrite) { + if (session->response_is_chunked && + http_chunked_decoder_finish(&session->chunked_decoder) != HTTP_CHUNKED_DECODE_DONE) { + logger(LOG_ERROR, "HTTP Proxy: Truncated chunked M3U response body"); + return -1; + } + return http_proxy_finalize_rewrite(session); + } + + http_proxy_set_state(session, HTTP_PROXY_STATE_COMPLETE); + return 0; +} + static int http_proxy_try_receive_response(http_proxy_session_t *session) { ssize_t received; int bytes_forwarded = 0; @@ -796,47 +921,18 @@ static int http_proxy_try_receive_response(http_proxy_session_t *session) { if (errno == EAGAIN) return 0; logger(LOG_ERROR, "HTTP Proxy: Recv failed: %s", strerror(errno)); + if (session->response_is_chunked) + return -1; return http_proxy_finalize_rewrite(session); } if (received == 0) { logger(LOG_DEBUG, "HTTP Proxy: Upstream closed, processing rewrite buffer"); - return http_proxy_finalize_rewrite(session); - } - - /* Append to rewrite buffer */ - size_t new_size = session->rewrite_body_buffer_used + (size_t)received; - if (new_size > REWRITE_MAX_BODY_SIZE) { - logger(LOG_ERROR, "HTTP Proxy: Rewrite body exceeds max size"); - return -1; - } - - if (new_size > session->rewrite_body_buffer_size) { - size_t new_alloc = session->rewrite_body_buffer_size == 0 ? 16384 : session->rewrite_body_buffer_size * 2; - while (new_alloc < new_size) - new_alloc *= 2; - if (new_alloc > REWRITE_MAX_BODY_SIZE) - new_alloc = REWRITE_MAX_BODY_SIZE; - - char *new_buf = realloc(session->rewrite_body_buffer, new_alloc); - if (!new_buf) { - logger(LOG_ERROR, "HTTP Proxy: Failed to grow rewrite buffer"); - return -1; - } - session->rewrite_body_buffer = new_buf; - session->rewrite_body_buffer_size = new_alloc; + return http_proxy_handle_upstream_close(session); } - memcpy(session->rewrite_body_buffer + session->rewrite_body_buffer_used, temp_buf, (size_t)received); - session->rewrite_body_buffer_used = new_size; session->bytes_received += received; - - /* All content received — finalize immediately */ - if (session->content_length >= 0 && session->bytes_received >= session->content_length) { - return http_proxy_finalize_rewrite(session); - } - - return (int)received; /* Progress: keep draining edge-triggered sockets */ + return http_proxy_consume_rewrite_body(session, temp_buf, (size_t)received); } /* Phase 2: Zero-copy streaming - recv directly to buffer pool */ @@ -870,8 +966,7 @@ static int http_proxy_try_receive_response(http_proxy_session_t *session) { if (received == 0) { buffer_ref_put(buf); logger(LOG_DEBUG, "HTTP Proxy: Upstream closed connection"); - http_proxy_set_state(session, HTTP_PROXY_STATE_COMPLETE); - return 0; + return http_proxy_handle_upstream_close(session); } /* Queue for zero-copy send */ @@ -939,31 +1034,13 @@ static int http_proxy_try_receive_response(http_proxy_session_t *session) { /* Forward any body data that came with headers (in response_buffer) */ if (session->headers_received && session->response_buffer_pos > 0) { if (session->needs_body_rewrite) { - /* Buffer mode: save initial body data for rewriting */ + /* Buffer or decode initial body data for rewriting */ size_t initial_size = session->response_buffer_pos; - if (initial_size > REWRITE_MAX_BODY_SIZE) { - logger(LOG_ERROR, "HTTP Proxy: Initial body exceeds max rewrite size"); - return -1; - } - - session->rewrite_body_buffer = malloc(initial_size + 1); - if (!session->rewrite_body_buffer) { - logger(LOG_ERROR, "HTTP Proxy: Failed to allocate rewrite buffer"); - return -1; - } - memcpy(session->rewrite_body_buffer, session->response_buffer, initial_size); - session->rewrite_body_buffer_size = initial_size + 1; - session->rewrite_body_buffer_used = initial_size; session->bytes_received += initial_size; session->response_buffer_pos = 0; - - /* Check if we've received all content */ - if (session->content_length >= 0 && session->bytes_received >= session->content_length) { - logger(LOG_DEBUG, "HTTP Proxy: All M3U content received with headers (%zd bytes)", session->bytes_received); - return http_proxy_finalize_rewrite(session); - } - - bytes_forwarded = (int)initial_size; + bytes_forwarded = http_proxy_consume_rewrite_body(session, session->response_buffer, initial_size); + if (bytes_forwarded < 0) + return -1; } else { /* Normal mode: forward immediately */ if (connection_queue_output(session->conn, session->response_buffer, session->response_buffer_pos) < 0) { @@ -1050,6 +1127,12 @@ static int http_proxy_parse_response_headers(http_proxy_session_t *session) { strncpy(session->response_content_type, value, sizeof(session->response_content_type) - 1); session->response_content_type[sizeof(session->response_content_type) - 1] = '\0'; logger(LOG_DEBUG, "HTTP Proxy: Content-Type: %s", session->response_content_type); + } else if (strncasecmp(line, "Transfer-Encoding:", 18) == 0) { + char *value = line + 18; + while (*value == ' ' || *value == '\t') + value++; + http_proxy_parse_transfer_encoding(session, value); + logger(LOG_DEBUG, "HTTP Proxy: Transfer-Encoding: %s", value); } else if (strncasecmp(line, "Location:", 9) == 0) { /* Extract Location header value for potential rewriting */ char *value = line + 9; @@ -1060,7 +1143,6 @@ static int http_proxy_parse_response_headers(http_proxy_session_t *session) { has_location = 1; logger(LOG_DEBUG, "HTTP Proxy: Location: %s", location_header); } - /* Note: Transfer-Encoding is passed through, not parsed */ } session->headers_received = 1; @@ -1073,8 +1155,17 @@ static int http_proxy_parse_response_headers(http_proxy_session_t *session) { is_m3u_response = rewrite_is_m3u_content_type(session->response_content_type); if (is_m3u_response && strcasecmp(session->method, "HEAD") != 0) { - /* Only rewrite if Content-Length is known and within limits */ - if (session->content_length > 0 && (size_t)session->content_length <= REWRITE_MAX_BODY_SIZE) { + if (session->transfer_encoding_seen && (!session->response_is_chunked || session->unsupported_transfer_coding)) { + logger(LOG_ERROR, "HTTP Proxy: Unsupported Transfer-Encoding for M3U rewrite"); + return -1; + } + + /* Transfer-Encoding takes precedence over Content-Length. */ + if (session->response_is_chunked) { + session->needs_body_rewrite = 1; + http_chunked_decoder_init(&session->chunked_decoder); + logger(LOG_DEBUG, "HTTP Proxy: Chunked M3U content detected, will decode and rewrite body"); + } else if (session->content_length > 0 && (size_t)session->content_length <= REWRITE_MAX_BODY_SIZE) { session->needs_body_rewrite = 1; logger(LOG_DEBUG, "HTTP Proxy: M3U content detected, will rewrite body"); } else if (session->content_length == -1) { @@ -1295,30 +1386,6 @@ int http_proxy_handle_socket_event(http_proxy_session_t *session, uint32_t event } } - /* Check for hard socket errors */ - if (events & POLLER_ERR) { - int sock_error = 0; - socklen_t error_len = sizeof(sock_error); - if (getsockopt(session->socket, SOL_SOCKET, SO_ERROR, &sock_error, &error_len) == 0 && sock_error != 0) { - logger(LOG_ERROR, "HTTP Proxy: Socket error: %s", strerror(sock_error)); - /* During STREAMING: drain pending client output before disconnecting */ - if (session->state == HTTP_PROXY_STATE_STREAMING) { - http_proxy_set_state(session, HTTP_PROXY_STATE_COMPLETE); - } else { - http_proxy_set_state(session, HTTP_PROXY_STATE_ERROR); - return -1; - } - } else if (session->state != HTTP_PROXY_STATE_SENDING_REQUEST) { - logger(LOG_ERROR, "HTTP Proxy: Socket error event received"); - if (session->state == HTTP_PROXY_STATE_STREAMING) { - http_proxy_set_state(session, HTTP_PROXY_STATE_COMPLETE); - } else { - http_proxy_set_state(session, HTTP_PROXY_STATE_ERROR); - return -1; - } - } - } - /* Handle writable socket - send pending request */ if ((events & POLLER_OUT) && session->state == HTTP_PROXY_STATE_SENDING_REQUEST) { result = http_proxy_try_send_pending(session); @@ -1366,6 +1433,43 @@ int http_proxy_handle_socket_event(http_proxy_session_t *session, uint32_t event } } + /* Check hard socket errors AFTER draining readable data. Linux may report + * EPOLLIN together with EPOLLERR when a peer sends the final response bytes + * and then resets the connection. The response is still valid if those + * bytes complete its framing, particularly the terminating chunk. */ + if ((events & POLLER_ERR) && session->state != HTTP_PROXY_STATE_COMPLETE) { + int sock_error = 0; + socklen_t error_len = sizeof(sock_error); + if (getsockopt(session->socket, SOL_SOCKET, SO_ERROR, &sock_error, &error_len) == 0 && sock_error != 0) { + logger(LOG_ERROR, "HTTP Proxy: Socket error: %s", strerror(sock_error)); + /* During STREAMING: drain pending client output before disconnecting */ + if (session->state == HTTP_PROXY_STATE_STREAMING) { + if (session->needs_body_rewrite && session->response_is_chunked) { + logger(LOG_ERROR, "HTTP Proxy: Chunked M3U upstream failed before the terminating chunk"); + http_proxy_set_state(session, HTTP_PROXY_STATE_ERROR); + return -1; + } + http_proxy_set_state(session, HTTP_PROXY_STATE_COMPLETE); + } else { + http_proxy_set_state(session, HTTP_PROXY_STATE_ERROR); + return -1; + } + } else if (session->state != HTTP_PROXY_STATE_SENDING_REQUEST) { + logger(LOG_ERROR, "HTTP Proxy: Socket error event received"); + if (session->state == HTTP_PROXY_STATE_STREAMING) { + if (session->needs_body_rewrite && session->response_is_chunked) { + logger(LOG_ERROR, "HTTP Proxy: Chunked M3U upstream failed before the terminating chunk"); + http_proxy_set_state(session, HTTP_PROXY_STATE_ERROR); + return -1; + } + http_proxy_set_state(session, HTTP_PROXY_STATE_COMPLETE); + } else { + http_proxy_set_state(session, HTTP_PROXY_STATE_ERROR); + return -1; + } + } + } + /* Check for connection hangup AFTER processing data. * Only when POLLER_IN was NOT set — if POLLER_IN was set, recv already * handled EOF (returning 0) and set COMPLETE as needed. Processing @@ -1375,6 +1479,11 @@ int http_proxy_handle_socket_event(http_proxy_session_t *session, uint32_t event /* Upstream closed connection */ if (session->state == HTTP_PROXY_STATE_STREAMING || session->state == HTTP_PROXY_STATE_AWAITING_HEADERS) { logger(LOG_DEBUG, "HTTP Proxy: Upstream closed connection (normal)"); + if (session->state == HTTP_PROXY_STATE_STREAMING && session->needs_body_rewrite && session->response_is_chunked) { + logger(LOG_ERROR, "HTTP Proxy: Chunked M3U upstream closed before the terminating chunk"); + http_proxy_set_state(session, HTTP_PROXY_STATE_ERROR); + return -1; + } http_proxy_set_state(session, HTTP_PROXY_STATE_COMPLETE); } else if (session->state != HTTP_PROXY_STATE_COMPLETE) { /* For other states (like SENDING_REQUEST), this is unexpected */ diff --git a/src/http_proxy.h b/src/http_proxy.h index 4b922c1a..a2da6de2 100644 --- a/src/http_proxy.h +++ b/src/http_proxy.h @@ -1,6 +1,7 @@ #ifndef __HTTP_PROXY_H__ #define __HTTP_PROXY_H__ +#include "http_chunked_decoder.h" #include #include @@ -79,6 +80,10 @@ typedef struct { ssize_t bytes_received; /* Bytes of body received so far */ int headers_received; /* Flag: headers fully received */ int headers_forwarded; /* Flag: headers forwarded to client */ + int transfer_encoding_seen; /* Upstream sent Transfer-Encoding */ + int response_is_chunked; /* Final supported transfer coding is chunked */ + int unsupported_transfer_coding; /* Transfer-Encoding includes unsupported coding */ + http_chunked_decoder_t chunked_decoder; /* Used only while rewriting chunked M3U bodies */ /* Non-blocking I/O state */ char pending_request[HTTP_PROXY_REQUEST_BUFFER_SIZE]; /* Request being sent */ From dd7c0b64701025f1cd63c2599adbf8c700e0762e Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Tue, 14 Jul 2026 19:24:58 +0800 Subject: [PATCH 2/3] fix(http-proxy): handle header copy failure --- e2e/test_http_proxy_m3u_rewrite.py | 7 +----- src/http_proxy.c | 38 +++++++++++++++++------------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/e2e/test_http_proxy_m3u_rewrite.py b/e2e/test_http_proxy_m3u_rewrite.py index 27225d46..69ef76a5 100644 --- a/e2e/test_http_proxy_m3u_rewrite.py +++ b/e2e/test_http_proxy_m3u_rewrite.py @@ -799,12 +799,7 @@ def test_empty_chunked_m3u_returns_empty_content_length_body(self, shared_r2h): def test_complete_chunked_m3u_survives_immediate_upstream_reset(self, shared_r2h): playlist = b"#EXTM3U\n#EXTINF:8.000,\nsegment.ts\n" - response = ( - _raw_chunked_headers() - + f"{len(playlist):x}\r\n".encode() - + playlist - + b"\r\n0\r\n\r\n" - ) + response = _raw_chunked_headers() + f"{len(playlist):x}\r\n".encode() + playlist + b"\r\n0\r\n\r\n" upstream = _RawHTTPResponseUpstream(response, keep_open=False, reset_after_send=True) upstream.start() try: diff --git a/src/http_proxy.c b/src/http_proxy.c index 73e84573..18f36da0 100644 --- a/src/http_proxy.c +++ b/src/http_proxy.c @@ -778,26 +778,30 @@ static int http_proxy_finalize_rewrite(http_proxy_session_t *session) { if (session->saved_response_headers && session->saved_response_headers_len > 0) { /* Parse and rebuild headers from saved original headers */ char *saved_copy = strdup(session->saved_response_headers); - if (saved_copy) { - char *line = strtok(saved_copy, "\r\n"); - while (line != NULL) { - /* Skip headers that need to be modified */ - if (strncasecmp(line, "Content-Length:", 15) == 0 || strncasecmp(line, "Transfer-Encoding:", 18) == 0 || - strncasecmp(line, "Trailer:", 8) == 0) { - /* Skip - will add correct Content-Length later */ - } else { - /* Pass through this header */ - int written = snprintf(hdr_ptr, hdr_remaining, "%s\r\n", line); - if (written > 0 && (size_t)written < hdr_remaining) { - hdr_ptr += written; - hdr_remaining -= written; - headers_len += written; - } + if (!saved_copy) { + free(rewritten); + logger(LOG_ERROR, "HTTP Proxy: Failed to copy saved response headers"); + return -1; + } + + char *line = strtok(saved_copy, "\r\n"); + while (line != NULL) { + /* Skip headers that need to be modified */ + if (strncasecmp(line, "Content-Length:", 15) == 0 || strncasecmp(line, "Transfer-Encoding:", 18) == 0 || + strncasecmp(line, "Trailer:", 8) == 0) { + /* Skip - will add correct Content-Length later */ + } else { + /* Pass through this header */ + int written = snprintf(hdr_ptr, hdr_remaining, "%s\r\n", line); + if (written > 0 && (size_t)written < hdr_remaining) { + hdr_ptr += written; + hdr_remaining -= written; + headers_len += written; } - line = strtok(NULL, "\r\n"); } - free(saved_copy); + line = strtok(NULL, "\r\n"); } + free(saved_copy); /* Add correct Content-Length */ int cl_written = snprintf(hdr_ptr, hdr_remaining, "Content-Length: %zu\r\n", rewritten_size); From b8e7504bbf7b576a005330f9fb9fc7d34290ce84 Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Tue, 14 Jul 2026 19:29:45 +0800 Subject: [PATCH 3/3] refactor(http-proxy): simplify rewrite finalization --- src/http_chunked_decoder.c | 1 - src/http_proxy.c | 74 ++++++++++++++------------------------ src/http_proxy.h | 1 - 3 files changed, 26 insertions(+), 50 deletions(-) diff --git a/src/http_chunked_decoder.c b/src/http_chunked_decoder.c index aea90eac..bdc1d9ad 100644 --- a/src/http_chunked_decoder.c +++ b/src/http_chunked_decoder.c @@ -1,5 +1,4 @@ #include "http_chunked_decoder.h" -#include #include static int http_chunked_hex_value(uint8_t ch) { diff --git a/src/http_proxy.c b/src/http_proxy.c index 18f36da0..da15a070 100644 --- a/src/http_proxy.c +++ b/src/http_proxy.c @@ -141,7 +141,6 @@ void http_proxy_session_init(http_proxy_session_t *session) { session->bytes_received = 0; session->headers_received = 0; session->headers_forwarded = 0; - http_chunked_decoder_init(&session->chunked_decoder); session->upstream_paused = 0; session->cleanup_done = 0; } @@ -760,12 +759,6 @@ static int http_proxy_finalize_rewrite(http_proxy_session_t *session) { logger(LOG_ERROR, "HTTP Proxy: M3U rewrite failed"); return -1; } - } else { - rewritten = strdup(""); - if (!rewritten) { - logger(LOG_ERROR, "HTTP Proxy: Failed to allocate empty rewrite body"); - return -1; - } } /* Build and send response headers with new Content-Length @@ -775,16 +768,9 @@ static int http_proxy_finalize_rewrite(http_proxy_session_t *session) { size_t hdr_remaining = sizeof(headers); int headers_len = 0; - if (session->saved_response_headers && session->saved_response_headers_len > 0) { - /* Parse and rebuild headers from saved original headers */ - char *saved_copy = strdup(session->saved_response_headers); - if (!saved_copy) { - free(rewritten); - logger(LOG_ERROR, "HTTP Proxy: Failed to copy saved response headers"); - return -1; - } - - char *line = strtok(saved_copy, "\r\n"); + if (session->saved_response_headers) { + /* Rebuild headers in place; the saved copy is not needed after finalization. */ + char *line = strtok(session->saved_response_headers, "\r\n"); while (line != NULL) { /* Skip headers that need to be modified */ if (strncasecmp(line, "Content-Length:", 15) == 0 || strncasecmp(line, "Transfer-Encoding:", 18) == 0 || @@ -801,7 +787,6 @@ static int http_proxy_finalize_rewrite(http_proxy_session_t *session) { } line = strtok(NULL, "\r\n"); } - free(saved_copy); /* Add correct Content-Length */ int cl_written = snprintf(hdr_ptr, hdr_remaining, "Content-Length: %zu\r\n", rewritten_size); @@ -889,7 +874,7 @@ static int http_proxy_consume_rewrite_body(http_proxy_session_t *session, const return (int)len; } -static int http_proxy_handle_upstream_close(http_proxy_session_t *session) { +static int http_proxy_handle_upstream_end(http_proxy_session_t *session) { if (session->needs_body_rewrite) { if (session->response_is_chunked && http_chunked_decoder_finish(&session->chunked_decoder) != HTTP_CHUNKED_DECODE_DONE) { @@ -925,14 +910,12 @@ static int http_proxy_try_receive_response(http_proxy_session_t *session) { if (errno == EAGAIN) return 0; logger(LOG_ERROR, "HTTP Proxy: Recv failed: %s", strerror(errno)); - if (session->response_is_chunked) - return -1; - return http_proxy_finalize_rewrite(session); + return http_proxy_handle_upstream_end(session); } if (received == 0) { logger(LOG_DEBUG, "HTTP Proxy: Upstream closed, processing rewrite buffer"); - return http_proxy_handle_upstream_close(session); + return http_proxy_handle_upstream_end(session); } session->bytes_received += received; @@ -963,14 +946,13 @@ static int http_proxy_try_receive_response(http_proxy_session_t *session) { return 0; } logger(LOG_ERROR, "HTTP Proxy: Recv failed: %s", strerror(errno)); - http_proxy_set_state(session, HTTP_PROXY_STATE_COMPLETE); - return 0; + return http_proxy_handle_upstream_end(session); } if (received == 0) { buffer_ref_put(buf); logger(LOG_DEBUG, "HTTP Proxy: Upstream closed connection"); - return http_proxy_handle_upstream_close(session); + return http_proxy_handle_upstream_end(session); } /* Queue for zero-copy send */ @@ -1186,7 +1168,6 @@ static int http_proxy_parse_response_headers(http_proxy_session_t *session) { if (session->saved_response_headers) { memcpy(session->saved_response_headers, session->response_buffer, header_len); session->saved_response_headers[header_len] = '\0'; - session->saved_response_headers_len = header_len; logger(LOG_DEBUG, "HTTP Proxy: Saved %zu bytes of response headers for rewrite", header_len); } } @@ -1443,30 +1424,24 @@ int http_proxy_handle_socket_event(http_proxy_session_t *session, uint32_t event * bytes complete its framing, particularly the terminating chunk. */ if ((events & POLLER_ERR) && session->state != HTTP_PROXY_STATE_COMPLETE) { int sock_error = 0; + int has_socket_error = 0; socklen_t error_len = sizeof(sock_error); if (getsockopt(session->socket, SOL_SOCKET, SO_ERROR, &sock_error, &error_len) == 0 && sock_error != 0) { logger(LOG_ERROR, "HTTP Proxy: Socket error: %s", strerror(sock_error)); - /* During STREAMING: drain pending client output before disconnecting */ - if (session->state == HTTP_PROXY_STATE_STREAMING) { - if (session->needs_body_rewrite && session->response_is_chunked) { - logger(LOG_ERROR, "HTTP Proxy: Chunked M3U upstream failed before the terminating chunk"); - http_proxy_set_state(session, HTTP_PROXY_STATE_ERROR); - return -1; - } - http_proxy_set_state(session, HTTP_PROXY_STATE_COMPLETE); - } else { - http_proxy_set_state(session, HTTP_PROXY_STATE_ERROR); - return -1; - } + has_socket_error = 1; } else if (session->state != HTTP_PROXY_STATE_SENDING_REQUEST) { logger(LOG_ERROR, "HTTP Proxy: Socket error event received"); + has_socket_error = 1; + } + + if (has_socket_error) { if (session->state == HTTP_PROXY_STATE_STREAMING) { - if (session->needs_body_rewrite && session->response_is_chunked) { - logger(LOG_ERROR, "HTTP Proxy: Chunked M3U upstream failed before the terminating chunk"); + result = http_proxy_handle_upstream_end(session); + if (result < 0) { http_proxy_set_state(session, HTTP_PROXY_STATE_ERROR); return -1; } - http_proxy_set_state(session, HTTP_PROXY_STATE_COMPLETE); + progress += result; } else { http_proxy_set_state(session, HTTP_PROXY_STATE_ERROR); return -1; @@ -1483,12 +1458,16 @@ int http_proxy_handle_socket_event(http_proxy_session_t *session, uint32_t event /* Upstream closed connection */ if (session->state == HTTP_PROXY_STATE_STREAMING || session->state == HTTP_PROXY_STATE_AWAITING_HEADERS) { logger(LOG_DEBUG, "HTTP Proxy: Upstream closed connection (normal)"); - if (session->state == HTTP_PROXY_STATE_STREAMING && session->needs_body_rewrite && session->response_is_chunked) { - logger(LOG_ERROR, "HTTP Proxy: Chunked M3U upstream closed before the terminating chunk"); - http_proxy_set_state(session, HTTP_PROXY_STATE_ERROR); - return -1; + if (session->state == HTTP_PROXY_STATE_STREAMING) { + result = http_proxy_handle_upstream_end(session); + if (result < 0) { + http_proxy_set_state(session, HTTP_PROXY_STATE_ERROR); + return -1; + } + progress += result; + } else { + http_proxy_set_state(session, HTTP_PROXY_STATE_COMPLETE); } - http_proxy_set_state(session, HTTP_PROXY_STATE_COMPLETE); } else if (session->state != HTTP_PROXY_STATE_COMPLETE) { /* For other states (like SENDING_REQUEST), this is unexpected */ logger(LOG_INFO, @@ -1550,7 +1529,6 @@ int http_proxy_session_cleanup(http_proxy_session_t *session) { if (session->saved_response_headers) { free(session->saved_response_headers); session->saved_response_headers = NULL; - session->saved_response_headers_len = 0; } session->cleanup_done = 1; diff --git a/src/http_proxy.h b/src/http_proxy.h index a2da6de2..10e2b179 100644 --- a/src/http_proxy.h +++ b/src/http_proxy.h @@ -111,7 +111,6 @@ typedef struct { /* Saved response headers for passthrough during body rewrite */ char *saved_response_headers; /* malloc'd copy of original response headers */ - size_t saved_response_headers_len; /* Request headers for base URL construction */ char host_header[HTTP_PROXY_HOST_SIZE]; /* Host header from client */