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