18 #include <QStringList>
19 #include <QCryptographicHash>
27 #include <sys/types.h>
33 #include <netinet/tcp.h>
44 #include "libmythbase/mythversion.h"
59 {
"gif" ,
"image/gif" },
60 {
"ico" ,
"image/x-icon" },
61 {
"jpeg",
"image/jpeg" },
62 {
"jpg" ,
"image/jpeg" },
63 {
"mng" ,
"image/x-mng" },
64 {
"png" ,
"image/png" },
65 {
"svg" ,
"image/svg+xml" },
66 {
"svgz",
"image/svg+xml" },
67 {
"tif" ,
"image/tiff" },
68 {
"tiff",
"image/tiff" },
70 {
"htm" ,
"text/html" },
71 {
"html",
"text/html" },
72 {
"qsp" ,
"text/html" },
73 {
"txt" ,
"text/plain" },
74 {
"xml" ,
"text/xml" },
75 {
"qxml",
"text/xml" },
76 {
"xslt",
"text/xml" },
77 {
"css" ,
"text/css" },
79 {
"crt" ,
"application/x-x509-ca-cert" },
80 {
"doc" ,
"application/vnd.ms-word" },
81 {
"gz" ,
"application/x-tar" },
82 {
"js" ,
"application/javascript" },
83 {
"m3u" ,
"application/x-mpegurl" },
84 {
"m3u8",
"application/x-mpegurl" },
85 {
"ogx" ,
"application/ogg" },
86 {
"pdf" ,
"application/pdf" },
87 {
"pem" ,
"application/x-x509-ca-cert" },
88 {
"qjs" ,
"application/javascript" },
89 {
"rm" ,
"application/vnd.rn-realmedia" },
90 {
"swf" ,
"application/x-shockwave-flash" },
91 {
"xls" ,
"application/vnd.ms-excel" },
92 {
"zip" ,
"application/x-tar" },
94 {
"aac" ,
"audio/mp4" },
95 {
"ac3" ,
"audio/vnd.dolby.dd-raw" },
96 {
"flac",
"audio/x-flac" },
97 {
"m4a" ,
"audio/x-m4a" },
98 {
"mid" ,
"audio/midi" },
99 {
"mka" ,
"audio/x-matroska" },
100 {
"mp3" ,
"audio/mpeg" },
101 {
"oga" ,
"audio/ogg" },
102 {
"ogg" ,
"audio/ogg" },
103 {
"wav" ,
"audio/wav" },
104 {
"wma" ,
"audio/x-ms-wma" },
106 {
"3gp" ,
"video/3gpp" },
107 {
"3g2" ,
"video/3gpp2" },
108 {
"asx" ,
"video/x-ms-asf" },
109 {
"asf" ,
"video/x-ms-asf" },
110 {
"avi" ,
"video/x-msvideo" },
111 {
"m2p" ,
"video/mp2p" },
112 {
"m4v" ,
"video/mp4" },
113 {
"mpeg",
"video/mp2p" },
114 {
"mpeg2",
"video/mp2p" },
115 {
"mpg" ,
"video/mp2p" },
116 {
"mpg2",
"video/mp2p" },
117 {
"mov" ,
"video/quicktime" },
118 {
"mp4" ,
"video/mp4" },
119 {
"mkv" ,
"video/x-matroska" },
120 {
"nuv" ,
"video/nupplevideo" },
121 {
"ogv" ,
"video/ogg" },
122 {
"ps" ,
"video/mp2p" },
123 {
"ts" ,
"video/mp2t" },
124 {
"vob" ,
"video/mpeg" },
125 {
"wmv" ,
"video/x-ms-wmv" },
127 {
"ttf" ,
"font/ttf" },
128 {
"woff" ,
"font/woff" },
129 {
"woff2",
"font/woff2" }
142 "<TITLE>Error %1</TITLE>"
143 "<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=ISO-8859-1\">"
145 "<BODY><H1>%2.</H1></BODY>"
157 if (!values.isEmpty())
158 return values.last();
182 LOG(VB_HTTP, LOG_INFO,
183 QString(
"HTTPRequest::SentRequestType( %1 ) - returning Unknown.")
256 if (sTransferMode.isEmpty())
260 sTransferMode =
"Streaming";
262 sTransferMode =
"Interactive";
265 if (sTransferMode ==
"Streaming")
267 else if (sTransferMode ==
"Background")
269 else if (sTransferMode ==
"Interactive")
274 SetResponseHeader(
"contentFeatures.dlna.org",
"DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01500000000000000000000000000000");
278 for (
const auto & value : values)
281 if (qEnvironmentVariableIsSet(
"HTTPREQUEST_DEBUG"))
284 QMap<QString, QString>::iterator it;
287 LOG(VB_HTTP, LOG_INFO, QString(
"(Response Header) %1: %2").arg(it.key(), it.value()));
310 LOG(VB_HTTP, LOG_INFO,
311 QString(
"HTTPRequest::SendResponse( None ) :%1 -> %2:")
325 if (
file.exists() &&
file.size() < (2LL * 1024 * 1024) &&
326 file.open(QIODevice::ReadOnly | QIODevice::Text))
337 LOG(VB_HTTP, LOG_INFO,
338 QString(
"HTTPRequest::SendResponse( File ) :%1 -> %2:")
347 LOG(VB_HTTP, LOG_INFO,
348 QString(
"HTTPRequest::SendResponse(xml/html) (%1) :%2 -> %3: %4")
360 LOG(VB_HTTP, LOG_INFO,
361 QString(
"HTTPRequest::SendResponse(%1) - Cached")
373 int nContentLen =
m_response.buffer().length();
379 if (qEnvironmentVariableIsSet(
"HTTPREQUEST_DEBUG"))
380 std::cout <<
m_response.buffer().constData() << std::endl;
383 LOG(VB_HTTP, LOG_DEBUG, QString(
"Reponse Content Length: %1").arg(nContentLen));
392 bool gzip_found = std::any_of(values.cbegin(), values.cend(),
393 [](
const auto & value)
394 {return value.contains(
"gzip" ); });
396 if (( nContentLen > 0 ) && gzip_found)
399 compBuffer.setData( compressed );
401 if (!compBuffer.buffer().isEmpty())
403 pBuffer = &compBuffer;
406 LOG(VB_HTTP, LOG_DEBUG, QString(
"Reponse Compressed Content Length: %1").arg(compBuffer.buffer().length()));
414 nContentLen = pBuffer->buffer().length();
418 QByteArray sHeader = rHeader.toUtf8();
419 LOG(VB_HTTP, LOG_DEBUG, QString(
"Response header size: %1 bytes").arg(sHeader.length()));
420 nBytes =
WriteBlock( sHeader.constData(), sHeader.length() );
422 if (nBytes < sHeader.length())
424 LOG( VB_HTTP, LOG_ERR, QString(
"HttpRequest::SendResponse(): "
425 "Incomplete write of header, "
427 .arg(nBytes).arg(sHeader.length()));
437 qint64 bytesWritten =
SendData( pBuffer, 0, nContentLen );
440 if (bytesWritten != nContentLen)
441 LOG(VB_HTTP, LOG_ERR,
"HttpRequest::SendResponse(): Error occurred while writing response body.");
443 nBytes += bytesWritten;
456 long long llSize = 0;
457 long long llStart = 0;
460 LOG(VB_HTTP, LOG_INFO, QString(
"SendResponseFile ( %1 )").arg(sFileName));
465 QFile tmpFile( sFileName );
466 if (tmpFile.exists( ) && tmpFile.open( QIODevice::ReadOnly ))
475 llSize = llEnd = tmpFile.size( );
486 if (!sRange.isEmpty())
488 bRange =
ParseRange( sRange, llSize, &llStart, &llEnd );
495 if ((llSize > llStart) && (llSize > llEnd) && (llEnd > llStart))
504 llSize = (llEnd - llStart) + 1;
516 LOG(VB_HTTP, LOG_INFO,
517 QString(
"HTTPRequest::SendResponseFile(%1) - "
518 "invalid byte range %2-%3/%4")
519 .arg(sFileName) .arg(llStart) .arg(llEnd)
537 LOG(VB_HTTP, LOG_INFO,
538 QString(
"HTTPRequest::SendResponseFile(%1) - cannot find file!")
551 QByteArray sHeader = rHeader.toUtf8();
552 LOG(VB_HTTP, LOG_DEBUG, QString(
"Response header size: %1 bytes").arg(sHeader.length()));
553 nBytes =
WriteBlock( sHeader.constData(), sHeader.length() );
555 if (nBytes < sHeader.length())
557 LOG( VB_HTTP, LOG_ERR, QString(
"HttpRequest::SendResponseFile(): "
558 "Incomplete write of header, "
560 .arg(nBytes).arg(sHeader.length()));
568 LOG(VB_HTTP, LOG_DEBUG,
569 QString(
"SendResponseFile : size = %1, start = %2, end = %3")
570 .arg(llSize).arg(llStart).arg(llEnd));
574 long long sent =
SendFile( tmpFile, llStart, llSize );
578 LOG(VB_HTTP, LOG_INFO,
579 QString(
"SendResponseFile( %1 ) Error: %2 [%3]" )
580 .arg(sFileName) .arg(errno) .arg(strerror(errno)));
600 bool bShouldClose =
false;
603 if (!pDevice->isOpen())
605 pDevice->open( QIODevice::ReadOnly );
613 if ( !pDevice->seek( llStart ))
616 std::array<char,SENDFILE_BUFFER_SIZE> aBuffer {};
618 qint64 llBytesRemaining = llBytes;
619 qint64 llBytesToRead = 0;
620 qint64 llBytesRead = 0;
622 while ((sent < llBytes) && !pDevice->atEnd())
626 if (( llBytesRead = pDevice->read( aBuffer.data(), llBytesToRead )) != -1 )
628 if (
WriteBlock( aBuffer.data(), llBytesRead ) == -1)
634 llBytesRemaining -= llBytesRead;
650 qint64 sent =
SendData( (QIODevice *)(&
file), llStart, llBytes );
661 const QString &sFaultString,
662 const QString &sDetails )
669 stream << R
"(<?xml version="1.0" encoding="utf-8"?>)";
671 QString sWhere = ( bServerError ) ? "s:Server" :
"s:Client";
679 <<
"<faultcode>" << sWhere <<
"</faultcode>"
680 <<
"<faultstring>" << sFaultString <<
"</faultstring>";
683 if (!sDetails.isEmpty())
685 stream <<
"<detail>" << sDetails <<
"</detail>";
722 stream <<
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n";
729 <<
"<u:" <<
m_sMethod <<
"Response xmlns:u=\""
733 stream <<
"<" <<
m_sMethod <<
"Response>\r\n";
735 for (
const auto & arg : qAsConst(
args))
737 stream <<
"<" << arg.m_sName;
739 if (arg.m_pAttributes)
741 for (
const auto & attr : qAsConst(*arg.m_pAttributes))
743 stream <<
" " << attr.m_sName <<
"='"
744 <<
Encode( attr.m_sValue ) <<
"'";
751 stream <<
Encode( arg.m_sValue );
753 stream << arg.m_sValue;
755 stream <<
"</" << arg.m_sName <<
">\r\n";
760 stream <<
"</u:" <<
m_sMethod <<
"Response>\r\n"
764 stream <<
"</" <<
m_sMethod <<
"Response>\r\n";
796 ims.setTimeSpec(Qt::OffsetFromUTC);
797 if (ims.isValid() && ims <=
file.lastModified())
818 LOG(VB_HTTP, LOG_INFO,
819 QString(
"HTTPRequest::FormatFileResponse('%1') - cannot find file")
830 m_sProtocol = sLine.section(
'/', 0, 0 ).trimmed();
831 QString sVersion = sLine.section(
'/', 1 ).trimmed();
833 m_nMajor = sVersion.section(
'.', 0, 0 ).toInt();
834 m_nMinor = sVersion.section(
'.', 1 ).toInt();
875 if ((sType ==
"application/x-www-form-urlencoded" ) ||
876 (sType.startsWith(
"application/x-www-form-urlencoded;")))
879 if ((sType ==
"text/xml" ) ||
880 (sType.startsWith(
"text/xml;") ))
883 if ((sType ==
"application/json") ||
884 sType.startsWith(
"application/json;"))
899 case 200:
return(
"200 OK" );
900 case 201:
return(
"201 Created" );
901 case 202:
return(
"202 Accepted" );
902 case 204:
return(
"204 No Content" );
903 case 205:
return(
"205 Reset Content" );
904 case 206:
return(
"206 Partial Content" );
905 case 300:
return(
"300 Multiple Choices" );
906 case 301:
return(
"301 Moved Permanently" );
907 case 302:
return(
"302 Found" );
908 case 303:
return(
"303 See Other" );
909 case 304:
return(
"304 Not Modified" );
910 case 305:
return(
"305 Use Proxy" );
911 case 307:
return(
"307 Temporary Redirect" );
912 case 308:
return(
"308 Permanent Redirect" );
913 case 400:
return(
"400 Bad Request" );
914 case 401:
return(
"401 Unauthorized" );
915 case 403:
return(
"403 Forbidden" );
916 case 404:
return(
"404 Not Found" );
917 case 405:
return(
"405 Method Not Allowed" );
918 case 406:
return(
"406 Not Acceptable" );
919 case 408:
return(
"408 Request Timeout" );
920 case 410:
return(
"410 Gone" );
921 case 411:
return(
"411 Length Required" );
922 case 412:
return(
"412 Precondition Failed" );
923 case 413:
return(
"413 Request Entity Too Large" );
924 case 414:
return(
"414 Request-URI Too Long" );
925 case 415:
return(
"415 Unsupported Media Type" );
926 case 416:
return(
"416 Requested Range Not Satisfiable" );
927 case 417:
return(
"417 Expectation Failed" );
929 case 428:
return(
"428 Precondition Required" );
930 case 429:
return(
"429 Too Many Requests" );
931 case 431:
return(
"431 Request Header Fields Too Large" );
932 case 500:
return(
"500 Internal Server Error" );
933 case 501:
return(
"501 Not Implemented" );
934 case 502:
return(
"502 Bad Gateway" );
935 case 503:
return(
"503 Service Unavailable" );
936 case 504:
return(
"504 Gateway Timeout" );
937 case 505:
return(
"505 HTTP Version Not Supported" );
938 case 510:
return(
"510 Not Extended" );
939 case 511:
return(
"511 Network Authentication Required" );
971 return(
"text/plain" );
984 ext =
type.pszExtension;
986 if ( sFileExtension.compare(ext, Qt::CaseInsensitive) == 0 )
987 return(
type.pszType );
990 return(
"text/plain" );
999 QStringList mimeTypes;
1003 if (!mimeTypes.contains(
type.pszType ))
1004 mimeTypes.append(
type.pszType );
1016 QFileInfo info( sFileName );
1017 QString sLOC =
"HTTPRequest::TestMimeType(" + sFileName +
") - ";
1018 QString sSuffix = info.suffix().toLower();
1021 if ( sSuffix ==
"nuv" )
1024 QFile
file( sFileName );
1026 if (
file.open(QIODevice::ReadOnly | QIODevice::Text) )
1028 QByteArray head =
file.read(8);
1029 QString sHex = head.toHex();
1031 LOG(VB_HTTP, LOG_DEBUG, sLOC +
"file starts with " + sHex);
1033 if ( sHex ==
"000001ba44000400" )
1034 sMIME =
"video/mp2p";
1036 if ( head ==
"MythTVVi" )
1039 head =
file.read(4);
1041 if ( head ==
"DIVX" )
1043 LOG(VB_HTTP, LOG_DEBUG, sLOC +
"('MythTVVi...DIVXLAME')");
1044 sMIME =
"video/mp4";
1054 LOG(VB_GENERAL, LOG_ERR, sLOC +
"Could not read file");
1057 LOG(VB_HTTP, LOG_INFO, sLOC +
"type is " + sMIME);
1069 LOG(VB_HTTP, LOG_INFO, QString(
"sParams: '%1'").arg(sParams));
1074 sParams.replace(
"&",
"&" );
1076 if (!sParams.isEmpty())
1078 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
1079 QStringList params = sParams.split(
'&', QString::SkipEmptyParts);
1081 QStringList params = sParams.split(
'&', Qt::SkipEmptyParts);
1084 for (
const auto & param : qAsConst(params))
1086 QString sName = param.section(
'=', 0, 0 );
1087 QString sValue = param.section(
'=', 1 );
1088 sValue.replace(
"+",
" ");
1090 if (!sName.isEmpty())
1092 sName = QUrl::fromPercentEncoding(sName.toUtf8());
1093 sValue = QUrl::fromPercentEncoding(sValue.toUtf8());
1095 mapParams.insert( sName.trimmed(), sValue );
1132 sHeader += it.key() +
": ";
1133 sHeader += *it +
"\r\n";
1149 bool bKeepAlive =
true;
1158 QString sConnection =
GetRequestHeader(
"connection",
"default" ).toLower();
1160 QStringList sValueList = sConnection.split(
",");
1162 if ( sValueList.contains(
"close") )
1164 LOG(VB_HTTP, LOG_DEBUG,
"Client requested the connection be closed");
1167 else if (sValueList.contains(
"keep-alive"))
1179 QStringList sCookieList =
m_mapHeaders.values(
"cookie");
1181 QStringList::iterator it;
1182 for (it = sCookieList.begin(); it != sCookieList.end(); ++it)
1184 QString key = (*it).section(
'=', 0, 0);
1185 QString value = (*it).section(
'=', 1);
1197 bool bSuccess =
false;
1202 QString sRequestLine =
ReadLine( 2s );
1204 if ( sRequestLine.isEmpty() )
1206 LOG(VB_GENERAL, LOG_ERR,
"Timeout reading first line of request." );
1238 while (( !sLine.isEmpty() ) && !bDone )
1240 if (sLine !=
"\r\n")
1242 QString sName = sLine.section(
':', 0, 0 ).trimmed();
1243 QString sValue = sLine.section(
':', 1 );
1245 sValue.truncate( sValue.length() - 2 );
1247 if (!sName.isEmpty() && !sValue.isEmpty())
1249 m_mapHeaders.insert(sName.toLower(), sValue.trimmed());
1261 LOG(VB_HTTP, LOG_INFO, QString(
"(Request Header) %1: %2")
1262 .arg(it.key(), *it));
1274 LOG(VB_GENERAL, LOG_INFO,
"Timeout waiting for request header." );
1328 long nPayloadSize =
GetLastHeader(
"content-length" ).toLong();
1330 if (nPayloadSize > 0)
1332 char *pszPayload =
new char[ nPayloadSize + 2 ];
1335 nBytes =
ReadBlock( pszPayload, nPayloadSize, 5s );
1336 if (nBytes == nPayloadSize )
1338 m_sPayload = QString::fromUtf8( pszPayload, nPayloadSize );
1348 LOG(VB_GENERAL, LOG_ERR,
1349 QString(
"Unable to read entire payload (read %1 of %2 bytes)")
1350 .arg( nBytes ) .arg( nPayloadSize ) );
1354 delete [] pszPayload;
1360 if (!sSOAPAction.isEmpty())
1367 LOG(VB_HTTP, LOG_DEBUG,
1368 QString(
"HTTPRequest::ParseRequest - Socket (%1) Base (%2) "
1369 "Method (%3) - Bytes in Socket Buffer (%4)")
1371 .arg(
m_sMethod) .arg(BytesAvailable()));
1376 LOG(VB_GENERAL, LOG_WARNING,
1377 "Unexpected exception in HTTPRequest::ParseRequest" );
1391 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
1392 QStringList tokens = sLine.split(
m_procReqLineExp, QString::SkipEmptyParts);
1396 int nCount = tokens.count();
1400 if ( sLine.startsWith( QString(
"HTTP/") ))
1424 m_sRequestUrl = QUrl::fromPercentEncoding(tokens[1].toUtf8());
1430 QString sQueryStr = tokens[1].section(
'?', 1, 1 );
1432 if (!sQueryStr.isEmpty())
1460 long long *pllStart,
1468 if (sRange.isEmpty())
1480 sRange.remove( 0, nIdx );
1486 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
1487 QStringList ranges = sRange.split(
',', QString::SkipEmptyParts);
1489 QStringList ranges = sRange.split(
',', Qt::SkipEmptyParts);
1492 if (ranges.count() == 0)
1499 QStringList parts = ranges[0].split(
'-');
1501 if (parts.count() != 2)
1504 if (parts[0].isEmpty() && parts[1].isEmpty())
1511 bool conv_ok =
false;
1512 if (parts[0].isEmpty())
1518 long long llValue = parts[1].toLongLong(&conv_ok);
1519 if (!conv_ok)
return false;
1521 *pllStart = llSize - llValue;
1522 *pllEnd = llSize - 1;
1524 else if (parts[1].isEmpty())
1530 *pllStart = parts[0].toLongLong(&conv_ok);
1535 *pllEnd = llSize - 1;
1543 *pllStart = parts[0].toLongLong(&conv_ok);
1544 if (!conv_ok)
return false;
1545 *pllEnd = parts[1].toLongLong(&conv_ok);
1546 if (!conv_ok)
return false;
1548 if (*pllStart > *pllEnd)
1552 LOG(VB_HTTP, LOG_DEBUG, QString(
"%1 Range Requested %2 - %3")
1567 static const QRegularExpression re {
"^http[s]?://.*?/"};
1570 #if QT_VERSION < QT_VERSION_CHECK(5,14,0)
1571 QStringList sList =
m_sBaseUrl.split(
'/', QString::SkipEmptyParts);
1573 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" );
1609 if (!doc.setContent(
m_sPayload,
true, &sErrMsg, &nErrLine, &nErrCol ))
1611 LOG(VB_GENERAL, LOG_ERR,
1612 QString(
"Error parsing request at line: %1 column: %2 : %3" )
1613 .arg(nErrLine) .arg(nErrCol) .arg(sErrMsg));
1623 if (sSOAPAction.contains(
'#' ))
1625 m_sNameSpace = sSOAPAction.section(
'#', 0, 0).remove( 0, 1);
1626 m_sMethod = sSOAPAction.section(
'#', 1 );
1631 if (sSOAPAction.contains(
'/' ))
1633 int nPos = sSOAPAction.lastIndexOf(
'/' );
1636 sSOAPAction.length() - nPos - 2);
1653 if (oNodeList.count() == 0)
1656 doc.elementsByTagNameNS(
"http://schemas.xmlsoap.org/soap/envelope/",
1660 if (oNodeList.count() > 0)
1662 QDomNode oMethod = oNodeList.item(0);
1664 if (!oMethod.isNull())
1668 for ( QDomNode oNode = oMethod.firstChild(); !oNode.isNull();
1669 oNode = oNode.nextSibling() )
1671 QDomElement e = oNode.toElement();
1675 QString sName = e.tagName();
1676 QString sValue =
"";
1678 QDomText oText = oNode.firstChild().toText();
1680 if (!oText.isNull())
1681 sValue = oText.nodeValue();
1683 sName = QUrl::fromPercentEncoding(sName.toUtf8());
1684 sValue = QUrl::fromPercentEncoding(sValue.toUtf8());
1686 m_mapParams.insert( sName.trimmed().toLower(), sValue );
1714 if (sAccept.contains(
"application/json", Qt::CaseInsensitive ) ||
1715 sAccept.contains(
"text/javascript", Qt::CaseInsensitive ))
1720 else if (sAccept.contains(
"text/x-apple-plist+xml", Qt::CaseInsensitive ))
1728 if (pSerializer ==
nullptr)
1742 LOG(VB_HTTP, LOG_DEBUG,
1743 QString(
"HTTPRequest::Encode Input : %1").arg(sStr));
1745 sStr.replace(
'&',
"&" );
1746 sStr.replace(
'<',
"<" );
1747 sStr.replace(
'>',
">" );
1748 sStr.replace(
'"',
""");
1749 sStr.replace(
"'",
"'");
1752 LOG(VB_HTTP, LOG_DEBUG,
1753 QString(
"HTTPRequest::Encode Output : %1").arg(sStr));
1765 sStr.replace(
"&",
"&");
1766 sStr.replace(
"<",
"<");
1767 sStr.replace(
">",
">");
1768 sStr.replace(
""",
"\"");
1769 sStr.replace(
"'",
"'");
1780 QByteArray hash = QCryptographicHash::hash( data.data(), QCryptographicHash::Sha1);
1782 return (
"\"" + hash.toHex() +
"\"");
1793 QStringList oList = sProtected.split(
';' );
1795 for(
int nIdx = 0; nIdx < oList.count(); nIdx++)
1797 if (sBaseUrl.startsWith( oList[nIdx], Qt::CaseInsensitive ))
1813 QString realm =
"MythTV";
1821 QString stale = isStale ?
"true" :
"false";
1822 authHeader = QString(
"Digest realm=\"%1\",nonce=\"%2\","
1823 "qop=\"auth\",stale=\"%3\",algorithm=\"MD5\"")
1824 .arg(realm, nonce, stale);
1828 authHeader = QString(
"Basic realm=\"%1\"").arg(realm);
1841 QString hash = QCryptographicHash::hash( uniqueID.toLatin1(), QCryptographicHash::Sha1).toHex();
1842 QString nonce = QString(
"%1%2").arg(timeStamp, hash);
1852 LOG(VB_HTTP, LOG_NOTICE,
"Attempting HTTP Basic Authentication");
1853 QStringList oList =
GetLastHeader(
"authorization" ).split(
' ' );
1857 LOG(VB_GENERAL, LOG_WARNING,
"Basic authentication is only allowed for HTTP 1.0");
1861 QString sCredentials = QByteArray::fromBase64( oList[1].toUtf8() );
1863 oList = sCredentials.split(
':' );
1865 if (oList.count() < 2)
1867 LOG(VB_GENERAL, LOG_WARNING,
"Authorization attempt with invalid number of tokens");
1871 QString sUsername = oList[0];
1872 QString sPassword = oList[1];
1874 if (sUsername ==
"nouser")
1880 LOG(VB_GENERAL, LOG_WARNING,
"Authorization attempt with invalid username");
1884 QString client = QString(
"WebFrontend_%1").arg(
GetPeerAddress());
1890 LOG(VB_GENERAL, LOG_WARNING,
"Authorization attempt with invalid password");
1894 LOG(VB_HTTP, LOG_NOTICE,
"Valid Authorization received");
1911 LOG(VB_HTTP, LOG_NOTICE,
"Attempting HTTP Digest Authentication");
1912 QString realm =
"MythTV";
1914 QString authMethod =
GetLastHeader(
"authorization" ).section(
' ', 0, 0).toLower();
1916 if (authMethod !=
"digest")
1918 LOG(VB_GENERAL, LOG_WARNING,
"Invalid method in Authorization header");
1922 QString parameterStr =
GetLastHeader(
"authorization" ).section(
' ', 1);
1924 QMap<QString, QString> paramMap;
1925 QStringList paramList = parameterStr.split(
',');
1926 QStringList::iterator it;
1927 for (it = paramList.begin(); it != paramList.end(); ++it)
1929 QString key = (*it).section(
'=', 0, 0).toLower().trimmed();
1931 QString value = (*it).section(
'=', 1).trimmed();
1934 paramMap[key] = value;
1937 if (paramMap.size() < 8)
1939 LOG(VB_GENERAL, LOG_WARNING,
"Invalid number of parameters in Authorization header");
1943 if (paramMap[
"nonce"].isEmpty() || paramMap[
"username"].isEmpty() ||
1944 paramMap[
"realm"].isEmpty() || paramMap[
"uri"].isEmpty() ||
1945 paramMap[
"response"].isEmpty() || paramMap[
"qop"].isEmpty() ||
1946 paramMap[
"cnonce"].isEmpty() || paramMap[
"nc"].isEmpty())
1948 LOG(VB_GENERAL, LOG_WARNING,
"Missing required parameters in Authorization header");
1952 if (paramMap[
"username"] ==
"nouser")
1957 LOG(VB_GENERAL, LOG_WARNING,
"Authorization URI doesn't match the "
1963 if (paramMap[
"realm"] != realm)
1965 LOG(VB_GENERAL, LOG_WARNING,
"Authorization realm doesn't match the "
1966 "realm of the requested content");
1970 QByteArray nonce = paramMap[
"nonce"].toLatin1();
1971 if (nonce.length() < 20)
1973 LOG(VB_GENERAL, LOG_WARNING,
"Authorization nonce is too short");
1977 QString nonceTimeStampStr = nonce.left(20);
1980 LOG(VB_GENERAL, LOG_WARNING,
"Authorization nonce doesn't match reference");
1981 LOG(VB_HTTP, LOG_DEBUG, QString(
"%1 vs %2").arg(QString(nonce),
1986 constexpr std::chrono::seconds AUTH_TIMEOUT { 2min };
1988 if (!nonceTimeStamp.isValid())
1990 LOG(VB_GENERAL, LOG_WARNING,
"Authorization nonce timestamp is invalid.");
1991 LOG(VB_HTTP, LOG_DEBUG, QString(
"Timestamp was '%1'").arg(nonceTimeStampStr));
1997 LOG(VB_HTTP, LOG_NOTICE,
"Authorization nonce timestamp is invalid or too old.");
2008 LOG(VB_GENERAL, LOG_WARNING,
"Authorization attempt with invalid username");
2012 if (paramMap[
"response"].length() != 32)
2014 LOG(VB_GENERAL, LOG_WARNING,
"Authorization response field is invalid length");
2022 QString methodDigest = QString(
"%1:%2").arg(
GetRequestType(), paramMap[
"uri"]);
2023 QByteArray a2 = QCryptographicHash::hash(methodDigest.toLatin1(),
2024 QCryptographicHash::Md5).toHex();
2026 QString responseDigest = QString(
"%1:%2:%3:%4:%5:%6").arg(a1,
2032 QByteArray kd = QCryptographicHash::hash(responseDigest.toLatin1(),
2033 QCryptographicHash::Md5).toHex();
2035 if (paramMap[
"response"].toLatin1() == kd)
2037 LOG(VB_HTTP, LOG_NOTICE,
"Valid Authorization received");
2038 QString client = QString(
"WebFrontend_%1").arg(
GetPeerAddress());
2044 LOG(VB_GENERAL, LOG_ERR,
"Valid Authorization received, but we "
2045 "failed to create a valid session");
2058 LOG(VB_GENERAL, LOG_WARNING,
"Authorization attempt with invalid password digest");
2059 LOG(VB_HTTP, LOG_DEBUG, QString(
"Received hash was '%1', calculated hash was '%2'")
2060 .arg(paramMap[
"response"], QString(kd)));
2075 QStringList oList =
GetLastHeader(
"authorization" ).split(
' ' );
2077 if (oList.count() < 2)
2080 if (oList[0].compare(
"basic", Qt::CaseInsensitive ) == 0)
2082 if (oList[0].compare(
"digest", Qt::CaseInsensitive ) == 0)
2106 const QDateTime &expiryDate,
bool secure)
2110 LOG(VB_GENERAL, LOG_WARNING, QString(
"HTTPRequest::SetCookie(%1=%2): "
2111 "A secure cookie cannot be set on an unencrypted connection.")
2112 .arg(sKey, sValue));
2116 QStringList cookieAttributes;
2119 cookieAttributes.append(QString(
"%1=%2").arg(sKey, sValue));
2125 cookieAttributes.append(
"Path=/");
2129 cookieAttributes.append(QString(
"Expires=%1").arg(expires));
2134 cookieAttributes.append(
"Secure");
2137 cookieAttributes.append(
"HttpOnly");
2159 return hostname.section(
"]:", 0 , 0);
2163 return hostname.section(
":", 0 , 0);
2202 type =
"UNSUBSCRIBE";
2240 QStringList allowedOrigins;
2244 serverStatusPort + 10);
2246 QString masterAddrPort = QString(
"%1:%2")
2248 .arg(serverStatusPort);
2249 QString masterTLSAddrPort = QString(
"%1:%2")
2251 .arg(backendSSLPort);
2253 allowedOrigins << QString(
"http://%1").arg(masterAddrPort);
2254 allowedOrigins << QString(
"https://%2").arg(masterTLSAddrPort);
2256 QString localhostname = QHostInfo::localHostName();
2257 if (!localhostname.isEmpty())
2259 allowedOrigins << QString(
"http://%1:%2")
2260 .arg(localhostname).arg(serverStatusPort);
2261 allowedOrigins << QString(
"https://%1:%2")
2262 .arg(localhostname).arg(backendSSLPort);
2265 QStringList allowedOriginsList =
2267 "https://chromecast.mythtv.org")).split(
",");
2269 for (
const auto & origin : qAsConst(allowedOriginsList))
2271 if (origin.isEmpty())
2274 if (origin ==
"*" || (!origin.startsWith(
"http://") &&
2275 !origin.startsWith(
"https://")))
2277 LOG(VB_GENERAL, LOG_ERR, QString(
"Illegal AllowedOriginsList"
2278 " entry '%1'. Must start with http[s]:// and not be *")
2283 allowedOrigins << origin;
2289 for (
const auto & origin : qAsConst(allowedOrigins))
2290 LOG(VB_HTTP, LOG_DEBUG, QString(
"Will allow Origin: %1").arg(origin));
2293 if (allowedOrigins.contains(sOrigin))
2298 LOG(VB_HTTP, LOG_DEBUG, QString(
"Allow-Origin: %1)").arg(sOrigin));
2302 LOG(VB_GENERAL, LOG_CRIT, QString(
"HTTPRequest: Cross-origin request "
2303 "received with origin (%1)")
2321 m_pSocket->state() == QAbstractSocket::ConnectedState)
2330 if ( timer.
elapsed() >= msecs )
2333 LOG(VB_HTTP, LOG_INFO,
"BufferedSocketDeviceRequest::ReadLine() - Exceeded Total Elapsed Wait Time." );
2349 std::chrono::milliseconds msecs)
2352 m_pSocket->state() == QAbstractSocket::ConnectedState)
2355 return(
m_pSocket->read( pData, nMaxLen ));
2357 bool bTimeout =
false;
2360 while ( (
m_pSocket->bytesAvailable() < (
int)nMaxLen) && !bTimeout )
2362 bTimeout = !(
m_pSocket->waitForReadyRead( msecs.count() ));
2364 if ( timer.
elapsed() >= msecs )
2367 LOG(VB_HTTP, LOG_INFO,
"BufferedSocketDeviceRequest::ReadBlock() - Exceeded Total Elapsed Wait Time." );
2373 return(
m_pSocket->read( pData, nMaxLen ));
2385 qint64 bytesWritten = -1;
2387 m_pSocket->state() == QAbstractSocket::ConnectedState)
2389 bytesWritten =
m_pSocket->write( pData, nLen );
2393 return( bytesWritten );
2402 return(
m_pSocket->localAddress().toString() );
2421 return(
m_pSocket->peerAddress().toString() );