diff --git a/trunk/conf/full.conf b/trunk/conf/full.conf index df40356cb..2fcfca22e 100644 --- a/trunk/conf/full.conf +++ b/trunk/conf/full.conf @@ -1540,8 +1540,11 @@ vhost hls.srs.com { hls_key_file_path ./objs/nginx/html; # the key root URL, use this can support https. # @remark It's optional. + # @remark Supports query string for authentication tokens (e.g., JWT). + # Example: http://localhost:8080/?token=abc123&sig=xyz789 + # Result in m3u8: http://localhost:8080/live/livestream-0.key?token=abc123&sig=xyz789 # Overwrite by env SRS_VHOST_HLS_HLS_KEY_URL for all vhosts. - hls_key_url https://localhost:8080; + hls_key_url http://localhost:8080/; # Special control controls. ########################################### diff --git a/trunk/conf/hls-encrypted-query.conf b/trunk/conf/hls-encrypted-query.conf new file mode 100644 index 000000000..7263f7365 --- /dev/null +++ b/trunk/conf/hls-encrypted-query.conf @@ -0,0 +1,35 @@ +# Test config for HLS encryption with query string in hls_key_url +# This tests the fix for issue #4426 + +listen 1935; +max_connections 1000; +daemon off; +srs_log_tank console; + +http_server { + enabled on; + listen 8080; + dir ./objs/nginx/html; +} + +vhost __defaultVhost__ { + hls { + enabled on; + hls_fragment 10; + hls_window 60; + hls_path ./objs/nginx/html; + hls_m3u8_file [app]/[stream].m3u8; + hls_ts_file [app]/[stream]-[seq].ts; + + # Enable AES-128 encryption + hls_keys on; + hls_fragments_per_key 5; + hls_key_file [app]/[stream]-[seq].key; + hls_key_file_path ./objs/nginx/html; + + # Test with query string - this should now work correctly + # Expected result in m3u8: http://localhost:8080/live/livestream-0.key?token=abc123&sig=xyz789 + hls_key_url http://localhost:8080/?token=abc123&sig=xyz789; + } +} + diff --git a/trunk/doc/CHANGELOG.md b/trunk/doc/CHANGELOG.md index e78cef07d..afd74ff51 100644 --- a/trunk/doc/CHANGELOG.md +++ b/trunk/doc/CHANGELOG.md @@ -7,6 +7,7 @@ The changelog for SRS. ## SRS 7.0 Changelog +* v7.0, 2025-11-07, AI: HLS: Support query string in hls_key_url for JWT tokens. v7.0.120 (#4426) * v7.0, 2025-11-07, AI: RTC: Support keep_original_ssrc to preserve SSRC and timestamps. v7.0.119 (#3850) * v7.0, 2025-11-05, AI: WebRTC: Report video/audio codec info and frame stats in HTTP API. v7.0.118 (#4554) * v7.0, 2025-11-04, AI: SRT: Report video/audio codec info and frame stats in HTTP API. v7.0.117 (#4554) diff --git a/trunk/src/app/srs_app_hls.cpp b/trunk/src/app/srs_app_hls.cpp index dcec05d49..6c5572697 100644 --- a/trunk/src/app/srs_app_hls.cpp +++ b/trunk/src/app/srs_app_hls.cpp @@ -33,6 +33,7 @@ using namespace std; #include #include #include +#include #include #include @@ -45,6 +46,33 @@ using namespace std; // reset the piece id when deviation overflow this. #define SRS_JUMP_WHEN_PIECE_DEVIATION 20 +// Build the full key URL by appending key_file to hls_key_url with proper query string handling. +// If hls_key_url contains query string like "http://localhost:8080/?token=abc", +// the result will be "http://localhost:8080/live/livestream-0.key?token=abc" +// @param hls_key_url The base URL which may contain query string +// @param key_file The key file path like "live/livestream-0.key" +// @return The full key URL with query string properly appended +string srs_hls_build_key_url(const string &hls_key_url, const string &key_file) +{ + if (hls_key_url.empty()) { + return key_file; + } + + // Find the query string separator + size_t pos = hls_key_url.find("?"); + if (pos != string::npos) { + // URL contains query string, split and rebuild + // Example: "http://localhost:8080/?token=abc" + "live/livestream-0.key" + // Result: "http://localhost:8080/live/livestream-0.key?token=abc" + string base_url = hls_key_url.substr(0, pos); + string query_string = hls_key_url.substr(pos); // Include the '?' + return base_url + key_file + query_string; + } + + // No query string, simple concatenation + return hls_key_url + key_file; +} + SrsHlsSegment::SrsHlsSegment(SrsTsContext *c, SrsAudioCodecId ac, SrsVideoCodecId vc, ISrsFileWriter *w) { sequence_no_ = 0; @@ -1107,11 +1135,7 @@ srs_error_t SrsHlsFmp4Muxer::do_refresh_m3u8_segment(SrsHlsM4sSegment *segment, string key_file = srs_path_build_stream(hls_key_file_, req_->vhost_, req_->app_, req_->stream_); key_file = srs_strings_replace(key_file, "[seq]", srs_strconv_format_int(segment->sequence_no_)); - string key_path = key_file; - // if key_url is not set,only use the file name - if (!hls_key_url_.empty()) { - key_path = hls_key_url_ + key_file; - } + string key_path = srs_hls_build_key_url(hls_key_url_, key_file); ss << "#EXT-X-KEY:METHOD=SAMPLE-AES,URI=" << "\"" << key_path << "\",IV=0x" << hexiv << SRS_CONSTS_LF; } @@ -2040,11 +2064,7 @@ srs_error_t SrsHlsMuxer::do_refresh_m3u8_segment(SrsHlsSegment *segment, std::st string key_file = srs_path_build_stream(hls_key_file_, req_->vhost_, req_->app_, req_->stream_); key_file = srs_strings_replace(key_file, "[seq]", srs_strconv_format_int(segment->sequence_no_)); - string key_path = key_file; - // if key_url is not set,only use the file name - if (!hls_key_url_.empty()) { - key_path = hls_key_url_ + key_file; - } + string key_path = srs_hls_build_key_url(hls_key_url_, key_file); ss << "#EXT-X-KEY:METHOD=AES-128,URI=" << "\"" << key_path << "\",IV=0x" << hexiv << SRS_CONSTS_LF; } diff --git a/trunk/src/core/srs_core_version7.hpp b/trunk/src/core/srs_core_version7.hpp index 9ca5f80fb..9ccc2ecd5 100644 --- a/trunk/src/core/srs_core_version7.hpp +++ b/trunk/src/core/srs_core_version7.hpp @@ -9,6 +9,6 @@ #define VERSION_MAJOR 7 #define VERSION_MINOR 0 -#define VERSION_REVISION 119 +#define VERSION_REVISION 120 #endif \ No newline at end of file diff --git a/trunk/src/utest/srs_utest_ai21.hpp b/trunk/src/utest/srs_utest_ai21.hpp index 3ecdd04ad..1ba6eace0 100644 --- a/trunk/src/utest/srs_utest_ai21.hpp +++ b/trunk/src/utest/srs_utest_ai21.hpp @@ -111,6 +111,7 @@ public: void reset(); }; +#ifdef SRS_RTSP // Forward declaration class SrsRtspConsumer; @@ -159,5 +160,6 @@ public: void set_send_error(srs_error_t err); void reset(); }; +#endif #endif diff --git a/trunk/src/utest/srs_utest_ai22.cpp b/trunk/src/utest/srs_utest_ai22.cpp index 1f5d583f1..cf7f8f07a 100644 --- a/trunk/src/utest/srs_utest_ai22.cpp +++ b/trunk/src/utest/srs_utest_ai22.cpp @@ -1549,6 +1549,7 @@ void MockStatisticForRtspPlayStream::reset() srs_freep(on_client_error_); } +#ifdef SRS_RTSP // MockRtspSourceManager implementation MockRtspSourceManager::MockRtspSourceManager() { @@ -2750,6 +2751,7 @@ VOID TEST(RtspTcpNetworkTest, WriteRtpPacket) // Verify the payload data (starts at offset 4) EXPECT_EQ(0, memcmp(rtp_packet, output + 4, kRtpPacketSize)); } +#endif // MockDvrPlan implementation MockDvrPlan::MockDvrPlan() diff --git a/trunk/src/utest/srs_utest_ai22.hpp b/trunk/src/utest/srs_utest_ai22.hpp index a1b580df0..ef9fdc948 100644 --- a/trunk/src/utest/srs_utest_ai22.hpp +++ b/trunk/src/utest/srs_utest_ai22.hpp @@ -354,6 +354,7 @@ public: void reset(); }; +#ifdef SRS_RTSP // Mock ISrsRtspSourceManager for testing SrsRtspPlayStream class MockRtspSourceManager : public ISrsRtspSourceManager { @@ -456,6 +457,7 @@ public: virtual void set_all_tracks_status(bool status); void reset(); }; +#endif // Mock ISrsDvrPlan for testing SrsDvrSegmenter class MockDvrPlan : public ISrsDvrPlan diff --git a/trunk/src/utest/srs_utest_ai23.cpp b/trunk/src/utest/srs_utest_ai23.cpp index 879e73d70..dd616ec3a 100644 --- a/trunk/src/utest/srs_utest_ai23.cpp +++ b/trunk/src/utest/srs_utest_ai23.cpp @@ -29,6 +29,7 @@ using namespace std; #include #include +#ifdef SRS_GB28181 // Mock ISrsGbMuxer implementation MockGbMuxer::MockGbMuxer() { @@ -2294,6 +2295,7 @@ VOID TEST(GB28181Test, GoApiGbPublishSuccess) srs_freep(conf); } +#endif // Mock ISrsRtcNetwork implementation MockRtcNetworkForNetworks::MockRtcNetworkForNetworks() diff --git a/trunk/src/utest/srs_utest_ai23.hpp b/trunk/src/utest/srs_utest_ai23.hpp index ab8cf2ca3..32fa9aeab 100644 --- a/trunk/src/utest/srs_utest_ai23.hpp +++ b/trunk/src/utest/srs_utest_ai23.hpp @@ -30,6 +30,7 @@ #include #endif +#ifdef SRS_GB28181 // Mock ISrsGbMuxer for testing SrsGbSession class MockGbMuxer : public ISrsGbMuxer { @@ -541,6 +542,7 @@ public: #endif void reset(); }; +#endif // Mock ISrsRtcNetwork for testing SrsRtcNetworks class MockRtcNetworkForNetworks : public ISrsRtcNetwork diff --git a/trunk/src/utest/srs_utest_manual_mock.hpp b/trunk/src/utest/srs_utest_manual_mock.hpp index dbdd005bb..bd8b34107 100644 --- a/trunk/src/utest/srs_utest_manual_mock.hpp +++ b/trunk/src/utest/srs_utest_manual_mock.hpp @@ -517,6 +517,7 @@ public: virtual int get_rtc_drop_for_pt(std::string vhost) { return rtc_drop_for_pt_; } virtual bool get_rtc_twcc_enabled(std::string vhost) { return rtc_twcc_enabled_; } virtual bool get_rtc_init_rate_from_sdp(std::string vhost) { return rtc_init_rate_from_sdp_; } + virtual bool get_rtc_keep_original_ssrc(std::string vhost) { return false; } virtual bool get_srt_enabled() { return srt_enabled_; } virtual bool get_srt_enabled(std::string vhost) { return srt_enabled_; } virtual std::string get_srt_default_streamid() { return "#!::r=live/livestream,m=request"; }