MythTV master
httprequest.cpp
Go to the documentation of this file.
1
2// Program Name: httprequest.cpp
3// Created : Oct. 21, 2005
4//
5// Purpose : Http Request/Response
6//
7// Copyright (c) 2005 David Blain <dblain@mythtv.org>
8//
9// Licensed under the GPL v2 or later, see LICENSE for details
10//
12
13#include "httprequest.h"
14
15#include <QtGlobal>
16#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
17#include <QtSystemDetection>
18#endif
19#include <QFile>
20#include <QFileInfo>
21#include <QHostInfo>
22#include <QStringList>
23#include <QCryptographicHash>
24#include <QDateTime>
25#include <Qt>
26#include <QUrl>
27
28#include <cerrno>
29#include <cstdlib>
30#include <fcntl.h>
31#include <sys/stat.h>
32#include <sys/types.h>
33#include <unistd.h> // for gethostname
34// FOR DEBUGGING
35#include <iostream>
36
37#ifndef Q_OS_WINDOWS
38#include <netinet/tcp.h>
39#endif
40
41#include "libmythbase/compat.h"
47#include "libmythbase/mythversion.h"
49
54
55#include "httpserver.h"
56
57#ifndef O_LARGEFILE
58#define O_LARGEFILE 0
59#endif
60
61static std::array<const MIMETypes,66> g_MIMETypes
62{{
63 // Image Mime Types
64 { "gif" , "image/gif" },
65 { "ico" , "image/x-icon" },
66 { "jpeg", "image/jpeg" },
67 { "jpg" , "image/jpeg" },
68 { "mng" , "image/x-mng" },
69 { "png" , "image/png" },
70 { "svg" , "image/svg+xml" },
71 { "svgz", "image/svg+xml" },
72 { "tif" , "image/tiff" },
73 { "tiff", "image/tiff" },
74 // Text Mime Types
75 { "htm" , "text/html" },
76 { "html", "text/html" },
77 { "qsp" , "text/html" },
78 { "txt" , "text/plain" },
79 { "xml" , "text/xml" },
80 { "qxml", "text/xml" },
81 { "xslt", "text/xml" },
82 { "css" , "text/css" },
83 // Application Mime Types
84 { "crt" , "application/x-x509-ca-cert" },
85 { "doc" , "application/vnd.ms-word" },
86 { "gz" , "application/x-tar" },
87 { "js" , "application/javascript" },
88 { "m3u" , "application/x-mpegurl" }, // HTTP Live Streaming
89 { "m3u8", "application/x-mpegurl" }, // HTTP Live Streaming
90 { "ogx" , "application/ogg" }, // http://wiki.xiph.org/index.php/MIME_Types_and_File_Extensions
91 { "pdf" , "application/pdf" },
92 { "pem" , "application/x-x509-ca-cert" },
93 { "qjs" , "application/javascript" },
94 { "rm" , "application/vnd.rn-realmedia" },
95 { "swf" , "application/x-shockwave-flash" },
96 { "xls" , "application/vnd.ms-excel" },
97 { "zip" , "application/x-tar" },
98 // Audio Mime Types:
99 { "aac" , "audio/mp4" },
100 { "ac3" , "audio/vnd.dolby.dd-raw" }, // DLNA?
101 { "flac", "audio/x-flac" }, // This may become audio/flac in the future
102 { "m4a" , "audio/x-m4a" },
103 { "mid" , "audio/midi" },
104 { "mka" , "audio/x-matroska" },
105 { "mp3" , "audio/mpeg" },
106 { "oga" , "audio/ogg" }, // Defined: http://wiki.xiph.org/index.php/MIME_Types_and_File_Extensions
107 { "ogg" , "audio/ogg" }, // Defined: http://wiki.xiph.org/index.php/MIME_Types_and_File_Extensions
108 { "wav" , "audio/wav" },
109 { "wma" , "audio/x-ms-wma" },
110 // Video Mime Types
111 { "3gp" , "video/3gpp" }, // Also audio/3gpp
112 { "3g2" , "video/3gpp2" }, // Also audio/3gpp2
113 { "asx" , "video/x-ms-asf" },
114 { "asf" , "video/x-ms-asf" },
115 { "avi" , "video/x-msvideo" }, // Also video/avi
116 { "m2p" , "video/mp2p" }, // RFC 3555
117 { "m4v" , "video/mp4" },
118 { "mpeg", "video/mp2p" }, // RFC 3555
119 { "mpeg2","video/mp2p" }, // RFC 3555
120 { "mpg" , "video/mp2p" }, // RFC 3555
121 { "mpg2", "video/mp2p" }, // RFC 3555
122 { "mov" , "video/quicktime" },
123 { "mp4" , "video/mp4" },
124 { "mkv" , "video/x-matroska" }, // See http://matroska.org/technical/specs/notes.html#MIME (See NOTE 1)
125 { "nuv" , "video/nupplevideo" },
126 { "ogv" , "video/ogg" }, // Defined: http://wiki.xiph.org/index.php/MIME_Types_and_File_Extensions
127 { "ps" , "video/mp2p" }, // RFC 3555
128 { "ts" , "video/mp2t" }, // RFC 3555
129 { "vob" , "video/mpeg" }, // Also video/dvd
130 { "wmv" , "video/x-ms-wmv" },
131 // Font Mime Types
132 { "ttf" , "font/ttf" },
133 { "woff" , "font/woff" },
134 { "woff2", "font/woff2" }
135}};
136
137// NOTE 1
138// This formerly was video/x-matroska, but got changed due to #8643
139// This was reverted from video/x-mkv, due to #10980
140// See http://matroska.org/technical/specs/notes.html#MIME
141// If you can't please everyone, may as well be correct as you piss some off
142
143static QString StaticPage =
144 "<!DOCTYPE html>"
145 "<HTML>"
146 "<HEAD>"
147 "<TITLE>Error %1</TITLE>"
148 "<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=ISO-8859-1\">"
149 "</HEAD>"
150 "<BODY><H1>%2.</H1></BODY>"
151 "</HTML>";
152
153const char *HTTPRequest::s_szServerHeaders = "Accept-Ranges: bytes\r\n";
154
156//
158
159QString HTTPRequest::GetLastHeader( const QString &sType ) const
160{
161 QStringList values = m_mapHeaders.values( sType );
162 if (!values.isEmpty())
163 return values.last();
164 return {};
165}
166
168//
170
172{
173 // HTTP
174 if (sType == "GET" ) return( m_eType = RequestTypeGet );
175 if (sType == "HEAD" ) return( m_eType = RequestTypeHead );
176 if (sType == "POST" ) return( m_eType = RequestTypePost );
177 if (sType == "OPTIONS" ) return( m_eType = RequestTypeOptions );
178
179 // UPnP
180 if (sType == "M-SEARCH" ) return( m_eType = RequestTypeMSearch );
181 if (sType == "NOTIFY" ) return( m_eType = RequestTypeNotify );
182 if (sType == "SUBSCRIBE" ) return( m_eType = RequestTypeSubscribe );
183 if (sType == "UNSUBSCRIBE") return( m_eType = RequestTypeUnsubscribe );
184
185 if (sType.startsWith( QString("HTTP/") )) return( m_eType = RequestTypeResponse );
186
187 LOG(VB_HTTP, LOG_INFO,
188 QString("HTTPRequest::SentRequestType( %1 ) - returning Unknown.")
189 .arg(sType));
190
191 return( m_eType = RequestTypeUnknown);
192}
193
195//
197
198QString HTTPRequest::BuildResponseHeader( long long nSize )
199{
200 QString sHeader;
201 QString sContentType = (m_eResponseType == ResponseTypeOther) ?
203 //-----------------------------------------------------------------------
204 // Headers describing the connection
205 //-----------------------------------------------------------------------
206
207 // The protocol string
208 sHeader = QString( "%1 %2\r\n" ).arg(GetResponseProtocol(),
210
213
214 SetResponseHeader("Connection", m_bKeepAlive ? "Keep-Alive" : "Close" );
215 if (m_bKeepAlive)
216 {
217 if (m_nKeepAliveTimeout == 0s) // Value wasn't passed in by the server, so go with the configured value
218 m_nKeepAliveTimeout = gCoreContext->GetDurSetting<std::chrono::seconds>("HTTP/KeepAliveTimeoutSecs", 10s);
219 SetResponseHeader("Keep-Alive", QString("timeout=%1").arg(m_nKeepAliveTimeout.count()));
220 }
221
222 //-----------------------------------------------------------------------
223 // Entity Headers - Describe the content and allowed methods
224 // RFC 2616 Section 7.1
225 //-----------------------------------------------------------------------
226 if (m_eResponseType != ResponseTypeHeader) // No entity headers
227 {
228 SetResponseHeader("Content-Language", gCoreContext->GetLanguageAndVariant().replace("_", "-"));
229 SetResponseHeader("Content-Type", sContentType);
230
231 // Default to 'inline' but we should support 'attachment' when it would
232 // be appropriate i.e. not when streaming a file to a upnp player or browser
233 // that can support it natively
234 if (!m_sFileName.isEmpty())
235 {
236 // TODO: Add support for utf8 encoding - RFC 5987
237 QString filename = QFileInfo(m_sFileName).fileName(); // Strip any path
238 SetResponseHeader("Content-Disposition", QString("inline; filename=\"%2\"").arg(QString(filename.toLatin1())));
239 }
240
241 SetResponseHeader("Content-Length", QString::number(nSize));
242
243 // See DLNA 7.4.1.3.11.4.3 Tolerance to unavailable contentFeatures.dlna.org header
244 //
245 // It is better not to return this header, than to return it containing
246 // invalid or incomplete information. We are unable to currently determine
247 // this information at this stage, so do not return it. Only older devices
248 // look for it. Newer devices use the information provided in the UPnP
249 // response
250
251// QString sValue = GetHeaderValue( "getContentFeatures.dlna.org", "0" );
252//
253// if (sValue == "1")
254// sHeader += "contentFeatures.dlna.org: DLNA.ORG_OP=01;DLNA.ORG_CI=0;"
255// "DLNA.ORG_FLAGS=01500000000000000000000000000000\r\n";
256
257
258 // DLNA 7.5.4.3.2.33 MT transfer mode indication
259 QString sTransferMode = GetRequestHeader( "transferMode.dlna.org", "" );
260
261 if (sTransferMode.isEmpty())
262 {
263 if (m_sResponseTypeText.startsWith("video/") ||
264 m_sResponseTypeText.startsWith("audio/"))
265 sTransferMode = "Streaming";
266 else
267 sTransferMode = "Interactive";
268 }
269
270 if (sTransferMode == "Streaming")
271 SetResponseHeader("transferMode.dlna.org", "Streaming");
272 else if (sTransferMode == "Background")
273 SetResponseHeader("transferMode.dlna.org", "Background");
274 else if (sTransferMode == "Interactive")
275 SetResponseHeader("transferMode.dlna.org", "Interactive");
276
277 // HACK Temporary hack for Samsung TVs - Needs to be moved later as it's not entirely DLNA compliant
278 if (!GetRequestHeader( "getcontentFeatures.dlna.org", "" ).isEmpty())
279 SetResponseHeader("contentFeatures.dlna.org", "DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01500000000000000000000000000000");
280 }
281
282 auto values = m_mapHeaders.values("origin");
283 for (const auto & value : std::as_const(values))
284 AddCORSHeaders(value);
285
286 if (qEnvironmentVariableIsSet("HTTPREQUEST_DEBUG"))
287 {
288 // Dump response header
289 QMap<QString, QString>::iterator it;
290 for ( it = m_mapRespHeaders.begin(); it != m_mapRespHeaders.end(); ++it )
291 {
292 LOG(VB_HTTP, LOG_INFO, QString("(Response Header) %1: %2").arg(it.key(), it.value()));
293 }
294 }
295
296 sHeader += GetResponseHeaders();
297 sHeader += "\r\n";
298
299 return sHeader;
300}
301
303//
305
307{
308 qint64 nBytes = 0;
309
310 switch( m_eResponseType )
311 {
312 // The following are all eligable for gzip compression
314 case ResponseTypeNone:
315 LOG(VB_HTTP, LOG_INFO,
316 QString("HTTPRequest::SendResponse( None ) :%1 -> %2:")
318 return( -1 );
319 case ResponseTypeJS:
320 case ResponseTypeCSS:
321 case ResponseTypeText:
322 case ResponseTypeSVG:
323 case ResponseTypeXML:
324 case ResponseTypeHTML:
325 // If the reponse isn't already in the buffer, then load it
326 if (m_sFileName.isEmpty() || !m_response.buffer().isEmpty())
327 break;
328 {
329 QFile file(m_sFileName);
330 if (file.exists() && file.size() < (2LL * 1024 * 1024) && // For security/stability, limit size of files read into buffer to 2MiB
331 file.open(QIODevice::ReadOnly | QIODevice::Text))
332 m_response.buffer() = file.readAll();
333
334 if (!m_response.buffer().isEmpty())
335 break;
336
337 // Let SendResponseFile try or send a 404
339 }
340 [[fallthrough]];
341 case ResponseTypeFile: // Binary files
342 LOG(VB_HTTP, LOG_INFO,
343 QString("HTTPRequest::SendResponse( File ) :%1 -> %2:")
345 return( SendResponseFile( m_sFileName ));
348 default:
349 break;
350 }
351
352 LOG(VB_HTTP, LOG_INFO,
353 QString("HTTPRequest::SendResponse(xml/html) (%1) :%2 -> %3: %4")
355 QString::number(m_eResponseType)));
356
357 // ----------------------------------------------------------------------
358 // Check for ETag match...
359 // ----------------------------------------------------------------------
360
361 QString sETag = GetRequestHeader( "If-None-Match", "" );
362
363 if ( !sETag.isEmpty() && sETag == m_mapRespHeaders[ "ETag" ] )
364 {
365 LOG(VB_HTTP, LOG_INFO,
366 QString("HTTPRequest::SendResponse(%1) - Cached")
367 .arg(sETag));
368
369 m_nResponseStatus = 304;
370 m_eResponseType = ResponseTypeHeader; // No entity headers
371
372 // no content can be returned.
373 m_response.buffer().clear();
374 }
375
376 // ----------------------------------------------------------------------
377
378 int nContentLen = m_response.buffer().length();
379
380 QBuffer *pBuffer = &m_response;
381
382 // ----------------------------------------------------------------------
383 // DEBUGGING
384 if (qEnvironmentVariableIsSet("HTTPREQUEST_DEBUG"))
385 std::cout << m_response.buffer().constData() << std::endl;
386 // ----------------------------------------------------------------------
387
388 LOG(VB_HTTP, LOG_DEBUG, QString("Reponse Content Length: %1").arg(nContentLen));
389
390 // ----------------------------------------------------------------------
391 // Should we try to return data gzip'd?
392 // ----------------------------------------------------------------------
393
394 QBuffer compBuffer;
395
396 auto values = m_mapHeaders.values("accept-encoding");
397 bool gzip_found = std::any_of(values.cbegin(), values.cend(),
398 [](const auto & value)
399 {return value.contains( "gzip" ); });
400
401 if (( nContentLen > 0 ) && gzip_found)
402 {
403 QByteArray compressed = gzipCompress( m_response.buffer() );
404 compBuffer.setData( compressed );
405
406 if (!compBuffer.buffer().isEmpty())
407 {
408 pBuffer = &compBuffer;
409
410 SetResponseHeader( "Content-Encoding", "gzip" );
411 LOG(VB_HTTP, LOG_DEBUG, QString("Reponse Compressed Content Length: %1").arg(compBuffer.buffer().length()));
412 }
413 }
414
415 // ----------------------------------------------------------------------
416 // Write out Header.
417 // ----------------------------------------------------------------------
418
419 nContentLen = pBuffer->buffer().length();
420
421 QString rHeader = BuildResponseHeader( nContentLen );
422
423 QByteArray sHeader = rHeader.toUtf8();
424 LOG(VB_HTTP, LOG_DEBUG, QString("Response header size: %1 bytes").arg(sHeader.length()));
425 nBytes = WriteBlock( sHeader.constData(), sHeader.length() );
426
427 if (nBytes < sHeader.length())
428 {
429 LOG( VB_HTTP, LOG_ERR, QString("HttpRequest::SendResponse(): "
430 "Incomplete write of header, "
431 "%1 written of %2")
432 .arg(nBytes).arg(sHeader.length()));
433 }
434
435 // ----------------------------------------------------------------------
436 // Write out Response buffer.
437 // ----------------------------------------------------------------------
438
439 if (( m_eType != RequestTypeHead ) &&
440 ( nContentLen > 0 ))
441 {
442 qint64 bytesWritten = SendData( pBuffer, 0, nContentLen );
443 //qint64 bytesWritten = WriteBlock( pBuffer->buffer(), pBuffer->buffer().length() );
444
445 if (bytesWritten != nContentLen)
446 LOG(VB_HTTP, LOG_ERR, "HttpRequest::SendResponse(): Error occurred while writing response body.");
447 else
448 nBytes += bytesWritten;
449 }
450
451 return( nBytes );
452}
453
455//
457
458qint64 HTTPRequest::SendResponseFile( const QString& sFileName )
459{
460 qint64 nBytes = 0;
461 long long llSize = 0;
462 long long llStart = 0;
463 long long llEnd = 0;
464
465 LOG(VB_HTTP, LOG_INFO, QString("SendResponseFile ( %1 )").arg(sFileName));
466
468 m_sResponseTypeText = "text/plain";
469
470 QFile tmpFile( sFileName );
471 if (tmpFile.exists( ) && tmpFile.open( QIODevice::ReadOnly ))
472 {
473
474 m_sResponseTypeText = TestMimeType( sFileName );
475
476 // ------------------------------------------------------------------
477 // Get File size
478 // ------------------------------------------------------------------
479
480 llSize = llEnd = tmpFile.size( );
481
482 m_nResponseStatus = 200;
483
484 // ------------------------------------------------------------------
485 // Process any Range Header
486 // ------------------------------------------------------------------
487
488 bool bRange = false;
489 QString sRange = GetRequestHeader( "range", "" );
490
491 if (!sRange.isEmpty())
492 {
493 bRange = ParseRange( sRange, llSize, &llStart, &llEnd );
494
495 // Adjust ranges that are too long.
496
497 if (llEnd >= llSize)
498 llEnd = llSize-1;
499
500 if ((llSize > llStart) && (llSize > llEnd) && (llEnd > llStart))
501 {
502 if (bRange)
503 {
504 m_nResponseStatus = 206;
505 m_mapRespHeaders[ "Content-Range" ] = QString("bytes %1-%2/%3")
506 .arg( llStart )
507 .arg( llEnd )
508 .arg( llSize );
509 llSize = (llEnd - llStart) + 1;
510 }
511 }
512 else
513 {
514 m_nResponseStatus = 416;
515 // RFC 7233 - A server generating a 416 (Range Not Satisfiable)
516 // response to a byte-range request SHOULD send a Content-Range
517 // header field with an unsatisfied-range value
518 m_mapRespHeaders[ "Content-Range" ] = QString("bytes */%3")
519 .arg( llSize );
520 llSize = 0;
521 LOG(VB_HTTP, LOG_INFO,
522 QString("HTTPRequest::SendResponseFile(%1) - "
523 "invalid byte range %2-%3/%4")
524 .arg(sFileName) .arg(llStart) .arg(llEnd)
525 .arg(llSize));
526 }
527 }
528
529 // HACK: D-Link DSM-320
530 // The following headers are only required by servers which don't support
531 // http keep alive. We do support it, so we don't need it. Keeping it in
532 // place to prevent someone re-adding it in future
533 //m_mapRespHeaders[ "X-User-Agent" ] = "redsonic";
534
535 // ------------------------------------------------------------------
536 //
537 // ------------------------------------------------------------------
538
539 }
540 else
541 {
542 LOG(VB_HTTP, LOG_INFO,
543 QString("HTTPRequest::SendResponseFile(%1) - cannot find file!")
544 .arg(sFileName));
545 m_nResponseStatus = 404;
546 m_response.write( GetResponsePage() );
547 }
548
549 // -=>TODO: Should set "Content-Length: *" if file is still recording
550
551 // ----------------------------------------------------------------------
552 // Write out Header.
553 // ----------------------------------------------------------------------
554
555 QString rHeader = BuildResponseHeader( llSize );
556 QByteArray sHeader = rHeader.toUtf8();
557 LOG(VB_HTTP, LOG_DEBUG, QString("Response header size: %1 bytes").arg(sHeader.length()));
558 nBytes = WriteBlock( sHeader.constData(), sHeader.length() );
559
560 if (nBytes < sHeader.length())
561 {
562 LOG( VB_HTTP, LOG_ERR, QString("HttpRequest::SendResponseFile(): "
563 "Incomplete write of header, "
564 "%1 written of %2")
565 .arg(nBytes).arg(sHeader.length()));
566 }
567
568 // ----------------------------------------------------------------------
569 // Write out File.
570 // ----------------------------------------------------------------------
571
572#if 0
573 LOG(VB_HTTP, LOG_DEBUG,
574 QString("SendResponseFile : size = %1, start = %2, end = %3")
575 .arg(llSize).arg(llStart).arg(llEnd));
576#endif
577 if (( m_eType != RequestTypeHead ) && (llSize != 0))
578 {
579 long long sent = SendFile( tmpFile, llStart, llSize );
580
581 if (sent == -1)
582 {
583 LOG(VB_HTTP, LOG_INFO,
584 QString("SendResponseFile( %1 ) Error: %2 [%3]" )
585 .arg(sFileName) .arg(errno) .arg(strerror(errno)));
586
587 nBytes = -1;
588 }
589 }
590
591 // -=>TODO: Only returns header length...
592 // should we change to return total bytes?
593
594 return nBytes;
595}
596
598//
600
601static constexpr size_t SENDFILE_BUFFER_SIZE { 65536 };
602
603qint64 HTTPRequest::SendData( QIODevice *pDevice, qint64 llStart, qint64 llBytes )
604{
605 bool bShouldClose = false;
606 qint64 sent = 0;
607
608 if (!pDevice->isOpen())
609 {
610 pDevice->open( QIODevice::ReadOnly );
611 bShouldClose = true;
612 }
613
614 // ----------------------------------------------------------------------
615 // Set out file position to requested start location.
616 // ----------------------------------------------------------------------
617
618 if ( !pDevice->seek( llStart ))
619 return -1;
620
621 std::array<char,SENDFILE_BUFFER_SIZE> aBuffer {};
622
623 qint64 llBytesRemaining = llBytes;
624 qint64 llBytesToRead = 0;
625 qint64 llBytesRead = 0;
626
627 while ((sent < llBytes) && !pDevice->atEnd())
628 {
629 llBytesToRead = std::min( (qint64)SENDFILE_BUFFER_SIZE, llBytesRemaining );
630 llBytesRead = pDevice->read( aBuffer.data(), llBytesToRead );
631 if ( llBytesRead != -1 )
632 {
633 if ( WriteBlock( aBuffer.data(), llBytesRead ) == -1)
634 return -1;
635
636 // -=>TODO: We don't handle the situation where we read more than was sent.
637
638 sent += llBytesRead;
639 llBytesRemaining -= llBytesRead;
640 }
641 }
642
643 if (bShouldClose)
644 pDevice->close();
645
646 return sent;
647}
648
650//
652
653qint64 HTTPRequest::SendFile( QFile &file, qint64 llStart, qint64 llBytes )
654{
655 qint64 sent = SendData( (QIODevice *)(&file), llStart, llBytes );
656
657 return( sent );
658}
659
660
662//
664
665void HTTPRequest::FormatErrorResponse( bool bServerError,
666 const QString &sFaultString,
667 const QString &sDetails )
668{
670 m_nResponseStatus = 500;
671
672 QTextStream stream( &m_response );
673
674 stream << R"(<?xml version="1.0" encoding="utf-8"?>)";
675
676 QString sWhere = ( bServerError ) ? "s:Server" : "s:Client";
677
678 if (m_bSOAPRequest)
679 {
680 m_mapRespHeaders[ "EXT" ] = "";
681
682 stream << SOAP_ENVELOPE_BEGIN
683 << "<s:Fault>"
684 << "<faultcode>" << sWhere << "</faultcode>"
685 << "<faultstring>" << sFaultString << "</faultstring>";
686 }
687
688 if (!sDetails.isEmpty())
689 {
690 stream << "<detail>" << sDetails << "</detail>";
691 }
692
693 if (m_bSOAPRequest)
694 {
695 stream << "</s:Fault>" << SOAP_ENVELOPE_END;
696 }
697
698 stream.flush();
699}
700
702//
704
706{
709 m_nResponseStatus = 200;
710
712
713 //m_response << pFormatter->ToString();
714}
715
717//
719
721{
723 m_nResponseStatus = 200;
724
725 QTextStream stream( &m_response );
726
727 stream << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n";
728
729 if (m_bSOAPRequest)
730 {
731 m_mapRespHeaders[ "EXT" ] = "";
732
733 stream << SOAP_ENVELOPE_BEGIN
734 << "<u:" << m_sMethod << "Response xmlns:u=\""
735 << m_sNameSpace << "\">\r\n";
736 }
737 else
738 {
739 stream << "<" << m_sMethod << "Response>\r\n";
740 }
741
742 for (const auto & arg : std::as_const(args))
743 {
744 stream << "<" << arg.m_sName;
745
746 if (arg.m_pAttributes)
747 {
748 for (const auto & attr : std::as_const(*arg.m_pAttributes))
749 {
750 stream << " " << attr.m_sName << "='"
751 << Encode( attr.m_sValue ) << "'";
752 }
753 }
754
755 stream << ">";
756
757 if (m_bSOAPRequest)
758 stream << Encode( arg.m_sValue );
759 else
760 stream << arg.m_sValue;
761
762 stream << "</" << arg.m_sName << ">\r\n";
763 }
764
765 if (m_bSOAPRequest)
766 {
767 stream << "</u:" << m_sMethod << "Response>\r\n"
769 }
770 else
771 {
772 stream << "</" << m_sMethod << "Response>\r\n";
773 }
774
775 stream.flush();
776}
777
779//
781
782void HTTPRequest::FormatRawResponse(const QString &sXML)
783{
785 m_nResponseStatus = 200;
786
787 QTextStream stream( &m_response );
788
789 stream << sXML;
790
791 stream.flush();
792}
794//
796
797void HTTPRequest::FormatFileResponse( const QString &sFileName )
798{
799 m_sFileName = sFileName;
800 QFileInfo file(m_sFileName);
801
802 if (!m_sFileName.isEmpty() && file.exists())
803 {
804 QDateTime ims = QDateTime::fromString(GetRequestHeader("if-modified-since", ""), Qt::RFC2822Date);
805#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
806 ims.setTimeSpec(Qt::UTC);
807#else
808 ims.setTimeZone(QTimeZone(QTimeZone::UTC));
809#endif
810 if (ims.isValid() && ims <= file.lastModified()) // Strong validator
811 {
813 m_nResponseStatus = 304; // Not Modified
814 }
815 else
816 {
819 m_nResponseStatus = 200; // OK
820 SetResponseHeader("Last-Modified", MythDate::toString(file.lastModified(),
822 MythDate::kRFC822))); // RFC 822
823 SetResponseHeader("Cache-Control", "no-cache=\"Ext\", max-age = 7200"); // 2 Hours
824 }
825 }
826 else
827 {
829 m_nResponseStatus = 404; // Resource not found
830 m_response.write( GetResponsePage() );
831 LOG(VB_HTTP, LOG_INFO,
832 QString("HTTPRequest::FormatFileResponse('%1') - cannot find file")
833 .arg(sFileName));
834 }
835}
836
838//
840
841void HTTPRequest::SetRequestProtocol( const QString &sLine )
842{
843 m_sProtocol = sLine.section( '/', 0, 0 ).trimmed();
844 QString sVersion = sLine.section( '/', 1 ).trimmed();
845
846 m_nMajor = sVersion.section( '.', 0, 0 ).toInt();
847 m_nMinor = sVersion.section( '.', 1 ).toInt();
848}
849
851//
853
855{
856 return QString("%1/%2.%3").arg(m_sProtocol,
857 QString::number(m_nMajor),
858 QString::number(m_nMinor));
859}
860
862//
864
866{
867 // RFC 2145
868 //
869 // An HTTP server SHOULD send a response version equal to the highest
870 // version for which the server is at least conditionally compliant, and
871 // whose major version is less than or equal to the one received in the
872 // request.
873
874// if (m_nMajor == 1)
875// QString("HTTP/1.1");
876// else if (m_nMajor == 2)
877// QString("HTTP/2.0");
878
879 return {"HTTP/1.1"};
880}
881
883//
885
887{
888 if ((sType == "application/x-www-form-urlencoded" ) ||
889 (sType.startsWith("application/x-www-form-urlencoded;")))
891
892 if ((sType == "text/xml" ) ||
893 (sType.startsWith("text/xml;") ))
895
896 if ((sType == "application/json") ||
897 sType.startsWith("application/json;"))
899
901}
902
903
905//
907
909{
910 switch( m_nResponseStatus )
911 {
912 case 200: return( "200 OK" );
913 case 201: return( "201 Created" );
914 case 202: return( "202 Accepted" );
915 case 204: return( "204 No Content" );
916 case 205: return( "205 Reset Content" );
917 case 206: return( "206 Partial Content" );
918 case 300: return( "300 Multiple Choices" );
919 case 301: return( "301 Moved Permanently" );
920 case 302: return( "302 Found" );
921 case 303: return( "303 See Other" );
922 case 304: return( "304 Not Modified" );
923 case 305: return( "305 Use Proxy" );
924 case 307: return( "307 Temporary Redirect" );
925 case 308: return( "308 Permanent Redirect" );
926 case 400: return( "400 Bad Request" );
927 case 401: return( "401 Unauthorized" );
928 case 403: return( "403 Forbidden" );
929 case 404: return( "404 Not Found" );
930 case 405: return( "405 Method Not Allowed" );
931 case 406: return( "406 Not Acceptable" );
932 case 408: return( "408 Request Timeout" );
933 case 410: return( "410 Gone" );
934 case 411: return( "411 Length Required" );
935 case 412: return( "412 Precondition Failed" );
936 case 413: return( "413 Request Entity Too Large" );
937 case 414: return( "414 Request-URI Too Long" );
938 case 415: return( "415 Unsupported Media Type" );
939 case 416: return( "416 Requested Range Not Satisfiable" );
940 case 417: return( "417 Expectation Failed" );
941 // I'm a teapot
942 case 428: return( "428 Precondition Required" ); // RFC 6585
943 case 429: return( "429 Too Many Requests" ); // RFC 6585
944 case 431: return( "431 Request Header Fields Too Large" ); // RFC 6585
945 case 500: return( "500 Internal Server Error" );
946 case 501: return( "501 Not Implemented" );
947 case 502: return( "502 Bad Gateway" );
948 case 503: return( "503 Service Unavailable" );
949 case 504: return( "504 Gateway Timeout" );
950 case 505: return( "505 HTTP Version Not Supported" );
951 case 510: return( "510 Not Extended" );
952 case 511: return( "511 Network Authentication Required" ); // RFC 6585
953 }
954
955 return( QString( "%1 Unknown" ).arg( m_nResponseStatus ));
956}
957
959//
961
963{
964 return StaticPage.arg(QString::number(m_nResponseStatus), GetResponseStatus()).toUtf8();
965}
966
968//
970
971QString HTTPRequest::GetResponseType( void ) const
972{
973 switch( m_eResponseType )
974 {
975 case ResponseTypeXML : return( "text/xml; charset=\"UTF-8\"" );
976 case ResponseTypeHTML : return( "text/html; charset=\"UTF-8\"" );
977 case ResponseTypeCSS : return( "text/css; charset=\"UTF-8\"" );
978 case ResponseTypeJS : return( "application/javascript" );
979 case ResponseTypeText : return( "text/plain; charset=\"UTF-8\"" );
980 case ResponseTypeSVG : return( "image/svg+xml" );
981 default: break;
982 }
983
984 return( "text/plain" );
985}
986
988//
990
991QString HTTPRequest::GetMimeType( const QString &sFileExtension )
992{
993 QString ext;
994
995 for (const auto & type : g_MIMETypes)
996 {
997 ext = type.pszExtension;
998
999 if ( sFileExtension.compare(ext, Qt::CaseInsensitive) == 0 )
1000 return( type.pszType );
1001 }
1002
1003 return( "text/plain" );
1004}
1005
1007//
1009
1011{
1012 QStringList mimeTypes;
1013
1014 for (const auto & type : g_MIMETypes)
1015 {
1016 if (!mimeTypes.contains( type.pszType ))
1017 mimeTypes.append( type.pszType );
1018 }
1019
1020 return mimeTypes;
1021}
1022
1024//
1026
1027QString HTTPRequest::TestMimeType( const QString &sFileName )
1028{
1029 QFileInfo info( sFileName );
1030 QString sLOC = "HTTPRequest::TestMimeType(" + sFileName + ") - ";
1031 QString sSuffix = info.suffix().toLower();
1032 QString sMIME = GetMimeType( sSuffix );
1033
1034 if ( sSuffix == "nuv" ) // If a very old recording, might be an MPEG?
1035 {
1036 // Read the header to find out:
1037 QFile file( sFileName );
1038
1039 if ( file.open(QIODevice::ReadOnly | QIODevice::Text) )
1040 {
1041 QByteArray head = file.read(8);
1042 QString sHex = head.toHex();
1043
1044 LOG(VB_HTTP, LOG_DEBUG, sLOC + "file starts with " + sHex);
1045
1046 if ( sHex == "000001ba44000400" ) // MPEG2 PS
1047 sMIME = "video/mp2p";
1048
1049 if ( head == "MythTVVi" )
1050 {
1051 file.seek(100);
1052 head = file.read(4);
1053
1054 if ( head == "DIVX" )
1055 {
1056 LOG(VB_HTTP, LOG_DEBUG, sLOC + "('MythTVVi...DIVXLAME')");
1057 sMIME = "video/mp4";
1058 }
1059 // NuppelVideo is "RJPG" at byte 612
1060 // We could also check the audio (LAME or RAWA),
1061 // but since most UPnP clients choke on Nuppel, no need
1062 }
1063
1064 file.close();
1065 }
1066 else
1067 {
1068 LOG(VB_GENERAL, LOG_ERR, sLOC + "Could not read file");
1069 }
1070 }
1071
1072 LOG(VB_HTTP, LOG_INFO, sLOC + "type is " + sMIME);
1073 return sMIME;
1074}
1075
1077//
1079
1080long HTTPRequest::GetParameters( QString sParams, QStringMap &mapParams )
1081{
1082 long nCount = 0;
1083
1084 LOG(VB_HTTP, LOG_INFO, QString("sParams: '%1'").arg(sParams));
1085
1086 // This looks odd, but it is here to cope with stupid UPnP clients that
1087 // forget to de-escape the URLs. We can't map %26 here as well, as that
1088 // breaks anything that is trying to pass & as part of a name or value.
1089 sParams.replace( "&amp;", "&" );
1090
1091 if (!sParams.isEmpty())
1092 {
1093 QStringList params = sParams.split('&', Qt::SkipEmptyParts);
1094 for (const auto & param : std::as_const(params))
1095 {
1096 QString sName = param.section( '=', 0, 0 );
1097 QString sValue = param.section( '=', 1 );
1098 sValue.replace("+"," ");
1099
1100 if (!sName.isEmpty())
1101 {
1102 sName = QUrl::fromPercentEncoding(sName.toUtf8());
1103 sValue = QUrl::fromPercentEncoding(sValue.toUtf8());
1104
1105 mapParams.insert( sName.trimmed(), sValue );
1106 nCount++;
1107 }
1108 }
1109 }
1110
1111 return nCount;
1112}
1113
1114
1116//
1118
1119QString HTTPRequest::GetRequestHeader( const QString &sKey, const QString &sDefault )
1120{
1121 auto it = m_mapHeaders.find( sKey.toLower() );
1122
1123 if ( it == m_mapHeaders.end())
1124 return( sDefault );
1125
1126 return *it;
1127}
1128
1129
1131//
1133
1135{
1136 QString sHeader = s_szServerHeaders;
1137
1138 for ( QStringMap::iterator it = m_mapRespHeaders.begin();
1139 it != m_mapRespHeaders.end();
1140 ++it )
1141 {
1142 sHeader += it.key() + ": ";
1143 sHeader += *it + "\r\n";
1144 }
1145
1146 return( sHeader );
1147}
1148
1150//
1152
1154{
1155 // TODO: Think about whether we should use a longer timeout if the client
1156 // has explicitly specified 'Keep-alive'
1157
1158 // HTTP 1.1 ... server may assume keep-alive
1159 bool bKeepAlive = true;
1160
1161 // if HTTP/1.0... must default to false
1162 if ((m_nMajor == 1) && (m_nMinor == 0))
1163 bKeepAlive = false;
1164
1165 // Read Connection Header to see whether the client has explicitly
1166 // asked for the connection to be kept alive or closed after the response
1167 // is sent
1168 QString sConnection = GetRequestHeader( "connection", "default" ).toLower();
1169
1170 QStringList sValueList = sConnection.split(",");
1171
1172 if ( sValueList.contains("close") )
1173 {
1174 LOG(VB_HTTP, LOG_DEBUG, "Client requested the connection be closed");
1175 bKeepAlive = false;
1176 }
1177 else if (sValueList.contains("keep-alive"))
1178 {
1179 bKeepAlive = true;
1180 }
1181
1182 return bKeepAlive;
1183}
1184
1186//
1188
1190{
1191 QStringList sCookieList = m_mapHeaders.values("cookie");
1192
1193 QStringList::iterator it;
1194 for (it = sCookieList.begin(); it != sCookieList.end(); ++it)
1195 {
1196 QString key = (*it).section('=', 0, 0);
1197 QString value = (*it).section('=', 1);
1198
1199 m_mapCookies.insert(key, value);
1200 }
1201}
1202
1204//
1206
1208{
1209 bool bSuccess = false;
1210
1211 try
1212 {
1213 // Read first line to determine requestType
1214 QString sRequestLine = ReadLine( 2s );
1215
1216 if ( sRequestLine.isEmpty() )
1217 {
1218 LOG(VB_GENERAL, LOG_ERR, "Timeout reading first line of request." );
1219 return false;
1220 }
1221
1222 // -=>TODO: Should read lines until a valid request???
1223 ProcessRequestLine( sRequestLine );
1224
1225 if (m_nMajor > 1 || m_nMajor < 0)
1226 {
1228 m_nResponseStatus = 505;
1229 m_response.write( GetResponsePage() );
1230
1231 return true;
1232 }
1233
1235 {
1237 m_nResponseStatus = 501; // Not Implemented
1238 // Conservative list, we can't really know what methods we
1239 // actually allow for an arbitrary resource without some sort of
1240 // high maintenance database
1241 SetResponseHeader("Allow", "GET, HEAD");
1242 m_response.write( GetResponsePage() );
1243 return true;
1244 }
1245
1246 // Read Header
1247 bool bDone = false;
1248 QString sLine = ReadLine( 2s );
1249
1250 while (( !sLine.isEmpty() ) && !bDone )
1251 {
1252 if (sLine != "\r\n")
1253 {
1254 QString sName = sLine.section( ':', 0, 0 ).trimmed();
1255 QString sValue = sLine.section( ':', 1 );
1256
1257 sValue.truncate( sValue.length() - 2 );
1258
1259 if (!sName.isEmpty() && !sValue.isEmpty())
1260 {
1261 m_mapHeaders.insert(sName.toLower(), sValue.trimmed());
1262 }
1263
1264 sLine = ReadLine( 2s );
1265 }
1266 else
1267 {
1268 bDone = true;
1269 }
1270 }
1271
1272 // Dump request header
1273 for ( auto it = m_mapHeaders.begin(); it != m_mapHeaders.end(); ++it )
1274 {
1275 LOG(VB_HTTP, LOG_INFO, QString("(Request Header) %1: %2")
1276 .arg(it.key(), *it));
1277 }
1278
1279 // Parse Cookies
1280 ParseCookies();
1281
1282 // Parse out keep alive
1284
1285 // Check to see if we found the end of the header or we timed out.
1286 if (!bDone)
1287 {
1288 LOG(VB_GENERAL, LOG_INFO, "Timeout waiting for request header." );
1289 return false;
1290 }
1291
1292 // HTTP/1.1 requires that the Host header be present, even if empty
1293 if ((m_nMinor == 1) && !m_mapHeaders.contains("host"))
1294 {
1296 m_nResponseStatus = 400;
1297 m_response.write( GetResponsePage() );
1298
1299 return true;
1300 }
1301
1302 // Destroy session if requested
1303 if (m_mapHeaders.contains("x-myth-clear-session"))
1304 {
1305 SetCookie("sessionToken", "", MythDate::current().addDays(-2), true);
1306 m_mapCookies.remove("sessionToken");
1307 }
1308
1309 // Allow session resumption for TLS connections
1310 if (m_mapCookies.contains("sessionToken"))
1311 {
1312 QString sessionToken = m_mapCookies["sessionToken"];
1314 MythUserSession session = sessionManager->GetSession(sessionToken);
1315
1316 if (session.IsValid())
1317 m_userSession = session;
1318 }
1319
1321 {
1322 if (!Authenticated())
1323 {
1325 m_nResponseStatus = 401;
1326 m_response.write( GetResponsePage() );
1327 // Since this may not be the first attempt at authentication,
1328 // Authenticated may have set the header with the appropriate
1329 // stale attribute
1330 SetResponseHeader("WWW-Authenticate", GetAuthenticationHeader(false));
1331
1332 return true;
1333 }
1334
1335 m_bProtected = true;
1336 }
1337
1338 bSuccess = true;
1339
1340 SetContentType( GetLastHeader( "content-type" ) );
1341 // Lets load payload if any.
1342 long nPayloadSize = GetLastHeader( "content-length" ).toLong();
1343
1344 if (nPayloadSize > 0)
1345 {
1346 char *pszPayload = new char[ nPayloadSize + 2 ];
1347 long nBytes = 0;
1348
1349 nBytes = ReadBlock( pszPayload, nPayloadSize, 5s );
1350 if (nBytes == nPayloadSize )
1351 {
1352 m_sPayload = QString::fromUtf8( pszPayload, nPayloadSize );
1353
1354 // See if the payload is just data from a form post
1358 m_mapParams.insert( "json", m_sPayload );
1359 }
1360 else
1361 {
1362 LOG(VB_GENERAL, LOG_ERR,
1363 QString("Unable to read entire payload (read %1 of %2 bytes)")
1364 .arg( nBytes ) .arg( nPayloadSize ) );
1365 bSuccess = false;
1366 }
1367
1368 delete [] pszPayload;
1369 }
1370
1371 // Check to see if this is a SOAP encoded message
1372 QString sSOAPAction = GetRequestHeader( "SOAPACTION", "" );
1373
1374 if (!sSOAPAction.isEmpty())
1375 bSuccess = ProcessSOAPPayload( sSOAPAction );
1376 else
1378
1379#if 0
1380 if (m_sMethod != "*" )
1381 LOG(VB_HTTP, LOG_DEBUG,
1382 QString("HTTPRequest::ParseRequest - Socket (%1) Base (%2) "
1383 "Method (%3) - Bytes in Socket Buffer (%4)")
1384 .arg(getSocketHandle()) .arg(m_sBaseUrl)
1385 .arg(m_sMethod) .arg(BytesAvailable()));
1386#endif
1387 }
1388 catch(...)
1389 {
1390 LOG(VB_GENERAL, LOG_WARNING,
1391 "Unexpected exception in HTTPRequest::ParseRequest" );
1392 }
1393
1394 return bSuccess;
1395}
1396
1398//
1400
1401void HTTPRequest::ProcessRequestLine( const QString &sLine )
1402{
1403 m_sRawRequest = sLine;
1404
1405 QStringList tokens = sLine.split(m_procReqLineExp, Qt::SkipEmptyParts);
1406 int nCount = tokens.count();
1407
1408 // ----------------------------------------------------------------------
1409
1410 if ( sLine.startsWith( QString("HTTP/") ))
1412 else
1414
1415 // ----------------------------------------------------------------------
1416 // if this is actually a response, then sLine's format will be:
1417 // HTTP/m.n <response code> <response text>
1418 // otherwise:
1419 // <method> <Resource URI> HTTP/m.n
1420 // ----------------------------------------------------------------------
1421
1423 {
1424 // ------------------------------------------------------------------
1425 // Process as a request
1426 // ------------------------------------------------------------------
1427
1428 if (nCount > 0)
1429 SetRequestType( tokens[0].trimmed() );
1430
1431 if (nCount > 1)
1432 {
1433 m_sOriginalUrl = tokens[1].toUtf8(); // Used by authorization check
1434 m_sRequestUrl = QUrl::fromPercentEncoding(tokens[1].toUtf8());
1435 m_sBaseUrl = m_sRequestUrl.section( '?', 0, 0).trimmed();
1436
1437 m_sResourceUrl = m_sBaseUrl; // Save complete url without parameters
1438
1439 // Process any Query String Parameters
1440 QString sQueryStr = tokens[1].section( '?', 1, 1 );
1441
1442 if (!sQueryStr.isEmpty())
1443 GetParameters( sQueryStr, m_mapParams );
1444 }
1445
1446 if (nCount > 2)
1447 SetRequestProtocol( tokens[2].trimmed() );
1448 }
1449 else
1450 {
1451 // ------------------------------------------------------------------
1452 // Process as a Response
1453 // ------------------------------------------------------------------
1454 if (nCount > 0)
1455 SetRequestProtocol( tokens[0].trimmed() );
1456
1457 if (nCount > 1)
1458 m_nResponseStatus = tokens[1].toInt();
1459 }
1460
1461
1462}
1463
1465//
1467
1468bool HTTPRequest::ParseRange( QString sRange,
1469 long long llSize,
1470 long long *pllStart,
1471 long long *pllEnd )
1472{
1473 // ----------------------------------------------------------------------
1474 // -=>TODO: Only handle 1 range at this time...
1475 // should make work with full spec.
1476 // ----------------------------------------------------------------------
1477
1478 if (sRange.isEmpty())
1479 return false;
1480
1481 // ----------------------------------------------------------------------
1482 // remove any "bytes="
1483 // ----------------------------------------------------------------------
1484 int nIdx = sRange.indexOf(m_parseRangeExp);
1485
1486 if (nIdx < 0)
1487 return false;
1488
1489 if (nIdx > 0)
1490 sRange.remove( 0, nIdx );
1491
1492 // ----------------------------------------------------------------------
1493 // Split multiple ranges
1494 // ----------------------------------------------------------------------
1495
1496 QStringList ranges = sRange.split(',', Qt::SkipEmptyParts);
1497 if (ranges.count() == 0)
1498 return false;
1499
1500 // ----------------------------------------------------------------------
1501 // Split first range into its components
1502 // ----------------------------------------------------------------------
1503
1504 QStringList parts = ranges[0].split('-');
1505
1506 if (parts.count() != 2)
1507 return false;
1508
1509 if (parts[0].isEmpty() && parts[1].isEmpty())
1510 return false;
1511
1512 // ----------------------------------------------------------------------
1513 //
1514 // ----------------------------------------------------------------------
1515
1516 bool conv_ok = false;
1517 if (parts[0].isEmpty())
1518 {
1519 // ------------------------------------------------------------------
1520 // Does it match "-####"
1521 // ------------------------------------------------------------------
1522
1523 long long llValue = parts[1].toLongLong(&conv_ok);
1524 if (!conv_ok) return false;
1525
1526 *pllStart = llSize - llValue;
1527 *pllEnd = llSize - 1;
1528 }
1529 else if (parts[1].isEmpty())
1530 {
1531 // ------------------------------------------------------------------
1532 // Does it match "####-"
1533 // ------------------------------------------------------------------
1534
1535 *pllStart = parts[0].toLongLong(&conv_ok);
1536
1537 if (!conv_ok)
1538 return false;
1539
1540 *pllEnd = llSize - 1;
1541 }
1542 else
1543 {
1544 // ------------------------------------------------------------------
1545 // Must be "####-####"
1546 // ------------------------------------------------------------------
1547
1548 *pllStart = parts[0].toLongLong(&conv_ok);
1549 if (!conv_ok) return false;
1550 *pllEnd = parts[1].toLongLong(&conv_ok);
1551 if (!conv_ok) return false;
1552
1553 if (*pllStart > *pllEnd)
1554 return false;
1555 }
1556
1557 LOG(VB_HTTP, LOG_DEBUG, QString("%1 Range Requested %2 - %3")
1558 .arg(getSocketHandle()) .arg(*pllStart) .arg(*pllEnd));
1559
1560 return true;
1561}
1562
1564//
1566
1568{
1569 // Strip out leading http://192.168.1.1:6544/ -> /
1570 // Should fix #8678
1571 // FIXME what about https?
1572 static const QRegularExpression re {"^http[s]?://.*?/"};
1573 m_sBaseUrl.replace(re, "/");
1574
1575 QStringList sList = m_sBaseUrl.split('/', Qt::SkipEmptyParts);
1576 m_sMethod = "";
1577
1578 if (!sList.isEmpty())
1579 {
1580 m_sMethod = sList.last();
1581 sList.pop_back();
1582 }
1583
1584 m_sBaseUrl = '/' + sList.join( "/" );
1585 LOG(VB_HTTP, LOG_INFO, QString("ExtractMethodFromURL(end) : %1 : %2")
1586 .arg(m_sMethod, m_sBaseUrl));
1587}
1588
1590//
1592
1593bool HTTPRequest::ProcessSOAPPayload( const QString &sSOAPAction )
1594{
1595 bool bSuccess = false;
1596
1597 // ----------------------------------------------------------------------
1598 // Open Supplied XML uPnp Description file.
1599 // ----------------------------------------------------------------------
1600
1601 LOG(VB_HTTP, LOG_INFO,
1602 QString("HTTPRequest::ProcessSOAPPayload : %1 : ").arg(sSOAPAction));
1603 QDomDocument doc ( "request" );
1604
1605#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
1606 QString sErrMsg;
1607 int nErrLine = 0;
1608 int nErrCol = 0;
1609
1610 if (!doc.setContent( m_sPayload, true, &sErrMsg, &nErrLine, &nErrCol ))
1611 {
1612 LOG(VB_GENERAL, LOG_ERR,
1613 QString( "Error parsing request at line: %1 column: %2 : %3" )
1614 .arg(nErrLine) .arg(nErrCol) .arg(sErrMsg));
1615 return( false );
1616 }
1617#else
1618 auto parseResult =doc.setContent( m_sPayload,
1619 QDomDocument::ParseOption::UseNamespaceProcessing );
1620 if (parseResult)
1621 {
1622 LOG(VB_GENERAL, LOG_ERR,
1623 QString( "Error parsing request at line: %1 column: %2 : %3" )
1624 .arg(parseResult.errorLine).arg(parseResult.errorColumn)
1625 .arg(parseResult.errorMessage));
1626 return( false );
1627 }
1628#endif
1629
1630 // --------------------------------------------------------------
1631 // XML Document Loaded... now parse it
1632 // --------------------------------------------------------------
1633
1634 QString sService;
1635
1636 if (sSOAPAction.contains( '#' ))
1637 {
1638 m_sNameSpace = sSOAPAction.section( '#', 0, 0).remove( 0, 1);
1639 m_sMethod = sSOAPAction.section( '#', 1 );
1640 m_sMethod.remove( m_sMethod.length()-1, 1 );
1641 }
1642 else
1643 {
1644 if (sSOAPAction.contains( '/' ))
1645 {
1646 int nPos = sSOAPAction.lastIndexOf( '/' );
1647 m_sNameSpace = sSOAPAction.mid(1, nPos);
1648 m_sMethod = sSOAPAction.mid(nPos + 1,
1649 sSOAPAction.length() - nPos - 2);
1650
1651 nPos = m_sNameSpace.lastIndexOf( '/', -2);
1652 sService = m_sNameSpace.mid(nPos + 1,
1653 m_sNameSpace.length() - nPos - 2);
1654 m_sNameSpace = m_sNameSpace.mid( 0, nPos );
1655 }
1656 else
1657 {
1658 m_sNameSpace.clear();
1659 m_sMethod = sSOAPAction;
1660 m_sMethod.remove( QChar( '\"' ) );
1661 }
1662 }
1663
1664 QDomNodeList oNodeList = doc.elementsByTagNameNS( m_sNameSpace, m_sMethod );
1665
1666 if (oNodeList.count() == 0)
1667 {
1668 oNodeList =
1669 doc.elementsByTagNameNS("http://schemas.xmlsoap.org/soap/envelope/",
1670 "Body");
1671 }
1672
1673 if (oNodeList.count() > 0)
1674 {
1675 QDomNode oMethod = oNodeList.item(0);
1676
1677 if (!oMethod.isNull())
1678 {
1679 m_bSOAPRequest = true;
1680
1681 for ( QDomNode oNode = oMethod.firstChild(); !oNode.isNull();
1682 oNode = oNode.nextSibling() )
1683 {
1684 QDomElement e = oNode.toElement();
1685
1686 if (!e.isNull())
1687 {
1688 QString sName = e.tagName();
1689 QString sValue = "";
1690
1691 QDomText oText = oNode.firstChild().toText();
1692
1693 if (!oText.isNull())
1694 sValue = oText.nodeValue();
1695
1696 sName = QUrl::fromPercentEncoding(sName.toUtf8());
1697 sValue = QUrl::fromPercentEncoding(sValue.toUtf8());
1698
1699 m_mapParams.insert( sName.trimmed().toLower(), sValue );
1700 }
1701 }
1702
1703 bSuccess = true;
1704 }
1705 }
1706
1707 return bSuccess;
1708}
1709
1711//
1713
1715{
1716 Serializer *pSerializer = nullptr;
1717
1718 if (m_bSOAPRequest)
1719 {
1720 pSerializer = (Serializer *)new SoapSerializer(&m_response,
1722 }
1723 else
1724 {
1725 QString sAccept = GetRequestHeader( "Accept", "*/*" );
1726
1727 if (sAccept.contains( "application/json", Qt::CaseInsensitive ) ||
1728 sAccept.contains( "text/javascript", Qt::CaseInsensitive ))
1729 {
1730 pSerializer = (Serializer *)new JSONSerializer(&m_response,
1731 m_sMethod);
1732 }
1733 else if (sAccept.contains( "text/x-apple-plist+xml", Qt::CaseInsensitive ))
1734 {
1735 pSerializer = (Serializer *)new XmlPListSerializer(&m_response);
1736 }
1737 }
1738
1739 // Default to XML
1740
1741 if (pSerializer == nullptr)
1742 pSerializer = (Serializer *)new XmlSerializer(&m_response, m_sMethod);
1743
1744 return pSerializer;
1745}
1746
1748//
1750
1751QString HTTPRequest::Encode(const QString &sIn)
1752{
1753 QString sStr = sIn;
1754#if 0
1755 LOG(VB_HTTP, LOG_DEBUG,
1756 QString("HTTPRequest::Encode Input : %1").arg(sStr));
1757#endif
1758 sStr.replace('&', "&amp;" ); // This _must_ come first
1759 sStr.replace('<', "&lt;" );
1760 sStr.replace('>', "&gt;" );
1761 sStr.replace('"', "&quot;");
1762 sStr.replace("'", "&apos;");
1763
1764#if 0
1765 LOG(VB_HTTP, LOG_DEBUG,
1766 QString("HTTPRequest::Encode Output : %1").arg(sStr));
1767#endif
1768 return sStr;
1769}
1770
1772//
1774
1775QString HTTPRequest::Decode(const QString& sIn)
1776{
1777 QString sStr = sIn;
1778 sStr.replace("&amp;", "&");
1779 sStr.replace("&lt;", "<");
1780 sStr.replace("&gt;", ">");
1781 sStr.replace("&quot;", "\"");
1782 sStr.replace("&apos;", "'");
1783
1784 return sStr;
1785}
1786
1788//
1790
1791QString HTTPRequest::GetETagHash(const QByteArray &data)
1792{
1793 QByteArray hash = QCryptographicHash::hash( data.data(), QCryptographicHash::Sha1);
1794
1795 return ("\"" + hash.toHex() + "\"");
1796}
1797
1799//
1801
1802bool HTTPRequest::IsUrlProtected( const QString &sBaseUrl )
1803{
1804 QString sProtected = XmlConfiguration().GetValue("HTTP/Protected/Urls", "/setup;/Config");
1805
1806 QStringList oList = sProtected.split( ';' );
1807
1808 for( int nIdx = 0; nIdx < oList.count(); nIdx++)
1809 {
1810 if (sBaseUrl.startsWith( oList[nIdx], Qt::CaseInsensitive ))
1811 return true;
1812 }
1813
1814 return false;
1815}
1816
1818//
1820
1822{
1823 QString authHeader;
1824
1825 // For now we support a single realm, that will change
1826 QString realm = "MythTV";
1827
1828 // Always use digest authentication where supported, it may be available
1829 // with HTTP 1.0 client as an extension, but we can't tell if that's the
1830 // case. It's guaranteed to be available for HTTP 1.1+
1831 if (m_nMajor >= 1 && m_nMinor > 0)
1832 {
1834 QString stale = isStale ? "true" : "false"; // FIXME
1835 authHeader = QString("Digest realm=\"%1\",nonce=\"%2\","
1836 "qop=\"auth\",stale=\"%3\",algorithm=\"MD5\"")
1837 .arg(realm, nonce, stale);
1838 }
1839 else
1840 {
1841 authHeader = QString("Basic realm=\"%1\"").arg(realm);
1842 }
1843
1844 return authHeader;
1845}
1846
1848//
1850
1851QString HTTPRequest::CalculateDigestNonce(const QString& timeStamp) const
1852{
1853 QString uniqueID = QString("%1:%2").arg(timeStamp, m_sPrivateToken);
1854 QString hash = QCryptographicHash::hash( uniqueID.toLatin1(), QCryptographicHash::Sha1).toHex(); // TODO: Change to Sha2 with QT5?
1855 QString nonce = QString("%1%2").arg(timeStamp, hash); // Note: since this is going in a header it should avoid illegal chars
1856 return nonce;
1857}
1858
1860//
1862
1864{
1865 LOG(VB_HTTP, LOG_NOTICE, "Attempting HTTP Basic Authentication");
1866 QStringList oList = GetLastHeader( "authorization" ).split( ' ' );
1867
1868 if (m_nMajor == 1 && m_nMinor == 0) // We only support Basic auth for http 1.0 clients
1869 {
1870 LOG(VB_GENERAL, LOG_WARNING, "Basic authentication is only allowed for HTTP 1.0");
1871 return false;
1872 }
1873
1874 QString sCredentials = QByteArray::fromBase64( oList[1].toUtf8() );
1875
1876 oList = sCredentials.split( ':' );
1877
1878 if (oList.count() < 2)
1879 {
1880 LOG(VB_GENERAL, LOG_WARNING, "Authorization attempt with invalid number of tokens");
1881 return false;
1882 }
1883
1884 QString sUsername = oList[0];
1885 QString sPassword = oList[1];
1886
1887 if (sUsername == "nouser") // Special logout username
1888 return false;
1889
1891 if (!MythSessionManager::IsValidUser(sUsername))
1892 {
1893 LOG(VB_GENERAL, LOG_WARNING, "Authorization attempt with invalid username");
1894 return false;
1895 }
1896
1897 QString client = QString("WebFrontend_%1").arg(GetPeerAddress());
1898 MythUserSession session = sessionManager->LoginUser(sUsername, sPassword,
1899 client);
1900
1901 if (!session.IsValid())
1902 {
1903 LOG(VB_GENERAL, LOG_WARNING, "Authorization attempt with invalid password");
1904 return false;
1905 }
1906
1907 LOG(VB_HTTP, LOG_NOTICE, "Valid Authorization received");
1908
1909 if (IsEncrypted()) // Only set a session cookie for encrypted connections, not safe otherwise
1910 SetCookie("sessionToken", session.GetSessionToken(),
1911 session.GetSessionExpires(), true);
1912
1913 m_userSession = session;
1914
1915 return false;
1916}
1917
1919//
1921
1923{
1924 LOG(VB_HTTP, LOG_NOTICE, "Attempting HTTP Digest Authentication");
1925 QString realm = "MythTV"; // TODO Check which realm applies for the request path
1926
1927 QString authMethod = GetLastHeader( "authorization" ).section(' ', 0, 0).toLower();
1928
1929 if (authMethod != "digest")
1930 {
1931 LOG(VB_GENERAL, LOG_WARNING, "Invalid method in Authorization header");
1932 return false;
1933 }
1934
1935 QString parameterStr = GetLastHeader( "authorization" ).section(' ', 1);
1936
1937 QMap<QString, QString> paramMap;
1938 QStringList paramList = parameterStr.split(',');
1939 QStringList::iterator it;
1940 for (it = paramList.begin(); it != paramList.end(); ++it)
1941 {
1942 QString key = (*it).section('=', 0, 0).toLower().trimmed();
1943 // Since the value may contain '=' return everything after first occurence
1944 QString value = (*it).section('=', 1).trimmed();
1945 // Remove any quotes surrounding the value
1946 value.remove("\"");
1947 paramMap[key] = value;
1948 }
1949
1950 if (paramMap.size() < 8)
1951 {
1952 LOG(VB_GENERAL, LOG_WARNING, "Invalid number of parameters in Authorization header");
1953 return false;
1954 }
1955
1956 if (paramMap["nonce"].isEmpty() || paramMap["username"].isEmpty() ||
1957 paramMap["realm"].isEmpty() || paramMap["uri"].isEmpty() ||
1958 paramMap["response"].isEmpty() || paramMap["qop"].isEmpty() ||
1959 paramMap["cnonce"].isEmpty() || paramMap["nc"].isEmpty())
1960 {
1961 LOG(VB_GENERAL, LOG_WARNING, "Missing required parameters in Authorization header");
1962 return false;
1963 }
1964
1965 if (paramMap["username"] == "nouser") // Special logout username
1966 return false;
1967
1968 if (paramMap["uri"] != m_sOriginalUrl)
1969 {
1970 LOG(VB_GENERAL, LOG_WARNING, "Authorization URI doesn't match the "
1971 "request URI");
1972 m_nResponseStatus = 400; // Bad Request
1973 return false;
1974 }
1975
1976 if (paramMap["realm"] != realm)
1977 {
1978 LOG(VB_GENERAL, LOG_WARNING, "Authorization realm doesn't match the "
1979 "realm of the requested content");
1980 return false;
1981 }
1982
1983 QByteArray nonce = paramMap["nonce"].toLatin1();
1984 if (nonce.length() < 20)
1985 {
1986 LOG(VB_GENERAL, LOG_WARNING, "Authorization nonce is too short");
1987 return false;
1988 }
1989
1990 QString nonceTimeStampStr = nonce.left(20); // ISO 8601 fixed length
1991 if (nonce != CalculateDigestNonce(nonceTimeStampStr))
1992 {
1993 LOG(VB_GENERAL, LOG_WARNING, "Authorization nonce doesn't match reference");
1994 LOG(VB_HTTP, LOG_DEBUG, QString("%1 vs %2").arg(QString(nonce),
1995 CalculateDigestNonce(nonceTimeStampStr)));
1996 return false;
1997 }
1998
1999 constexpr std::chrono::seconds AUTH_TIMEOUT { 2min }; // 2 Minute timeout to login, to reduce replay attack window
2000 QDateTime nonceTimeStamp = MythDate::fromString(nonceTimeStampStr);
2001 if (!nonceTimeStamp.isValid())
2002 {
2003 LOG(VB_GENERAL, LOG_WARNING, "Authorization nonce timestamp is invalid.");
2004 LOG(VB_HTTP, LOG_DEBUG, QString("Timestamp was '%1'").arg(nonceTimeStampStr));
2005 return false;
2006 }
2007
2008 if (MythDate::secsInPast(nonceTimeStamp) > AUTH_TIMEOUT)
2009 {
2010 LOG(VB_HTTP, LOG_NOTICE, "Authorization nonce timestamp is invalid or too old.");
2011 // Tell the client that the submitted nonce has expired at which
2012 // point they may wish to try again with a fresh nonce instead of
2013 // telling the user that their credentials were invalid
2014 SetResponseHeader("WWW-Authenticate", GetAuthenticationHeader(true), true);
2015 return false;
2016 }
2017
2019 if (!MythSessionManager::IsValidUser(paramMap["username"]))
2020 {
2021 LOG(VB_GENERAL, LOG_WARNING, "Authorization attempt with invalid username");
2022 return false;
2023 }
2024
2025 if (paramMap["response"].length() != 32)
2026 {
2027 LOG(VB_GENERAL, LOG_WARNING, "Authorization response field is invalid length");
2028 return false;
2029 }
2030
2031 // If you're still reading this, well done, not far to go now
2032
2033 QByteArray a1 = MythSessionManager::GetPasswordDigest(paramMap["username"]).toLatin1();
2034 //QByteArray a1 = "bcd911b2ecb15ffbd6d8e6e744d60cf6";
2035 QString methodDigest = QString("%1:%2").arg(GetRequestType(), paramMap["uri"]);
2036 QByteArray a2 = QCryptographicHash::hash(methodDigest.toLatin1(),
2037 QCryptographicHash::Md5).toHex();
2038
2039 QString responseDigest = QString("%1:%2:%3:%4:%5:%6").arg(a1,
2040 paramMap["nonce"],
2041 paramMap["nc"],
2042 paramMap["cnonce"],
2043 paramMap["qop"],
2044 a2);
2045 QByteArray kd = QCryptographicHash::hash(responseDigest.toLatin1(),
2046 QCryptographicHash::Md5).toHex();
2047
2048 if (paramMap["response"].toLatin1() == kd)
2049 {
2050 LOG(VB_HTTP, LOG_NOTICE, "Valid Authorization received");
2051 QString client = QString("WebFrontend_%1").arg(GetPeerAddress());
2052 MythUserSession session = sessionManager->LoginUser(paramMap["username"],
2053 a1,
2054 client);
2055 if (!session.IsValid())
2056 {
2057 LOG(VB_GENERAL, LOG_ERR, "Valid Authorization received, but we "
2058 "failed to create a valid session");
2059 return false;
2060 }
2061
2062 if (IsEncrypted()) // Only set a session cookie for encrypted connections, not safe otherwise
2063 SetCookie("sessionToken", session.GetSessionToken(),
2064 session.GetSessionExpires(), true);
2065
2066 m_userSession = session;
2067
2068 return true;
2069 }
2070
2071 LOG(VB_GENERAL, LOG_WARNING, "Authorization attempt with invalid password digest");
2072 LOG(VB_HTTP, LOG_DEBUG, QString("Received hash was '%1', calculated hash was '%2'")
2073 .arg(paramMap["response"], QString(kd)));
2074
2075 return false;
2076}
2077
2079//
2081
2083{
2084 // Check if the existing user has permission to access this resource
2085 if (m_userSession.IsValid()) //m_userSession.CheckPermission())
2086 return true;
2087
2088 QStringList oList = GetLastHeader( "authorization" ).split( ' ' );
2089
2090 if (oList.count() < 2)
2091 return false;
2092
2093 if (oList[0].compare( "basic", Qt::CaseInsensitive ) == 0)
2094 return BasicAuthentication();
2095 if (oList[0].compare( "digest", Qt::CaseInsensitive ) == 0)
2096 return DigestAuthentication();
2097
2098 return false;
2099}
2100
2102//
2104
2105void HTTPRequest::SetResponseHeader(const QString& sKey, const QString& sValue,
2106 bool replace)
2107{
2108 if (!replace && m_mapRespHeaders.contains(sKey))
2109 return;
2110
2111 m_mapRespHeaders[sKey] = sValue;
2112}
2113
2115//
2117
2118void HTTPRequest::SetCookie(const QString &sKey, const QString &sValue,
2119 const QDateTime &expiryDate, bool secure)
2120{
2121 if (secure && !IsEncrypted())
2122 {
2123 LOG(VB_GENERAL, LOG_WARNING, QString("HTTPRequest::SetCookie(%1=%2): "
2124 "A secure cookie cannot be set on an unencrypted connection.")
2125 .arg(sKey, sValue));
2126 return;
2127 }
2128
2129 QStringList cookieAttributes;
2130
2131 // Key=Value
2132 cookieAttributes.append(QString("%1=%2").arg(sKey, sValue));
2133
2134 // Domain - Most browsers have problems with a hostname, so it's better to omit this
2135// cookieAttributes.append(QString("Domain=%1").arg(GetHostName()));
2136
2137 // Path - Fix to root, no call for restricting to other paths yet
2138 cookieAttributes.append("Path=/");
2139
2140 // Expires - Expiry date, always set one, just good practice
2141 QString expires = MythDate::toString(expiryDate, MythDate::kRFC822); // RFC 822
2142 cookieAttributes.append(QString("Expires=%1").arg(expires)); // Cookie Expiry date
2143
2144 // Secure - Only send this cookie over encrypted connections, it contains
2145 // sensitive info SECURITY
2146 if (secure)
2147 cookieAttributes.append("Secure");
2148
2149 // HttpOnly - No cookie stealing javascript SECURITY
2150 cookieAttributes.append("HttpOnly");
2151
2152 SetResponseHeader("Set-Cookie", cookieAttributes.join("; "));
2153}
2154
2156//
2158
2160{
2161 // TODO: This only deals with the HTTP 1.1 case, 1.0 should be rare but we
2162 // should probably still handle it
2163
2164 // RFC 3875 - The is the hostname or ip address in the client request, not
2165 // the name or ip we might otherwise know for this server
2166 QString hostname = GetLastHeader("host");
2167 if (!hostname.isEmpty())
2168 {
2169 // Strip the port
2170 if (hostname.contains("]:")) // IPv6 port
2171 {
2172 return hostname.section("]:", 0 , 0);
2173 }
2174 if (hostname.contains(":")) // IPv4 port
2175 {
2176 return hostname.section(":", 0 , 0);
2177 }
2178 return hostname;
2179 }
2180
2181 return GetHostAddress();
2182}
2183
2184
2186{
2187 QString type;
2188 switch ( m_eType )
2189 {
2190 case RequestTypeUnknown :
2191 type = "UNKNOWN";
2192 break;
2193 case RequestTypeGet :
2194 type = "GET";
2195 break;
2196 case RequestTypeHead :
2197 type = "HEAD";
2198 break;
2199 case RequestTypePost :
2200 type = "POST";
2201 break;
2202 case RequestTypeOptions:
2203 type = "OPTIONS";
2204 break;
2205 case RequestTypeMSearch:
2206 type = "M-SEARCH";
2207 break;
2208 case RequestTypeNotify:
2209 type = "NOTIFY";
2210 break;
2212 type = "SUBSCRIBE";
2213 break;
2215 type = "UNSUBSCRIBE";
2216 break;
2217 case RequestTypeResponse :
2218 type = "RESPONSE";
2219 break;
2220 }
2221
2222 return type;
2223}
2224
2225void HTTPRequest::AddCORSHeaders( const QString &sOrigin )
2226{
2227 // ----------------------------------------------------------------------
2228 // SECURITY: Access-Control-Allow-Origin Wildcard
2229 //
2230 // This is a REALLY bad idea, so bad in fact that I'm including it here but
2231 // commented out in the hope that anyone thinking of adding it in the future
2232 // will see it and then read this comment.
2233 //
2234 // Browsers do not verify that the origin is on the same network. This means
2235 // that a malicious script embedded or included into ANY webpage you visit
2236 // could then access servers on your local network including MythTV. They
2237 // can grab data, delete data including recordings and videos, schedule
2238 // recordings and generally ruin your day.
2239 //
2240 // This might seem paranoid and a remote possibility, but then that's how
2241 // a lot of exploits are born. Do NOT allow wildcards.
2242 //
2243 //m_mapRespHeaders[ "Access-Control-Allow-Origin" ] = "*";
2244 // ----------------------------------------------------------------------
2245
2246 // ----------------------------------------------------------------------
2247 // SECURITY: Allow the WebFrontend on the Master backend and ONLY this
2248 // machine to access resources on a frontend or slave web server
2249 //
2250 // http://www.w3.org/TR/cors/#introduction
2251 // ----------------------------------------------------------------------
2252
2253 QStringList allowedOrigins;
2254
2255 int serverStatusPort = gCoreContext->GetMasterServerStatusPort();
2256 int backendSSLPort = gCoreContext->GetNumSetting( "BackendSSLPort",
2257 serverStatusPort + 10);
2258
2259 QString masterAddrPort = QString("%1:%2")
2261 .arg(serverStatusPort);
2262 QString masterTLSAddrPort = QString("%1:%2")
2264 .arg(backendSSLPort);
2265
2266 allowedOrigins << QString("http://%1").arg(masterAddrPort);
2267 allowedOrigins << QString("https://%2").arg(masterTLSAddrPort);
2268
2269 QString localhostname = QHostInfo::localHostName();
2270 if (!localhostname.isEmpty())
2271 {
2272 allowedOrigins << QString("http://%1:%2")
2273 .arg(localhostname).arg(serverStatusPort);
2274 allowedOrigins << QString("https://%1:%2")
2275 .arg(localhostname).arg(backendSSLPort);
2276 }
2277
2278 QStringList allowedOriginsList =
2279 gCoreContext->GetSetting("AllowedOriginsList", QString(
2280 "https://chromecast.mythtv.org")).split(",");
2281
2282 for (const auto & origin : std::as_const(allowedOriginsList))
2283 {
2284 if (origin.isEmpty())
2285 continue;
2286
2287 if (origin == "*" || (!origin.startsWith("http://") &&
2288 !origin.startsWith("https://")))
2289 {
2290 LOG(VB_GENERAL, LOG_ERR, QString("Illegal AllowedOriginsList"
2291 " entry '%1'. Must start with http[s]:// and not be *")
2292 .arg(origin));
2293 }
2294 else
2295 {
2296 allowedOrigins << origin;
2297 }
2298 }
2299
2300 if (VERBOSE_LEVEL_CHECK(VB_HTTP, LOG_DEBUG))
2301 {
2302 for (const auto & origin : std::as_const(allowedOrigins))
2303 LOG(VB_HTTP, LOG_DEBUG, QString("Will allow Origin: %1").arg(origin));
2304 }
2305
2306 if (allowedOrigins.contains(sOrigin))
2307 {
2308 SetResponseHeader( "Access-Control-Allow-Origin" , sOrigin);
2309 SetResponseHeader( "Access-Control-Allow-Credentials" , "true");
2310 SetResponseHeader( "Access-Control-Allow-Headers" , "Content-Type");
2311 LOG(VB_HTTP, LOG_DEBUG, QString("Allow-Origin: %1)").arg(sOrigin));
2312 }
2313 else
2314 {
2315 LOG(VB_GENERAL, LOG_CRIT, QString("HTTPRequest: Cross-origin request "
2316 "received with origin (%1)")
2317 .arg(sOrigin));
2318 }
2319}
2320
2323//
2324// BufferedSocketDeviceRequest Class Implementation
2325//
2328
2329QString BufferedSocketDeviceRequest::ReadLine( std::chrono::milliseconds msecs )
2330{
2331 QString sLine;
2332
2333 if (m_pSocket && m_pSocket->isValid() &&
2334 m_pSocket->state() == QAbstractSocket::ConnectedState)
2335 {
2336 bool timeout = false;
2337 MythTimer timer;
2338 timer.start();
2339 while (!m_pSocket->canReadLine() && !timeout)
2340 {
2341 timeout = !(m_pSocket->waitForReadyRead( msecs.count() ));
2342
2343 if ( timer.elapsed() >= msecs )
2344 {
2345 timeout = true;
2346 LOG(VB_HTTP, LOG_INFO, "BufferedSocketDeviceRequest::ReadLine() - Exceeded Total Elapsed Wait Time." );
2347 }
2348 }
2349
2350 if (!timeout)
2351 sLine = m_pSocket->readLine();
2352 }
2353
2354 return( sLine );
2355}
2356
2358//
2360
2361qint64 BufferedSocketDeviceRequest::ReadBlock(char *pData, qint64 nMaxLen,
2362 std::chrono::milliseconds msecs)
2363{
2364 if (m_pSocket && m_pSocket->isValid() &&
2365 m_pSocket->state() == QAbstractSocket::ConnectedState)
2366 {
2367 if (msecs == 0ms)
2368 return( m_pSocket->read( pData, nMaxLen ));
2369
2370 bool bTimeout = false;
2371 MythTimer timer;
2372 timer.start();
2373 while ( (m_pSocket->bytesAvailable() < (int)nMaxLen) && !bTimeout ) // This can end up waiting far longer than msecs
2374 {
2375 bTimeout = !(m_pSocket->waitForReadyRead( msecs.count() ));
2376
2377 if ( timer.elapsed() >= msecs )
2378 {
2379 bTimeout = true;
2380 LOG(VB_HTTP, LOG_INFO, "BufferedSocketDeviceRequest::ReadBlock() - Exceeded Total Elapsed Wait Time." );
2381 }
2382 }
2383
2384 // Just return what we have even if timed out.
2385
2386 return( m_pSocket->read( pData, nMaxLen ));
2387 }
2388
2389 return( -1 );
2390}
2391
2393//
2395
2396qint64 BufferedSocketDeviceRequest::WriteBlock(const char *pData, qint64 nLen)
2397{
2398 qint64 bytesWritten = -1;
2399 if (m_pSocket && m_pSocket->isValid() &&
2400 m_pSocket->state() == QAbstractSocket::ConnectedState)
2401 {
2402 bytesWritten = m_pSocket->write( pData, nLen );
2403 m_pSocket->waitForBytesWritten();
2404 }
2405
2406 return( bytesWritten );
2407}
2408
2410//
2412
2414{
2415 return( m_pSocket->localAddress().toString() );
2416}
2417
2419//
2421
2423{
2424 return( m_pSocket->localPort() );
2425}
2426
2427
2429//
2431
2433{
2434 return( m_pSocket->peerAddress().toString() );
2435}
quint16 GetHostPort() override
QString GetPeerAddress() override
QString ReadLine(std::chrono::milliseconds msecs) override
qint64 ReadBlock(char *pData, qint64 nMaxLen, std::chrono::milliseconds msecs=0ms) override
qint64 WriteBlock(const char *pData, qint64 nLen) override
QString GetHostAddress() override
QByteArray GetResponsePage(void)
bool IsEncrypted() const
Definition: httprequest.h:199
QString GetAuthenticationHeader(bool isStale=false)
QString BuildResponseHeader(long long nSize)
HttpResponseType m_eResponseType
Definition: httprequest.h:150
bool m_bProtected
Definition: httprequest.h:142
QString m_sFileName
Definition: httprequest.h:156
long m_nResponseStatus
Definition: httprequest.h:153
static QString GetETagHash(const QByteArray &data)
virtual QString GetHostAddress()=0
QString m_sPrivateToken
Definition: httprequest.h:162
qint64 SendResponse(void)
QString m_sResponseTypeText
Definition: httprequest.h:151
QString m_sNameSpace
Definition: httprequest.h:146
bool ParseRange(QString sRange, long long llSize, long long *pllStart, long long *pllEnd)
void FormatRawResponse(const QString &sXML)
void FormatErrorResponse(bool bServerError, const QString &sFaultString, const QString &sDetails)
HttpContentType m_eContentType
Definition: httprequest.h:122
QString m_sMethod
Definition: httprequest.h:130
bool BasicAuthentication()
QRegularExpression m_parseRangeExp
Definition: httprequest.h:117
static QStringList GetSupportedMimeTypes()
QString GetRequestHeader(const QString &sKey, const QString &sDefault)
void FormatFileResponse(const QString &sFileName)
static QString GetResponseProtocol()
QString m_sRequestUrl
Definition: httprequest.h:127
static QString TestMimeType(const QString &sFileName)
QString GetResponseType(void) const
bool ParseRequest()
void ProcessRequestLine(const QString &sLine)
Serializer * GetSerializer()
void ParseCookies(void)
static bool IsUrlProtected(const QString &sBaseUrl)
virtual int getSocketHandle()=0
MythUserSession m_userSession
Definition: httprequest.h:163
qint64 SendFile(QFile &file, qint64 llStart, qint64 llBytes)
void ExtractMethodFromURL()
std::chrono::seconds m_nKeepAliveTimeout
Definition: httprequest.h:168
QString GetResponseStatus(void) const
QStringMultiMap m_mapHeaders
Definition: httprequest.h:133
static QString GetMimeType(const QString &sFileExtension)
HttpContentType SetContentType(const QString &sType)
virtual QString GetHostName()
bool DigestAuthentication()
bool Authenticated()
HttpRequestType m_eType
Definition: httprequest.h:121
bool m_bSOAPRequest
Definition: httprequest.h:145
void FormatActionResponse(Serializer *ser)
HttpRequestType SetRequestType(const QString &sType)
virtual QString ReadLine(std::chrono::milliseconds msecs)=0
QString GetRequestProtocol() const
void AddCORSHeaders(const QString &sOrigin)
QString GetLastHeader(const QString &sType) const
QStringMap m_mapRespHeaders
Definition: httprequest.h:154
QString m_sOriginalUrl
Definition: httprequest.h:126
QString CalculateDigestNonce(const QString &timeStamp) const
virtual qint64 ReadBlock(char *pData, qint64 nMaxLen, std::chrono::milliseconds msecs=0ms)=0
virtual qint64 WriteBlock(const char *pData, qint64 nLen)=0
QString m_sResourceUrl
Definition: httprequest.h:129
QString m_sBaseUrl
Definition: httprequest.h:128
bool ParseKeepAlive(void)
QString m_sProtocol
Definition: httprequest.h:138
QStringMap m_mapCookies
Definition: httprequest.h:134
QString m_sRawRequest
Definition: httprequest.h:124
void SetRequestProtocol(const QString &sLine)
void SetCookie(const QString &sKey, const QString &sValue, const QDateTime &expiryDate, bool secure)
bool ProcessSOAPPayload(const QString &sSOAPAction)
static long GetParameters(QString sParams, QStringMap &mapParams)
void SetResponseHeader(const QString &sKey, const QString &sValue, bool replace=false)
static const char * s_szServerHeaders
Definition: httprequest.h:114
QStringMap m_mapParams
Definition: httprequest.h:132
QString m_sPayload
Definition: httprequest.h:136
QString GetRequestType() const
qint64 SendData(QIODevice *pDevice, qint64 llStart, qint64 llBytes)
qint64 SendResponseFile(const QString &sFileName)
bool m_bKeepAlive
Definition: httprequest.h:167
QBuffer m_response
Definition: httprequest.h:158
virtual QString GetPeerAddress()=0
static QString Decode(const QString &sIn)
QString GetResponseHeaders(void)
QRegularExpression m_procReqLineExp
Definition: httprequest.h:116
static QString Encode(const QString &sIn)
static QString GetServerVersion(void)
Definition: httpserver.cpp:280
QString GetMasterServerIP(void)
Returns the Master Backend IP address If the address is an IPv6 address, the scope Id is removed.
MythSessionManager * GetSessionManager(void)
QString GetSetting(const QString &key, const QString &defaultval="")
int GetMasterServerStatusPort(void)
Returns the Master Backend status port If no master server status port has been defined in the databa...
int GetNumSetting(const QString &key, int defaultval=0)
QString GetLanguageAndVariant(void)
Returns the user-set language and variant.
std::enable_if_t< std::chrono::__is_duration< T >::value, T > GetDurSetting(const QString &key, T defaultval=T::zero())
We use digest authentication because it protects the password over unprotected networks.
Definition: mythsession.h:106
static bool IsValidUser(const QString &username)
Check if the given user exists but not whether there is a valid session open for them!
static QString GetPasswordDigest(const QString &username)
Load the password digest for comparison in the HTTP Auth code.
MythUserSession GetSession(const QString &sessionToken)
Load the session details and return.
MythUserSession LoginUser(const QString &username, const QByteArray &digest, const QString &client="")
Login user by digest.
A QElapsedTimer based timer to replace use of QTime as a timer.
Definition: mythtimer.h:14
std::chrono::milliseconds elapsed(void)
Returns milliseconds elapsed since last start() or restart()
Definition: mythtimer.cpp:91
void start(void)
starts measuring elapsed time.
Definition: mythtimer.cpp:47
bool IsValid(void) const
Check if this session object appears properly constructed, it DOES NOT validate whether it is a valid...
Definition: mythsession.cpp:17
QString GetSessionToken(void) const
Definition: mythsession.h:45
QDateTime GetSessionExpires() const
Definition: mythsession.h:50
virtual void AddHeaders(QStringMap &headers)
Definition: serializer.cpp:22
virtual QString GetContentType()=0
QString GetValue(const QString &setting)
static std::array< const MIMETypes, 66 > g_MIMETypes
Definition: httprequest.cpp:62
static constexpr size_t SENDFILE_BUFFER_SIZE
static QString StaticPage
HttpContentType
Definition: httprequest.h:68
@ ContentType_XML
Definition: httprequest.h:71
@ ContentType_JSON
Definition: httprequest.h:72
@ ContentType_Unknown
Definition: httprequest.h:69
@ ContentType_Urlencoded
Definition: httprequest.h:70
static constexpr const char * SOAP_ENVELOPE_END
Definition: httprequest.h:38
@ ResponseTypeSVG
Definition: httprequest.h:84
@ ResponseTypeCSS
Definition: httprequest.h:82
@ ResponseTypeFile
Definition: httprequest.h:85
@ ResponseTypeHTML
Definition: httprequest.h:80
@ ResponseTypeNone
Definition: httprequest.h:77
@ ResponseTypeUnknown
Definition: httprequest.h:78
@ ResponseTypeXML
Definition: httprequest.h:79
@ ResponseTypeJS
Definition: httprequest.h:81
@ ResponseTypeHeader
Definition: httprequest.h:87
@ ResponseTypeText
Definition: httprequest.h:83
@ ResponseTypeOther
Definition: httprequest.h:86
static constexpr const char * SOAP_ENVELOPE_BEGIN
Definition: httprequest.h:35
HttpRequestType
Definition: httprequest.h:46
@ RequestTypeMSearch
Definition: httprequest.h:58
@ RequestTypeSubscribe
Definition: httprequest.h:59
@ RequestTypeNotify
Definition: httprequest.h:61
@ RequestTypePost
Definition: httprequest.h:51
@ RequestTypeOptions
Definition: httprequest.h:55
@ RequestTypeUnsubscribe
Definition: httprequest.h:60
@ RequestTypeGet
Definition: httprequest.h:49
@ RequestTypeHead
Definition: httprequest.h:50
@ RequestTypeResponse
Definition: httprequest.h:63
@ RequestTypeUnknown
Definition: httprequest.h:47
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
static bool VERBOSE_LEVEL_CHECK(uint64_t mask, LogLevel_t level)
Definition: mythlogging.h:29
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
QString current_iso_string(bool stripped)
Returns current Date and Time in UTC as a string.
Definition: mythdate.cpp:23
std::chrono::seconds secsInPast(const QDateTime &past)
Definition: mythdate.cpp:212
QString toString(const QDateTime &raw_dt, uint format)
Returns formatted string representing the time.
Definition: mythdate.cpp:93
@ kOverrideUTC
Present date/time in UTC.
Definition: mythdate.h:31
@ kRFC822
HTTP Date format.
Definition: mythdate.h:30
QDateTime fromString(const QString &dtstr)
Converts kFilename && kISODate formats to QDateTime.
Definition: mythdate.cpp:39
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:15
dictionary info
Definition: azlyrics.py:7
string hostname
Definition: caa.py:17
QByteArray gzipCompress(const QByteArray &data)
Definition: unziputil.cpp:93
QMap< QString, QString > QStringMap
Definition: upnputil.h:28