MythTV  master
mythhttpencoding.cpp
Go to the documentation of this file.
1 // MythTV
2 #include "mythlogging.h"
3 #include "unziputil.h"
5 #include "http/mythhttpdata.h"
6 #include "http/mythhttpfile.h"
9 
10 // Qt
11 #include <QDomDocument>
12 #include <QJsonDocument>
13 #include <QJsonObject>
14 #include <QJsonValue>
15 
16 #define LOC QString("HTTPEnc: ")
17 
29 using MimePair = std::pair<float,QString>;
30 QStringList MythHTTPEncoding::GetMimeTypes(const QString &Accept)
31 {
32  // Split out mime types
33  auto types = Accept.split(",", Qt::SkipEmptyParts);
34 
35  std::vector<MimePair> weightings;
36  for (const auto & type : std::as_const(types))
37  {
38  QString mime = type.trimmed();
39  auto quality = 1.0F;
40  // Find any quality value (defaults to 1)
41  if (auto index = type.lastIndexOf(";"); index > -1)
42  {
43  mime = type.mid(0, index).trimmed().toLower();
44  auto qual = type.mid(index + 1).trimmed();
45  if (auto index2 = qual.lastIndexOf("="); index2 > -1)
46  {
47  bool ok = false;
48  auto newquality = qual.mid(index2 + 1).toFloat(&ok);
49  if (ok)
50  quality = newquality;
51  }
52  }
53  weightings.emplace_back(quality, mime);
54  }
55 
56  // Sort the list
57  auto comp = [](const MimePair& First, const MimePair& Second) { return First.first > Second.first; };
58  std::sort(weightings.begin(), weightings.end(), comp);
59 
60  // Build the final result. This will pass through invalid types - which should
61  // be handled by the consumer (e.g. wildcard specifiers are not handled).
62  QStringList result;
63  for (const auto & weight : weightings)
64  result.append(weight.second);
65 
66  // Default to xml
67  if (result.empty())
68  result.append("application/xml");
69  return result;
70 }
71 
75 {
76  if (!Request || !Request->m_content.get())
77  return;
78 
79  auto contenttype = MythHTTP::GetHeader(Request->m_headers, "content-type");
80 
81  // type is e.g. text/html; charset=UTF-8 or multipart/form-data; boundary=something
82  auto types = contenttype.split(";", Qt::SkipEmptyParts);
83  if (types.isEmpty())
84  return;
85 
86  // Note: This can produce an invalid mime type but there is no sensible fallback
87  if (auto mime = MythMimeDatabase::MimeTypeForName(types[0].trimmed().toLower()); mime.IsValid())
88  {
89  Request->m_content->m_mimeType = mime;
90  if (mime.Name() == "application/x-www-form-urlencoded")
92  else if (mime.Name() == "text/xml" || mime.Name() == "application/xml" ||
93  mime.Name() == "application/soap+xml")
95  else if (mime.Name() == "application/json")
97  else
98  LOG(VB_HTTP, LOG_ERR, QString("Don't know how to get the parameters for MIME type: '%1'").arg(mime.Name()));
99  }
100  else
101  {
102  LOG(VB_HTTP, LOG_ERR, QString("Unknown MIME type: '%1'").arg(types[0]));
103  }
104 
105 }
106 
108 {
109  if (!Request || !Request->m_content.get())
110  return;
111 
112  auto payload = QString::fromUtf8(Request->m_content->constData(), Request->m_content->size());
113 
114  // This looks odd, but it is here to cope with stupid UPnP clients that
115  // forget to de-escape the URLs. We can't map %26 here as well, as that
116  // breaks anything that is trying to pass & as part of a name or value.
117  payload.replace("&amp;", "&");
118  if (!payload.isEmpty())
119  {
120  QStringList params = payload.split('&', Qt::SkipEmptyParts);
121  for (const auto & param : std::as_const(params))
122  {
123  QString name = param.section('=', 0, 0);
124  QString value = param.section('=', 1);
125  value.replace("+", " ");
126  if (!name.isEmpty())
127  {
128  name = QUrl::fromPercentEncoding(name.toUtf8());
129  value = QUrl::fromPercentEncoding(value.toUtf8());
130  Request->m_queries.insert(name.trimmed().toLower(), value);
131  }
132  }
133  }
134 }
135 
137 {
138  LOG(VB_HTTP, LOG_DEBUG, "Inspecting XML payload");
139 
140  if (!Request || !Request->m_content.get())
141  return;
142 
143  // soapaction is formatted like "\"http://mythtv.org/Dvr/GetRecordedList\""
144  QString soapaction = Request->m_headers->value("soapaction");
145  soapaction.remove('"');
146  int lastSlashPos= soapaction.lastIndexOf('/');
147  if (lastSlashPos < 0)
148  return;
149  if (Request->m_path == "/")
150  Request->m_path.append(Request->m_fileName).append("/");
151  Request->m_fileName = soapaction.right(soapaction.size()-lastSlashPos-1);
152  LOG(VB_HTTP, LOG_DEBUG, QString("Found method call (%1)").arg(Request->m_fileName));
153 
154  auto payload = QDomDocument();
155 #if QT_VERSION < QT_VERSION_CHECK(6,5,0)
156  QString err_msg;
157  int err_line {-1};
158  int err_col {-1};
159  if (!payload.setContent(static_cast<QByteArray>(Request->m_content->constData()),
160  true, &err_msg, &err_line, &err_col))
161  {
162  LOG(VB_HTTP, LOG_WARNING, "Unable to parse XML request body");
163  LOG(VB_HTTP, LOG_WARNING, QString("- Error at line %1, column %2, msg: %3")
164  .arg(err_line).arg(err_col).arg(err_msg));
165  return;
166  }
167 #else
168  auto parseresult = payload.setContent(Request->m_content->constData(),
169  QDomDocument::ParseOption::UseNamespaceProcessing);
170  if (!parseresult)
171  {
172  LOG(VB_HTTP, LOG_WARNING, "Unable to parse XML request body");
173  LOG(VB_HTTP, LOG_WARNING, QString("- Error at line %1, column %2, msg: %3")
174  .arg(parseresult.errorLine).arg(parseresult.errorColumn)
175  .arg(parseresult.errorMessage));
176  return;
177  }
178 #endif
179  QString doc_name = payload.documentElement().localName();
180  if (doc_name.compare("envelope", Qt::CaseInsensitive) == 0)
181  {
182  LOG(VB_HTTP, LOG_DEBUG, "Found SOAP XML message envelope");
183  auto doc_body = payload.documentElement().namedItem("Body");
184  if (doc_body.isNull() || !doc_body.hasChildNodes()) // None or empty body
185  {
186  LOG(VB_HTTP, LOG_DEBUG, "Missing or empty SOAP body");
187  return;
188  }
189  auto body_contents = doc_body.firstChild();
190  if (body_contents.hasChildNodes()) // params for the method
191  {
192  for (QDomNode node = body_contents.firstChild(); !node.isNull(); node = node.nextSibling())
193  {
194  QString name = node.localName();
195  QString value = node.toElement().text();
196  if (!name.isEmpty())
197  {
198  // TODO: html decode entities if required
199  Request->m_queries.insert(name.trimmed().toLower(), value);
200  LOG(VB_HTTP, LOG_DEBUG, QString("Found URL param (%1=%2)").arg(name, value));
201  }
202  }
203  }
204  }
205 }
206 
208 {
209  LOG(VB_HTTP, LOG_DEBUG, "Inspecting JSON payload");
210 
211  if (!Request || !Request->m_content.get())
212  return;
213 
214  QByteArray jstr = static_cast<QByteArray>(Request->m_content->constData());
215  QJsonParseError parseError {};
216  QJsonDocument doc = QJsonDocument::fromJson(jstr, &parseError);
217  if (parseError.error != QJsonParseError::NoError)
218  {
219  LOG(VB_HTTP, LOG_WARNING,
220  QString("Unable to parse JSON request body - Error at position %1, msg: %2")
221  .arg(parseError.offset).arg(parseError.errorString()));
222  return;
223  }
224 
225  QJsonObject json = doc.object();
226  foreach(const QString& key, json.keys())
227  {
228  if (!key.isEmpty())
229  {
230  QString value;
231  if (json.value(key).isObject())
232  {
233  QJsonDocument vd(json.value(key).toObject());
234  value = vd.toJson(QJsonDocument::Compact);
235  }
236  else
237  {
238  value = json.value(key).toVariant().toString();
239 
240  if (value.isEmpty())
241  {
242  LOG(VB_HTTP, LOG_WARNING,
243  QString("Failed to parse value for key '%1' from %2")
244  .arg(key, QString(jstr)));
245  }
246  }
247 
248  Request->m_queries.insert(key.trimmed().toLower(), value);
249  LOG(VB_HTTP, LOG_DEBUG,
250  QString("Found URL param (%1=%2)").arg(key, value));
251  }
252  }
253 }
254 
258 {
259  auto * data = std::get_if<HTTPData>(&Content);
260  auto * file = std::get_if<HTTPFile>(&Content);
261  if (!(data || file))
262  return {};
263 
264  QString filename;
265  if (data)
266  filename = (*data)->m_fileName;
267  else if (file)
268  filename = (*file)->m_fileName;
269 
270  // Look for unambiguous mime type
272  if (types.size() == 1)
273  return types.front();
274 
275  // Look for an override. QMimeDatabase gets it wrong sometimes when the result
276  // is ambiguous and it resorts to probing. Add to this list as necessary
277  static const std::map<QString,QString> s_mimeOverrides =
278  {
279  { "ts", "video/mp2t"}
280  };
281 
283  for (const auto & type : s_mimeOverrides)
284  if (suffix.compare(type.first, Qt::CaseInsensitive) == 0)
286 
287  // Try interrogating content as well
288  if (data)
289  if (auto mime = MythMimeDatabase::MimeTypeForFileNameAndData(filename, **data); mime.IsValid())
290  return mime;
291  if (file)
292  if (auto mime = MythMimeDatabase::MimeTypeForFileNameAndData(filename, (*file).get()); mime.IsValid())
293  return mime;
294 
295  // Default to text/plain (possibly use application/octet-stream as well?)
296  return MythMimeDatabase::MimeTypeForName("text/plain");
297 }
298 
307 {
308  auto result = HTTPNoEncode;
309  if (!Response || !Response->m_requestHeaders)
310  return result;
311 
312  // We need something to compress/chunk
313  auto * data = std::get_if<HTTPData>(&Response->m_response);
314  auto * file = std::get_if<HTTPFile>(&Response->m_response);
315  if (!(data || file))
316  return result;
317 
318  // Don't touch range requests. They do not work with compression and there
319  // is no point in chunking gzipped content as the client still has to wait
320  // for the entire payload before unzipping
321  // Note: It is permissible to chunk a range request - but ignore for the
322  // timebeing to keep the code simple.
323  if ((data && !(*data)->m_ranges.empty()) || (file && !(*file)->m_ranges.empty()))
324  return result;
325 
326  // Has the client actually requested compression
327  bool wantgzip = MythHTTP::GetHeader(Response->m_requestHeaders, "accept-encoding").toLower().contains("gzip");
328 
329  // Chunking is HTTP/1.1 only - and must be supported
330  bool chunkable = Response->m_version == HTTPOneDotOne;
331 
332  // Has the client requested no chunking by specifying identity?
333  bool allowchunk = ! MythHTTP::GetHeader(Response->m_requestHeaders, "accept-encoding").toLower().contains("identity");
334 
335  // and restrict to 'chunky' files
336  bool chunky = Size > 102400; // 100KB
337 
338  // Don't compress anything that is too large. Under normal circumstances this
339  // should not be a problem as we only compress text based data - but avoid
340  // potentially memory hungry compression operations.
341  // On the flip side, don't compress trivial amounts of data
342  bool gzipsize = Size > 512 && !chunky; // 0.5KB <-> 100KB
343 
344  // Only consider compressing text based content. No point in compressing audio,
345  // video and images.
346  bool compressable = (data ? (*data)->m_mimeType : (*file)->m_mimeType).Inherits("text/plain");
347 
348  // Decision time
349  bool gzip = wantgzip && gzipsize && compressable;
350  bool chunk = chunkable && chunky && allowchunk;
351 
352  if (!gzip)
353  {
354  // Chunking happens as we write to the socket, so flag it as required
355  if (chunk)
356  {
357  result = HTTPChunked;
358  if (data) (*data)->m_encoding = result;
359  if (file) (*file)->m_encoding = result;
360  }
361  return result;
362  }
363 
364  // As far as I can tell, Qt's implicit sharing of data should ensure we aren't
365  // copying data unnecessarily here - but I can't be sure. We could definitely
366  // improve compressing files by avoiding the copy into a temporary buffer.
367  HTTPData buffer = MythHTTPData::Create(data ? gzipCompress(**data) : gzipCompress((*file)->readAll()));
368 
369  // Add the required header
370  Response->AddHeader("Content-Encoding", "gzip");
371 
372  LOG(VB_HTTP, LOG_INFO, LOC + QString("'%1' compressed from %2 to %3 bytes")
373  .arg(data ? (*data)->m_fileName : (*file)->fileName())
374  .arg(Size).arg(buffer->size()));
375 
376  // Copy the filename and last modified, set the new buffer and set the content size
377  buffer->m_lastModified = data ? (*data)->m_lastModified : (*file)->m_lastModified;
378  buffer->m_etag = data ? (*data)->m_etag : (*file)->m_etag;
379  buffer->m_fileName = data ? (*data)->m_fileName : (*file)->m_fileName;
380  buffer->m_cacheType = data ? (*data)->m_cacheType : (*file)->m_cacheType;
381  buffer->m_encoding = HTTPGzip;
382  Response->m_response = buffer;
383  Size = buffer->size();
384  return HTTPGzip;
385 }
HTTPChunked
@ HTTPChunked
Definition: mythhttptypes.h:139
MythHTTPRequest
Limited parsing of HTTP method and some headers to determine validity of request.
Definition: mythhttprequest.h:12
unziputil.h
HTTPVariant
std::variant< std::monostate, HTTPData, HTTPFile > HTTPVariant
Definition: mythhttptypes.h:42
MythHTTP::GetHeader
static QString GetHeader(const HTTPHeaders &Headers, const QString &Value, const QString &Default="")
Definition: mythhttptypes.h:318
MythMimeDatabase::MimeTypesForFileName
static MythMimeTypes MimeTypesForFileName(const QString &FileName)
Return a vector of mime types that match the given filename.
Definition: mythmimedatabase.cpp:175
MythHTTPResponse
Definition: mythhttpresponse.h:16
types
static const struct wl_interface * types[]
Definition: idle_inhibit_unstable_v1.c:39
LOG
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
build_compdb.file
file
Definition: build_compdb.py:55
MythHTTPEncoding::GetContentType
static void GetContentType(MythHTTPRequest *Request)
Parse the incoming Content-Type header for POST/PUT content.
Definition: mythhttpencoding.cpp:74
MythHTTPResponse::m_version
MythHTTPVersion m_version
Definition: mythhttpresponse.h:49
mythhttpdata.h
HTTPData
std::shared_ptr< MythHTTPData > HTTPData
Definition: mythhttptypes.h:37
mythhttpfile.h
MythHTTPEncode
MythHTTPEncode
Definition: mythhttptypes.h:135
mythlogging.h
MythHTTPEncoding::GetMimeType
static MythMimeType GetMimeType(HTTPVariant Content)
Return a QMimeType that represents Content.
Definition: mythhttpencoding.cpp:257
MythHTTPData::Create
static HTTPData Create()
Definition: mythhttpdata.cpp:4
MythHTTPResponse::m_response
HTTPVariant m_response
Definition: mythhttpresponse.h:57
MythHTTPEncoding::GetURLEncodedParameters
static void GetURLEncodedParameters(MythHTTPRequest *Request)
Definition: mythhttpencoding.cpp:107
gzipCompress
QByteArray gzipCompress(const QByteArray &data)
Definition: unziputil.cpp:93
mythhttpencoding.h
MythMimeDatabase::SuffixForFileName
static QString SuffixForFileName(const QString &FileName)
Return the preferred suffix for the given filename.
Definition: mythmimedatabase.cpp:187
mythhttpresponse.h
MimePair
std::pair< float, QString > MimePair
Parse the incoming HTTP 'Accept' header and return an ordered list of preferences.
Definition: mythhttpencoding.cpp:29
mythmimedatabase.h
LOC
#define LOC
Definition: mythhttpencoding.cpp:16
MythMimeDatabase::MimeTypeForName
static MythMimeType MimeTypeForName(const QString &Name)
Return a mime type that matches the given name.
Definition: mythmimedatabase.cpp:201
HTTPOneDotOne
@ HTTPOneDotOne
Definition: mythhttptypes.h:87
hardwareprofile.distros.mythtv_data.request.Request
def Request(url=None)
Definition: request.py:62
MythHTTPEncoding::GetJSONEncodedParameters
static void GetJSONEncodedParameters(MythHTTPRequest *Request)
Definition: mythhttpencoding.cpp:207
MythHTTPEncoding::GetXMLEncodedParameters
static void GetXMLEncodedParameters(MythHTTPRequest *Request)
Definition: mythhttpencoding.cpp:136
MythMimeType::IsValid
bool IsValid() const
Definition: mythmimetype.cpp:26
MythMimeType
Definition: mythmimetype.h:16
HTTPNoEncode
@ HTTPNoEncode
Definition: mythhttptypes.h:137
MythHTTPEncoding::Compress
static MythHTTPEncode Compress(MythHTTPResponse *Response, int64_t &Size)
Compress the response content under certain circumstances or mark the content as 'chunkable'.
Definition: mythhttpencoding.cpp:306
MythHTTPResponse::m_requestHeaders
HTTPHeaders m_requestHeaders
Definition: mythhttpresponse.h:55
HTTPGzip
@ HTTPGzip
Definition: mythhttptypes.h:138
MythHTTPResponse::AddHeader
std::enable_if_t< std::is_convertible_v< T, QString >, void > AddHeader(const QString &key, const T &val)
Definition: mythhttpresponse.h:35
MythHTTPEncoding::GetMimeTypes
static QStringList GetMimeTypes(const QString &Accept)
Definition: mythhttpencoding.cpp:30
build_compdb.filename
filename
Definition: build_compdb.py:21
MythMimeDatabase::MimeTypeForFileNameAndData
static MythMimeType MimeTypeForFileNameAndData(const QString &FileName, const QByteArray &Data)
Return a mime type for the given FileName and data.
Definition: mythmimedatabase.cpp:217