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 #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 << "<?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  foreach (const auto & arg, args)
789  {
790  stream << "<" << arg.m_sName;
791 
792  if (arg.m_pAttributes)
793  {
794  foreach (const auto & attr, *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 
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 
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 (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 (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  QStringList params = sParams.split('&', QString::SkipEmptyParts);
1128 
1129  foreach (auto & param, params)
1130  {
1131  QString sName = param.section( '=', 0, 0 );
1132  QString sValue = param.section( '=', 1 );
1133  sValue.replace("+"," ");
1134 
1135  if (!sName.isEmpty())
1136  {
1137  sName = QUrl::fromPercentEncoding(sName.toUtf8());
1138  sValue = QUrl::fromPercentEncoding(sValue.toUtf8());
1139 
1140  mapParams.insert( sName.trimmed(), sValue );
1141  nCount++;
1142  }
1143  }
1144  }
1145 
1146  return nCount;
1147 }
1148 
1149 
1151 //
1153 
1154 QString HTTPRequest::GetRequestHeader( const QString &sKey, QString sDefault )
1155 {
1156  QStringMap::iterator it = m_mapHeaders.find( sKey.toLower() );
1157 
1158  if ( it == m_mapHeaders.end())
1159  return( sDefault );
1160 
1161  return *it;
1162 }
1163 
1164 
1166 //
1168 
1170 {
1171  QString sHeader = s_szServerHeaders;
1172 
1173  for ( QStringMap::iterator it = m_mapRespHeaders.begin();
1174  it != m_mapRespHeaders.end();
1175  ++it )
1176  {
1177  sHeader += it.key() + ": ";
1178  sHeader += *it + "\r\n";
1179  }
1180 
1181  return( sHeader );
1182 }
1183 
1185 //
1187 
1189 {
1190  // TODO: Think about whether we should use a longer timeout if the client
1191  // has explicitly specified 'Keep-alive'
1192 
1193  // HTTP 1.1 ... server may assume keep-alive
1194  bool bKeepAlive = true;
1195 
1196  // if HTTP/1.0... must default to false
1197  if ((m_nMajor == 1) && (m_nMinor == 0))
1198  bKeepAlive = false;
1199 
1200  // Read Connection Header to see whether the client has explicitly
1201  // asked for the connection to be kept alive or closed after the response
1202  // is sent
1203  QString sConnection = GetRequestHeader( "connection", "default" ).toLower();
1204 
1205  QStringList sValueList = sConnection.split(",");
1206 
1207  if ( sValueList.contains("close") )
1208  {
1209  LOG(VB_HTTP, LOG_DEBUG, "Client requested the connection be closed");
1210  bKeepAlive = false;
1211  }
1212  else if (sValueList.contains("keep-alive"))
1213  bKeepAlive = true;
1214 
1215  return bKeepAlive;
1216 }
1217 
1219 //
1221 
1223 {
1224  QStringList sCookieList = m_mapHeaders.values("cookie");
1225 
1226  QStringList::iterator it;
1227  for (it = sCookieList.begin(); it != sCookieList.end(); ++it)
1228  {
1229  QString key = (*it).section('=', 0, 0);
1230  QString value = (*it).section('=', 1);
1231 
1232  m_mapCookies.insert(key, value);
1233  }
1234 }
1235 
1237 //
1239 
1241 {
1242  bool bSuccess = false;
1243 
1244  try
1245  {
1246  // Read first line to determine requestType
1247  QString sRequestLine = ReadLine( 2000 );
1248 
1249  if ( sRequestLine.isEmpty() )
1250  {
1251  LOG(VB_GENERAL, LOG_ERR, "Timeout reading first line of request." );
1252  return false;
1253  }
1254 
1255  // -=>TODO: Should read lines until a valid request???
1256  ProcessRequestLine( sRequestLine );
1257 
1258  if (m_nMajor > 1 || m_nMajor < 0)
1259  {
1260  m_eResponseType = ResponseTypeHTML;
1261  m_nResponseStatus = 505;
1262  m_response.write( GetResponsePage() );
1263 
1264  return true;
1265  }
1266 
1267  if (m_eType == RequestTypeUnknown)
1268  {
1269  m_eResponseType = ResponseTypeHTML;
1270  m_nResponseStatus = 501; // Not Implemented
1271  // Conservative list, we can't really know what methods we
1272  // actually allow for an arbitrary resource without some sort of
1273  // high maintenance database
1274  SetResponseHeader("Allow", "GET, HEAD");
1275  m_response.write( GetResponsePage() );
1276  return true;
1277  }
1278 
1279  // Read Header
1280  bool bDone = false;
1281  QString sLine = ReadLine( 2000 );
1282 
1283  while (( !sLine.isEmpty() ) && !bDone )
1284  {
1285  if (sLine != "\r\n")
1286  {
1287  QString sName = sLine.section( ':', 0, 0 ).trimmed();
1288  QString sValue = sLine.section( ':', 1 );
1289 
1290  sValue.truncate( sValue.length() - 2 );
1291 
1292  if (!sName.isEmpty() && !sValue.isEmpty())
1293  {
1294  m_mapHeaders.insertMulti(sName.toLower(), sValue.trimmed());
1295  }
1296 
1297  sLine = ReadLine( 2000 );
1298  }
1299  else
1300  bDone = true;
1301  }
1302 
1303  // Dump request header
1304  for ( QStringMap::iterator it = m_mapHeaders.begin();
1305  it != m_mapHeaders.end();
1306  ++it )
1307  {
1308  LOG(VB_HTTP, LOG_INFO, QString("(Request Header) %1: %2")
1309  .arg(it.key()).arg(*it));
1310  }
1311 
1312  // Parse Cookies
1313  ParseCookies();
1314 
1315  // Parse out keep alive
1316  m_bKeepAlive = ParseKeepAlive();
1317 
1318  // Check to see if we found the end of the header or we timed out.
1319  if (!bDone)
1320  {
1321  LOG(VB_GENERAL, LOG_INFO, "Timeout waiting for request header." );
1322  return false;
1323  }
1324 
1325  // HTTP/1.1 requires that the Host header be present, even if empty
1326  if ((m_nMinor == 1) && !m_mapHeaders.contains("host"))
1327  {
1328  m_eResponseType = ResponseTypeHTML;
1329  m_nResponseStatus = 400;
1330  m_response.write( GetResponsePage() );
1331 
1332  return true;
1333  }
1334 
1335  // Destroy session if requested
1336  if (m_mapHeaders.contains("x-myth-clear-session"))
1337  {
1338  SetCookie("sessionToken", "", MythDate::current().addDays(-2), true);
1339  m_mapCookies.remove("sessionToken");
1340  }
1341 
1342  // Allow session resumption for TLS connections
1343  if (m_mapCookies.contains("sessionToken"))
1344  {
1345  QString sessionToken = m_mapCookies["sessionToken"];
1346  MythSessionManager *sessionManager = gCoreContext->GetSessionManager();
1347  MythUserSession session = sessionManager->GetSession(sessionToken);
1348 
1349  if (session.IsValid())
1350  m_userSession = session;
1351  }
1352 
1353  if (IsUrlProtected( m_sBaseUrl ))
1354  {
1355  if (!Authenticated())
1356  {
1357  m_eResponseType = ResponseTypeHTML;
1358  m_nResponseStatus = 401;
1359  m_response.write( GetResponsePage() );
1360  // Since this may not be the first attempt at authentication,
1361  // Authenticated may have set the header with the appropriate
1362  // stale attribute
1363  SetResponseHeader("WWW-Authenticate", GetAuthenticationHeader(false));
1364 
1365  return true;
1366  }
1367 
1368  m_bProtected = true;
1369  }
1370 
1371  bSuccess = true;
1372 
1373  SetContentType( m_mapHeaders[ "content-type" ] );
1374  // Lets load payload if any.
1375  long nPayloadSize = m_mapHeaders[ "content-length" ].toLong();
1376 
1377  if (nPayloadSize > 0)
1378  {
1379  char *pszPayload = new char[ nPayloadSize + 2 ];
1380  long nBytes = 0;
1381 
1382  nBytes = ReadBlock( pszPayload, nPayloadSize, 5000 );
1383  if (nBytes == nPayloadSize )
1384  {
1385  m_sPayload = QString::fromUtf8( pszPayload, nPayloadSize );
1386 
1387  // See if the payload is just data from a form post
1388  if (m_eContentType == ContentType_Urlencoded)
1389  GetParameters( m_sPayload, m_mapParams );
1390  }
1391  else
1392  {
1393  LOG(VB_GENERAL, LOG_ERR,
1394  QString("Unable to read entire payload (read %1 of %2 bytes)")
1395  .arg( nBytes ) .arg( nPayloadSize ) );
1396  bSuccess = false;
1397  }
1398 
1399  delete [] pszPayload;
1400  }
1401 
1402  // Check to see if this is a SOAP encoded message
1403  QString sSOAPAction = GetRequestHeader( "SOAPACTION", "" );
1404 
1405  if (!sSOAPAction.isEmpty())
1406  bSuccess = ProcessSOAPPayload( sSOAPAction );
1407  else
1408  ExtractMethodFromURL();
1409 
1410 #if 0
1411  if (m_sMethod != "*" )
1412  LOG(VB_HTTP, LOG_DEBUG,
1413  QString("HTTPRequest::ParseRequest - Socket (%1) Base (%2) "
1414  "Method (%3) - Bytes in Socket Buffer (%4)")
1415  .arg(getSocketHandle()) .arg(m_sBaseUrl)
1416  .arg(m_sMethod) .arg(BytesAvailable()));
1417 #endif
1418  }
1419  catch(...)
1420  {
1421  LOG(VB_GENERAL, LOG_WARNING,
1422  "Unexpected exception in HTTPRequest::ParseRequest" );
1423  }
1424 
1425  return bSuccess;
1426 }
1427 
1429 //
1431 
1432 void HTTPRequest::ProcessRequestLine( const QString &sLine )
1433 {
1434  m_sRawRequest = sLine;
1435 
1436  QString sToken;
1437  QStringList tokens = sLine.split(m_procReqLineExp, QString::SkipEmptyParts);
1438  int nCount = tokens.count();
1439 
1440  // ----------------------------------------------------------------------
1441 
1442  if ( sLine.startsWith( QString("HTTP/") ))
1443  m_eType = RequestTypeResponse;
1444  else
1445  m_eType = RequestTypeUnknown;
1446 
1447  // ----------------------------------------------------------------------
1448  // if this is actually a response, then sLine's format will be:
1449  // HTTP/m.n <response code> <response text>
1450  // otherwise:
1451  // <method> <Resource URI> HTTP/m.n
1452  // ----------------------------------------------------------------------
1453 
1454  if (m_eType != RequestTypeResponse)
1455  {
1456  // ------------------------------------------------------------------
1457  // Process as a request
1458  // ------------------------------------------------------------------
1459 
1460  if (nCount > 0)
1461  SetRequestType( tokens[0].trimmed() );
1462 
1463  if (nCount > 1)
1464  {
1465  m_sOriginalUrl = tokens[1].toUtf8(); // Used by authorization check
1466  m_sRequestUrl = QUrl::fromPercentEncoding(tokens[1].toUtf8());
1467  m_sBaseUrl = m_sRequestUrl.section( '?', 0, 0).trimmed();
1468 
1469  m_sResourceUrl = m_sBaseUrl; // Save complete url without parameters
1470 
1471  // Process any Query String Parameters
1472  QString sQueryStr = tokens[1].section( '?', 1, 1 );
1473 
1474  if (!sQueryStr.isEmpty())
1475  GetParameters( sQueryStr, m_mapParams );
1476  }
1477 
1478  if (nCount > 2)
1479  SetRequestProtocol( tokens[2].trimmed() );
1480  }
1481  else
1482  {
1483  // ------------------------------------------------------------------
1484  // Process as a Response
1485  // ------------------------------------------------------------------
1486  if (nCount > 0)
1487  SetRequestProtocol( tokens[0].trimmed() );
1488 
1489  if (nCount > 1)
1490  m_nResponseStatus = tokens[1].toInt();
1491  }
1492 
1493 
1494 }
1495 
1497 //
1499 
1500 bool HTTPRequest::ParseRange( QString sRange,
1501  long long llSize,
1502  long long *pllStart,
1503  long long *pllEnd )
1504 {
1505  // ----------------------------------------------------------------------
1506  // -=>TODO: Only handle 1 range at this time...
1507  // should make work with full spec.
1508  // ----------------------------------------------------------------------
1509 
1510  if (sRange.isEmpty())
1511  return false;
1512 
1513  // ----------------------------------------------------------------------
1514  // remove any "bytes="
1515  // ----------------------------------------------------------------------
1516  int nIdx = sRange.indexOf(m_parseRangeExp);
1517 
1518  if (nIdx < 0)
1519  return false;
1520 
1521  if (nIdx > 0)
1522  sRange.remove( 0, nIdx );
1523 
1524  // ----------------------------------------------------------------------
1525  // Split multiple ranges
1526  // ----------------------------------------------------------------------
1527 
1528  QStringList ranges = sRange.split(',', QString::SkipEmptyParts);
1529 
1530  if (ranges.count() == 0)
1531  return false;
1532 
1533  // ----------------------------------------------------------------------
1534  // Split first range into its components
1535  // ----------------------------------------------------------------------
1536 
1537  QStringList parts = ranges[0].split('-');
1538 
1539  if (parts.count() != 2)
1540  return false;
1541 
1542  if (parts[0].isEmpty() && parts[1].isEmpty())
1543  return false;
1544 
1545  // ----------------------------------------------------------------------
1546  //
1547  // ----------------------------------------------------------------------
1548 
1549  bool conv_ok = false;
1550  if (parts[0].isEmpty())
1551  {
1552  // ------------------------------------------------------------------
1553  // Does it match "-####"
1554  // ------------------------------------------------------------------
1555 
1556  long long llValue = parts[1].toLongLong(&conv_ok);
1557  if (!conv_ok) return false;
1558 
1559  *pllStart = llSize - llValue;
1560  *pllEnd = llSize - 1;
1561  }
1562  else if (parts[1].isEmpty())
1563  {
1564  // ------------------------------------------------------------------
1565  // Does it match "####-"
1566  // ------------------------------------------------------------------
1567 
1568  *pllStart = parts[0].toLongLong(&conv_ok);
1569 
1570  if (!conv_ok)
1571  return false;
1572 
1573  *pllEnd = llSize - 1;
1574  }
1575  else
1576  {
1577  // ------------------------------------------------------------------
1578  // Must be "####-####"
1579  // ------------------------------------------------------------------
1580 
1581  *pllStart = parts[0].toLongLong(&conv_ok);
1582  if (!conv_ok) return false;
1583  *pllEnd = parts[1].toLongLong(&conv_ok);
1584  if (!conv_ok) return false;
1585 
1586  if (*pllStart > *pllEnd)
1587  return false;
1588  }
1589 
1590  LOG(VB_HTTP, LOG_DEBUG, QString("%1 Range Requested %2 - %3")
1591  .arg(getSocketHandle()) .arg(*pllStart) .arg(*pllEnd));
1592 
1593  return true;
1594 }
1595 
1597 //
1599 
1601 {
1602  // Strip out leading http://192.168.1.1:6544/ -> /
1603  // Should fix #8678
1604  // FIXME what about https?
1605  QRegExp sRegex("^http://.*/");
1606  sRegex.setMinimal(true);
1607  m_sBaseUrl.replace(sRegex, "/");
1608 
1609  QStringList sList = m_sBaseUrl.split('/', QString::SkipEmptyParts);
1610 
1611  m_sMethod = "";
1612 
1613  if (!sList.isEmpty())
1614  {
1615  m_sMethod = sList.last();
1616  sList.pop_back();
1617  }
1618 
1619  m_sBaseUrl = '/' + sList.join( "/" );
1620  LOG(VB_HTTP, LOG_INFO, QString("ExtractMethodFromURL(end) : %1 : %2")
1621  .arg(m_sMethod).arg(m_sBaseUrl));
1622 }
1623 
1625 //
1627 
1628 bool HTTPRequest::ProcessSOAPPayload( const QString &sSOAPAction )
1629 {
1630  bool bSuccess = false;
1631 
1632  // ----------------------------------------------------------------------
1633  // Open Supplied XML uPnp Description file.
1634  // ----------------------------------------------------------------------
1635 
1636  LOG(VB_HTTP, LOG_INFO,
1637  QString("HTTPRequest::ProcessSOAPPayload : %1 : ").arg(sSOAPAction));
1638  QDomDocument doc ( "request" );
1639 
1640  QString sErrMsg;
1641  int nErrLine = 0;
1642  int nErrCol = 0;
1643 
1644  if (!doc.setContent( m_sPayload, true, &sErrMsg, &nErrLine, &nErrCol ))
1645  {
1646  LOG(VB_GENERAL, LOG_ERR,
1647  QString( "Error parsing request at line: %1 column: %2 : %3" )
1648  .arg(nErrLine) .arg(nErrCol) .arg(sErrMsg));
1649  return( false );
1650  }
1651 
1652  // --------------------------------------------------------------
1653  // XML Document Loaded... now parse it
1654  // --------------------------------------------------------------
1655 
1656  QString sService;
1657 
1658  if (sSOAPAction.contains( '#' ))
1659  {
1660  m_sNameSpace = sSOAPAction.section( '#', 0, 0).remove( 0, 1);
1661  m_sMethod = sSOAPAction.section( '#', 1 );
1662  m_sMethod.remove( m_sMethod.length()-1, 1 );
1663  }
1664  else
1665  {
1666  if (sSOAPAction.contains( '/' ))
1667  {
1668  int nPos = sSOAPAction.lastIndexOf( '/' );
1669  m_sNameSpace = sSOAPAction.mid(1, nPos);
1670  m_sMethod = sSOAPAction.mid(nPos + 1,
1671  sSOAPAction.length() - nPos - 2);
1672 
1673  nPos = m_sNameSpace.lastIndexOf( '/', -2);
1674  sService = m_sNameSpace.mid(nPos + 1,
1675  m_sNameSpace.length() - nPos - 2);
1676  m_sNameSpace = m_sNameSpace.mid( 0, nPos );
1677  }
1678  else
1679  {
1680  m_sNameSpace.clear();
1681  m_sMethod = sSOAPAction;
1682  m_sMethod.remove( QChar( '\"' ) );
1683  }
1684  }
1685 
1686  QDomNodeList oNodeList = doc.elementsByTagNameNS( m_sNameSpace, m_sMethod );
1687 
1688  if (oNodeList.count() == 0)
1689  {
1690  oNodeList =
1691  doc.elementsByTagNameNS("http://schemas.xmlsoap.org/soap/envelope/",
1692  "Body");
1693  }
1694 
1695  if (oNodeList.count() > 0)
1696  {
1697  QDomNode oMethod = oNodeList.item(0);
1698 
1699  if (!oMethod.isNull())
1700  {
1701  m_bSOAPRequest = true;
1702 
1703  for ( QDomNode oNode = oMethod.firstChild(); !oNode.isNull();
1704  oNode = oNode.nextSibling() )
1705  {
1706  QDomElement e = oNode.toElement();
1707 
1708  if (!e.isNull())
1709  {
1710  QString sName = e.tagName();
1711  QString sValue = "";
1712 
1713  QDomText oText = oNode.firstChild().toText();
1714 
1715  if (!oText.isNull())
1716  sValue = oText.nodeValue();
1717 
1718  sName = QUrl::fromPercentEncoding(sName.toUtf8());
1719  sValue = QUrl::fromPercentEncoding(sValue.toUtf8());
1720 
1721  m_mapParams.insert( sName.trimmed().toLower(), sValue );
1722  }
1723  }
1724 
1725  bSuccess = true;
1726  }
1727  }
1728 
1729  return bSuccess;
1730 }
1731 
1733 //
1735 
1737 {
1738  Serializer *pSerializer = nullptr;
1739 
1740  if (m_bSOAPRequest)
1741  {
1742  pSerializer = (Serializer *)new SoapSerializer(&m_response,
1743  m_sNameSpace, m_sMethod);
1744  }
1745  else
1746  {
1747  QString sAccept = GetRequestHeader( "Accept", "*/*" );
1748 
1749  if (sAccept.contains( "application/json", Qt::CaseInsensitive ) ||
1750  sAccept.contains( "text/javascript", Qt::CaseInsensitive ))
1751  {
1752  pSerializer = (Serializer *)new JSONSerializer(&m_response,
1753  m_sMethod);
1754  }
1755  else if (sAccept.contains( "text/x-apple-plist+xml", Qt::CaseInsensitive ))
1756  {
1757  pSerializer = (Serializer *)new XmlPListSerializer(&m_response);
1758  }
1759  }
1760 
1761  // Default to XML
1762 
1763  if (pSerializer == nullptr)
1764  pSerializer = (Serializer *)new XmlSerializer(&m_response, m_sMethod);
1765 
1766  return pSerializer;
1767 }
1768 
1770 //
1772 
1773 QString HTTPRequest::Encode(const QString &sIn)
1774 {
1775  QString sStr = sIn;
1776 #if 0
1777  LOG(VB_HTTP, LOG_DEBUG,
1778  QString("HTTPRequest::Encode Input : %1").arg(sStr));
1779 #endif
1780  sStr.replace('&', "&amp;" ); // This _must_ come first
1781  sStr.replace('<', "&lt;" );
1782  sStr.replace('>', "&gt;" );
1783  sStr.replace('"', "&quot;");
1784  sStr.replace("'", "&apos;");
1785 
1786 #if 0
1787  LOG(VB_HTTP, LOG_DEBUG,
1788  QString("HTTPRequest::Encode Output : %1").arg(sStr));
1789 #endif
1790  return sStr;
1791 }
1792 
1794 //
1796 
1797 QString HTTPRequest::Decode(const QString& sIn)
1798 {
1799  QString sStr = sIn;
1800  sStr.replace("&amp;", "&");
1801  sStr.replace("&lt;", "<");
1802  sStr.replace("&gt;", ">");
1803  sStr.replace("&quot;", "\"");
1804  sStr.replace("&apos;", "'");
1805 
1806  return sStr;
1807 }
1808 
1810 //
1812 
1813 QString HTTPRequest::GetETagHash(const QByteArray &data)
1814 {
1815  QByteArray hash = QCryptographicHash::hash( data.data(), QCryptographicHash::Sha1);
1816 
1817  return ("\"" + hash.toHex() + "\"");
1818 }
1819 
1821 //
1823 
1824 bool HTTPRequest::IsUrlProtected( const QString &sBaseUrl )
1825 {
1826  QString sProtected = UPnp::GetConfiguration()->GetValue( "HTTP/Protected/Urls", "/setup;/Config" );
1827 
1828  QStringList oList = sProtected.split( ';' );
1829 
1830  for( int nIdx = 0; nIdx < oList.count(); nIdx++)
1831  {
1832  if (sBaseUrl.startsWith( oList[nIdx], Qt::CaseInsensitive ))
1833  return true;
1834  }
1835 
1836  return false;
1837 }
1838 
1840 //
1842 
1844 {
1845  QString authHeader;
1846 
1847  // For now we support a single realm, that will change
1848  QString realm = "MythTV";
1849 
1850  // Always use digest authentication where supported, it may be available
1851  // with HTTP 1.0 client as an extension, but we can't tell if that's the
1852  // case. It's guaranteed to be available for HTTP 1.1+
1853  if (m_nMajor >= 1 && m_nMinor > 0)
1854  {
1855  QString nonce = CalculateDigestNonce(MythDate::current_iso_string());
1856  QString stale = isStale ? "true" : "false"; // FIXME
1857  authHeader = QString("Digest realm=\"%1\",nonce=\"%2\","
1858  "qop=\"auth\",stale=\"%3\",algorithm=\"MD5\"")
1859  .arg(realm).arg(nonce).arg(stale);
1860  }
1861  else
1862  {
1863  authHeader = QString("Basic realm=\"%1\"").arg(realm);
1864  }
1865 
1866  return authHeader;
1867 }
1868 
1870 //
1872 
1873 QString HTTPRequest::CalculateDigestNonce(const QString& timeStamp)
1874 {
1875  QString uniqueID = QString("%1:%2").arg(timeStamp).arg(m_sPrivateToken);
1876  QString hash = QCryptographicHash::hash( uniqueID.toLatin1(), QCryptographicHash::Sha1).toHex(); // TODO: Change to Sha2 with QT5?
1877  QString nonce = QString("%1%2").arg(timeStamp).arg(hash); // Note: since this is going in a header it should avoid illegal chars
1878  return nonce;
1879 }
1880 
1882 //
1884 
1886 {
1887  LOG(VB_HTTP, LOG_NOTICE, "Attempting HTTP Basic Authentication");
1888  QStringList oList = m_mapHeaders[ "authorization" ].split( ' ' );
1889 
1890  if (m_nMajor == 1 && m_nMinor == 0) // We only support Basic auth for http 1.0 clients
1891  {
1892  LOG(VB_GENERAL, LOG_WARNING, "Basic authentication is only allowed for HTTP 1.0");
1893  return false;
1894  }
1895 
1896  QString sCredentials = QByteArray::fromBase64( oList[1].toUtf8() );
1897 
1898  oList = sCredentials.split( ':' );
1899 
1900  if (oList.count() < 2)
1901  {
1902  LOG(VB_GENERAL, LOG_WARNING, "Authorization attempt with invalid number of tokens");
1903  return false;
1904  }
1905 
1906  QString sUsername = oList[0];
1907  QString sPassword = oList[1];
1908 
1909  if (sUsername == "nouser") // Special logout username
1910  return false;
1911 
1912  MythSessionManager *sessionManager = gCoreContext->GetSessionManager();
1913  if (!MythSessionManager::IsValidUser(sUsername))
1914  {
1915  LOG(VB_GENERAL, LOG_WARNING, "Authorization attempt with invalid username");
1916  return false;
1917  }
1918 
1919  QString client = QString("WebFrontend_%1").arg(GetPeerAddress());
1920  MythUserSession session = sessionManager->LoginUser(sUsername, sPassword,
1921  client);
1922 
1923  if (!session.IsValid())
1924  {
1925  LOG(VB_GENERAL, LOG_WARNING, "Authorization attempt with invalid password");
1926  return false;
1927  }
1928 
1929  LOG(VB_HTTP, LOG_NOTICE, "Valid Authorization received");
1930 
1931  if (IsEncrypted()) // Only set a session cookie for encrypted connections, not safe otherwise
1932  SetCookie("sessionToken", session.GetSessionToken(),
1933  session.GetSessionExpires(), true);
1934 
1935  m_userSession = session;
1936 
1937  return false;
1938 }
1939 
1941 //
1943 
1945 {
1946  LOG(VB_HTTP, LOG_NOTICE, "Attempting HTTP Digest Authentication");
1947  QString realm = "MythTV"; // TODO Check which realm applies for the request path
1948 
1949  QString authMethod = m_mapHeaders[ "authorization" ].section(' ', 0, 0).toLower();
1950 
1951  if (authMethod != "digest")
1952  {
1953  LOG(VB_GENERAL, LOG_WARNING, "Invalid method in Authorization header");
1954  return false;
1955  }
1956 
1957  QString parameterStr = m_mapHeaders[ "authorization" ].section(' ', 1);
1958 
1959  QMap<QString, QString> paramMap;
1960  QStringList paramList = parameterStr.split(',');
1961  QStringList::iterator it;
1962  for (it = paramList.begin(); it != paramList.end(); ++it)
1963  {
1964  QString key = (*it).section('=', 0, 0).toLower().trimmed();
1965  // Since the value may contain '=' return everything after first occurence
1966  QString value = (*it).section('=', 1).trimmed();
1967  // Remove any quotes surrounding the value
1968  value.remove("\"");
1969  paramMap[key] = value;
1970  }
1971 
1972  if (paramMap.size() < 8)
1973  {
1974  LOG(VB_GENERAL, LOG_WARNING, "Invalid number of parameters in Authorization header");
1975  return false;
1976  }
1977 
1978  if (paramMap["nonce"].isEmpty() || paramMap["username"].isEmpty() ||
1979  paramMap["realm"].isEmpty() || paramMap["uri"].isEmpty() ||
1980  paramMap["response"].isEmpty() || paramMap["qop"].isEmpty() ||
1981  paramMap["cnonce"].isEmpty() || paramMap["nc"].isEmpty())
1982  {
1983  LOG(VB_GENERAL, LOG_WARNING, "Missing required parameters in Authorization header");
1984  return false;
1985  }
1986 
1987  if (paramMap["username"] == "nouser") // Special logout username
1988  return false;
1989 
1990  if (paramMap["uri"] != m_sOriginalUrl)
1991  {
1992  LOG(VB_GENERAL, LOG_WARNING, "Authorization URI doesn't match the "
1993  "request URI");
1994  m_nResponseStatus = 400; // Bad Request
1995  return false;
1996  }
1997 
1998  if (paramMap["realm"] != realm)
1999  {
2000  LOG(VB_GENERAL, LOG_WARNING, "Authorization realm doesn't match the "
2001  "realm of the requested content");
2002  return false;
2003  }
2004 
2005  QByteArray nonce = paramMap["nonce"].toLatin1();
2006  if (nonce.length() < 20)
2007  {
2008  LOG(VB_GENERAL, LOG_WARNING, "Authorization nonce is too short");
2009  return false;
2010  }
2011 
2012  QString nonceTimeStampStr = nonce.left(20); // ISO 8601 fixed length
2013  if (nonce != CalculateDigestNonce(nonceTimeStampStr))
2014  {
2015  LOG(VB_GENERAL, LOG_WARNING, "Authorization nonce doesn't match reference");
2016  LOG(VB_HTTP, LOG_DEBUG, QString("%1 vs %2").arg(QString(nonce))
2017  .arg(CalculateDigestNonce(nonceTimeStampStr)));
2018  return false;
2019  }
2020 
2021  const int AUTH_TIMEOUT = 2 * 60; // 2 Minute timeout to login, to reduce replay attack window
2022  QDateTime nonceTimeStamp = MythDate::fromString(nonceTimeStampStr);
2023  if (!nonceTimeStamp.isValid())
2024  {
2025  LOG(VB_GENERAL, LOG_WARNING, "Authorization nonce timestamp is invalid.");
2026  LOG(VB_HTTP, LOG_DEBUG, QString("Timestamp was '%1'").arg(nonceTimeStampStr));
2027  return false;
2028  }
2029 
2030  if (nonceTimeStamp.secsTo(MythDate::current()) > AUTH_TIMEOUT)
2031  {
2032  LOG(VB_HTTP, LOG_NOTICE, "Authorization nonce timestamp is invalid or too old.");
2033  // Tell the client that the submitted nonce has expired at which
2034  // point they may wish to try again with a fresh nonce instead of
2035  // telling the user that their credentials were invalid
2036  SetResponseHeader("WWW-Authenticate", GetAuthenticationHeader(true), true);
2037  return false;
2038  }
2039 
2040  MythSessionManager *sessionManager = gCoreContext->GetSessionManager();
2041  if (!MythSessionManager::IsValidUser(paramMap["username"]))
2042  {
2043  LOG(VB_GENERAL, LOG_WARNING, "Authorization attempt with invalid username");
2044  return false;
2045  }
2046 
2047  if (paramMap["response"].length() != 32)
2048  {
2049  LOG(VB_GENERAL, LOG_WARNING, "Authorization response field is invalid length");
2050  return false;
2051  }
2052 
2053  // If you're still reading this, well done, not far to go now
2054 
2055  QByteArray a1 = MythSessionManager::GetPasswordDigest(paramMap["username"]).toLatin1();
2056  //QByteArray a1 = "bcd911b2ecb15ffbd6d8e6e744d60cf6";
2057  QString methodDigest = QString("%1:%2").arg(GetRequestType()).arg(paramMap["uri"]);
2058  QByteArray a2 = QCryptographicHash::hash(methodDigest.toLatin1(),
2059  QCryptographicHash::Md5).toHex();
2060 
2061  QString responseDigest = QString("%1:%2:%3:%4:%5:%6").arg(QString(a1))
2062  .arg(paramMap["nonce"])
2063  .arg(paramMap["nc"])
2064  .arg(paramMap["cnonce"])
2065  .arg(paramMap["qop"])
2066  .arg(QString(a2));
2067  QByteArray kd = QCryptographicHash::hash(responseDigest.toLatin1(),
2068  QCryptographicHash::Md5).toHex();
2069 
2070  if (paramMap["response"].toLatin1() == kd)
2071  {
2072  LOG(VB_HTTP, LOG_NOTICE, "Valid Authorization received");
2073  QString client = QString("WebFrontend_%1").arg(GetPeerAddress());
2074  MythUserSession session = sessionManager->LoginUser(paramMap["username"],
2075  a1,
2076  client);
2077  if (!session.IsValid())
2078  {
2079  LOG(VB_GENERAL, LOG_ERR, "Valid Authorization received, but we "
2080  "failed to create a valid session");
2081  return false;
2082  }
2083 
2084  if (IsEncrypted()) // Only set a session cookie for encrypted connections, not safe otherwise
2085  SetCookie("sessionToken", session.GetSessionToken(),
2086  session.GetSessionExpires(), true);
2087 
2088  m_userSession = session;
2089 
2090  return true;
2091  }
2092 
2093  LOG(VB_GENERAL, LOG_WARNING, "Authorization attempt with invalid password digest");
2094  LOG(VB_HTTP, LOG_DEBUG, QString("Received hash was '%1', calculated hash was '%2'")
2095  .arg(paramMap["response"])
2096  .arg(QString(kd)));
2097 
2098  return false;
2099 }
2100 
2102 //
2104 
2106 {
2107  // Check if the existing user has permission to access this resource
2108  if (m_userSession.IsValid()) //m_userSession.CheckPermission())
2109  return true;
2110 
2111  QStringList oList = m_mapHeaders[ "authorization" ].split( ' ' );
2112 
2113  if (oList.count() < 2)
2114  return false;
2115 
2116  if (oList[0].compare( "basic", Qt::CaseInsensitive ) == 0)
2117  return BasicAuthentication();
2118  if (oList[0].compare( "digest", Qt::CaseInsensitive ) == 0)
2119  return DigestAuthentication();
2120 
2121  return false;
2122 }
2123 
2125 //
2127 
2128 void HTTPRequest::SetResponseHeader(const QString& sKey, const QString& sValue,
2129  bool replace)
2130 {
2131  if (!replace && m_mapRespHeaders.contains(sKey))
2132  return;
2133 
2134  m_mapRespHeaders[sKey] = sValue;
2135 }
2136 
2138 //
2140 
2141 void HTTPRequest::SetCookie(const QString &sKey, const QString &sValue,
2142  const QDateTime &expiryDate, bool secure)
2143 {
2144  if (secure && !IsEncrypted())
2145  {
2146  LOG(VB_GENERAL, LOG_WARNING, QString("HTTPRequest::SetCookie(%1=%2): "
2147  "A secure cookie cannot be set on an unencrypted connection.")
2148  .arg(sKey).arg(sValue));
2149  return;
2150  }
2151 
2152  QStringList cookieAttributes;
2153 
2154  // Key=Value
2155  cookieAttributes.append(QString("%1=%2").arg(sKey).arg(sValue));
2156 
2157  // Domain - Most browsers have problems with a hostname, so it's better to omit this
2158 // cookieAttributes.append(QString("Domain=%1").arg(GetHostName()));
2159 
2160  // Path - Fix to root, no call for restricting to other paths yet
2161  cookieAttributes.append("Path=/");
2162 
2163  // Expires - Expiry date, always set one, just good practice
2164  QString expires = MythDate::toString(expiryDate, MythDate::kRFC822); // RFC 822
2165  cookieAttributes.append(QString("Expires=%1").arg(expires)); // Cookie Expiry date
2166 
2167  // Secure - Only send this cookie over encrypted connections, it contains
2168  // sensitive info SECURITY
2169  if (secure)
2170  cookieAttributes.append("Secure");
2171 
2172  // HttpOnly - No cookie stealing javascript SECURITY
2173  cookieAttributes.append("HttpOnly");
2174 
2175  SetResponseHeader("Set-Cookie", cookieAttributes.join("; "));
2176 }
2177 
2179 //
2181 
2183 {
2184  // TODO: This only deals with the HTTP 1.1 case, 1.0 should be rare but we
2185  // should probably still handle it
2186 
2187  // RFC 3875 - The is the hostname or ip address in the client request, not
2188  // the name or ip we might otherwise know for this server
2189  QString hostname = m_mapHeaders["host"];
2190  if (!hostname.isEmpty())
2191  {
2192  // Strip the port
2193  if (hostname.contains("]:")) // IPv6 port
2194  {
2195  return hostname.section("]:", 0 , 0);
2196  }
2197  if (hostname.contains(":")) // IPv4 port
2198  {
2199  return hostname.section(":", 0 , 0);
2200  }
2201  return hostname;
2202  }
2203 
2204  return GetHostAddress();
2205 }
2206 
2207 
2209 {
2210  QString type;
2211  switch ( m_eType )
2212  {
2213  case RequestTypeUnknown :
2214  type = "UNKNOWN";
2215  break;
2216  case RequestTypeGet :
2217  type = "GET";
2218  break;
2219  case RequestTypeHead :
2220  type = "HEAD";
2221  break;
2222  case RequestTypePost :
2223  type = "POST";
2224  break;
2225  case RequestTypeOptions:
2226  type = "OPTIONS";
2227  break;
2228  case RequestTypeMSearch:
2229  type = "M-SEARCH";
2230  break;
2231  case RequestTypeNotify:
2232  type = "NOTIFY";
2233  break;
2234  case RequestTypeSubscribe :
2235  type = "SUBSCRIBE";
2236  break;
2237  case RequestTypeUnsubscribe :
2238  type = "UNSUBSCRIBE";
2239  break;
2240  case RequestTypeResponse :
2241  type = "RESPONSE";
2242  break;
2243  }
2244 
2245  return type;
2246 }
2247 
2248 void HTTPRequest::AddCORSHeaders( const QString &sOrigin )
2249 {
2250  // ----------------------------------------------------------------------
2251  // SECURITY: Access-Control-Allow-Origin Wildcard
2252  //
2253  // This is a REALLY bad idea, so bad in fact that I'm including it here but
2254  // commented out in the hope that anyone thinking of adding it in the future
2255  // will see it and then read this comment.
2256  //
2257  // Browsers do not verify that the origin is on the same network. This means
2258  // that a malicious script embedded or included into ANY webpage you visit
2259  // could then access servers on your local network including MythTV. They
2260  // can grab data, delete data including recordings and videos, schedule
2261  // recordings and generally ruin your day.
2262  //
2263  // This might seem paranoid and a remote possibility, but then that's how
2264  // a lot of exploits are born. Do NOT allow wildcards.
2265  //
2266  //m_mapRespHeaders[ "Access-Control-Allow-Origin" ] = "*";
2267  // ----------------------------------------------------------------------
2268 
2269  // ----------------------------------------------------------------------
2270  // SECURITY: Allow the WebFrontend on the Master backend and ONLY this
2271  // machine to access resources on a frontend or slave web server
2272  //
2273  // http://www.w3.org/TR/cors/#introduction
2274  // ----------------------------------------------------------------------
2275 
2276  QStringList allowedOrigins;
2277  char localhostname[1024]; // about HOST_NAME_MAX * 4
2278 
2279  int serverStatusPort = gCoreContext->GetMasterServerStatusPort();
2280  int backendSSLPort = gCoreContext->GetNumSetting( "BackendSSLPort",
2281  serverStatusPort + 10);
2282 
2283  QString masterAddrPort = QString("%1:%2")
2285  .arg(serverStatusPort);
2286  QString masterTLSAddrPort = QString("%1:%2")
2288  .arg(backendSSLPort);
2289 
2290  allowedOrigins << QString("http://%1").arg(masterAddrPort);
2291  allowedOrigins << QString("https://%2").arg(masterTLSAddrPort);
2292 
2293  if (!gethostname(localhostname, 1024))
2294  {
2295  allowedOrigins << QString("http://%1:%2")
2296  .arg(localhostname).arg(serverStatusPort);
2297  allowedOrigins << QString("https://%1:%2")
2298  .arg(localhostname).arg(backendSSLPort);
2299  }
2300 
2301  QStringList allowedOriginsList =
2302  gCoreContext->GetSetting("AllowedOriginsList", QString(
2303  "https://chromecast.mythtv.org,"
2304  "http://chromecast.mythtvcast.com"
2305  )).split(",");
2306 
2307  for (QStringList::const_iterator it = allowedOriginsList.begin();
2308  it != allowedOriginsList.end(); it++)
2309  {
2310  if ((*it).isEmpty())
2311  continue;
2312 
2313  if (*it == "*" || (!(*it).startsWith("http://") &&
2314  !(*it).startsWith("https://")))
2315  {
2316  LOG(VB_GENERAL, LOG_ERR, QString("Illegal AllowedOriginsList"
2317  " entry '%1'. Must start with http[s]:// and not be *")
2318  .arg(*it));
2319  }
2320  else
2321  {
2322  allowedOrigins << *it;
2323  }
2324  }
2325 
2326  if (VERBOSE_LEVEL_CHECK(VB_HTTP, LOG_DEBUG))
2327  {
2328  for (QStringList::const_iterator it = allowedOrigins.begin();
2329  it != allowedOrigins.end(); it++)
2330  LOG(VB_HTTP, LOG_DEBUG, QString("Will allow Origin: %1").arg(*it));
2331  }
2332 
2333  if (allowedOrigins.contains(sOrigin))
2334  {
2335  SetResponseHeader( "Access-Control-Allow-Origin" , sOrigin);
2336  SetResponseHeader( "Access-Control-Allow-Credentials" , "true");
2337  SetResponseHeader( "Access-Control-Allow-Headers" , "Content-Type");
2338  LOG(VB_HTTP, LOG_DEBUG, QString("Allow-Origin: %1)").arg(sOrigin));
2339  }
2340  else
2341  {
2342  LOG(VB_GENERAL, LOG_CRIT, QString("HTTPRequest: Cross-origin request "
2343  "received with origin (%1)")
2344  .arg(sOrigin));
2345  }
2346 }
2347 
2350 //
2351 // BufferedSocketDeviceRequest Class Implementation
2352 //
2355 
2357 {
2358  QString sLine;
2359 
2360  if (m_pSocket && m_pSocket->isValid() &&
2361  m_pSocket->state() == QAbstractSocket::ConnectedState)
2362  {
2363  bool timeout = false;
2364  MythTimer timer;
2365  timer.start();
2366  while (!m_pSocket->canReadLine() && !timeout)
2367  {
2368  timeout = !(m_pSocket->waitForReadyRead( msecs ));
2369 
2370  if ( timer.elapsed() >= msecs )
2371  {
2372  timeout = true;
2373  LOG(VB_HTTP, LOG_INFO, "BufferedSocketDeviceRequest::ReadLine() - Exceeded Total Elapsed Wait Time." );
2374  }
2375  }
2376 
2377  if (!timeout)
2378  sLine = m_pSocket->readLine();
2379  }
2380 
2381  return( sLine );
2382 }
2383 
2385 //
2387 
2388 qint64 BufferedSocketDeviceRequest::ReadBlock(char *pData, qint64 nMaxLen,
2389  int msecs)
2390 {
2391  if (m_pSocket && m_pSocket->isValid() &&
2392  m_pSocket->state() == QAbstractSocket::ConnectedState)
2393  {
2394  if (msecs == 0)
2395  return( m_pSocket->read( pData, nMaxLen ));
2396 
2397  bool bTimeout = false;
2398  MythTimer timer;
2399  timer.start();
2400  while ( (m_pSocket->bytesAvailable() < (int)nMaxLen) && !bTimeout ) // This can end up waiting far longer than msecs
2401  {
2402  bTimeout = !(m_pSocket->waitForReadyRead( msecs ));
2403 
2404  if ( timer.elapsed() >= msecs )
2405  {
2406  bTimeout = true;
2407  LOG(VB_HTTP, LOG_INFO, "BufferedSocketDeviceRequest::ReadBlock() - Exceeded Total Elapsed Wait Time." );
2408  }
2409  }
2410 
2411  // Just return what we have even if timed out.
2412 
2413  return( m_pSocket->read( pData, nMaxLen ));
2414  }
2415 
2416  return( -1 );
2417 }
2418 
2420 //
2422 
2423 qint64 BufferedSocketDeviceRequest::WriteBlock(const char *pData, qint64 nLen)
2424 {
2425  qint64 bytesWritten = -1;
2426  if (m_pSocket && m_pSocket->isValid() &&
2427  m_pSocket->state() == QAbstractSocket::ConnectedState)
2428  {
2429  bytesWritten = m_pSocket->write( pData, nLen );
2430  m_pSocket->waitForBytesWritten();
2431  }
2432 
2433  return( bytesWritten );
2434 }
2435 
2437 //
2439 
2441 {
2442  return( m_pSocket->localAddress().toString() );
2443 }
2444 
2446 //
2448 
2450 {
2451  return( m_pSocket->localPort() );
2452 }
2453 
2454 
2456 //
2458 
2460 {
2461  return( m_pSocket->peerAddress().toString() );
2462 }
2463 
HttpRequestType
Definition: httprequest.h:43
QString GetLanguageAndVariant(void)
Returns the user-set language and variant.
QMap< QString, QString > QStringMap
Definition: upnputil.h:44
virtual int GetValue(const QString &sSetting, int Default)=0
A QElapsedTimer based timer to replace use of QTime as a timer.
Definition: mythtimer.h:13
void FormatRawResponse(const QString &sXML)
bool DigestAuthentication()
QString GetResponseStatus(void)
HttpRequestType SetRequestType(const QString &sType)
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
QDateTime fromString(const QString &dtstr)
Converts kFilename && kISODate formats to QDateTime.
Definition: mythdate.cpp:30
MythSessionManager * GetSessionManager(void)
Serializer * GetSerializer()
#define SOAP_ENVELOPE_BEGIN
Definition: httprequest.h:33
QString GetResponseHeaders(void)
HttpContentType SetContentType(const QString &sType)
QString GetRequestHeader(const QString &sKey, QString sDefault)
HttpContentType
Definition: httprequest.h:65
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)
void SetCookie(const QString &sKey, const QString &sValue, const QDateTime &expiryDate, bool secure)
static QString GetResponseProtocol()
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
bool ParseRequest()
Present date/time in UTC.
Definition: mythdate.h:28
bool Authenticated()
qint64 SendResponseFile(const QString &sFileName)
static QString GetMimeType(const QString &sFileExtension)
qint64 WriteBlock(const char *pData, qint64 nLen) override
static const char * s_szServerHeaders
Definition: httprequest.h:111
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)
void ExtractMethodFromURL()
static 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
static QString TestMimeType(const QString &sFileName)
QDateTime GetSessionExpires() const
Definition: mythsession.h:43
#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)
bool ParseRange(QString sRange, long long llSize, long long *pllStart, long long *pllEnd)
QString BuildResponseHeader(long long nSize)
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
static Configuration * GetConfiguration()
Definition: upnp.cpp:71
void SetResponseHeader(const QString &sKey, const QString &sValue, bool replace=false)
QString GetPeerAddress() override
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)
static QString StaticPage
static QString GetPasswordDigest(const QString &username)
Load the password digest for comparison in the HTTP Auth code.
virtual QString GetHostName()
int GetNumSetting(const QString &key, int defaultval=0)
void AddCORSHeaders(const QString &sOrigin)
qint64 SendFile(QFile &file, qint64 llStart, qint64 llBytes)
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: mythlogging.h:41
void ProcessRequestLine(const QString &sLine)
int elapsed(void)
Returns milliseconds elapsed since last start() or restart()
Definition: mythtimer.cpp:90
static long GetParameters(QString sParams, QStringMap &mapParams)
qint64 SendData(QIODevice *pDevice, qint64 llStart, qint64 llBytes)
#define SOAP_ENVELOPE_END
Definition: httprequest.h:36
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)
static QString GetServerVersion(void)
Definition: httpserver.cpp:288
QString GetAuthenticationHeader(bool isStale=false)
#define SENDFILE_BUFFER_SIZE
QString GetRequestProtocol() const
void FormatErrorResponse(bool bServerError, const QString &sFaultString, const QString &sDetails)
HTTP Date format.
Definition: mythdate.h:27
QString GetSessionToken(void) const
Definition: mythsession.h:38
static MIMETypes g_MIMETypes[]
Definition: httprequest.cpp:63
void FormatFileResponse(const QString &sFileName)
QString CalculateDigestNonce(const QString &timeStamp)
bool ParseKeepAlive(void)
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
static bool IsUrlProtected(const QString &sBaseUrl)
quint16 GetHostPort() override
void SetRequestProtocol(const QString &sLine)