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