16#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
17#include <QtSystemDetection>
23#include <QCryptographicHash>
38#include <netinet/tcp.h>
47#include "libmythbase/mythversion.h"
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" },
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" },
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" },
89 {
"m3u8",
"application/x-mpegurl" },
90 {
"ogx" ,
"application/ogg" },
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" },
99 {
"aac" ,
"audio/mp4" },
100 {
"ac3" ,
"audio/vnd.dolby.dd-raw" },
101 {
"flac",
"audio/x-flac" },
102 {
"m4a" ,
"audio/x-m4a" },
103 {
"mid" ,
"audio/midi" },
104 {
"mka" ,
"audio/x-matroska" },
105 {
"mp3" ,
"audio/mpeg" },
106 {
"oga" ,
"audio/ogg" },
107 {
"ogg" ,
"audio/ogg" },
108 {
"wav" ,
"audio/wav" },
109 {
"wma" ,
"audio/x-ms-wma" },
111 {
"3gp" ,
"video/3gpp" },
112 {
"3g2" ,
"video/3gpp2" },
113 {
"asx" ,
"video/x-ms-asf" },
114 {
"asf" ,
"video/x-ms-asf" },
115 {
"avi" ,
"video/x-msvideo" },
116 {
"m2p" ,
"video/mp2p" },
117 {
"m4v" ,
"video/mp4" },
118 {
"mpeg",
"video/mp2p" },
119 {
"mpeg2",
"video/mp2p" },
120 {
"mpg" ,
"video/mp2p" },
121 {
"mpg2",
"video/mp2p" },
122 {
"mov" ,
"video/quicktime" },
123 {
"mp4" ,
"video/mp4" },
124 {
"mkv" ,
"video/x-matroska" },
125 {
"nuv" ,
"video/nupplevideo" },
126 {
"ogv" ,
"video/ogg" },
127 {
"ps" ,
"video/mp2p" },
128 {
"ts" ,
"video/mp2t" },
129 {
"vob" ,
"video/mpeg" },
130 {
"wmv" ,
"video/x-ms-wmv" },
132 {
"ttf" ,
"font/ttf" },
133 {
"woff" ,
"font/woff" },
134 {
"woff2",
"font/woff2" }
147 "<TITLE>Error %1</TITLE>"
148 "<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=ISO-8859-1\">"
150 "<BODY><H1>%2.</H1></BODY>"
162 if (!values.isEmpty())
163 return values.last();
187 LOG(VB_HTTP, LOG_INFO,
188 QString(
"HTTPRequest::SentRequestType( %1 ) - returning Unknown.")
261 if (sTransferMode.isEmpty())
265 sTransferMode =
"Streaming";
267 sTransferMode =
"Interactive";
270 if (sTransferMode ==
"Streaming")
272 else if (sTransferMode ==
"Background")
274 else if (sTransferMode ==
"Interactive")
279 SetResponseHeader(
"contentFeatures.dlna.org",
"DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01500000000000000000000000000000");
283 for (
const auto & value : std::as_const(values))
286 if (qEnvironmentVariableIsSet(
"HTTPREQUEST_DEBUG"))
289 QMap<QString, QString>::iterator it;
292 LOG(VB_HTTP, LOG_INFO, QString(
"(Response Header) %1: %2").arg(it.key(), it.value()));
315 LOG(VB_HTTP, LOG_INFO,
316 QString(
"HTTPRequest::SendResponse( None ) :%1 -> %2:")
330 if (
file.exists() &&
file.size() < (2LL * 1024 * 1024) &&
331 file.open(QIODevice::ReadOnly | QIODevice::Text))
342 LOG(VB_HTTP, LOG_INFO,
343 QString(
"HTTPRequest::SendResponse( File ) :%1 -> %2:")
352 LOG(VB_HTTP, LOG_INFO,
353 QString(
"HTTPRequest::SendResponse(xml/html) (%1) :%2 -> %3: %4")
365 LOG(VB_HTTP, LOG_INFO,
366 QString(
"HTTPRequest::SendResponse(%1) - Cached")
378 int nContentLen =
m_response.buffer().length();
384 if (qEnvironmentVariableIsSet(
"HTTPREQUEST_DEBUG"))
385 std::cout <<
m_response.buffer().constData() << std::endl;
388 LOG(VB_HTTP, LOG_DEBUG, QString(
"Reponse Content Length: %1").arg(nContentLen));
397 bool gzip_found = std::any_of(values.cbegin(), values.cend(),
398 [](
const auto & value)
399 {return value.contains(
"gzip" ); });
401 if (( nContentLen > 0 ) && gzip_found)
404 compBuffer.setData( compressed );
406 if (!compBuffer.buffer().isEmpty())
408 pBuffer = &compBuffer;
411 LOG(VB_HTTP, LOG_DEBUG, QString(
"Reponse Compressed Content Length: %1").arg(compBuffer.buffer().length()));
419 nContentLen = pBuffer->buffer().length();
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() );
427 if (nBytes < sHeader.length())
429 LOG( VB_HTTP, LOG_ERR, QString(
"HttpRequest::SendResponse(): "
430 "Incomplete write of header, "
432 .arg(nBytes).arg(sHeader.length()));
442 qint64 bytesWritten =
SendData( pBuffer, 0, nContentLen );
445 if (bytesWritten != nContentLen)
446 LOG(VB_HTTP, LOG_ERR,
"HttpRequest::SendResponse(): Error occurred while writing response body.");
448 nBytes += bytesWritten;
461 long long llSize = 0;
462 long long llStart = 0;
465 LOG(VB_HTTP, LOG_INFO, QString(
"SendResponseFile ( %1 )").arg(sFileName));
470 QFile tmpFile( sFileName );
471 if (tmpFile.exists( ) && tmpFile.open( QIODevice::ReadOnly ))
480 llSize = llEnd = tmpFile.size( );
491 if (!sRange.isEmpty())
493 bRange =
ParseRange( sRange, llSize, &llStart, &llEnd );
500 if ((llSize > llStart) && (llSize > llEnd) && (llEnd > llStart))
509 llSize = (llEnd - llStart) + 1;
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)
542 LOG(VB_HTTP, LOG_INFO,
543 QString(
"HTTPRequest::SendResponseFile(%1) - cannot find file!")
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() );
560 if (nBytes < sHeader.length())
562 LOG( VB_HTTP, LOG_ERR, QString(
"HttpRequest::SendResponseFile(): "
563 "Incomplete write of header, "
565 .arg(nBytes).arg(sHeader.length()));
573 LOG(VB_HTTP, LOG_DEBUG,
574 QString(
"SendResponseFile : size = %1, start = %2, end = %3")
575 .arg(llSize).arg(llStart).arg(llEnd));
579 long long sent =
SendFile( tmpFile, llStart, llSize );
583 LOG(VB_HTTP, LOG_INFO,
584 QString(
"SendResponseFile( %1 ) Error: %2 [%3]" )
585 .arg(sFileName) .arg(errno) .arg(strerror(errno)));
605 bool bShouldClose =
false;
608 if (!pDevice->isOpen())
610 pDevice->open( QIODevice::ReadOnly );
618 if ( !pDevice->seek( llStart ))
621 std::array<char,SENDFILE_BUFFER_SIZE> aBuffer {};
623 qint64 llBytesRemaining = llBytes;
624 qint64 llBytesToRead = 0;
625 qint64 llBytesRead = 0;
627 while ((sent < llBytes) && !pDevice->atEnd())
630 llBytesRead = pDevice->read( aBuffer.data(), llBytesToRead );
631 if ( llBytesRead != -1 )
633 if (
WriteBlock( aBuffer.data(), llBytesRead ) == -1)
639 llBytesRemaining -= llBytesRead;
655 qint64 sent =
SendData( (QIODevice *)(&
file), llStart, llBytes );
666 const QString &sFaultString,
667 const QString &sDetails )
674 stream << R
"(<?xml version="1.0" encoding="utf-8"?>)";
676 QString sWhere = ( bServerError ) ? "s:Server" :
"s:Client";
684 <<
"<faultcode>" << sWhere <<
"</faultcode>"
685 <<
"<faultstring>" << sFaultString <<
"</faultstring>";
688 if (!sDetails.isEmpty())
690 stream <<
"<detail>" << sDetails <<
"</detail>";
727 stream <<
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n";
734 <<
"<u:" <<
m_sMethod <<
"Response xmlns:u=\""
739 stream <<
"<" <<
m_sMethod <<
"Response>\r\n";
742 for (
const auto & arg : std::as_const(
args))
744 stream <<
"<" << arg.m_sName;
746 if (arg.m_pAttributes)
748 for (
const auto & attr : std::as_const(*arg.m_pAttributes))
750 stream <<
" " << attr.m_sName <<
"='"
751 <<
Encode( attr.m_sValue ) <<
"'";
758 stream <<
Encode( arg.m_sValue );
760 stream << arg.m_sValue;
762 stream <<
"</" << arg.m_sName <<
">\r\n";
767 stream <<
"</u:" <<
m_sMethod <<
"Response>\r\n"
772 stream <<
"</" <<
m_sMethod <<
"Response>\r\n";
805#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
806 ims.setTimeSpec(Qt::UTC);
808 ims.setTimeZone(QTimeZone(QTimeZone::UTC));
810 if (ims.isValid() && ims <=
file.lastModified())
831 LOG(VB_HTTP, LOG_INFO,
832 QString(
"HTTPRequest::FormatFileResponse('%1') - cannot find file")
843 m_sProtocol = sLine.section(
'/', 0, 0 ).trimmed();
844 QString sVersion = sLine.section(
'/', 1 ).trimmed();
846 m_nMajor = sVersion.section(
'.', 0, 0 ).toInt();
847 m_nMinor = sVersion.section(
'.', 1 ).toInt();
888 if ((sType ==
"application/x-www-form-urlencoded" ) ||
889 (sType.startsWith(
"application/x-www-form-urlencoded;")))
892 if ((sType ==
"text/xml" ) ||
893 (sType.startsWith(
"text/xml;") ))
896 if ((sType ==
"application/json") ||
897 sType.startsWith(
"application/json;"))
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" );
942 case 428:
return(
"428 Precondition Required" );
943 case 429:
return(
"429 Too Many Requests" );
944 case 431:
return(
"431 Request Header Fields Too Large" );
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" );
984 return(
"text/plain" );
997 ext =
type.pszExtension;
999 if ( sFileExtension.compare(ext, Qt::CaseInsensitive) == 0 )
1000 return(
type.pszType );
1003 return(
"text/plain" );
1012 QStringList mimeTypes;
1016 if (!mimeTypes.contains(
type.pszType ))
1017 mimeTypes.append(
type.pszType );
1029 QFileInfo
info( sFileName );
1030 QString sLOC =
"HTTPRequest::TestMimeType(" + sFileName +
") - ";
1031 QString sSuffix =
info.suffix().toLower();
1034 if ( sSuffix ==
"nuv" )
1037 QFile
file( sFileName );
1039 if (
file.open(QIODevice::ReadOnly | QIODevice::Text) )
1041 QByteArray head =
file.read(8);
1042 QString sHex = head.toHex();
1044 LOG(VB_HTTP, LOG_DEBUG, sLOC +
"file starts with " + sHex);
1046 if ( sHex ==
"000001ba44000400" )
1047 sMIME =
"video/mp2p";
1049 if ( head ==
"MythTVVi" )
1052 head =
file.read(4);
1054 if ( head ==
"DIVX" )
1056 LOG(VB_HTTP, LOG_DEBUG, sLOC +
"('MythTVVi...DIVXLAME')");
1057 sMIME =
"video/mp4";
1068 LOG(VB_GENERAL, LOG_ERR, sLOC +
"Could not read file");
1072 LOG(VB_HTTP, LOG_INFO, sLOC +
"type is " + sMIME);
1084 LOG(VB_HTTP, LOG_INFO, QString(
"sParams: '%1'").arg(sParams));
1089 sParams.replace(
"&",
"&" );
1091 if (!sParams.isEmpty())
1093 QStringList params = sParams.split(
'&', Qt::SkipEmptyParts);
1094 for (
const auto & param : std::as_const(params))
1096 QString sName = param.section(
'=', 0, 0 );
1097 QString sValue = param.section(
'=', 1 );
1098 sValue.replace(
"+",
" ");
1100 if (!sName.isEmpty())
1102 sName = QUrl::fromPercentEncoding(sName.toUtf8());
1103 sValue = QUrl::fromPercentEncoding(sValue.toUtf8());
1105 mapParams.insert( sName.trimmed(), sValue );
1142 sHeader += it.key() +
": ";
1143 sHeader += *it +
"\r\n";
1159 bool bKeepAlive =
true;
1168 QString sConnection =
GetRequestHeader(
"connection",
"default" ).toLower();
1170 QStringList sValueList = sConnection.split(
",");
1172 if ( sValueList.contains(
"close") )
1174 LOG(VB_HTTP, LOG_DEBUG,
"Client requested the connection be closed");
1177 else if (sValueList.contains(
"keep-alive"))
1191 QStringList sCookieList =
m_mapHeaders.values(
"cookie");
1193 QStringList::iterator it;
1194 for (it = sCookieList.begin(); it != sCookieList.end(); ++it)
1196 QString key = (*it).section(
'=', 0, 0);
1197 QString value = (*it).section(
'=', 1);
1209 bool bSuccess =
false;
1214 QString sRequestLine =
ReadLine( 2s );
1216 if ( sRequestLine.isEmpty() )
1218 LOG(VB_GENERAL, LOG_ERR,
"Timeout reading first line of request." );
1250 while (( !sLine.isEmpty() ) && !bDone )
1252 if (sLine !=
"\r\n")
1254 QString sName = sLine.section(
':', 0, 0 ).trimmed();
1255 QString sValue = sLine.section(
':', 1 );
1257 sValue.truncate( sValue.length() - 2 );
1259 if (!sName.isEmpty() && !sValue.isEmpty())
1261 m_mapHeaders.insert(sName.toLower(), sValue.trimmed());
1275 LOG(VB_HTTP, LOG_INFO, QString(
"(Request Header) %1: %2")
1276 .arg(it.key(), *it));
1288 LOG(VB_GENERAL, LOG_INFO,
"Timeout waiting for request header." );
1342 long nPayloadSize =
GetLastHeader(
"content-length" ).toLong();
1344 if (nPayloadSize > 0)
1346 char *pszPayload =
new char[ nPayloadSize + 2 ];
1349 nBytes =
ReadBlock( pszPayload, nPayloadSize, 5s );
1350 if (nBytes == nPayloadSize )
1352 m_sPayload = QString::fromUtf8( pszPayload, nPayloadSize );
1362 LOG(VB_GENERAL, LOG_ERR,
1363 QString(
"Unable to read entire payload (read %1 of %2 bytes)")
1364 .arg( nBytes ) .arg( nPayloadSize ) );
1368 delete [] pszPayload;
1374 if (!sSOAPAction.isEmpty())
1381 LOG(VB_HTTP, LOG_DEBUG,
1382 QString(
"HTTPRequest::ParseRequest - Socket (%1) Base (%2) "
1383 "Method (%3) - Bytes in Socket Buffer (%4)")
1385 .arg(
m_sMethod) .arg(BytesAvailable()));
1390 LOG(VB_GENERAL, LOG_WARNING,
1391 "Unexpected exception in HTTPRequest::ParseRequest" );
1406 int nCount = tokens.count();
1410 if ( sLine.startsWith( QString(
"HTTP/") ))
1434 m_sRequestUrl = QUrl::fromPercentEncoding(tokens[1].toUtf8());
1440 QString sQueryStr = tokens[1].section(
'?', 1, 1 );
1442 if (!sQueryStr.isEmpty())
1470 long long *pllStart,
1478 if (sRange.isEmpty())
1490 sRange.remove( 0, nIdx );
1496 QStringList ranges = sRange.split(
',', Qt::SkipEmptyParts);
1497 if (ranges.count() == 0)
1504 QStringList parts = ranges[0].split(
'-');
1506 if (parts.count() != 2)
1509 if (parts[0].isEmpty() && parts[1].isEmpty())
1516 bool conv_ok =
false;
1517 if (parts[0].isEmpty())
1523 long long llValue = parts[1].toLongLong(&conv_ok);
1524 if (!conv_ok)
return false;
1526 *pllStart = llSize - llValue;
1527 *pllEnd = llSize - 1;
1529 else if (parts[1].isEmpty())
1535 *pllStart = parts[0].toLongLong(&conv_ok);
1540 *pllEnd = llSize - 1;
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;
1553 if (*pllStart > *pllEnd)
1557 LOG(VB_HTTP, LOG_DEBUG, QString(
"%1 Range Requested %2 - %3")
1572 static const QRegularExpression re {
"^http[s]?://.*?/"};
1575 QStringList sList =
m_sBaseUrl.split(
'/', Qt::SkipEmptyParts);
1578 if (!sList.isEmpty())
1585 LOG(VB_HTTP, LOG_INFO, QString(
"ExtractMethodFromURL(end) : %1 : %2")
1595 bool bSuccess =
false;
1601 LOG(VB_HTTP, LOG_INFO,
1602 QString(
"HTTPRequest::ProcessSOAPPayload : %1 : ").arg(sSOAPAction));
1603 QDomDocument doc (
"request" );
1605#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
1610 if (!doc.setContent(
m_sPayload,
true, &sErrMsg, &nErrLine, &nErrCol ))
1612 LOG(VB_GENERAL, LOG_ERR,
1613 QString(
"Error parsing request at line: %1 column: %2 : %3" )
1614 .arg(nErrLine) .arg(nErrCol) .arg(sErrMsg));
1618 auto parseResult =doc.setContent(
m_sPayload,
1619 QDomDocument::ParseOption::UseNamespaceProcessing );
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));
1636 if (sSOAPAction.contains(
'#' ))
1638 m_sNameSpace = sSOAPAction.section(
'#', 0, 0).remove( 0, 1);
1639 m_sMethod = sSOAPAction.section(
'#', 1 );
1644 if (sSOAPAction.contains(
'/' ))
1646 int nPos = sSOAPAction.lastIndexOf(
'/' );
1649 sSOAPAction.length() - nPos - 2);
1666 if (oNodeList.count() == 0)
1669 doc.elementsByTagNameNS(
"http://schemas.xmlsoap.org/soap/envelope/",
1673 if (oNodeList.count() > 0)
1675 QDomNode oMethod = oNodeList.item(0);
1677 if (!oMethod.isNull())
1681 for ( QDomNode oNode = oMethod.firstChild(); !oNode.isNull();
1682 oNode = oNode.nextSibling() )
1684 QDomElement e = oNode.toElement();
1688 QString sName = e.tagName();
1689 QString sValue =
"";
1691 QDomText oText = oNode.firstChild().toText();
1693 if (!oText.isNull())
1694 sValue = oText.nodeValue();
1696 sName = QUrl::fromPercentEncoding(sName.toUtf8());
1697 sValue = QUrl::fromPercentEncoding(sValue.toUtf8());
1699 m_mapParams.insert( sName.trimmed().toLower(), sValue );
1727 if (sAccept.contains(
"application/json", Qt::CaseInsensitive ) ||
1728 sAccept.contains(
"text/javascript", Qt::CaseInsensitive ))
1733 else if (sAccept.contains(
"text/x-apple-plist+xml", Qt::CaseInsensitive ))
1741 if (pSerializer ==
nullptr)
1755 LOG(VB_HTTP, LOG_DEBUG,
1756 QString(
"HTTPRequest::Encode Input : %1").arg(sStr));
1758 sStr.replace(
'&',
"&" );
1759 sStr.replace(
'<',
"<" );
1760 sStr.replace(
'>',
">" );
1761 sStr.replace(
'"',
""");
1762 sStr.replace(
"'",
"'");
1765 LOG(VB_HTTP, LOG_DEBUG,
1766 QString(
"HTTPRequest::Encode Output : %1").arg(sStr));
1778 sStr.replace(
"&",
"&");
1779 sStr.replace(
"<",
"<");
1780 sStr.replace(
">",
">");
1781 sStr.replace(
""",
"\"");
1782 sStr.replace(
"'",
"'");
1793 QByteArray hash = QCryptographicHash::hash( data.data(), QCryptographicHash::Sha1);
1795 return (
"\"" + hash.toHex() +
"\"");
1806 QStringList oList = sProtected.split(
';' );
1808 for(
int nIdx = 0; nIdx < oList.count(); nIdx++)
1810 if (sBaseUrl.startsWith( oList[nIdx], Qt::CaseInsensitive ))
1826 QString realm =
"MythTV";
1834 QString stale = isStale ?
"true" :
"false";
1835 authHeader = QString(
"Digest realm=\"%1\",nonce=\"%2\","
1836 "qop=\"auth\",stale=\"%3\",algorithm=\"MD5\"")
1837 .arg(realm, nonce, stale);
1841 authHeader = QString(
"Basic realm=\"%1\"").arg(realm);
1854 QString hash = QCryptographicHash::hash( uniqueID.toLatin1(), QCryptographicHash::Sha1).toHex();
1855 QString nonce = QString(
"%1%2").arg(timeStamp, hash);
1865 LOG(VB_HTTP, LOG_NOTICE,
"Attempting HTTP Basic Authentication");
1866 QStringList oList =
GetLastHeader(
"authorization" ).split(
' ' );
1870 LOG(VB_GENERAL, LOG_WARNING,
"Basic authentication is only allowed for HTTP 1.0");
1874 QString sCredentials = QByteArray::fromBase64( oList[1].toUtf8() );
1876 oList = sCredentials.split(
':' );
1878 if (oList.count() < 2)
1880 LOG(VB_GENERAL, LOG_WARNING,
"Authorization attempt with invalid number of tokens");
1884 QString sUsername = oList[0];
1885 QString sPassword = oList[1];
1887 if (sUsername ==
"nouser")
1893 LOG(VB_GENERAL, LOG_WARNING,
"Authorization attempt with invalid username");
1897 QString client = QString(
"WebFrontend_%1").arg(
GetPeerAddress());
1903 LOG(VB_GENERAL, LOG_WARNING,
"Authorization attempt with invalid password");
1907 LOG(VB_HTTP, LOG_NOTICE,
"Valid Authorization received");
1924 LOG(VB_HTTP, LOG_NOTICE,
"Attempting HTTP Digest Authentication");
1925 QString realm =
"MythTV";
1927 QString authMethod =
GetLastHeader(
"authorization" ).section(
' ', 0, 0).toLower();
1929 if (authMethod !=
"digest")
1931 LOG(VB_GENERAL, LOG_WARNING,
"Invalid method in Authorization header");
1935 QString parameterStr =
GetLastHeader(
"authorization" ).section(
' ', 1);
1937 QMap<QString, QString> paramMap;
1938 QStringList paramList = parameterStr.split(
',');
1939 QStringList::iterator it;
1940 for (it = paramList.begin(); it != paramList.end(); ++it)
1942 QString key = (*it).section(
'=', 0, 0).toLower().trimmed();
1944 QString value = (*it).section(
'=', 1).trimmed();
1947 paramMap[key] = value;
1950 if (paramMap.size() < 8)
1952 LOG(VB_GENERAL, LOG_WARNING,
"Invalid number of parameters in Authorization header");
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())
1961 LOG(VB_GENERAL, LOG_WARNING,
"Missing required parameters in Authorization header");
1965 if (paramMap[
"username"] ==
"nouser")
1970 LOG(VB_GENERAL, LOG_WARNING,
"Authorization URI doesn't match the "
1976 if (paramMap[
"realm"] != realm)
1978 LOG(VB_GENERAL, LOG_WARNING,
"Authorization realm doesn't match the "
1979 "realm of the requested content");
1983 QByteArray nonce = paramMap[
"nonce"].toLatin1();
1984 if (nonce.length() < 20)
1986 LOG(VB_GENERAL, LOG_WARNING,
"Authorization nonce is too short");
1990 QString nonceTimeStampStr = nonce.left(20);
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),
1999 constexpr std::chrono::seconds AUTH_TIMEOUT { 2min };
2001 if (!nonceTimeStamp.isValid())
2003 LOG(VB_GENERAL, LOG_WARNING,
"Authorization nonce timestamp is invalid.");
2004 LOG(VB_HTTP, LOG_DEBUG, QString(
"Timestamp was '%1'").arg(nonceTimeStampStr));
2010 LOG(VB_HTTP, LOG_NOTICE,
"Authorization nonce timestamp is invalid or too old.");
2021 LOG(VB_GENERAL, LOG_WARNING,
"Authorization attempt with invalid username");
2025 if (paramMap[
"response"].length() != 32)
2027 LOG(VB_GENERAL, LOG_WARNING,
"Authorization response field is invalid length");
2035 QString methodDigest = QString(
"%1:%2").arg(
GetRequestType(), paramMap[
"uri"]);
2036 QByteArray a2 = QCryptographicHash::hash(methodDigest.toLatin1(),
2037 QCryptographicHash::Md5).toHex();
2039 QString responseDigest = QString(
"%1:%2:%3:%4:%5:%6").arg(a1,
2045 QByteArray kd = QCryptographicHash::hash(responseDigest.toLatin1(),
2046 QCryptographicHash::Md5).toHex();
2048 if (paramMap[
"response"].toLatin1() == kd)
2050 LOG(VB_HTTP, LOG_NOTICE,
"Valid Authorization received");
2051 QString client = QString(
"WebFrontend_%1").arg(
GetPeerAddress());
2057 LOG(VB_GENERAL, LOG_ERR,
"Valid Authorization received, but we "
2058 "failed to create a valid session");
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)));
2088 QStringList oList =
GetLastHeader(
"authorization" ).split(
' ' );
2090 if (oList.count() < 2)
2093 if (oList[0].compare(
"basic", Qt::CaseInsensitive ) == 0)
2095 if (oList[0].compare(
"digest", Qt::CaseInsensitive ) == 0)
2119 const QDateTime &expiryDate,
bool secure)
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));
2129 QStringList cookieAttributes;
2132 cookieAttributes.append(QString(
"%1=%2").arg(sKey, sValue));
2138 cookieAttributes.append(
"Path=/");
2142 cookieAttributes.append(QString(
"Expires=%1").arg(expires));
2147 cookieAttributes.append(
"Secure");
2150 cookieAttributes.append(
"HttpOnly");
2172 return hostname.section(
"]:", 0 , 0);
2176 return hostname.section(
":", 0 , 0);
2215 type =
"UNSUBSCRIBE";
2253 QStringList allowedOrigins;
2257 serverStatusPort + 10);
2259 QString masterAddrPort = QString(
"%1:%2")
2261 .arg(serverStatusPort);
2262 QString masterTLSAddrPort = QString(
"%1:%2")
2264 .arg(backendSSLPort);
2266 allowedOrigins << QString(
"http://%1").arg(masterAddrPort);
2267 allowedOrigins << QString(
"https://%2").arg(masterTLSAddrPort);
2269 QString localhostname = QHostInfo::localHostName();
2270 if (!localhostname.isEmpty())
2272 allowedOrigins << QString(
"http://%1:%2")
2273 .arg(localhostname).arg(serverStatusPort);
2274 allowedOrigins << QString(
"https://%1:%2")
2275 .arg(localhostname).arg(backendSSLPort);
2278 QStringList allowedOriginsList =
2280 "https://chromecast.mythtv.org")).split(
",");
2282 for (
const auto & origin : std::as_const(allowedOriginsList))
2284 if (origin.isEmpty())
2287 if (origin ==
"*" || (!origin.startsWith(
"http://") &&
2288 !origin.startsWith(
"https://")))
2290 LOG(VB_GENERAL, LOG_ERR, QString(
"Illegal AllowedOriginsList"
2291 " entry '%1'. Must start with http[s]:// and not be *")
2296 allowedOrigins << origin;
2302 for (
const auto & origin : std::as_const(allowedOrigins))
2303 LOG(VB_HTTP, LOG_DEBUG, QString(
"Will allow Origin: %1").arg(origin));
2306 if (allowedOrigins.contains(sOrigin))
2311 LOG(VB_HTTP, LOG_DEBUG, QString(
"Allow-Origin: %1)").arg(sOrigin));
2315 LOG(VB_GENERAL, LOG_CRIT, QString(
"HTTPRequest: Cross-origin request "
2316 "received with origin (%1)")
2334 m_pSocket->state() == QAbstractSocket::ConnectedState)
2343 if ( timer.
elapsed() >= msecs )
2346 LOG(VB_HTTP, LOG_INFO,
"BufferedSocketDeviceRequest::ReadLine() - Exceeded Total Elapsed Wait Time." );
2362 std::chrono::milliseconds msecs)
2365 m_pSocket->state() == QAbstractSocket::ConnectedState)
2368 return(
m_pSocket->read( pData, nMaxLen ));
2370 bool bTimeout =
false;
2373 while ( (
m_pSocket->bytesAvailable() < (
int)nMaxLen) && !bTimeout )
2375 bTimeout = !(
m_pSocket->waitForReadyRead( msecs.count() ));
2377 if ( timer.
elapsed() >= msecs )
2380 LOG(VB_HTTP, LOG_INFO,
"BufferedSocketDeviceRequest::ReadBlock() - Exceeded Total Elapsed Wait Time." );
2386 return(
m_pSocket->read( pData, nMaxLen ));
2398 qint64 bytesWritten = -1;
2400 m_pSocket->state() == QAbstractSocket::ConnectedState)
2402 bytesWritten =
m_pSocket->write( pData, nLen );
2406 return( bytesWritten );
2415 return(
m_pSocket->localAddress().toString() );
2434 return(
m_pSocket->peerAddress().toString() );
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)
QString GetAuthenticationHeader(bool isStale=false)
QString BuildResponseHeader(long long nSize)
HttpResponseType m_eResponseType
static QString GetETagHash(const QByteArray &data)
virtual QString GetHostAddress()=0
qint64 SendResponse(void)
QString m_sResponseTypeText
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
bool BasicAuthentication()
QRegularExpression m_parseRangeExp
static QStringList GetSupportedMimeTypes()
QString GetRequestHeader(const QString &sKey, const QString &sDefault)
void FormatFileResponse(const QString &sFileName)
static QString GetResponseProtocol()
static QString TestMimeType(const QString &sFileName)
QString GetResponseType(void) const
void ProcessRequestLine(const QString &sLine)
Serializer * GetSerializer()
static bool IsUrlProtected(const QString &sBaseUrl)
virtual int getSocketHandle()=0
MythUserSession m_userSession
qint64 SendFile(QFile &file, qint64 llStart, qint64 llBytes)
void ExtractMethodFromURL()
std::chrono::seconds m_nKeepAliveTimeout
QString GetResponseStatus(void) const
QStringMultiMap m_mapHeaders
static QString GetMimeType(const QString &sFileExtension)
HttpContentType SetContentType(const QString &sType)
virtual QString GetHostName()
bool DigestAuthentication()
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
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
bool ParseKeepAlive(void)
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
QString GetRequestType() const
qint64 SendData(QIODevice *pDevice, qint64 llStart, qint64 llBytes)
qint64 SendResponseFile(const QString &sFileName)
virtual QString GetPeerAddress()=0
static QString Decode(const QString &sIn)
QString GetResponseHeaders(void)
QRegularExpression m_procReqLineExp
static QString Encode(const QString &sIn)
static QString GetServerVersion(void)
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.
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.
std::chrono::milliseconds elapsed(void)
Returns milliseconds elapsed since last start() or restart()
void start(void)
starts measuring elapsed time.
bool IsValid(void) const
Check if this session object appears properly constructed, it DOES NOT validate whether it is a valid...
QString GetSessionToken(void) const
QDateTime GetSessionExpires() const
virtual void AddHeaders(QStringMap &headers)
virtual QString GetContentType()=0
QString GetValue(const QString &setting)
static std::array< const MIMETypes, 66 > g_MIMETypes
static constexpr size_t SENDFILE_BUFFER_SIZE
static QString StaticPage
static constexpr const char * SOAP_ENVELOPE_END
static constexpr const char * SOAP_ENVELOPE_BEGIN
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
static bool VERBOSE_LEVEL_CHECK(uint64_t mask, LogLevel_t level)
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
QString current_iso_string(bool stripped)
Returns current Date and Time in UTC as a string.
std::chrono::seconds secsInPast(const QDateTime &past)
QString toString(const QDateTime &raw_dt, uint format)
Returns formatted string representing the time.
@ kOverrideUTC
Present date/time in UTC.
@ kRFC822
HTTP Date format.
QDateTime fromString(const QString &dtstr)
Converts kFilename && kISODate formats to QDateTime.
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
QByteArray gzipCompress(const QByteArray &data)
QMap< QString, QString > QStringMap