MythTV master
mythhttpresponse.cpp
Go to the documentation of this file.
1// Qt
2#include <QChar> // Fix Qt6 GCC SFINAE warning
3#include <QCryptographicHash>
4#include <QMimeDatabase>
5
6// MythTV
7#include "mythlogging.h"
8#include "mythdate.h"
10#include "http/mythhttpdata.h"
11#include "http/mythhttpfile.h"
12#include "http/mythhttpranges.h"
15#include "http/mythhttpcache.h"
16
17#define LOC QString("HTTPResp: ")
18
22 : m_serverName(Request->m_serverName),
23 m_version(Request->m_version),
24 m_connection(Request->m_connection),
25 m_timeout(Request->m_timeout),
26 m_status(Request->m_status),
27 m_requestType(Request->m_type),
28 m_allowed(Request->m_allowed),
29 m_requestHeaders(Request->m_headers)
30{
31}
32
36{
37 // Remaining entity headers
38 auto * data = std::get_if<HTTPData>(&m_response);
39 auto * file = std::get_if<HTTPFile>(&m_response);
40 if ((data || file) && m_requestHeaders)
41 {
42 // Language
43 if (!Config.m_language.isEmpty())
44 AddHeader("Content-Language", Config.m_language);
45
46 // Content disposition
47 QString filename = data ? (*data)->m_fileName : (*file)->m_fileName;
48 QString download = MythHTTP::GetHeader(m_requestHeaders, "mythtv-download");
49 if (!download.isEmpty())
50 {
51 int lastDot = filename.lastIndexOf('.');
52 if (lastDot > 0)
53 {
54 QString extension = filename.right(filename.length() - lastDot);
55 download = download + extension;
56 }
57 filename = download;
58 }
59 // Warn about programmer error
60 if (filename.isEmpty())
61 LOG(VB_GENERAL, LOG_WARNING, LOC + "Response has no name");
62
63 // Default to 'inline' but we should support 'attachment' when it would
64 // be appropriate i.e. not when streaming a file to a upnp player or browser
65 // that can support it natively
66 // TODO: Add support for utf8 encoding - RFC 5987
67 AddHeader("Content-Disposition", QString("inline; filename=\"%2\"").arg(qPrintable(filename)));
68
69 // TODO Should these be moved to UPnP handlers?
70 // UPnP headers
71 // DLNA 7.5.4.3.2.33 MT transfer mode indication
72 QString mode = MythHTTP::GetHeader(m_requestHeaders, "transferMode.dlna.org");
73 if (mode.isEmpty())
74 {
75 QString mime = data ? (*data)->m_mimeType.Name() : (*file)->m_mimeType.Name();
76 if (mime.startsWith("video/") || mime.startsWith("audio/"))
77 mode = "Streaming";
78 else
79 mode = "Interactive";
80 }
81
82 if (mode == "Streaming" || mode == "Background" || mode == "Interactive")
83 AddHeader("transferMode.dlna.org", mode);
84
85 // See DLNA 7.4.1.3.11.4.3 Tolerance to unavailable contentFeatures.dlna.org header
86 //
87 // It is better not to return this header, than to return it containing
88 // invalid or incomplete information. We are unable to currently determine
89 // this information at this stage, so do not return it. Only older devices
90 // look for it. Newer devices use the information provided in the UPnP
91 // response
92
93 // HACK Temporary hack for Samsung TVs - Needs to be moved later as it's not entirely DLNA compliant
94 if (!MythHTTP::GetHeader(m_requestHeaders, "getcontentFeatures.dlna.org", "").isEmpty())
95 {
96 AddHeader("contentFeatures.dlna.org",
97 "DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01500000000000000000000000000000");
98 }
99
100 // Security (mostly copied from previous HTTP server implementation)
101 // TODO Are these all needed for all content?
102
103 // Force IE into 'standards' mode
104 AddHeader("X-UA-Compatible", "IE=Edge");
105 // SECURITY: Set X-Content-Type-Options to 'nosniff'
106 AddHeader("X-Content-Type-Options", "nosniff");
107 // SECURITY: Set Content Security Policy
108 //
109 // *No external content allowed*
110 //
111 // This is an important safeguard. Third party content
112 // should never be permitted. It compromises security,
113 // privacy and violates the key principal that the
114 // WebFrontend should work on an isolated network with no
115 // internet access. Keep all content hosted locally!
116 //
117 // For now the following are disabled as we use xhr to
118 // trigger playback on frontends if we switch to triggering
119 // that through an internal request then these would be
120 // better enabled
121 //"default-src 'self'; "
122 //"connect-src 'self' https://services.mythtv.org; "
123
124 // FIXME unsafe-inline should be phased out, replaced by nonce-{csp_nonce} but it requires
125 // all inline event handlers and style attributes to be removed ...
126 QString policy = "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://services.mythtv.org; "
127 "style-src 'self' 'unsafe-inline'; "
128 "frame-src 'self'; "
129 "object-src 'none'; "
130 "media-src 'self'; "
131 "font-src 'self'; "
132 // This img-src is needed for displaying icons in channel icon search
133 // These icons come from many different urls
134 "img-src http: https: data:; "
135 "form-action 'self'; "
136 "frame-ancestors 'self'; ";
137
138 // For standards compliant browsers
139 AddHeader("Content-Security-Policy", policy);
140 // For Internet Explorer
141 AddHeader("X-Content-Security-Policy", policy);
142 AddHeader("X-XSS-Protection", "1; mode=block");
143 }
144
145 // Validate CORS requests
147 {
148 QString origin = MythHTTP::GetHeader(m_requestHeaders, "origin").trimmed().toLower();
149 if (!origin.isEmpty())
150 {
151 // Try allowed origins first
152 bool allow = Config.m_allowedOrigins.contains(origin);
153 if (!allow)
154 {
155 // Our list of hosts do not include the scheme (e.g. http) - so strip
156 // this from the origin.
157 if (auto index = origin.lastIndexOf("://"); index > -1)
158 {
159 auto scheme = origin.mid(0, index);
160 if (scheme == "http" || scheme == "https")
161 {
162 auto host = origin.mid(index + 3);
163 allow = Config.m_hosts.contains(host);
164 }
165 }
166 }
167 if (allow)
168 {
169 AddHeader("Access-Control-Allow-Origin" , origin);
170 AddHeader("Access-Control-Allow-Credentials" , "true");
171 AddHeader("Access-Control-Allow-Headers" , "Content-Type, Accept, Range");
172 AddHeader("Access-Control-Request-Method", MythHTTP::AllowedRequestsToString(m_allowed));
173 LOG(VB_HTTP, LOG_INFO, LOC + QString("Allowing CORS for origin: '%1'").arg(origin));
174 }
175 else
176 {
177 LOG(VB_HTTP, LOG_INFO, LOC + QString("Disallowing CORS for origin: '%1'").arg(origin));
178 }
179 }
180 }
181
182 // Add line break after headers
183 m_responseHeaders.emplace_back(MythHTTPData::Create("", "\r\n"));
184
185 // remove actual content for HEAD requests, failed range requests and 304 Not Modified's
188 {
189 m_response = std::monostate();
190 }
191}
192
194{
195 if (!Request)
196 return nullptr;
197
198 // The default allowed methods in MythHTTPRequest are HEAD, GET, OPTIONS.
199 // Override if necessary before calling this functions.
200 // N.B. GET and HEAD must be supported for HTTP/1.1
201 if ((Request->m_type & Request->m_allowed) != Request->m_type)
202 {
203 LOG(VB_GENERAL, LOG_WARNING, LOC + QString("'%1' is not allowed for '%2' (Allowed: %3)")
204 .arg(MythHTTP::RequestToString(Request->m_type), Request->m_fileName,
206 Request->m_status = HTTPMethodNotAllowed;
208 }
209
210 // Options
211 if (Request->m_type == HTTPOptions)
213
214 return nullptr;
215}
216
218{
219 auto response = std::make_shared<MythHTTPResponse>();
220 response->m_serverName = ServerName;
221 response->m_status = Status;
222 if (Status != HTTPServiceUnavailable)
223 {
224 response->m_response = MythHTTPData::Create("error.html",
225 s_defaultHTTPPage.arg(MythHTTP::StatusToString(Status)).toUtf8().constData());
226 }
227 response->AddDefaultHeaders();
228 if (Status == HTTPMethodNotAllowed)
229 response->AddHeader("Allow", MythHTTP::AllowedRequestsToString(response->m_allowed));
230 response->AddContentHeaders();
231 return response;
232}
233
235{
236 Request->m_status = HTTPMovedPermanently;
237 auto response = std::make_shared<MythHTTPResponse>(Request);
238 response->AddDefaultHeaders();
239 response->AddHeader("Location", Redirect);
240 response->AddContentHeaders();
241 return response;
242}
243
245{
246 auto response = std::make_shared<MythHTTPResponse>(Request);
247 response->m_response = MythHTTPData::Create("error.html", s_defaultHTTPPage
248 .arg(Message.isEmpty() ? MythHTTP::StatusToString(Request->m_status) : Message).toUtf8().constData());
249 response->AddDefaultHeaders();
250 response->AddContentHeaders();
251 return response;
252}
253
255{
256 auto response = std::make_shared<MythHTTPResponse>(Request);
257 response->AddDefaultHeaders();
258 response->AddHeader("Allow", MythHTTP::AllowedRequestsToString(response->m_allowed));
259 response->AddContentHeaders();
260 return response;
261}
262
264{
265 auto response = std::make_shared<MythHTTPResponse>(Request);
266 response->m_response = Data;
268 response->AddDefaultHeaders();
269 response->AddContentHeaders();
271 return response;
272}
273
275{
276 auto response = std::make_shared<MythHTTPResponse>(Request);
277 response->m_response = File;
279 response->AddDefaultHeaders();
280 response->AddContentHeaders();
282 return response;
283}
284
286{
287 auto response = std::make_shared<MythHTTPResponse>(Request);
288 response->AddDefaultHeaders();
289 response->AddContentHeaders();
290 return response;
291}
292
294{
295 // Handle range requests early as they influence the status, compression etc
296 QString range = MythHTTP::GetHeader(m_requestHeaders, "range", "");
297 if (!range.isEmpty())
299
300 QByteArray def = QString("%1 %2\r\n").arg(MythHTTP::VersionToString(m_version),
302 m_responseHeaders.emplace_back(MythHTTPData::Create(def));
304 AddHeader("Server", m_serverName);
305 // Range requests are supported
306 AddHeader("Accept-Ranges", "bytes");
307 AddHeader("Connection", m_connection == HTTPConnectionClose ? "Close" : "Keep-Alive");
309 AddHeader("Keep-Alive", QString("timeout=%1").arg(m_timeout.count() / 1000));
310
311 // Required error specific headers
313 AddHeader("Retry-After", HTTP_SOCKET_TIMEOUT_MS / 1000);
314}
315
317{
318 // Check content type and size first
319 auto * data = std::get_if<HTTPData>(&m_response);
320 auto * file = std::get_if<HTTPFile>(&m_response);
321 int64_t size {0};
322
323 if (data)
324 size = (*data)->size();
325 else if (file)
326 size = (*file)->size();
327
328 // Always add a zero length content header to keep some clients happy
329 if (size < 1)
330 {
331 AddHeader("Content-Length", 0);
332 return;
333 }
334
335 // Check mime type if not already set
336 auto & mime = data ? (*data)->m_mimeType : (*file)->m_mimeType;
337 if (!mime.IsValid())
339
340 // Range request?
341 HTTPRanges& ranges = data ? (*data)->m_ranges : (*file)->m_ranges;
342 bool rangerequest = !ranges.empty();
343 bool multipart = ranges.size() > 1;
344
345 // We now have the mime type and we can generate the multipart headers for a
346 // multipart request
347 if (multipart)
349
350 // Set the content type - with special handling for multipart ranges
351 AddHeader("Content-Type", multipart ? MythHTTPRanges::GetRangeHeader(ranges, size) :
353
355 {
356 // Mandatory 416 (Range Not Satisfiable) response
357 // Note - we will remove content before sending
358 AddHeader("Content-Range", QString("bytes */%1").arg(size));
359 AddHeader("Content-Length", 0);
360 return;
361 }
362
363 // Compress/chunk the result depending on client preferences, content and transfer type
364 auto encode = MythHTTPEncoding::Compress(this, size);
365
366 // and finally set the length if we aren't chunking or the transfer-encoding
367 // header if we are
368 if (encode == HTTPChunked)
369 {
370 AddHeader("Transfer-Encoding", "chunked");
371 }
372 else
373 {
374 if (rangerequest)
375 {
376 // Inform the client of the (single) range being served
377 if (!multipart)
378 AddHeader("Content-Range", MythHTTPRanges::GetRangeHeader(ranges, size));
379 // Content-Length is now the number of bytes served, not the total
380 size = data ? (*data)->m_partialSize : (*file)->m_partialSize;
381 }
382
383 // Add the size of the multipart headers to the content length
384 if (multipart)
385 size += data ? (*data)->m_multipartHeaderSize : (*file)->m_multipartHeaderSize;
386 AddHeader("Content-Length", size);
387 }
388}
389
391{
392 // Assume the worst:) and create a default error response
393 Request->m_status = HTTPBadRequest;
394 auto response = std::make_shared<MythHTTPResponse>(Request);
395 response->AddDefaultHeaders();
396 response->AddContentHeaders();
397
398 // This shouldn't happen
399 if (!Request)
400 return response;
401
402 /* Excerpt from RFC 6455
403 The requirements for this handshake are as follows.
404 1. The handshake MUST be a valid HTTP request as specified by
405 [RFC2616].
406 2. The method of the request MUST be GET, and the HTTP version MUST
407 be at least 1.1.
408 For example, if the WebSocket URI is "ws://example.com/chat",
409 the first line sent should be "GET /chat HTTP/1.1".
410 */
411
412 if ((Request->m_type != HTTPGet || Request->m_version != HTTPOneDotOne))
413 {
414 LOG(VB_HTTP, LOG_ERR, LOC + "Must be GET and HTTP/1.1");
415 return response;
416 }
417
418 /*
419 3. The "Request-URI" part of the request MUST match the /resource
420 name/ defined in Section 3 (a relative URI) or be an absolute
421 http/https URI that, when parsed, has a /resource name/, /host/,
422 and /port/ that match the corresponding ws/wss URI.
423 */
424
425 if (Request->m_path.isEmpty())
426 {
427 LOG(VB_HTTP, LOG_ERR, LOC + "Invalid Request-URI");
428 return response;
429 }
430
431 /*
432 4. The request MUST contain a |Host| header field whose value
433 contains /host/ plus optionally ":" followed by /port/ (when not
434 using the default port).
435 */
436
437 // Already checked in MythHTTPRequest
438
439 /*
440 5. The request MUST contain an |Upgrade| header field whose value
441 MUST include the "websocket" keyword.
442 */
443
444 auto header = MythHTTP::GetHeader(Request->m_headers, "upgrade");
445 if (header.isEmpty() || !header.contains("websocket", Qt::CaseInsensitive))
446 {
447 LOG(VB_HTTP, LOG_ERR, LOC + "Invalid/missing 'Upgrade' header");
448 return response;
449 }
450
451 /*
452 6. The request MUST contain a |Connection| header field whose value
453 MUST include the "Upgrade" token.
454 */
455
456 header = MythHTTP::GetHeader(Request->m_headers, "connection");
457 if (header.isEmpty() || !header.contains("upgrade", Qt::CaseInsensitive))
458 {
459 LOG(VB_HTTP, LOG_ERR, LOC + "Invalid/missing 'Connection' header");
460 return response;
461 }
462
463 /*
464 7. The request MUST include a header field with the name
465 |Sec-WebSocket-Key|. The value of this header field MUST be a
466 nonce consisting of a randomly selected 16-byte value that has
467 been base64-encoded (see Section 4 of [RFC4648]). The nonce
468 MUST be selected randomly for each connection.
469 NOTE: As an example, if the randomly selected value was the
470 sequence of bytes 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09
471 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f 0x10, the value of the header
472 field would be "AQIDBAUGBwgJCgsMDQ4PEC=="
473 */
474
475 auto key = MythHTTP::GetHeader(Request->m_headers, "sec-websocket-key").trimmed();
476 if (key.isEmpty())
477 {
478 LOG(VB_HTTP, LOG_ERR, LOC + "No Sec-WebSocket-Key header");
479 return response;
480 }
481 auto nonce = QByteArray::fromBase64(key.toLatin1());
482 if (nonce.length() != 16)
483 {
484 LOG(VB_HTTP, LOG_ERR, LOC + QString("Invalid Sec-WebSocket-Key header (length: %1)").arg(nonce.length()));
485 return response;
486 }
487
488 /*
489 8. The request MUST include a header field with the name |Origin|
490 [RFC6454] if the request is coming from a browser client. If
491 the connection is from a non-browser client, the request MAY
492 include this header field if the semantics of that client match
493 the use-case described here for browser clients. The value of
494 this header field is the ASCII serialization of origin of the
495 context in which the code establishing the connection is
496 running. See [RFC6454] for the details of how this header field
497 value is constructed.
498
499 As an example, if code downloaded from www.example.com attempts
500 to establish a connection to ww2.example.com, the value of the
501 header field would be "http://www.example.com".
502 */
503
504 // No reasonable way of knowing if the client is a browser. May need more work.
505
506 /*
507 9. The request MUST include a header field with the name
508 |Sec-WebSocket-Version|. The value of this header field MUST be
509 13.
510 */
511
512 if (header = MythHTTP::GetHeader(Request->m_headers, "sec-websocket-version"); header.trimmed().toInt() != 13)
513 {
514 LOG(VB_HTTP, LOG_ERR, LOC + QString("Unsupported websocket version %1").arg(header));
515 response->AddHeader(QStringLiteral("Sec-WebSocket-Version"), QStringLiteral("13"));
516 return response;
517 }
518
519 /*
520 10. The request MAY include a header field with the name
521 |Sec-WebSocket-Protocol|. If present, this value indicates one
522 or more comma-separated subprotocol the client wishes to speak,
523 ordered by preference. The elements that comprise this value
524 MUST be non-empty strings with characters in the range U+0021 to
525 U+007E not including separator characters as defined in
526 [RFC2616] and MUST all be unique strings. The ABNF for the
527 value of this header field is 1#token, where the definitions of
528 constructs and rules are as given in [RFC2616].
529 */
530
531 Protocol = MythHTTPWS::ProtocolFromString(MythHTTP::GetHeader(Request->m_headers, "sec-websocket-protocol"));
532
533 // If we've got this far, everything is OK, we have set the protocol that will
534 // be used and we need to respond positively.
535
536 static const auto magic = QStringLiteral("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
537 QString hash = QCryptographicHash::hash((key + magic).toUtf8(), QCryptographicHash::Sha1).toBase64();
538
539 // Replace response
541 response = std::make_shared<MythHTTPResponse>(Request);
542 response->AddDefaultHeaders();
543 response->AddContentHeaders();
544 response->AddHeader(QStringLiteral("Connection"), QStringLiteral("Upgrade"));
545 response->AddHeader(QStringLiteral("Upgrade"), QStringLiteral("websocket"));
546 response->AddHeader(QStringLiteral("Sec-WebSocket-Accept"), hash);
547 if (Protocol != ProtFrame)
548 response->AddHeader(QStringLiteral("Sec-WebSocket-Protocol"), MythHTTPWS::ProtocolToString(Protocol));
549
550 LOG(VB_HTTP, LOG_INFO, LOC + QString("Successful WebSocket upgrade (protocol: %1)")
551 .arg(MythHTTPWS::ProtocolToString(Protocol)));
552
553 // Check for Autobahn test suite
554 if (header = MythHTTP::GetHeader(Request->m_headers, "user-agent"); header.contains("AutobahnTestSuite"))
555 {
556 LOG(VB_GENERAL, LOG_INFO, LOC + "Autobahn test suite detected. Will echooooo...");
557 Testing = true;
558 }
559
560 // Ensure we pass handling to the websocket code once the response is sent
561 response->m_connection = HTTPConnectionUpgrade;
562 return response;
563}
static void PreConditionHeaders(const HTTPResponse &Response)
Add precondition (cache) headers to the response.
static void PreConditionCheck(const HTTPResponse &Response)
Process precondition checks.
QStringList m_allowedOrigins
Definition: mythhttptypes.h:72
QStringList m_hosts
Definition: mythhttptypes.h:71
QString m_language
Definition: mythhttptypes.h:70
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 MythMimeType GetMimeType(HTTPVariant Content)
Return a QMimeType that represents Content.
static QString GetRangeHeader(HTTPRanges &Ranges, int64_t Size)
static void BuildMultipartHeaders(MythHTTPResponse *Response)
static void HandleRangeRequest(MythHTTPResponse *Response, const QString &Request)
HTTPVariant m_response
static HTTPResponse OptionsResponse(const HTTPRequest2 &Request)
static HTTPResponse RedirectionResponse(const HTTPRequest2 &Request, const QString &Redirect)
MythHTTPStatus m_status
MythHTTPVersion m_version
void AddHeader(const QString &key, const T &val)
MythHTTPResponse()=default
static HTTPResponse FileResponse(const HTTPRequest2 &Request, const HTTPFile &File)
HTTPHeaders m_requestHeaders
static HTTPResponse UpgradeResponse(const HTTPRequest2 &Request, MythSocketProtocol &Protocol, bool &Testing)
static HTTPResponse HandleOptions(const HTTPRequest2 &Request)
std::chrono::milliseconds m_timeout
MythHTTPRequestType m_requestType
static HTTPResponse ErrorResponse(MythHTTPStatus Status, const QString &ServerName)
HTTPContents m_responseHeaders
static HTTPResponse EmptyResponse(const HTTPRequest2 &Request)
MythHTTPConnection m_connection
void Finalise(const MythHTTPConfig &Config)
Complete all necessary headers, add final line break after headers, remove data etc.
static HTTPResponse DataResponse(const HTTPRequest2 &Request, const HTTPData &Data)
static QString ProtocolToString(MythSocketProtocol Protocol)
static MythSocketProtocol ProtocolFromString(const QString &Protocols)
static QString GetContentType(const MythMimeType &Mime)
static QString AllowedRequestsToString(int Allowed)
static QString RequestToString(MythHTTPRequestType Type)
static QString GetHeader(const HTTPHeaders &Headers, const QString &Value, const QString &Default="")
static QString VersionToString(MythHTTPVersion Version)
static QString StatusToString(MythHTTPStatus Status)
MythSocketProtocol
@ ProtFrame
std::vector< HTTPRange > HTTPRanges
#define LOC
@ HTTPGet
Definition: mythhttptypes.h:94
@ HTTPHead
Definition: mythhttptypes.h:93
@ HTTPOptions
Definition: mythhttptypes.h:98
std::shared_ptr< MythHTTPFile > HTTPFile
Definition: mythhttptypes.h:41
std::shared_ptr< MythHTTPRequest > HTTPRequest2
Definition: mythhttptypes.h:39
static constexpr int HTTP_SOCKET_TIMEOUT_MS
Definition: mythhttptypes.h:25
@ HTTPChunked
std::shared_ptr< MythHTTPResponse > HTTPResponse
Definition: mythhttptypes.h:40
@ HTTPConnectionClose
@ HTTPConnectionKeepAlive
@ HTTPConnectionUpgrade
std::shared_ptr< MythHTTPData > HTTPData
Definition: mythhttptypes.h:37
@ HTTPOneDotOne
Definition: mythhttptypes.h:87
static QString s_defaultHTTPPage
MythHTTPStatus
@ HTTPRequestedRangeNotSatisfiable
@ HTTPMovedPermanently
@ HTTPBadRequest
@ HTTPServiceUnavailable
@ HTTPSwitchingProtocols
@ HTTPNotModified
@ HTTPMethodNotAllowed
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
QString toString(const QDateTime &raw_dt, uint format)
Returns formatted string representing the time.
Definition: mythdate.cpp:93
@ kRFC822
HTTP Date format.
Definition: mythdate.h:31
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:15