19#include <QCryptographicHash>
34#include <netinet/tcp.h>
43#include "libmythbase/mythversion.h"
60 {
"gif" ,
"image/gif" },
61 {
"ico" ,
"image/x-icon" },
62 {
"jpeg",
"image/jpeg" },
63 {
"jpg" ,
"image/jpeg" },
64 {
"mng" ,
"image/x-mng" },
65 {
"png" ,
"image/png" },
66 {
"svg" ,
"image/svg+xml" },
67 {
"svgz",
"image/svg+xml" },
68 {
"tif" ,
"image/tiff" },
69 {
"tiff",
"image/tiff" },
71 {
"htm" ,
"text/html" },
72 {
"html",
"text/html" },
73 {
"qsp" ,
"text/html" },
74 {
"txt" ,
"text/plain" },
75 {
"xml" ,
"text/xml" },
76 {
"qxml",
"text/xml" },
77 {
"xslt",
"text/xml" },
78 {
"css" ,
"text/css" },
80 {
"crt" ,
"application/x-x509-ca-cert" },
81 {
"doc" ,
"application/vnd.ms-word" },
82 {
"gz" ,
"application/x-tar" },
83 {
"js" ,
"application/javascript" },
84 {
"m3u" ,
"application/x-mpegurl" },
85 {
"m3u8",
"application/x-mpegurl" },
86 {
"ogx" ,
"application/ogg" },
87 {
"pdf" ,
"application/pdf" },
88 {
"pem" ,
"application/x-x509-ca-cert" },
89 {
"qjs" ,
"application/javascript" },
90 {
"rm" ,
"application/vnd.rn-realmedia" },
91 {
"swf" ,
"application/x-shockwave-flash" },
92 {
"xls" ,
"application/vnd.ms-excel" },
93 {
"zip" ,
"application/x-tar" },
95 {
"aac" ,
"audio/mp4" },
96 {
"ac3" ,
"audio/vnd.dolby.dd-raw" },
97 {
"flac",
"audio/x-flac" },
98 {
"m4a" ,
"audio/x-m4a" },
99 {
"mid" ,
"audio/midi" },
100 {
"mka" ,
"audio/x-matroska" },
101 {
"mp3" ,
"audio/mpeg" },
102 {
"oga" ,
"audio/ogg" },
103 {
"ogg" ,
"audio/ogg" },
104 {
"wav" ,
"audio/wav" },
105 {
"wma" ,
"audio/x-ms-wma" },
107 {
"3gp" ,
"video/3gpp" },
108 {
"3g2" ,
"video/3gpp2" },
109 {
"asx" ,
"video/x-ms-asf" },
110 {
"asf" ,
"video/x-ms-asf" },
111 {
"avi" ,
"video/x-msvideo" },
112 {
"m2p" ,
"video/mp2p" },
113 {
"m4v" ,
"video/mp4" },
114 {
"mpeg",
"video/mp2p" },
115 {
"mpeg2",
"video/mp2p" },
116 {
"mpg" ,
"video/mp2p" },
117 {
"mpg2",
"video/mp2p" },
118 {
"mov" ,
"video/quicktime" },
119 {
"mp4" ,
"video/mp4" },
120 {
"mkv" ,
"video/x-matroska" },
121 {
"nuv" ,
"video/nupplevideo" },
122 {
"ogv" ,
"video/ogg" },
123 {
"ps" ,
"video/mp2p" },
124 {
"ts" ,
"video/mp2t" },
125 {
"vob" ,
"video/mpeg" },
126 {
"wmv" ,
"video/x-ms-wmv" },
128 {
"ttf" ,
"font/ttf" },
129 {
"woff" ,
"font/woff" },
130 {
"woff2",
"font/woff2" }
143 "<TITLE>Error %1</TITLE>"
144 "<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=ISO-8859-1\">"
146 "<BODY><H1>%2.</H1></BODY>"
158 if (!values.isEmpty())
159 return values.last();
183 LOG(VB_HTTP, LOG_INFO,
184 QString(
"HTTPRequest::SentRequestType( %1 ) - returning Unknown.")
257 if (sTransferMode.isEmpty())
261 sTransferMode =
"Streaming";
263 sTransferMode =
"Interactive";
266 if (sTransferMode ==
"Streaming")
268 else if (sTransferMode ==
"Background")
270 else if (sTransferMode ==
"Interactive")
275 SetResponseHeader(
"contentFeatures.dlna.org",
"DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01500000000000000000000000000000");
279 for (
const auto & value : std::as_const(values))
282 if (qEnvironmentVariableIsSet(
"HTTPREQUEST_DEBUG"))
285 QMap<QString, QString>::iterator it;
288 LOG(VB_HTTP, LOG_INFO, QString(
"(Response Header) %1: %2").arg(it.key(), it.value()));
311 LOG(VB_HTTP, LOG_INFO,
312 QString(
"HTTPRequest::SendResponse( None ) :%1 -> %2:")
326 if (
file.exists() &&
file.size() < (2LL * 1024 * 1024) &&
327 file.open(QIODevice::ReadOnly | QIODevice::Text))
338 LOG(VB_HTTP, LOG_INFO,
339 QString(
"HTTPRequest::SendResponse( File ) :%1 -> %2:")
348 LOG(VB_HTTP, LOG_INFO,
349 QString(
"HTTPRequest::SendResponse(xml/html) (%1) :%2 -> %3: %4")
361 LOG(VB_HTTP, LOG_INFO,
362 QString(
"HTTPRequest::SendResponse(%1) - Cached")
374 int nContentLen =
m_response.buffer().length();
380 if (qEnvironmentVariableIsSet(
"HTTPREQUEST_DEBUG"))
381 std::cout <<
m_response.buffer().constData() << std::endl;
384 LOG(VB_HTTP, LOG_DEBUG, QString(
"Reponse Content Length: %1").arg(nContentLen));
393 bool gzip_found = std::any_of(values.cbegin(), values.cend(),
394 [](
const auto & value)
395 {return value.contains(
"gzip" ); });
397 if (( nContentLen > 0 ) && gzip_found)
400 compBuffer.setData( compressed );
402 if (!compBuffer.buffer().isEmpty())
404 pBuffer = &compBuffer;
407 LOG(VB_HTTP, LOG_DEBUG, QString(
"Reponse Compressed Content Length: %1").arg(compBuffer.buffer().length()));
415 nContentLen = pBuffer->buffer().length();
419 QByteArray sHeader = rHeader.toUtf8();
420 LOG(VB_HTTP, LOG_DEBUG, QString(
"Response header size: %1 bytes").arg(sHeader.length()));
421 nBytes =
WriteBlock( sHeader.constData(), sHeader.length() );
423 if (nBytes < sHeader.length())
425 LOG( VB_HTTP, LOG_ERR, QString(
"HttpRequest::SendResponse(): "
426 "Incomplete write of header, "
428 .arg(nBytes).arg(sHeader.length()));
438 qint64 bytesWritten =
SendData( pBuffer, 0, nContentLen );
441 if (bytesWritten != nContentLen)
442 LOG(VB_HTTP, LOG_ERR,
"HttpRequest::SendResponse(): Error occurred while writing response body.");
444 nBytes += bytesWritten;
457 long long llSize = 0;
458 long long llStart = 0;
461 LOG(VB_HTTP, LOG_INFO, QString(
"SendResponseFile ( %1 )").arg(sFileName));
466 QFile tmpFile( sFileName );
467 if (tmpFile.exists( ) && tmpFile.open( QIODevice::ReadOnly ))
476 llSize = llEnd = tmpFile.size( );
487 if (!sRange.isEmpty())
489 bRange =
ParseRange( sRange, llSize, &llStart, &llEnd );
496 if ((llSize > llStart) && (llSize > llEnd) && (llEnd > llStart))
505 llSize = (llEnd - llStart) + 1;
517 LOG(VB_HTTP, LOG_INFO,
518 QString(
"HTTPRequest::SendResponseFile(%1) - "
519 "invalid byte range %2-%3/%4")
520 .arg(sFileName) .arg(llStart) .arg(llEnd)
538 LOG(VB_HTTP, LOG_INFO,
539 QString(
"HTTPRequest::SendResponseFile(%1) - cannot find file!")
552 QByteArray sHeader = rHeader.toUtf8();
553 LOG(VB_HTTP, LOG_DEBUG, QString(
"Response header size: %1 bytes").arg(sHeader.length()));
554 nBytes =
WriteBlock( sHeader.constData(), sHeader.length() );
556 if (nBytes < sHeader.length())
558 LOG( VB_HTTP, LOG_ERR, QString(
"HttpRequest::SendResponseFile(): "
559 "Incomplete write of header, "
561 .arg(nBytes).arg(sHeader.length()));
569 LOG(VB_HTTP, LOG_DEBUG,
570 QString(
"SendResponseFile : size = %1, start = %2, end = %3")
571 .arg(llSize).arg(llStart).arg(llEnd));
575 long long sent =
SendFile( tmpFile, llStart, llSize );
579 LOG(VB_HTTP, LOG_INFO,
580 QString(
"SendResponseFile( %1 ) Error: %2 [%3]" )
581 .arg(sFileName) .arg(errno) .arg(strerror(errno)));
601 bool bShouldClose =
false;
604 if (!pDevice->isOpen())
606 pDevice->open( QIODevice::ReadOnly );
614 if ( !pDevice->seek( llStart ))
617 std::array<char,SENDFILE_BUFFER_SIZE> aBuffer {};
619 qint64 llBytesRemaining = llBytes;
620 qint64 llBytesToRead = 0;
621 qint64 llBytesRead = 0;
623 while ((sent < llBytes) && !pDevice->atEnd())
626 llBytesRead = pDevice->read( aBuffer.data(), llBytesToRead );
627 if ( llBytesRead != -1 )
629 if (
WriteBlock( aBuffer.data(), llBytesRead ) == -1)
635 llBytesRemaining -= llBytesRead;
651 qint64 sent =
SendData( (QIODevice *)(&
file), llStart, llBytes );
662 const QString &sFaultString,
663 const QString &sDetails )
670 stream << R
"(<?xml version="1.0" encoding="utf-8"?>)";
672 QString sWhere = ( bServerError ) ? "s:Server" :
"s:Client";
680 <<
"<faultcode>" << sWhere <<
"</faultcode>"
681 <<
"<faultstring>" << sFaultString <<
"</faultstring>";
684 if (!sDetails.isEmpty())
686 stream <<
"<detail>" << sDetails <<
"</detail>";
723 stream <<
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n";
730 <<
"<u:" <<
m_sMethod <<
"Response xmlns:u=\""
735 stream <<
"<" <<
m_sMethod <<
"Response>\r\n";
738 for (
const auto & arg : std::as_const(
args))
740 stream <<
"<" << arg.m_sName;
742 if (arg.m_pAttributes)
744 for (
const auto & attr : std::as_const(*arg.m_pAttributes))
746 stream <<
" " << attr.m_sName <<
"='"
747 <<
Encode( attr.m_sValue ) <<
"'";
754 stream <<
Encode( arg.m_sValue );
756 stream << arg.m_sValue;
758 stream <<
"</" << arg.m_sName <<
">\r\n";
763 stream <<
"</u:" <<
m_sMethod <<
"Response>\r\n"
768 stream <<
"</" <<
m_sMethod <<
"Response>\r\n";
801#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
802 ims.setTimeSpec(Qt::UTC);
804 ims.setTimeZone(QTimeZone(QTimeZone::UTC));
806 if (ims.isValid() && ims <=
file.lastModified())
827 LOG(VB_HTTP, LOG_INFO,
828 QString(
"HTTPRequest::FormatFileResponse('%1') - cannot find file")
839 m_sProtocol = sLine.section(
'/', 0, 0 ).trimmed();
840 QString sVersion = sLine.section(
'/', 1 ).trimmed();
842 m_nMajor = sVersion.section(
'.', 0, 0 ).toInt();
843 m_nMinor = sVersion.section(
'.', 1 ).toInt();
884 if ((sType ==
"application/x-www-form-urlencoded" ) ||
885 (sType.startsWith(
"application/x-www-form-urlencoded;")))
888 if ((sType ==
"text/xml" ) ||
889 (sType.startsWith(
"text/xml;") ))
892 if ((sType ==
"application/json") ||
893 sType.startsWith(
"application/json;"))
908 case 200:
return(
"200 OK" );
909 case 201:
return(
"201 Created" );
910 case 202:
return(
"202 Accepted" );
911 case 204:
return(
"204 No Content" );
912 case 205:
return(
"205 Reset Content" );
913 case 206:
return(
"206 Partial Content" );
914 case 300:
return(
"300 Multiple Choices" );
915 case 301:
return(
"301 Moved Permanently" );
916 case 302:
return(
"302 Found" );
917 case 303:
return(
"303 See Other" );
918 case 304:
return(
"304 Not Modified" );
919 case 305:
return(
"305 Use Proxy" );
920 case 307:
return(
"307 Temporary Redirect" );
921 case 308:
return(
"308 Permanent Redirect" );
922 case 400:
return(
"400 Bad Request" );
923 case 401:
return(
"401 Unauthorized" );
924 case 403:
return(
"403 Forbidden" );
925 case 404:
return(
"404 Not Found" );
926 case 405:
return(
"405 Method Not Allowed" );
927 case 406:
return(
"406 Not Acceptable" );
928 case 408:
return(
"408 Request Timeout" );
929 case 410:
return(
"410 Gone" );
930 case 411:
return(
"411 Length Required" );
931 case 412:
return(
"412 Precondition Failed" );
932 case 413:
return(
"413 Request Entity Too Large" );
933 case 414:
return(
"414 Request-URI Too Long" );
934 case 415:
return(
"415 Unsupported Media Type" );
935 case 416:
return(
"416 Requested Range Not Satisfiable" );
936 case 417:
return(
"417 Expectation Failed" );
938 case 428:
return(
"428 Precondition Required" );
939 case 429:
return(
"429 Too Many Requests" );
940 case 431:
return(
"431 Request Header Fields Too Large" );
941 case 500:
return(
"500 Internal Server Error" );
942 case 501:
return(
"501 Not Implemented" );
943 case 502:
return(
"502 Bad Gateway" );
944 case 503:
return(
"503 Service Unavailable" );
945 case 504:
return(
"504 Gateway Timeout" );
946 case 505:
return(
"505 HTTP Version Not Supported" );
947 case 510:
return(
"510 Not Extended" );
948 case 511:
return(
"511 Network Authentication Required" );
980 return(
"text/plain" );
993 ext =
type.pszExtension;
995 if ( sFileExtension.compare(ext, Qt::CaseInsensitive) == 0 )
996 return(
type.pszType );
999 return(
"text/plain" );
1008 QStringList mimeTypes;
1012 if (!mimeTypes.contains(
type.pszType ))
1013 mimeTypes.append(
type.pszType );
1025 QFileInfo
info( sFileName );
1026 QString sLOC =
"HTTPRequest::TestMimeType(" + sFileName +
") - ";
1027 QString sSuffix =
info.suffix().toLower();
1030 if ( sSuffix ==
"nuv" )
1033 QFile
file( sFileName );
1035 if (
file.open(QIODevice::ReadOnly | QIODevice::Text) )
1037 QByteArray head =
file.read(8);
1038 QString sHex = head.toHex();
1040 LOG(VB_HTTP, LOG_DEBUG, sLOC +
"file starts with " + sHex);
1042 if ( sHex ==
"000001ba44000400" )
1043 sMIME =
"video/mp2p";
1045 if ( head ==
"MythTVVi" )
1048 head =
file.read(4);
1050 if ( head ==
"DIVX" )
1052 LOG(VB_HTTP, LOG_DEBUG, sLOC +
"('MythTVVi...DIVXLAME')");
1053 sMIME =
"video/mp4";
1064 LOG(VB_GENERAL, LOG_ERR, sLOC +
"Could not read file");
1068 LOG(VB_HTTP, LOG_INFO, sLOC +
"type is " + sMIME);
1080 LOG(VB_HTTP, LOG_INFO, QString(
"sParams: '%1'").arg(sParams));
1085 sParams.replace(
"&",
"&" );
1087 if (!sParams.isEmpty())
1089 QStringList params = sParams.split(
'&', Qt::SkipEmptyParts);
1090 for (
const auto & param : std::as_const(params))
1092 QString sName = param.section(
'=', 0, 0 );
1093 QString sValue = param.section(
'=', 1 );
1094 sValue.replace(
"+",
" ");
1096 if (!sName.isEmpty())
1098 sName = QUrl::fromPercentEncoding(sName.toUtf8());
1099 sValue = QUrl::fromPercentEncoding(sValue.toUtf8());
1101 mapParams.insert( sName.trimmed(), sValue );
1138 sHeader += it.key() +
": ";
1139 sHeader += *it +
"\r\n";
1155 bool bKeepAlive =
true;
1164 QString sConnection =
GetRequestHeader(
"connection",
"default" ).toLower();
1166 QStringList sValueList = sConnection.split(
",");
1168 if ( sValueList.contains(
"close") )
1170 LOG(VB_HTTP, LOG_DEBUG,
"Client requested the connection be closed");
1173 else if (sValueList.contains(
"keep-alive"))
1187 QStringList sCookieList =
m_mapHeaders.values(
"cookie");
1189 QStringList::iterator it;
1190 for (it = sCookieList.begin(); it != sCookieList.end(); ++it)
1192 QString key = (*it).section(
'=', 0, 0);
1193 QString value = (*it).section(
'=', 1);
1205 bool bSuccess =
false;
1210 QString sRequestLine =
ReadLine( 2s );
1212 if ( sRequestLine.isEmpty() )
1214 LOG(VB_GENERAL, LOG_ERR,
"Timeout reading first line of request." );
1246 while (( !sLine.isEmpty() ) && !bDone )
1248 if (sLine !=
"\r\n")
1250 QString sName = sLine.section(
':', 0, 0 ).trimmed();
1251 QString sValue = sLine.section(
':', 1 );
1253 sValue.truncate( sValue.length() - 2 );
1255 if (!sName.isEmpty() && !sValue.isEmpty())
1257 m_mapHeaders.insert(sName.toLower(), sValue.trimmed());
1271 LOG(VB_HTTP, LOG_INFO, QString(
"(Request Header) %1: %2")
1272 .arg(it.key(), *it));
1284 LOG(VB_GENERAL, LOG_INFO,
"Timeout waiting for request header." );
1338 long nPayloadSize =
GetLastHeader(
"content-length" ).toLong();
1340 if (nPayloadSize > 0)
1342 char *pszPayload =
new char[ nPayloadSize + 2 ];
1345 nBytes =
ReadBlock( pszPayload, nPayloadSize, 5s );
1346 if (nBytes == nPayloadSize )
1348 m_sPayload = QString::fromUtf8( pszPayload, nPayloadSize );
1358 LOG(VB_GENERAL, LOG_ERR,
1359 QString(
"Unable to read entire payload (read %1 of %2 bytes)")
1360 .arg( nBytes ) .arg( nPayloadSize ) );
1364 delete [] pszPayload;
1370 if (!sSOAPAction.isEmpty())
1377 LOG(VB_HTTP, LOG_DEBUG,
1378 QString(
"HTTPRequest::ParseRequest - Socket (%1) Base (%2) "
1379 "Method (%3) - Bytes in Socket Buffer (%4)")
1381 .arg(
m_sMethod) .arg(BytesAvailable()));
1386 LOG(VB_GENERAL, LOG_WARNING,
1387 "Unexpected exception in HTTPRequest::ParseRequest" );
1402 int nCount = tokens.count();
1406 if ( sLine.startsWith( QString(
"HTTP/") ))
1430 m_sRequestUrl = QUrl::fromPercentEncoding(tokens[1].toUtf8());
1436 QString sQueryStr = tokens[1].section(
'?', 1, 1 );
1438 if (!sQueryStr.isEmpty())
1466 long long *pllStart,
1474 if (sRange.isEmpty())
1486 sRange.remove( 0, nIdx );
1492 QStringList ranges = sRange.split(
',', Qt::SkipEmptyParts);
1493 if (ranges.count() == 0)
1500 QStringList parts = ranges[0].split(
'-');
1502 if (parts.count() != 2)
1505 if (parts[0].isEmpty() && parts[1].isEmpty())
1512 bool conv_ok =
false;
1513 if (parts[0].isEmpty())
1519 long long llValue = parts[1].toLongLong(&conv_ok);
1520 if (!conv_ok)
return false;
1522 *pllStart = llSize - llValue;
1523 *pllEnd = llSize - 1;
1525 else if (parts[1].isEmpty())
1531 *pllStart = parts[0].toLongLong(&conv_ok);
1536 *pllEnd = llSize - 1;
1544 *pllStart = parts[0].toLongLong(&conv_ok);
1545 if (!conv_ok)
return false;
1546 *pllEnd = parts[1].toLongLong(&conv_ok);
1547 if (!conv_ok)
return false;
1549 if (*pllStart > *pllEnd)
1553 LOG(VB_HTTP, LOG_DEBUG, QString(
"%1 Range Requested %2 - %3")
1568 static const QRegularExpression re {
"^http[s]?://.*?/"};
1571 QStringList sList =
m_sBaseUrl.split(
'/', Qt::SkipEmptyParts);
1574 if (!sList.isEmpty())
1581 LOG(VB_HTTP, LOG_INFO, QString(
"ExtractMethodFromURL(end) : %1 : %2")
1591 bool bSuccess =
false;
1597 LOG(VB_HTTP, LOG_INFO,
1598 QString(
"HTTPRequest::ProcessSOAPPayload : %1 : ").arg(sSOAPAction));
1599 QDomDocument doc (
"request" );
1601#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
1606 if (!doc.setContent(
m_sPayload,
true, &sErrMsg, &nErrLine, &nErrCol ))
1608 LOG(VB_GENERAL, LOG_ERR,
1609 QString(
"Error parsing request at line: %1 column: %2 : %3" )
1610 .arg(nErrLine) .arg(nErrCol) .arg(sErrMsg));
1614 auto parseResult =doc.setContent(
m_sPayload,
1615 QDomDocument::ParseOption::UseNamespaceProcessing );
1618 LOG(VB_GENERAL, LOG_ERR,
1619 QString(
"Error parsing request at line: %1 column: %2 : %3" )
1620 .arg(parseResult.errorLine).arg(parseResult.errorColumn)
1621 .arg(parseResult.errorMessage));
1632 if (sSOAPAction.contains(
'#' ))
1634 m_sNameSpace = sSOAPAction.section(
'#', 0, 0).remove( 0, 1);
1635 m_sMethod = sSOAPAction.section(
'#', 1 );
1640 if (sSOAPAction.contains(
'/' ))
1642 int nPos = sSOAPAction.lastIndexOf(
'/' );
1645 sSOAPAction.length() - nPos - 2);
1662 if (oNodeList.count() == 0)
1665 doc.elementsByTagNameNS(
"http://schemas.xmlsoap.org/soap/envelope/",
1669 if (oNodeList.count() > 0)
1671 QDomNode oMethod = oNodeList.item(0);
1673 if (!oMethod.isNull())
1677 for ( QDomNode oNode = oMethod.firstChild(); !oNode.isNull();
1678 oNode = oNode.nextSibling() )
1680 QDomElement e = oNode.toElement();
1684 QString sName = e.tagName();
1685 QString sValue =
"";
1687 QDomText oText = oNode.firstChild().toText();
1689 if (!oText.isNull())
1690 sValue = oText.nodeValue();
1692 sName = QUrl::fromPercentEncoding(sName.toUtf8());
1693 sValue = QUrl::fromPercentEncoding(sValue.toUtf8());
1695 m_mapParams.insert( sName.trimmed().toLower(), sValue );
1723 if (sAccept.contains(
"application/json", Qt::CaseInsensitive ) ||
1724 sAccept.contains(
"text/javascript", Qt::CaseInsensitive ))
1729 else if (sAccept.contains(
"text/x-apple-plist+xml", Qt::CaseInsensitive ))
1737 if (pSerializer ==
nullptr)
1751 LOG(VB_HTTP, LOG_DEBUG,
1752 QString(
"HTTPRequest::Encode Input : %1").arg(sStr));
1754 sStr.replace(
'&',
"&" );
1755 sStr.replace(
'<',
"<" );
1756 sStr.replace(
'>',
">" );
1757 sStr.replace(
'"',
""");
1758 sStr.replace(
"'",
"'");
1761 LOG(VB_HTTP, LOG_DEBUG,
1762 QString(
"HTTPRequest::Encode Output : %1").arg(sStr));
1774 sStr.replace(
"&",
"&");
1775 sStr.replace(
"<",
"<");
1776 sStr.replace(
">",
">");
1777 sStr.replace(
""",
"\"");
1778 sStr.replace(
"'",
"'");
1789 QByteArray hash = QCryptographicHash::hash( data.data(), QCryptographicHash::Sha1);
1791 return (
"\"" + hash.toHex() +
"\"");
1802 QStringList oList = sProtected.split(
';' );
1804 for(
int nIdx = 0; nIdx < oList.count(); nIdx++)
1806 if (sBaseUrl.startsWith( oList[nIdx], Qt::CaseInsensitive ))
1822 QString realm =
"MythTV";
1830 QString stale = isStale ?
"true" :
"false";
1831 authHeader = QString(
"Digest realm=\"%1\",nonce=\"%2\","
1832 "qop=\"auth\",stale=\"%3\",algorithm=\"MD5\"")
1833 .arg(realm, nonce, stale);
1837 authHeader = QString(
"Basic realm=\"%1\"").arg(realm);
1850 QString hash = QCryptographicHash::hash( uniqueID.toLatin1(), QCryptographicHash::Sha1).toHex();
1851 QString nonce = QString(
"%1%2").arg(timeStamp, hash);
1861 LOG(VB_HTTP, LOG_NOTICE,
"Attempting HTTP Basic Authentication");
1862 QStringList oList =
GetLastHeader(
"authorization" ).split(
' ' );
1866 LOG(VB_GENERAL, LOG_WARNING,
"Basic authentication is only allowed for HTTP 1.0");
1870 QString sCredentials = QByteArray::fromBase64( oList[1].toUtf8() );
1872 oList = sCredentials.split(
':' );
1874 if (oList.count() < 2)
1876 LOG(VB_GENERAL, LOG_WARNING,
"Authorization attempt with invalid number of tokens");
1880 QString sUsername = oList[0];
1881 QString sPassword = oList[1];
1883 if (sUsername ==
"nouser")
1889 LOG(VB_GENERAL, LOG_WARNING,
"Authorization attempt with invalid username");
1893 QString client = QString(
"WebFrontend_%1").arg(
GetPeerAddress());
1899 LOG(VB_GENERAL, LOG_WARNING,
"Authorization attempt with invalid password");
1903 LOG(VB_HTTP, LOG_NOTICE,
"Valid Authorization received");
1920 LOG(VB_HTTP, LOG_NOTICE,
"Attempting HTTP Digest Authentication");
1921 QString realm =
"MythTV";
1923 QString authMethod =
GetLastHeader(
"authorization" ).section(
' ', 0, 0).toLower();
1925 if (authMethod !=
"digest")
1927 LOG(VB_GENERAL, LOG_WARNING,
"Invalid method in Authorization header");
1931 QString parameterStr =
GetLastHeader(
"authorization" ).section(
' ', 1);
1933 QMap<QString, QString> paramMap;
1934 QStringList paramList = parameterStr.split(
',');
1935 QStringList::iterator it;
1936 for (it = paramList.begin(); it != paramList.end(); ++it)
1938 QString key = (*it).section(
'=', 0, 0).toLower().trimmed();
1940 QString value = (*it).section(
'=', 1).trimmed();
1943 paramMap[key] = value;
1946 if (paramMap.size() < 8)
1948 LOG(VB_GENERAL, LOG_WARNING,
"Invalid number of parameters in Authorization header");
1952 if (paramMap[
"nonce"].isEmpty() || paramMap[
"username"].isEmpty() ||
1953 paramMap[
"realm"].isEmpty() || paramMap[
"uri"].isEmpty() ||
1954 paramMap[
"response"].isEmpty() || paramMap[
"qop"].isEmpty() ||
1955 paramMap[
"cnonce"].isEmpty() || paramMap[
"nc"].isEmpty())
1957 LOG(VB_GENERAL, LOG_WARNING,
"Missing required parameters in Authorization header");
1961 if (paramMap[
"username"] ==
"nouser")
1966 LOG(VB_GENERAL, LOG_WARNING,
"Authorization URI doesn't match the "
1972 if (paramMap[
"realm"] != realm)
1974 LOG(VB_GENERAL, LOG_WARNING,
"Authorization realm doesn't match the "
1975 "realm of the requested content");
1979 QByteArray nonce = paramMap[
"nonce"].toLatin1();
1980 if (nonce.length() < 20)
1982 LOG(VB_GENERAL, LOG_WARNING,
"Authorization nonce is too short");
1986 QString nonceTimeStampStr = nonce.left(20);
1989 LOG(VB_GENERAL, LOG_WARNING,
"Authorization nonce doesn't match reference");
1990 LOG(VB_HTTP, LOG_DEBUG, QString(
"%1 vs %2").arg(QString(nonce),
1995 constexpr std::chrono::seconds AUTH_TIMEOUT { 2min };
1997 if (!nonceTimeStamp.isValid())
1999 LOG(VB_GENERAL, LOG_WARNING,
"Authorization nonce timestamp is invalid.");
2000 LOG(VB_HTTP, LOG_DEBUG, QString(
"Timestamp was '%1'").arg(nonceTimeStampStr));
2006 LOG(VB_HTTP, LOG_NOTICE,
"Authorization nonce timestamp is invalid or too old.");
2017 LOG(VB_GENERAL, LOG_WARNING,
"Authorization attempt with invalid username");
2021 if (paramMap[
"response"].length() != 32)
2023 LOG(VB_GENERAL, LOG_WARNING,
"Authorization response field is invalid length");
2031 QString methodDigest = QString(
"%1:%2").arg(
GetRequestType(), paramMap[
"uri"]);
2032 QByteArray a2 = QCryptographicHash::hash(methodDigest.toLatin1(),
2033 QCryptographicHash::Md5).toHex();
2035 QString responseDigest = QString(
"%1:%2:%3:%4:%5:%6").arg(a1,
2041 QByteArray kd = QCryptographicHash::hash(responseDigest.toLatin1(),
2042 QCryptographicHash::Md5).toHex();
2044 if (paramMap[
"response"].toLatin1() == kd)
2046 LOG(VB_HTTP, LOG_NOTICE,
"Valid Authorization received");
2047 QString client = QString(
"WebFrontend_%1").arg(
GetPeerAddress());
2053 LOG(VB_GENERAL, LOG_ERR,
"Valid Authorization received, but we "
2054 "failed to create a valid session");
2067 LOG(VB_GENERAL, LOG_WARNING,
"Authorization attempt with invalid password digest");
2068 LOG(VB_HTTP, LOG_DEBUG, QString(
"Received hash was '%1', calculated hash was '%2'")
2069 .arg(paramMap[
"response"], QString(kd)));
2084 QStringList oList =
GetLastHeader(
"authorization" ).split(
' ' );
2086 if (oList.count() < 2)
2089 if (oList[0].compare(
"basic", Qt::CaseInsensitive ) == 0)
2091 if (oList[0].compare(
"digest", Qt::CaseInsensitive ) == 0)
2115 const QDateTime &expiryDate,
bool secure)
2119 LOG(VB_GENERAL, LOG_WARNING, QString(
"HTTPRequest::SetCookie(%1=%2): "
2120 "A secure cookie cannot be set on an unencrypted connection.")
2121 .arg(sKey, sValue));
2125 QStringList cookieAttributes;
2128 cookieAttributes.append(QString(
"%1=%2").arg(sKey, sValue));
2134 cookieAttributes.append(
"Path=/");
2138 cookieAttributes.append(QString(
"Expires=%1").arg(expires));
2143 cookieAttributes.append(
"Secure");
2146 cookieAttributes.append(
"HttpOnly");
2168 return hostname.section(
"]:", 0 , 0);
2172 return hostname.section(
":", 0 , 0);
2211 type =
"UNSUBSCRIBE";
2249 QStringList allowedOrigins;
2253 serverStatusPort + 10);
2255 QString masterAddrPort = QString(
"%1:%2")
2257 .arg(serverStatusPort);
2258 QString masterTLSAddrPort = QString(
"%1:%2")
2260 .arg(backendSSLPort);
2262 allowedOrigins << QString(
"http://%1").arg(masterAddrPort);
2263 allowedOrigins << QString(
"https://%2").arg(masterTLSAddrPort);
2265 QString localhostname = QHostInfo::localHostName();
2266 if (!localhostname.isEmpty())
2268 allowedOrigins << QString(
"http://%1:%2")
2269 .arg(localhostname).arg(serverStatusPort);
2270 allowedOrigins << QString(
"https://%1:%2")
2271 .arg(localhostname).arg(backendSSLPort);
2274 QStringList allowedOriginsList =
2276 "https://chromecast.mythtv.org")).split(
",");
2278 for (
const auto & origin : std::as_const(allowedOriginsList))
2280 if (origin.isEmpty())
2283 if (origin ==
"*" || (!origin.startsWith(
"http://") &&
2284 !origin.startsWith(
"https://")))
2286 LOG(VB_GENERAL, LOG_ERR, QString(
"Illegal AllowedOriginsList"
2287 " entry '%1'. Must start with http[s]:// and not be *")
2292 allowedOrigins << origin;
2298 for (
const auto & origin : std::as_const(allowedOrigins))
2299 LOG(VB_HTTP, LOG_DEBUG, QString(
"Will allow Origin: %1").arg(origin));
2302 if (allowedOrigins.contains(sOrigin))
2307 LOG(VB_HTTP, LOG_DEBUG, QString(
"Allow-Origin: %1)").arg(sOrigin));
2311 LOG(VB_GENERAL, LOG_CRIT, QString(
"HTTPRequest: Cross-origin request "
2312 "received with origin (%1)")
2330 m_pSocket->state() == QAbstractSocket::ConnectedState)
2339 if ( timer.
elapsed() >= msecs )
2342 LOG(VB_HTTP, LOG_INFO,
"BufferedSocketDeviceRequest::ReadLine() - Exceeded Total Elapsed Wait Time." );
2358 std::chrono::milliseconds msecs)
2361 m_pSocket->state() == QAbstractSocket::ConnectedState)
2364 return(
m_pSocket->read( pData, nMaxLen ));
2366 bool bTimeout =
false;
2369 while ( (
m_pSocket->bytesAvailable() < (
int)nMaxLen) && !bTimeout )
2371 bTimeout = !(
m_pSocket->waitForReadyRead( msecs.count() ));
2373 if ( timer.
elapsed() >= msecs )
2376 LOG(VB_HTTP, LOG_INFO,
"BufferedSocketDeviceRequest::ReadBlock() - Exceeded Total Elapsed Wait Time." );
2382 return(
m_pSocket->read( pData, nMaxLen ));
2394 qint64 bytesWritten = -1;
2396 m_pSocket->state() == QAbstractSocket::ConnectedState)
2398 bytesWritten =
m_pSocket->write( pData, nLen );
2402 return( bytesWritten );
2411 return(
m_pSocket->localAddress().toString() );
2430 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