MythTV master
mythhttpranges.cpp
Go to the documentation of this file.
1// C++ headers
2#include <algorithm>
3
4// MythTV
5#include "mythlogging.h"
7#include "http/mythhttpdata.h"
8#include "http/mythhttpfile.h"
10
11#define LOC QString("HTTPRange: ")
12
13auto sumrange = [](uint64_t Cum, HTTPRange Range) { return ((Range.second + 1) - Range.first) + Cum; };
14
16{
17 if (!Response || Request.isEmpty())
18 return;
19
20 // Check content type and size first
21 auto * data = std::get_if<HTTPData>(&(Response->m_response));
22 auto * file = std::get_if<HTTPFile>(&(Response->m_response));
23 int64_t size {0};
24 if (data)
25 size = (*data)->size();
26 else if (file)
27 size = (*file)->size();
28 if (size < 1)
29 return;
30
31 // Parse
32 HTTPRanges& ranges = data ? (*data)->m_ranges : (*file)->m_ranges;
33 int64_t& partialsize = data ? (*data)->m_partialSize : (*file)->m_partialSize;
34 MythHTTPStatus status = MythHTTPRanges::ParseRanges(Request, size, ranges, partialsize);
35 if ((status == HTTPRequestedRangeNotSatisfiable) || (status == HTTPPartialContent))
36 Response->m_status = status;
37}
38
40{
41 if (!Response || (Response->m_status != HTTPPartialContent))
42 return;
43
44 auto * data = std::get_if<HTTPData>(&(Response->m_response));
45 auto * file = std::get_if<HTTPFile>(&(Response->m_response));
46 int64_t size {0};
47 if (data)
48 size = (*data)->size();
49 else if (file)
50 size = (*file)->size();
51 if (size < 1)
52 return;
53
54 HTTPRanges& ranges = data ? (*data)->m_ranges : (*file)->m_ranges;
55 if (ranges.size() < 2)
56 return;
57
58 auto & mime = data ? (*data)->m_mimeType : (*file)->m_mimeType;
60 for (auto & range : ranges)
61 {
62 auto header = QString("\r\n--%1\r\nContent-Type: %2\r\nContent-Range: %3\r\n\r\n")
65 headers.emplace_back(MythHTTPData::Create(qPrintable(header)));
66
67 }
68 headers.emplace_back(MythHTTPData::Create(qPrintable(QString("\r\n--%1--")
69 .arg(s_multipartBoundary))));
70 std::ranges::reverse(headers);
71 int64_t headersize = 0;
72 for (auto & header : headers)
73 headersize += header->size();
74 if (data)
75 {
76 (*data)->m_multipartHeaders = headers;
77 (*data)->m_multipartHeaderSize = headersize;
78 }
79 if (file)
80 {
81 (*file)->m_multipartHeaders = headers;
82 (*file)->m_multipartHeaderSize = headersize;
83 }
84}
85
86QString MythHTTPRanges::GetRangeHeader(HTTPRange& Range, int64_t Size)
87{
88 return QString("bytes %1-%2/%3").arg(Range.first).arg(Range.second).arg(Size);
89}
90
91QString MythHTTPRanges::GetRangeHeader(HTTPRanges& Ranges, int64_t Size)
92{
93 if (Ranges.empty())
94 return "ErRoR";
95 if (Ranges.size() == 1)
96 return MythHTTPRanges::GetRangeHeader(Ranges[0], Size);
97 return "multipart/byteranges; boundary=" + s_multipartBoundary;
98}
99
100HTTPMulti MythHTTPRanges::HandleRangeWrite(HTTPVariant Data, int64_t Available, int64_t &ToWrite, int64_t &Offset)
101{
102 HTTPMulti result { nullptr, nullptr };
103 auto * data = std::get_if<HTTPData>(&Data);
104 auto * file = std::get_if<HTTPFile>(&Data);
105 if (!(data || file))
106 return result;
107
108 int64_t partialsize = data ? (*data)->m_partialSize : (*file)->m_partialSize;
109 auto written = static_cast<uint64_t>(data ? (*data)->m_written : (*file)->m_written);
110 HTTPRanges& ranges = data ? (*data)->m_ranges : (*file)->m_ranges;
111 HTTPContents& headers = data ? (*data)->m_multipartHeaders : (*file)->m_multipartHeaders;
112
113 uint64_t oldsum = 0;
114 for (const auto& range : ranges)
115 {
116 uint64_t newsum = sumrange(oldsum, range);
117 if (oldsum <= written && written < newsum)
118 {
119 // This is the start of a multipart range. Add the start headers.
120 if ((oldsum == written) && !headers.empty())
121 {
122 result.first = headers.back();
123 headers.pop_back();
124 }
125
126 // Restrict the write to the remainder of this range if necessary
127 ToWrite = std::min(Available, static_cast<int64_t>(newsum - written));
128
129 // We need to ensure we send from the correct offset in the data
130 Offset = static_cast<int64_t>(range.first - oldsum);
131 if (file)
132 Offset += written;
133
134 // This is the end of the multipart ranges. Add the closing header
135 // (We add this first so we can pop the contents when sending the headers)
136 if (((static_cast<int64_t>(written) + ToWrite) >= partialsize) && !headers.empty())
137 {
138 result.second = headers.back();
139 headers.pop_back();
140 }
141
142 return result;
143 }
144 oldsum = newsum;
145 }
146 return result;
147}
148
160MythHTTPStatus MythHTTPRanges::ParseRanges(const QString& Request, int64_t TotalSize,
161 HTTPRanges& Ranges, int64_t& PartialSize)
162{
163 MythHTTPStatus result = HTTPOK;
164 Ranges.clear();
165
166 // Just don't...
167 if (TotalSize < 2)
168 return result;
169
170 LOG(VB_HTTP, LOG_DEBUG, LOC + QString("Parsing: '%1'").arg(Request));
171
172 // Split unit and range(s)
173 QStringList initial = Request.toLower().split("=");
174 if (initial.size() != 2)
175 {
176 LOG(VB_HTTP, LOG_INFO, LOC + QString("Failed to parse ranges: '%1'").arg(Request));
177 return result;
178 }
179
180 // We only recognise bytes
181 if (!initial.at(0).contains("bytes"))
182 {
183 LOG(VB_HTTP, LOG_INFO, LOC + QString("Unkown range units: '%1'").arg(initial.at(0)));
184 return result;
185 }
186
187 // Split out individual ranges
188 QStringList rangelist = initial.at(1).split(",", Qt::SkipEmptyParts);
189
190 // No ranges
191 if (rangelist.isEmpty())
192 {
193 LOG(VB_HTTP, LOG_INFO, LOC + QString("Failed to find ranges: '%1'").arg(initial.at(1)));
194 return result;
195 }
196
197 // Iterate over items
198 HTTPRanges ranges;
199 for (auto & range : rangelist)
200 {
201 QStringList parts = range.split("-");
202 if (parts.size() != 2)
203 {
204 LOG(VB_HTTP, LOG_INFO, LOC + QString("Failed to parse range: '%1'").arg(range));
205 return result;
206 }
207
208 bool validrange = false;
209 bool startvalid = false;
210 bool endvalid = false;
211 bool startstr = !parts.at(0).isEmpty();
212 bool endstr = !parts.at(1).isEmpty();
213 uint64_t start = parts.at(0).toULongLong(&startvalid);
214 uint64_t end = parts.at(1).toULongLong(&endvalid);
215
216 // Regular range
217 if (startstr && endstr && startvalid && endvalid)
218 {
219 validrange = ((end < static_cast<uint64_t>(TotalSize)) && (start <= end));
220 }
221 // Start only
222 else if (startstr && startvalid)
223 {
224 end = static_cast<uint64_t>(TotalSize - 1);
225 validrange = start <= end;
226 }
227 // End only
228 else if (endstr && endvalid)
229 {
230 uint64_t size = end;
231 end = static_cast<uint64_t>(TotalSize) - 1;
232 start = static_cast<uint64_t>(TotalSize) - size;
233 validrange = start <= end;
234 }
235
236 if (!validrange)
237 {
238 LOG(VB_HTTP, LOG_INFO, LOC + QString("Invalid HTTP range: '%1'").arg(range));
240 }
241
242 ranges.emplace_back(start, end);
243 }
244
245 // Rationalise so that we have the simplest, most efficient list of ranges:
246 // - sort
247 // - merge overlaps (also allowing for minimum multipart header overhead)
248 // - remove duplicates
249 static const int s_overhead = 90; // rough worst case overhead per part for multipart requests
250 if (ranges.size() > 1)
251 {
252 auto equals = [](HTTPRange First, HTTPRange Second)
253 { return (First.first == Second.first) && (First.second == Second.second); };
254 auto lessthan = [](HTTPRange First, HTTPRange Second)
255 { return First.first < Second.first; };
256
257 // we MUST sort first
258 std::ranges::sort(ranges, lessthan);
259
260 if (VERBOSE_LEVEL_CHECK(VB_HTTP, LOG_INFO))
261 {
262 QStringList debug;
263 for (const auto & range : ranges)
264 debug.append(QString("%1:%2").arg(range.first).arg(range.second));
265 LOG(VB_HTTP, LOG_INFO, LOC + QString("Sorted ranges: %1").arg(debug.join(" ")));
266 }
267
268 // merge, de-duplicate, repeat...
269 bool finished = false;
270 while (!finished)
271 {
272 finished = true;
273 for (uint i = 0; i < (ranges.size() - 1); ++i)
274 {
275 if ((ranges[i].second + s_overhead) >= ranges[i + 1].first)
276 {
277 finished = false;
278 ranges[i + 1].first = ranges[i].first;
279 // N.B we have sorted by start byte - not end
280 uint64_t end = std::max(ranges[i].second, ranges[i + 1].second);
281 ranges[i].second = ranges[i + 1].second = end;
282 }
283 }
284
285 auto [first, last] = std::ranges::unique(ranges, equals);
286 ranges.erase(first, last);
287 }
288 }
289
290 // Sum the expected number of bytes to be sent
291 PartialSize = std::accumulate(ranges.cbegin(), ranges.cend(), static_cast<uint64_t>(0), sumrange);
292 Ranges = ranges;
293
294 if (VERBOSE_LEVEL_CHECK(VB_HTTP, LOG_INFO))
295 {
296 QStringList debug;
297 for (const auto & range : ranges)
298 debug.append(QString("%1:%2").arg(range.first).arg(range.second));
299 LOG(VB_HTTP, LOG_INFO, LOC + QString("Final ranges : %1").arg(debug.join(" ")));
300 LOG(VB_HTTP, LOG_INFO, LOC + QString("Bytes to send: %1").arg(PartialSize));
301 }
302
303 return HTTPPartialContent;
304}
static HTTPData Create()
Definition: mythhttpdata.cpp:4
static QString GetRangeHeader(HTTPRanges &Ranges, int64_t Size)
static MythHTTPStatus ParseRanges(const QString &Request, int64_t TotalSize, HTTPRanges &Ranges, int64_t &PartialSize)
Parse a range request header.
static void BuildMultipartHeaders(MythHTTPResponse *Response)
static HTTPMulti HandleRangeWrite(HTTPVariant Data, int64_t Available, int64_t &ToWrite, int64_t &Offset)
static void HandleRangeRequest(MythHTTPResponse *Response, const QString &Request)
HTTPVariant m_response
MythHTTPStatus m_status
static QString GetContentType(const MythMimeType &Mime)
unsigned int uint
Definition: compat.h:60
#define LOC
auto sumrange
std::vector< HTTPRange > HTTPRanges
std::pair< uint64_t, uint64_t > HTTPRange
Definition: mythhttpranges.h:9
MythHTTPStatus
@ HTTPRequestedRangeNotSatisfiable
@ HTTPOK
@ HTTPPartialContent
std::variant< std::monostate, HTTPData, HTTPFile > HTTPVariant
Definition: mythhttptypes.h:42
std::vector< HTTPData > HTTPContents
Definition: mythhttptypes.h:38
std::pair< HTTPData, HTTPData > HTTPMulti
Definition: mythhttptypes.h:49
static QString s_multipartBoundary
static bool VERBOSE_LEVEL_CHECK(uint64_t mask, LogLevel_t level)
Definition: mythlogging.h:29
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
VERBOSE_PREAMBLE Most debug(nodatabase, notimestamp, noextra)") VERBOSE_MAP(VB_GENERAL