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