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