Skip to content

Commit eb790bb

Browse files
stackiaclaude
andcommitted
feat(http-proxy): add Location header rewriting for 30x redirects
Rewrites Location headers in 30x redirect responses to route through the proxy. Moves is_http_proxy_url and build_http_proxy_url from m3u.c to http_proxy.c for reuse. Uses absolute path format (/http/host/path) to work correctly with reverse proxies. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9a91efb commit eb790bb

4 files changed

Lines changed: 252 additions & 119 deletions

File tree

src/http_proxy.c

Lines changed: 223 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#include "buffer_pool.h"
33
#include "configuration.h"
44
#include "connection.h"
5-
#include "http.h"
5+
#include "http.h" /* For http_url_encode */
66
#include "multicast.h"
77
#include "status.h"
88
#include "utils.h"
@@ -571,12 +571,22 @@ static int http_proxy_try_receive_response(http_proxy_session_t *session) {
571571
return bytes_forwarded;
572572
}
573573

574+
/* Check if status code is a redirect that may have Location header */
575+
static int http_proxy_is_redirect_status(int status_code) {
576+
return (status_code == 301 || status_code == 302 || status_code == 303 ||
577+
status_code == 307 || status_code == 308);
578+
}
579+
574580
static int http_proxy_parse_response_headers(http_proxy_session_t *session) {
575581
char *header_end;
576582
char *line;
577583
char *body_start;
578584
size_t header_len;
579585
char headers_copy[HTTP_PROXY_RESPONSE_BUFFER_SIZE];
586+
char location_header[HTTP_PROXY_PATH_SIZE];
587+
int has_location = 0;
588+
589+
location_header[0] = '\0';
580590

581591
/* Look for end of headers (double CRLF) */
582592
session->response_buffer[session->response_buffer_pos] = '\0';
@@ -631,6 +641,15 @@ static int http_proxy_parse_response_headers(http_proxy_session_t *session) {
631641
1] = '\0';
632642
logger(LOG_DEBUG, "HTTP Proxy: Content-Type: %s",
633643
session->response_content_type);
644+
} else if (strncasecmp(line, "Location:", 9) == 0) {
645+
/* Extract Location header value for potential rewriting */
646+
char *value = line + 9;
647+
while (*value == ' ')
648+
value++;
649+
strncpy(location_header, value, sizeof(location_header) - 1);
650+
location_header[sizeof(location_header) - 1] = '\0';
651+
has_location = 1;
652+
logger(LOG_DEBUG, "HTTP Proxy: Location: %s", location_header);
634653
}
635654
/* Note: Transfer-Encoding is passed through, not parsed */
636655
}
@@ -640,44 +659,142 @@ static int http_proxy_parse_response_headers(http_proxy_session_t *session) {
640659
/* Forward response headers to client - flush immediately */
641660
if (!session->headers_forwarded && session->conn) {
642661
/*
643-
* We need to inject Set-Cookie header if should_set_r2h_cookie is set.
644-
* Strategy: send headers without final \r\n\r\n, optionally inject
645-
* Set-Cookie, then send \r\n\r\n.
662+
* For redirect responses (30x), we need to rewrite the Location header
663+
* to point back through the proxy. We rebuild headers line by line.
664+
*
665+
* For non-redirect responses, we need to inject Set-Cookie header if
666+
* should_set_r2h_cookie is set.
646667
*/
647-
size_t headers_without_crlf = header_len - 2; /* Exclude final \r\n */
648-
649-
/* Send headers up to (but not including) final \r\n\r\n */
650-
if (connection_queue_output(session->conn, session->response_buffer,
651-
headers_without_crlf) < 0) {
652-
logger(LOG_ERROR, "HTTP Proxy: Failed to forward headers to client");
653-
return -1;
668+
int is_redirect = http_proxy_is_redirect_status(session->response_status_code);
669+
char rewritten_location[HTTP_PROXY_PATH_SIZE];
670+
int location_rewritten = 0;
671+
672+
/* Rewrite Location header for redirects */
673+
if (is_redirect && has_location) {
674+
if (http_proxy_build_url(location_header, "/", rewritten_location,
675+
sizeof(rewritten_location)) == 0) {
676+
location_rewritten = 1;
677+
logger(LOG_DEBUG, "HTTP Proxy: Rewritten Location: %s -> %s",
678+
location_header, rewritten_location);
679+
}
654680
}
655681

656-
/* Inject Set-Cookie header if needed */
657-
if (session->conn->should_set_r2h_cookie && config.r2h_token &&
658-
config.r2h_token[0] != '\0') {
659-
char set_cookie_header[512];
660-
int cookie_len =
661-
snprintf(set_cookie_header, sizeof(set_cookie_header),
662-
"Set-Cookie: r2h-token=%s; Path=/; HttpOnly; "
663-
"SameSite=Strict\r\n",
664-
config.r2h_token);
665-
if (cookie_len > 0 && cookie_len < (int)sizeof(set_cookie_header)) {
666-
if (connection_queue_output(session->conn,
667-
(const uint8_t *)set_cookie_header,
668-
cookie_len) < 0) {
669-
logger(LOG_ERROR, "HTTP Proxy: Failed to send Set-Cookie header");
682+
if (location_rewritten) {
683+
/*
684+
* Need to rebuild headers with modified Location.
685+
* Parse original headers again and rebuild with new Location value.
686+
*/
687+
char rebuilt_headers[HTTP_PROXY_RESPONSE_BUFFER_SIZE];
688+
char *rebuild_ptr = rebuilt_headers;
689+
size_t rebuild_remaining = sizeof(rebuilt_headers);
690+
char *orig_line;
691+
char orig_headers[HTTP_PROXY_RESPONSE_BUFFER_SIZE];
692+
int first_line = 1;
693+
694+
/* Copy headers again for parsing */
695+
memcpy(orig_headers, session->response_buffer, header_len - 2);
696+
orig_headers[header_len - 2] = '\0';
697+
698+
/* Rebuild headers line by line */
699+
orig_line = strtok(orig_headers, "\r\n");
700+
while (orig_line != NULL) {
701+
int written;
702+
703+
if (strncasecmp(orig_line, "Location:", 9) == 0) {
704+
/* Replace Location header with rewritten value */
705+
written = snprintf(rebuild_ptr, rebuild_remaining, "Location: %s\r\n",
706+
rewritten_location);
707+
} else {
708+
/* Copy other headers as-is */
709+
written = snprintf(rebuild_ptr, rebuild_remaining, "%s\r\n", orig_line);
710+
}
711+
712+
if (written < 0 || (size_t)written >= rebuild_remaining) {
713+
logger(LOG_ERROR, "HTTP Proxy: Rebuilt headers too large");
670714
return -1;
671715
}
672-
logger(LOG_DEBUG, "HTTP Proxy: Injected Set-Cookie header for r2h-token");
716+
717+
rebuild_ptr += written;
718+
rebuild_remaining -= written;
719+
first_line = 0;
720+
orig_line = strtok(NULL, "\r\n");
673721
}
674-
session->conn->should_set_r2h_cookie = 0; /* Only set once */
675-
}
676722

677-
/* Send final \r\n to end headers */
678-
if (connection_queue_output(session->conn, (const uint8_t *)"\r\n", 2) < 0) {
679-
logger(LOG_ERROR, "HTTP Proxy: Failed to send header terminator");
680-
return -1;
723+
(void)first_line; /* Suppress unused warning */
724+
725+
/* Send rebuilt headers */
726+
size_t rebuilt_len = rebuild_ptr - rebuilt_headers;
727+
if (connection_queue_output(session->conn, (const uint8_t *)rebuilt_headers,
728+
rebuilt_len) < 0) {
729+
logger(LOG_ERROR, "HTTP Proxy: Failed to forward rebuilt headers");
730+
return -1;
731+
}
732+
733+
/* Inject Set-Cookie header if needed */
734+
if (session->conn->should_set_r2h_cookie && config.r2h_token &&
735+
config.r2h_token[0] != '\0') {
736+
char set_cookie_header[512];
737+
int cookie_len =
738+
snprintf(set_cookie_header, sizeof(set_cookie_header),
739+
"Set-Cookie: r2h-token=%s; Path=/; HttpOnly; "
740+
"SameSite=Strict\r\n",
741+
config.r2h_token);
742+
if (cookie_len > 0 && cookie_len < (int)sizeof(set_cookie_header)) {
743+
if (connection_queue_output(session->conn,
744+
(const uint8_t *)set_cookie_header,
745+
cookie_len) < 0) {
746+
logger(LOG_ERROR, "HTTP Proxy: Failed to send Set-Cookie header");
747+
return -1;
748+
}
749+
logger(LOG_DEBUG,
750+
"HTTP Proxy: Injected Set-Cookie header for r2h-token");
751+
}
752+
session->conn->should_set_r2h_cookie = 0;
753+
}
754+
755+
/* Send final CRLF to end headers */
756+
if (connection_queue_output(session->conn, (const uint8_t *)"\r\n", 2) < 0) {
757+
logger(LOG_ERROR, "HTTP Proxy: Failed to send header terminator");
758+
return -1;
759+
}
760+
} else {
761+
/* No Location rewriting needed - use original logic */
762+
size_t headers_without_crlf = header_len - 2; /* Exclude final \r\n */
763+
764+
/* Send headers up to (but not including) final \r\n\r\n */
765+
if (connection_queue_output(session->conn, session->response_buffer,
766+
headers_without_crlf) < 0) {
767+
logger(LOG_ERROR, "HTTP Proxy: Failed to forward headers to client");
768+
return -1;
769+
}
770+
771+
/* Inject Set-Cookie header if needed */
772+
if (session->conn->should_set_r2h_cookie && config.r2h_token &&
773+
config.r2h_token[0] != '\0') {
774+
char set_cookie_header[512];
775+
int cookie_len =
776+
snprintf(set_cookie_header, sizeof(set_cookie_header),
777+
"Set-Cookie: r2h-token=%s; Path=/; HttpOnly; "
778+
"SameSite=Strict\r\n",
779+
config.r2h_token);
780+
if (cookie_len > 0 && cookie_len < (int)sizeof(set_cookie_header)) {
781+
if (connection_queue_output(session->conn,
782+
(const uint8_t *)set_cookie_header,
783+
cookie_len) < 0) {
784+
logger(LOG_ERROR, "HTTP Proxy: Failed to send Set-Cookie header");
785+
return -1;
786+
}
787+
logger(LOG_DEBUG,
788+
"HTTP Proxy: Injected Set-Cookie header for r2h-token");
789+
}
790+
session->conn->should_set_r2h_cookie = 0; /* Only set once */
791+
}
792+
793+
/* Send final \r\n to end headers */
794+
if (connection_queue_output(session->conn, (const uint8_t *)"\r\n", 2) < 0) {
795+
logger(LOG_ERROR, "HTTP Proxy: Failed to send header terminator");
796+
return -1;
797+
}
681798
}
682799

683800
session->headers_forwarded = 1;
@@ -871,3 +988,77 @@ int http_proxy_session_cleanup(http_proxy_session_t *session) {
871988

872989
return 0;
873990
}
991+
992+
int http_proxy_is_proxy_url(const char *url) {
993+
/* Only support http:// URLs for proxying (not https://) */
994+
if (strncasecmp(url, "http://", 7) == 0) {
995+
/* Make sure it's not already a wrapped URL (http://host/rtp/...) */
996+
const char *path_start = strchr(url + 7, '/');
997+
if (path_start) {
998+
/* Check if path starts with /rtp/, /udp/, /rtsp/, or /http/ */
999+
if (strncmp(path_start, "/rtp/", 5) == 0 ||
1000+
strncmp(path_start, "/udp/", 5) == 0 ||
1001+
strncmp(path_start, "/rtsp/", 6) == 0 ||
1002+
strncmp(path_start, "/http/", 6) == 0) {
1003+
return 0; /* Already wrapped, don't treat as plain HTTP */
1004+
}
1005+
}
1006+
return 1;
1007+
}
1008+
return 0;
1009+
}
1010+
1011+
int http_proxy_build_url(const char *http_url, const char *base_url_placeholder,
1012+
char *output, size_t output_size) {
1013+
const char *host_start;
1014+
char *encoded_token = NULL;
1015+
int result;
1016+
int has_r2h_token = (config.r2h_token && config.r2h_token[0] != '\0');
1017+
1018+
/* Skip http:// prefix */
1019+
if (strncasecmp(http_url, "http://", 7) != 0) {
1020+
logger(LOG_ERROR, "http_proxy_build_url: URL must start with http://");
1021+
return -1;
1022+
}
1023+
host_start = http_url + 7; /* Points to host:port/path */
1024+
1025+
/* URL encode r2h-token if configured */
1026+
if (has_r2h_token) {
1027+
encoded_token = http_url_encode(config.r2h_token);
1028+
if (!encoded_token) {
1029+
logger(LOG_ERROR, "Failed to URL encode r2h-token");
1030+
return -1;
1031+
}
1032+
}
1033+
1034+
/* Build proxy URL: {BASE_URL}http/host:port/path[?r2h-token=xxx] */
1035+
/* Check if original URL has query parameters */
1036+
const char *query_start = strchr(host_start, '?');
1037+
1038+
if (has_r2h_token && encoded_token) {
1039+
if (query_start) {
1040+
/* Original URL has query params, append r2h-token with & */
1041+
result = snprintf(output, output_size, "%shttp/%s&r2h-token=%s",
1042+
base_url_placeholder, host_start, encoded_token);
1043+
} else {
1044+
/* No query params, add r2h-token with ? */
1045+
result = snprintf(output, output_size, "%shttp/%s?r2h-token=%s",
1046+
base_url_placeholder, host_start, encoded_token);
1047+
}
1048+
} else {
1049+
/* No r2h-token, just transform the URL */
1050+
result =
1051+
snprintf(output, output_size, "%shttp/%s", base_url_placeholder, host_start);
1052+
}
1053+
1054+
if (encoded_token)
1055+
free(encoded_token);
1056+
1057+
if (result >= (int)output_size) {
1058+
logger(LOG_ERROR, "HTTP proxy URL too long");
1059+
return -1;
1060+
}
1061+
1062+
return 0;
1063+
}
1064+

src/http_proxy.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,27 @@ int http_proxy_handle_socket_event(http_proxy_session_t *session,
158158
*/
159159
int http_proxy_session_cleanup(http_proxy_session_t *session);
160160

161+
/**
162+
* Check if URL is a plain HTTP URL that should be proxied
163+
* Only http:// URLs (not https://) that are not already wrapped are proxied.
164+
*
165+
* @param url URL string to check
166+
* @return 1 if URL should be proxied, 0 otherwise
167+
*/
168+
int http_proxy_is_proxy_url(const char *url);
169+
170+
/**
171+
* Build HTTP proxy URL for transformed M3U
172+
* Converts http://host:port/path to {BASE_URL}http/host:port/path
173+
*
174+
* @param http_url Original HTTP URL (must start with http://)
175+
* @param base_url_placeholder Placeholder string for base URL (e.g.,
176+
* "{BASE_URL}")
177+
* @param output Buffer to store transformed URL
178+
* @param output_size Size of output buffer
179+
* @return 0 on success, -1 on error
180+
*/
181+
int http_proxy_build_url(const char *http_url, const char *base_url_placeholder,
182+
char *output, size_t output_size);
183+
161184
#endif /* __HTTP_PROXY_H__ */

0 commit comments

Comments
 (0)