|
9 | 9 | */ |
10 | 10 | #include "video.h" |
11 | 11 |
|
| 12 | +#include <algorithm> |
12 | 13 | #include <initializer_list> |
13 | 14 | #include <memory> |
14 | 15 | #include <vector> |
@@ -116,6 +117,19 @@ struct SpliceQueue { |
116 | 117 | // instead of this process buffering without limit. |
117 | 118 | #define SPLICE_QUEUE_MAX (1u * 1024 * 1024) |
118 | 119 |
|
| 120 | +/* |
| 121 | + RTSP is relayed byte-for-byte, but admission credentials can occur on |
| 122 | + any request URI in the session. Hold only the current request header |
| 123 | + long enough to inspect its request line. Bodies and interleaved RTP are |
| 124 | + counted and streamed without interpretation. |
| 125 | + */ |
| 126 | +struct RtspRequestGuard { |
| 127 | + std::vector<uint8_t> buffered; |
| 128 | + size_t opaque_left = 0; |
| 129 | + |
| 130 | + void clear(void) { buffered.clear(); opaque_left = 0; } |
| 131 | +}; |
| 132 | + |
119 | 133 | /* |
120 | 134 | One RTMP handshake that has not published yet. |
121 | 135 |
|
@@ -155,6 +169,7 @@ struct Slot { |
155 | 169 | int rtsp_client_fd = -1; |
156 | 170 | SpliceQueue to_backend; // bytes read from the client, owed to ffmpeg |
157 | 171 | SpliceQueue to_client; // and the other way |
| 172 | + RtspRequestGuard rtsp_guard; |
158 | 173 | /* |
159 | 174 | The RTMP session that owns the slot, once one has published and |
160 | 175 | been admitted. Null until then. |
@@ -227,6 +242,8 @@ class VideoChild { |
227 | 242 | splice_proto_t proto); |
228 | 243 | void close_rtsp(Slot &s, int idx, const char *why); |
229 | 244 | bool pump_rtsp(Slot &s, int idx, int fd, time_t now); |
| 245 | + bool guard_rtsp_requests(Slot &s, int idx, const uint8_t *buf, size_t n, |
| 246 | + time_t now); |
230 | 247 | bool pump_rtmp(Slot &s, int idx, int fd, time_t now); |
231 | 248 | bool rtmp_start_backend(Slot &s, int idx); |
232 | 249 | bool rtmp_drain_owner(Slot &s, int idx, bool alive); |
@@ -691,6 +708,7 @@ void VideoChild::handle_rtsp(Slot &s, int idx, int fd, |
691 | 708 | } |
692 | 709 | fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK); |
693 | 710 | s.rtsp_client_fd = fd; |
| 711 | + s.rtsp_guard.clear(); |
694 | 712 | s.pub_ip_be = uint32_t(from.sin_addr.s_addr); |
695 | 713 | s.pub_port_be = from.sin_port; |
696 | 714 | latch_publisher(s, idx, now); |
@@ -734,6 +752,7 @@ void VideoChild::close_rtsp(Slot &s, int idx, const char *why) |
734 | 752 | s.rtmp.reset(); |
735 | 753 | s.to_backend.clear(); |
736 | 754 | s.to_client.clear(); |
| 755 | + s.rtsp_guard.clear(); |
737 | 756 | s.rtsp.stop(); |
738 | 757 | s.rec.close_segment(); |
739 | 758 | s.recording = false; |
@@ -778,6 +797,119 @@ bool VideoChild::splice_flush(int to_fd, SpliceQueue &q) |
778 | 797 | return true; |
779 | 798 | } |
780 | 799 |
|
| 800 | +/* |
| 801 | + Filter the client-to-backend half of an RTSP splice. The first request |
| 802 | + was already checked before the backend started, but a client can put a |
| 803 | + credential on a later ANNOUNCE. Without continued inspection an absent |
| 804 | + OPTIONS could take session fallback and a later wrong password would be |
| 805 | + silently ignored. |
| 806 | + */ |
| 807 | +bool VideoChild::guard_rtsp_requests(Slot &s, int idx, const uint8_t *buf, |
| 808 | + size_t n, time_t now) |
| 809 | +{ |
| 810 | + RtspRequestGuard &g = s.rtsp_guard; |
| 811 | + g.buffered.insert(g.buffered.end(), buf, buf + n); |
| 812 | + |
| 813 | + while (!g.buffered.empty()) { |
| 814 | + if (g.opaque_left > 0) { |
| 815 | + const size_t take = std::min(g.opaque_left, g.buffered.size()); |
| 816 | + s.to_backend.buf.insert(s.to_backend.buf.end(), |
| 817 | + g.buffered.begin(), |
| 818 | + g.buffered.begin() + long(take)); |
| 819 | + g.buffered.erase(g.buffered.begin(), |
| 820 | + g.buffered.begin() + long(take)); |
| 821 | + g.opaque_left -= take; |
| 822 | + continue; |
| 823 | + } |
| 824 | + |
| 825 | + // Interleaved RTP/RTCP: '$', channel, 16-bit big-endian length. |
| 826 | + if (g.buffered[0] == '$') { |
| 827 | + if (g.buffered.size() < 4) { |
| 828 | + return true; |
| 829 | + } |
| 830 | + g.opaque_left = (size_t(g.buffered[2]) << 8) | g.buffered[3]; |
| 831 | + s.to_backend.buf.insert(s.to_backend.buf.end(), |
| 832 | + g.buffered.begin(), |
| 833 | + g.buffered.begin() + 4); |
| 834 | + g.buffered.erase(g.buffered.begin(), g.buffered.begin() + 4); |
| 835 | + continue; |
| 836 | + } |
| 837 | + |
| 838 | + size_t header_len = 0; |
| 839 | + for (size_t i = 0; i + 1 < g.buffered.size(); i++) { |
| 840 | + if (g.buffered[i] == '\n' && g.buffered[i + 1] == '\n') { |
| 841 | + header_len = i + 2; |
| 842 | + break; |
| 843 | + } |
| 844 | + if (i + 3 < g.buffered.size() |
| 845 | + && g.buffered[i] == '\r' && g.buffered[i + 1] == '\n' |
| 846 | + && g.buffered[i + 2] == '\r' |
| 847 | + && g.buffered[i + 3] == '\n') { |
| 848 | + header_len = i + 4; |
| 849 | + break; |
| 850 | + } |
| 851 | + } |
| 852 | + if (header_len == 0) { |
| 853 | + return g.buffered.size() <= HTTP_MAX_REQUEST; |
| 854 | + } |
| 855 | + |
| 856 | + HttpRequest req; |
| 857 | + if (req.feed(g.buffered.data(), header_len) != 1) { |
| 858 | + return false; |
| 859 | + } |
| 860 | + std::string pw; |
| 861 | + if (http_query_value(req.target(), "pw", pw)) { |
| 862 | + bool have_pw = false; |
| 863 | + for (uint8_t b : ke_.video_publish_key) { |
| 864 | + have_pw |= b != 0; |
| 865 | + } |
| 866 | + bool matches = video_password_matches(ke_.video_publish_key, pw); |
| 867 | + /* |
| 868 | + ffmpeg resolves an SDP control path after the whole source |
| 869 | + URI, producing e.g. ?pw=secret/streamid=0. URI syntax makes |
| 870 | + that suffix part of the query value. Accept it only when a |
| 871 | + slash-delimited prefix is itself the exact password; the |
| 872 | + peer still has to know the configured credential. |
| 873 | + */ |
| 874 | + const size_t slash = pw.rfind('/'); |
| 875 | + if (!matches && slash != std::string::npos) { |
| 876 | + matches = video_password_matches( |
| 877 | + ke_.video_publish_key, pw.substr(0, slash)); |
| 878 | + } |
| 879 | + if (have_pw && !matches) { |
| 880 | + log_reject(s, idx, s.pub_ip_be, VIDEO_ADMIT_BAD_PASSWORD, |
| 881 | + now); |
| 882 | + return false; |
| 883 | + } |
| 884 | + } |
| 885 | + |
| 886 | + const size_t content_length_count = |
| 887 | + req.header_count("Content-Length"); |
| 888 | + const std::string content_length = req.header("Content-Length"); |
| 889 | + if (content_length_count > 1 |
| 890 | + || (content_length_count == 1 && content_length.empty())) { |
| 891 | + return false; |
| 892 | + } |
| 893 | + if (!content_length.empty()) { |
| 894 | + char *end = nullptr; |
| 895 | + errno = 0; |
| 896 | + const unsigned long long body = |
| 897 | + strtoull(content_length.c_str(), &end, 10); |
| 898 | + if (errno != 0 || end == content_length.c_str() || *end != '\0' |
| 899 | + || body > 16u * 1024u * 1024u) { |
| 900 | + return false; |
| 901 | + } |
| 902 | + g.opaque_left = size_t(body); |
| 903 | + } |
| 904 | + s.to_backend.buf.insert(s.to_backend.buf.end(), |
| 905 | + g.buffered.begin(), |
| 906 | + g.buffered.begin() + long(header_len)); |
| 907 | + g.buffered.erase(g.buffered.begin(), |
| 908 | + g.buffered.begin() + long(header_len)); |
| 909 | + } |
| 910 | + return true; |
| 911 | +} |
| 912 | + |
781 | 913 | // EPOLLOUT only while something is queued. Armed unconditionally it |
782 | 914 | // would make epoll_wait return immediately for ever on an idle splice, |
783 | 915 | // which is the same idle-viewer CPU burn measured earlier. |
@@ -1156,7 +1288,13 @@ bool VideoChild::pump_rtsp(Slot &s, int idx, int fd, time_t now) |
1156 | 1288 | return false; |
1157 | 1289 | } |
1158 | 1290 | if (n > 0) { |
1159 | | - out.buf.insert(out.buf.end(), buf, buf + n); |
| 1291 | + if (fd == s.rtsp_client_fd) { |
| 1292 | + if (!guard_rtsp_requests(s, idx, buf, size_t(n), now)) { |
| 1293 | + return false; |
| 1294 | + } |
| 1295 | + } else { |
| 1296 | + out.buf.insert(out.buf.end(), buf, buf + n); |
| 1297 | + } |
1160 | 1298 | if (!splice_flush(to_fd, out)) { |
1161 | 1299 | return false; |
1162 | 1300 | } |
|
0 commit comments