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