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