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