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::s_szServerHeaders = "Accept-Ranges: bytes\r\n";
158 
160 //
162 
164 {
165  // HTTP
166  if (sType == "GET" ) return( m_eType = RequestTypeGet );
167  if (sType == "HEAD" ) return( m_eType = RequestTypeHead );
168  if (sType == "POST" ) return( m_eType = RequestTypePost );
169  if (sType == "OPTIONS" ) return( m_eType = RequestTypeOptions );
170 
171  // UPnP
172  if (sType == "M-SEARCH" ) return( m_eType = RequestTypeMSearch );
173  if (sType == "NOTIFY" ) return( m_eType = RequestTypeNotify );
174  if (sType == "SUBSCRIBE" ) return( m_eType = RequestTypeSubscribe );
175  if (sType == "UNSUBSCRIBE") return( m_eType = RequestTypeUnsubscribe );
176 
177  if (sType.startsWith( QString("HTTP/") )) return( m_eType = RequestTypeResponse );
178 
179  LOG(VB_HTTP, LOG_INFO,
180  QString("HTTPRequest::SentRequestType( %1 ) - returning Unknown.")
181  .arg(sType));
182 
183  return( m_eType = RequestTypeUnknown);
184 }
185 
187 //
189 
190 QString HTTPRequest::BuildResponseHeader( long long nSize )
191 {
192  QString sHeader;
193  QString sContentType = (m_eResponseType == ResponseTypeOther) ?
194  m_sResponseTypeText : GetResponseType();
195  //-----------------------------------------------------------------------
196  // Headers describing the connection
197  //-----------------------------------------------------------------------
198 
199  // The protocol string
200  sHeader = QString( "%1 %2\r\n" ).arg(GetResponseProtocol())
201  .arg(GetResponseStatus());
202 
203  SetResponseHeader("Date", MythDate::toString(MythDate::current(), MythDate::kRFC822)); // RFC 822
204  SetResponseHeader("Server", HttpServer::GetServerVersion());
205 
206  SetResponseHeader("Connection", m_bKeepAlive ? "Keep-Alive" : "Close" );
207  if (m_bKeepAlive)
208  {
209  if (m_nKeepAliveTimeout == 0) // Value wasn't passed in by the server, so go with the configured value
210  m_nKeepAliveTimeout = gCoreContext->GetNumSetting("HTTP/KeepAliveTimeoutSecs", 10);
211  SetResponseHeader("Keep-Alive", QString("timeout=%1").arg(m_nKeepAliveTimeout));
212  }
213 
214  //-----------------------------------------------------------------------
215  // Entity Headers - Describe the content and allowed methods
216  // RFC 2616 Section 7.1
217  //-----------------------------------------------------------------------
218  if (m_eResponseType != ResponseTypeHeader) // No entity headers
219  {
220  SetResponseHeader("Content-Language", gCoreContext->GetLanguageAndVariant().replace("_", "-"));
221  SetResponseHeader("Content-Type", sContentType);
222 
223  // Default to 'inline' but we should support 'attachment' when it would
224  // be appropriate i.e. not when streaming a file to a upnp player or browser
225  // that can support it natively
226  if (!m_sFileName.isEmpty())
227  {
228  // TODO: Add support for utf8 encoding - RFC 5987
229  QString filename = QFileInfo(m_sFileName).fileName(); // Strip any path
230  SetResponseHeader("Content-Disposition", QString("inline; filename=\"%2\"").arg(QString(filename.toLatin1())));
231  }
232 
233  SetResponseHeader("Content-Length", QString::number(nSize));
234 
235  // See DLNA 7.4.1.3.11.4.3 Tolerance to unavailable contentFeatures.dlna.org header
236  //
237  // It is better not to return this header, than to return it containing
238  // invalid or incomplete information. We are unable to currently determine
239  // this information at this stage, so do not return it. Only older devices
240  // look for it. Newer devices use the information provided in the UPnP
241  // response
242 
243 // QString sValue = GetHeaderValue( "getContentFeatures.dlna.org", "0" );
244 //
245 // if (sValue == "1")
246 // sHeader += "contentFeatures.dlna.org: DLNA.ORG_OP=01;DLNA.ORG_CI=0;"
247 // "DLNA.ORG_FLAGS=01500000000000000000000000000000\r\n";
248 
249 
250  // DLNA 7.5.4.3.2.33 MT transfer mode indication
251  QString sTransferMode = GetRequestHeader( "transferMode.dlna.org", "" );
252 
253  if (sTransferMode.isEmpty())
254  {
255  if (m_sResponseTypeText.startsWith("video/") ||
256  m_sResponseTypeText.startsWith("audio/"))
257  sTransferMode = "Streaming";
258  else
259  sTransferMode = "Interactive";
260  }
261 
262  if (sTransferMode == "Streaming")
263  SetResponseHeader("transferMode.dlna.org", "Streaming");
264  else if (sTransferMode == "Background")
265  SetResponseHeader("transferMode.dlna.org", "Background");
266  else if (sTransferMode == "Interactive")
267  SetResponseHeader("transferMode.dlna.org", "Interactive");
268 
269  // HACK Temporary hack for Samsung TVs - Needs to be moved later as it's not entirely DLNA compliant
270  if (!GetRequestHeader( "getcontentFeatures.dlna.org", "" ).isEmpty())
271  SetResponseHeader("contentFeatures.dlna.org", "DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01500000000000000000000000000000");
272  }
273 
274  if (!m_mapHeaders[ "origin" ].isEmpty())
275  AddCORSHeaders(m_mapHeaders[ "origin" ]);
276 
277  if (getenv("HTTPREQUEST_DEBUG"))
278  {
279  // Dump response header
280  QMap<QString, QString>::iterator it;
281  for ( it = m_mapRespHeaders.begin(); it != m_mapRespHeaders.end(); ++it )
282  {
283  LOG(VB_HTTP, LOG_INFO, QString("(Response Header) %1: %2").arg(it.key()).arg(it.value()));
284  }
285  }
286 
287  sHeader += GetResponseHeaders();
288  sHeader += "\r\n";
289 
290  return sHeader;
291 }
292 
294 //
296 
298 {
299  qint64 nBytes = 0;
300 
301  switch( m_eResponseType )
302  {
303  // The following are all eligable for gzip compression
304  case ResponseTypeUnknown:
305  case ResponseTypeNone:
306  LOG(VB_HTTP, LOG_INFO,
307  QString("HTTPRequest::SendResponse( None ) :%1 -> %2:")
308  .arg(GetResponseStatus()) .arg(GetPeerAddress()));
309  return( -1 );
310  case ResponseTypeJS:
311  case ResponseTypeCSS:
312  case ResponseTypeText:
313  case ResponseTypeSVG:
314  case ResponseTypeXML:
315  case ResponseTypeHTML:
316  // If the reponse isn't already in the buffer, then load it
317  if (m_sFileName.isEmpty() || !m_response.buffer().isEmpty())
318  break;
319  {
320  QByteArray fileBuffer;
321  QFile file(m_sFileName);
322  if (file.exists() && file.size() < (2 * 1024 * 1024) && // For security/stability, limit size of files read into buffer to 2MiB
323  file.open(QIODevice::ReadOnly | QIODevice::Text))
324  m_response.buffer() = file.readAll();
325 
326  if (!m_response.buffer().isEmpty())
327  break;
328 
329  // Let SendResponseFile try or send a 404
330  m_eResponseType = ResponseTypeFile;
331  }
332  [[clang::fallthrough]];
333  case ResponseTypeFile: // Binary files
334  LOG(VB_HTTP, LOG_INFO,
335  QString("HTTPRequest::SendResponse( File ) :%1 -> %2:")
336  .arg(GetResponseStatus()) .arg(GetPeerAddress()));
337  return( SendResponseFile( m_sFileName ));
338  case ResponseTypeOther:
339  case ResponseTypeHeader:
340  default:
341  break;
342  }
343 
344  LOG(VB_HTTP, LOG_INFO,
345  QString("HTTPRequest::SendResponse(xml/html) (%1) :%2 -> %3: %4")
346  .arg(m_sFileName) .arg(GetResponseStatus())
347  .arg(GetPeerAddress()) .arg(m_eResponseType));
348 
349  // ----------------------------------------------------------------------
350  // Make it so the header is sent with the data
351  // ----------------------------------------------------------------------
352 
353 #ifdef USE_SETSOCKOPT
354 // // Never send out partially complete segments
355 // if (setsockopt(getSocketHandle(), SOL_TCP, TCP_CORK,
356 // &g_on, sizeof( g_on )) < 0)
357 // {
358 // LOG(VB_HTTP, LOG_INFO,
359 // QString("HTTPRequest::SendResponse(xml/html) "
360 // "setsockopt error setting TCP_CORK on ") + ENO);
361 // }
362 #endif
363 
364 
365 
366  // ----------------------------------------------------------------------
367  // Check for ETag match...
368  // ----------------------------------------------------------------------
369 
370  QString sETag = GetRequestHeader( "If-None-Match", "" );
371 
372  if ( !sETag.isEmpty() && sETag == m_mapRespHeaders[ "ETag" ] )
373  {
374  LOG(VB_HTTP, LOG_INFO,
375  QString("HTTPRequest::SendResponse(%1) - Cached")
376  .arg(sETag));
377 
378  m_nResponseStatus = 304;
379  m_eResponseType = ResponseTypeHeader; // No entity headers
380 
381  // no content can be returned.
382  m_response.buffer().clear();
383  }
384 
385  // ----------------------------------------------------------------------
386 
387  int nContentLen = m_response.buffer().length();
388 
389  QBuffer *pBuffer = &m_response;
390 
391  // ----------------------------------------------------------------------
392  // DEBUGGING
393  if (getenv("HTTPREQUEST_DEBUG"))
394  cout << m_response.buffer().constData() << endl;
395  // ----------------------------------------------------------------------
396 
397  LOG(VB_HTTP, LOG_DEBUG, QString("Reponse Content Length: %1").arg(nContentLen));
398 
399  // ----------------------------------------------------------------------
400  // Should we try to return data gzip'd?
401  // ----------------------------------------------------------------------
402 
403  QBuffer compBuffer;
404 
405  if (( nContentLen > 0 ) && m_mapHeaders[ "accept-encoding" ].contains( "gzip" ))
406  {
407  QByteArray compressed = gzipCompress( m_response.buffer() );
408  compBuffer.setData( compressed );
409 
410  if (!compBuffer.buffer().isEmpty())
411  {
412  pBuffer = &compBuffer;
413 
414  SetResponseHeader( "Content-Encoding", "gzip" );
415  LOG(VB_HTTP, LOG_DEBUG, QString("Reponse Compressed Content Length: %1").arg(compBuffer.buffer().length()));
416  }
417  }
418 
419  // ----------------------------------------------------------------------
420  // Write out Header.
421  // ----------------------------------------------------------------------
422 
423  nContentLen = pBuffer->buffer().length();
424 
425  QString rHeader = BuildResponseHeader( nContentLen );
426 
427  QByteArray sHeader = rHeader.toUtf8();
428  LOG(VB_HTTP, LOG_DEBUG, QString("Response header size: %1 bytes").arg(sHeader.length()));
429  nBytes = WriteBlock( sHeader.constData(), sHeader.length() );
430 
431  if (nBytes < sHeader.length())
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  // Write out Response buffer.
439  // ----------------------------------------------------------------------
440 
441  if (( m_eType != RequestTypeHead ) &&
442  ( nContentLen > 0 ))
443  {
444  qint64 bytesWritten = SendData( pBuffer, 0, nContentLen );
445  //qint64 bytesWritten = WriteBlock( pBuffer->buffer(), pBuffer->buffer().length() );
446 
447  if (bytesWritten != nContentLen)
448  LOG(VB_HTTP, LOG_ERR, "HttpRequest::SendResponse(): Error occurred while writing response body.");
449  else
450  nBytes += bytesWritten;
451  }
452 
453  // ----------------------------------------------------------------------
454  // Turn off the option so any small remaining packets will be sent
455  // ----------------------------------------------------------------------
456 
457 #ifdef USE_SETSOCKOPT
458 // if (setsockopt(getSocketHandle(), SOL_TCP, TCP_CORK,
459 // &g_off, sizeof( g_off )) < 0)
460 // {
461 // LOG(VB_HTTP, LOG_INFO,
462 // QString("HTTPRequest::SendResponse(xml/html) "
463 // "setsockopt error setting TCP_CORK off ") + ENO);
464 // }
465 #endif
466 
467  return( nBytes );
468 }
469 
471 //
473 
474 qint64 HTTPRequest::SendResponseFile( const QString& sFileName )
475 {
476  qint64 nBytes = 0;
477  long long llSize = 0;
478  long long llStart = 0;
479  long long llEnd = 0;
480 
481  LOG(VB_HTTP, LOG_INFO, QString("SendResponseFile ( %1 )").arg(sFileName));
482 
483  m_eResponseType = ResponseTypeOther;
484  m_sResponseTypeText = "text/plain";
485 
486  // ----------------------------------------------------------------------
487  // Make it so the header is sent with the data
488  // ----------------------------------------------------------------------
489 
490 #ifdef USE_SETSOCKOPT
491 // // Never send out partially complete segments
492 // if (setsockopt(getSocketHandle(), SOL_TCP, TCP_CORK,
493 // &g_on, sizeof( g_on )) < 0)
494 // {
495 // LOG(VB_HTTP, LOG_INFO,
496 // QString("HTTPRequest::SendResponseFile(%1) "
497 // "setsockopt error setting TCP_CORK on " ).arg(sFileName) +
498 // ENO);
499 // }
500 #endif
501 
502  QFile tmpFile( sFileName );
503  if (tmpFile.exists( ) && tmpFile.open( QIODevice::ReadOnly ))
504  {
505 
506  m_sResponseTypeText = TestMimeType( sFileName );
507 
508  // ------------------------------------------------------------------
509  // Get File size
510  // ------------------------------------------------------------------
511 
512  llSize = llEnd = tmpFile.size( );
513 
514  m_nResponseStatus = 200;
515 
516  // ------------------------------------------------------------------
517  // Process any Range Header
518  // ------------------------------------------------------------------
519 
520  bool bRange = false;
521  QString sRange = GetRequestHeader( "range", "" );
522 
523  if (!sRange.isEmpty())
524  {
525  bRange = ParseRange( sRange, llSize, &llStart, &llEnd );
526 
527  // Adjust ranges that are too long.
528 
529  if (llEnd >= llSize)
530  llEnd = llSize-1;
531 
532  if ((llSize > llStart) && (llSize > llEnd) && (llEnd > llStart))
533  {
534  if (bRange)
535  {
536  m_nResponseStatus = 206;
537  m_mapRespHeaders[ "Content-Range" ] = QString("bytes %1-%2/%3")
538  .arg( llStart )
539  .arg( llEnd )
540  .arg( llSize );
541  llSize = (llEnd - llStart) + 1;
542  }
543  }
544  else
545  {
546  m_nResponseStatus = 416;
547  // RFC 7233 - A server generating a 416 (Range Not Satisfiable)
548  // response to a byte-range request SHOULD send a Content-Range
549  // header field with an unsatisfied-range value
550  m_mapRespHeaders[ "Content-Range" ] = QString("bytes */%3")
551  .arg( llSize );
552  llSize = 0;
553  LOG(VB_HTTP, LOG_INFO,
554  QString("HTTPRequest::SendResponseFile(%1) - "
555  "invalid byte range %2-%3/%4")
556  .arg(sFileName) .arg(llStart) .arg(llEnd)
557  .arg(llSize));
558  }
559  }
560 
561  // HACK: D-Link DSM-320
562  // The following headers are only required by servers which don't support
563  // http keep alive. We do support it, so we don't need it. Keeping it in
564  // place to prevent someone re-adding it in future
565  //m_mapRespHeaders[ "X-User-Agent" ] = "redsonic";
566 
567  // ------------------------------------------------------------------
568  //
569  // ------------------------------------------------------------------
570 
571  }
572  else
573  {
574  LOG(VB_HTTP, LOG_INFO,
575  QString("HTTPRequest::SendResponseFile(%1) - cannot find file!")
576  .arg(sFileName));
577  m_nResponseStatus = 404;
578  m_response.write( GetResponsePage() );
579  }
580 
581  // -=>TODO: Should set "Content-Length: *" if file is still recording
582 
583  // ----------------------------------------------------------------------
584  // Write out Header.
585  // ----------------------------------------------------------------------
586 
587  QString rHeader = BuildResponseHeader( llSize );
588  QByteArray sHeader = rHeader.toUtf8();
589  LOG(VB_HTTP, LOG_DEBUG, QString("Response header size: %1 bytes").arg(sHeader.length()));
590  nBytes = WriteBlock( sHeader.constData(), sHeader.length() );
591 
592  if (nBytes < sHeader.length())
593  LOG( VB_HTTP, LOG_ERR, QString("HttpRequest::SendResponseFile(): "
594  "Incomplete write of header, "
595  "%1 written of %2")
596  .arg(nBytes).arg(sHeader.length()));
597 
598  // ----------------------------------------------------------------------
599  // Write out File.
600  // ----------------------------------------------------------------------
601 
602 #if 0
603  LOG(VB_HTTP, LOG_DEBUG,
604  QString("SendResponseFile : size = %1, start = %2, end = %3")
605  .arg(llSize).arg(llStart).arg(llEnd));
606 #endif
607  if (( m_eType != RequestTypeHead ) && (llSize != 0))
608  {
609  long long sent = SendFile( tmpFile, llStart, llSize );
610 
611  if (sent == -1)
612  {
613  LOG(VB_HTTP, LOG_INFO,
614  QString("SendResponseFile( %1 ) Error: %2 [%3]" )
615  .arg(sFileName) .arg(errno) .arg(strerror(errno)));
616 
617  nBytes = -1;
618  }
619  }
620 
621  // ----------------------------------------------------------------------
622  // Turn off the option so any small remaining packets will be sent
623  // ----------------------------------------------------------------------
624 
625 #ifdef USE_SETSOCKOPT
626 // if (setsockopt(getSocketHandle(), SOL_TCP, TCP_CORK,
627 // &g_off, sizeof( g_off )) < 0)
628 // {
629 // LOG(VB_HTTP, LOG_INFO,
630 // QString("HTTPRequest::SendResponseFile(%1) "
631 // "setsockopt error setting TCP_CORK off ").arg(sFileName) +
632 // ENO);
633 // }
634 #endif
635 
636  // -=>TODO: Only returns header length...
637  // should we change to return total bytes?
638 
639  return nBytes;
640 }
641 
643 //
645 
646 #define SENDFILE_BUFFER_SIZE 65536
647 
648 qint64 HTTPRequest::SendData( QIODevice *pDevice, qint64 llStart, qint64 llBytes )
649 {
650  bool bShouldClose = false;
651  qint64 sent = 0;
652 
653  if (!pDevice->isOpen())
654  {
655  pDevice->open( QIODevice::ReadOnly );
656  bShouldClose = true;
657  }
658 
659  // ----------------------------------------------------------------------
660  // Set out file position to requested start location.
661  // ----------------------------------------------------------------------
662 
663  if ( !pDevice->seek( llStart ))
664  return -1;
665 
666  char aBuffer[ SENDFILE_BUFFER_SIZE ];
667 
668  qint64 llBytesRemaining = llBytes;
669  qint64 llBytesToRead = 0;
670  qint64 llBytesRead = 0;
671  qint64 llBytesWritten = 0;
672 
673  memset (aBuffer, 0, sizeof(aBuffer));
674 
675  while ((sent < llBytes) && !pDevice->atEnd())
676  {
677  llBytesToRead = std::min( (qint64)SENDFILE_BUFFER_SIZE, llBytesRemaining );
678 
679  if (( llBytesRead = pDevice->read( aBuffer, llBytesToRead )) != -1 )
680  {
681  if (( llBytesWritten = WriteBlock( aBuffer, llBytesRead )) == -1)
682  return -1;
683 
684  // -=>TODO: We don't handle the situation where we read more than was sent.
685 
686  sent += llBytesRead;
687  llBytesRemaining -= llBytesRead;
688  }
689  }
690 
691  if (bShouldClose)
692  pDevice->close();
693 
694  return sent;
695 }
696 
698 //
700 
701 qint64 HTTPRequest::SendFile( QFile &file, qint64 llStart, qint64 llBytes )
702 {
703  qint64 sent = SendData( (QIODevice *)(&file), llStart, llBytes );
704 
705  return( sent );
706 }
707 
708 
710 //
712 
713 void HTTPRequest::FormatErrorResponse( bool bServerError,
714  const QString &sFaultString,
715  const QString &sDetails )
716 {
717  m_eResponseType = ResponseTypeXML;
718  m_nResponseStatus = 500;
719 
720  QTextStream stream( &m_response );
721 
722  stream << "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
723 
724  QString sWhere = ( bServerError ) ? "s:Server" : "s:Client";
725 
726  if (m_bSOAPRequest)
727  {
728  m_mapRespHeaders[ "EXT" ] = "";
729 
730  stream << SOAP_ENVELOPE_BEGIN
731  << "<s:Fault>"
732  << "<faultcode>" << sWhere << "</faultcode>"
733  << "<faultstring>" << sFaultString << "</faultstring>";
734  }
735 
736  if (!sDetails.isEmpty())
737  {
738  stream << "<detail>" << sDetails << "</detail>";
739  }
740 
741  if (m_bSOAPRequest)
742  stream << "</s:Fault>" << SOAP_ENVELOPE_END;
743 
744  stream.flush();
745 }
746 
748 //
750 
752 {
753  m_eResponseType = ResponseTypeOther;
754  m_sResponseTypeText = pSer->GetContentType();
755  m_nResponseStatus = 200;
756 
757  pSer->AddHeaders( m_mapRespHeaders );
758 
759  //m_response << pFormatter->ToString();
760 }
761 
763 //
765 
767 {
768  m_eResponseType = ResponseTypeXML;
769  m_nResponseStatus = 200;
770 
771  QTextStream stream( &m_response );
772 
773  stream << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n";
774 
775  if (m_bSOAPRequest)
776  {
777  m_mapRespHeaders[ "EXT" ] = "";
778 
779  stream << SOAP_ENVELOPE_BEGIN
780  << "<u:" << m_sMethod << "Response xmlns:u=\""
781  << m_sNameSpace << "\">\r\n";
782  }
783  else
784  stream << "<" << m_sMethod << "Response>\r\n";
785 
786  NameValues::const_iterator nit = args.begin();
787  for (; nit != args.end(); ++nit)
788  {
789  stream << "<" << (*nit).m_sName;
790 
791  if ((*nit).m_pAttributes)
792  {
793  NameValues::const_iterator nit2 = (*nit).m_pAttributes->begin();
794  for (; nit2 != (*nit).m_pAttributes->end(); ++nit2)
795  {
796  stream << " " << (*nit2).m_sName << "='"
797  << Encode( (*nit2).m_sValue ) << "'";
798  }
799  }
800 
801  stream << ">";
802 
803  if (m_bSOAPRequest)
804  stream << Encode( (*nit).m_sValue );
805  else
806  stream << (*nit).m_sValue;
807 
808  stream << "</" << (*nit).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 (int i = 0; i < g_nMIMELength; i++)
1032  {
1033  ext = g_MIMETypes[i].pszExtension;
1034 
1035  if ( sFileExtension.toUpper() == ext.toUpper() )
1036  return( g_MIMETypes[i].pszType );
1037  }
1038 
1039  return( "text/plain" );
1040 }
1041 
1043 //
1045 
1047 {
1048  QStringList mimeTypes;
1049 
1050  for (int i = 0; i < g_nMIMELength; i++)
1051  {
1052  if (!mimeTypes.contains( g_MIMETypes[i].pszType ))
1053  mimeTypes.append( g_MIMETypes[i].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  for ( QStringList::Iterator it = params.begin();
1130  it != params.end(); ++it )
1131  {
1132  QString sName = (*it).section( '=', 0, 0 );
1133  QString sValue = (*it).section( '=', 1 );
1134  sValue.replace("+"," ");
1135 
1136  if (!sName.isEmpty())
1137  {
1138  sName = QUrl::fromPercentEncoding(sName.toUtf8());
1139  sValue = QUrl::fromPercentEncoding(sValue.toUtf8());
1140 
1141  mapParams.insert( sName.trimmed(), sValue );
1142  nCount++;
1143  }
1144  }
1145  }
1146 
1147  return nCount;
1148 }
1149 
1150 
1152 //
1154 
1155 QString HTTPRequest::GetRequestHeader( const QString &sKey, QString sDefault )
1156 {
1157  QStringMap::iterator it = m_mapHeaders.find( sKey.toLower() );
1158 
1159  if ( it == m_mapHeaders.end())
1160  return( sDefault );
1161 
1162  return *it;
1163 }
1164 
1165 
1167 //
1169 
1171 {
1172  QString sHeader = s_szServerHeaders;
1173 
1174  for ( QStringMap::iterator it = m_mapRespHeaders.begin();
1175  it != m_mapRespHeaders.end();
1176  ++it )
1177  {
1178  sHeader += it.key() + ": ";
1179  sHeader += *it + "\r\n";
1180  }
1181 
1182  return( sHeader );
1183 }
1184 
1186 //
1188 
1190 {
1191  // TODO: Think about whether we should use a longer timeout if the client
1192  // has explicitly specified 'Keep-alive'
1193 
1194  // HTTP 1.1 ... server may assume keep-alive
1195  bool bKeepAlive = true;
1196 
1197  // if HTTP/1.0... must default to false
1198  if ((m_nMajor == 1) && (m_nMinor == 0))
1199  bKeepAlive = false;
1200 
1201  // Read Connection Header to see whether the client has explicitly
1202  // asked for the connection to be kept alive or closed after the response
1203  // is sent
1204  QString sConnection = GetRequestHeader( "connection", "default" ).toLower();
1205 
1206  QStringList sValueList = sConnection.split(",");
1207 
1208  if ( sValueList.contains("close") )
1209  {
1210  LOG(VB_HTTP, LOG_DEBUG, "Client requested the connection be closed");
1211  bKeepAlive = false;
1212  }
1213  else if (sValueList.contains("keep-alive"))
1214  bKeepAlive = true;
1215 
1216  return bKeepAlive;
1217 }
1218 
1220 //
1222 
1224 {
1225  QStringList sCookieList = m_mapHeaders.values("cookie");
1226 
1227  QStringList::iterator it;
1228  for (it = sCookieList.begin(); it != sCookieList.end(); ++it)
1229  {
1230  QString key = (*it).section('=', 0, 0);
1231  QString value = (*it).section('=', 1);
1232 
1233  m_mapCookies.insert(key, value);
1234  }
1235 }
1236 
1238 //
1240 
1242 {
1243  bool bSuccess = false;
1244 
1245  try
1246  {
1247  // Read first line to determine requestType
1248  QString sRequestLine = ReadLine( 2000 );
1249 
1250  if ( sRequestLine.isEmpty() )
1251  {
1252  LOG(VB_GENERAL, LOG_ERR, "Timeout reading first line of request." );
1253  return false;
1254  }
1255 
1256  // -=>TODO: Should read lines until a valid request???
1257  ProcessRequestLine( sRequestLine );
1258 
1259  if (m_nMajor > 1 || m_nMajor < 0)
1260  {
1261  m_eResponseType = ResponseTypeHTML;
1262  m_nResponseStatus = 505;
1263  m_response.write( GetResponsePage() );
1264 
1265  return true;
1266  }
1267 
1268  if (m_eType == RequestTypeUnknown)
1269  {
1270  m_eResponseType = ResponseTypeHTML;
1271  m_nResponseStatus = 501; // Not Implemented
1272  // Conservative list, we can't really know what methods we
1273  // actually allow for an arbitrary resource without some sort of
1274  // high maintenance database
1275  SetResponseHeader("Allow", "GET, HEAD");
1276  m_response.write( GetResponsePage() );
1277  return true;
1278  }
1279 
1280  // Read Header
1281  bool bDone = false;
1282  QString sLine = ReadLine( 2000 );
1283 
1284  while (( !sLine.isEmpty() ) && !bDone )
1285  {
1286  if (sLine != "\r\n")
1287  {
1288  QString sName = sLine.section( ':', 0, 0 ).trimmed();
1289  QString sValue = sLine.section( ':', 1 );
1290 
1291  sValue.truncate( sValue.length() - 2 );
1292 
1293  if (!sName.isEmpty() && !sValue.isEmpty())
1294  {
1295  m_mapHeaders.insertMulti(sName.toLower(), sValue.trimmed());
1296  }
1297 
1298  sLine = ReadLine( 2000 );
1299  }
1300  else
1301  bDone = true;
1302  }
1303 
1304  // Dump request header
1305  for ( QStringMap::iterator it = m_mapHeaders.begin();
1306  it != m_mapHeaders.end();
1307  ++it )
1308  {
1309  LOG(VB_HTTP, LOG_INFO, QString("(Request Header) %1: %2")
1310  .arg(it.key()).arg(*it));
1311  }
1312 
1313  // Parse Cookies
1314  ParseCookies();
1315 
1316  // Parse out keep alive
1317  m_bKeepAlive = ParseKeepAlive();
1318 
1319  // Check to see if we found the end of the header or we timed out.
1320  if (!bDone)
1321  {
1322  LOG(VB_GENERAL, LOG_INFO, "Timeout waiting for request header." );
1323  return false;
1324  }
1325 
1326  // HTTP/1.1 requires that the Host header be present, even if empty
1327  if ((m_nMinor == 1) && !m_mapHeaders.contains("host"))
1328  {
1329  m_eResponseType = ResponseTypeHTML;
1330  m_nResponseStatus = 400;
1331  m_response.write( GetResponsePage() );
1332 
1333  return true;
1334  }
1335 
1336  // Destroy session if requested
1337  if (m_mapHeaders.contains("x-myth-clear-session"))
1338  {
1339  SetCookie("sessionToken", "", MythDate::current().addDays(-2), true);
1340  m_mapCookies.remove("sessionToken");
1341  }
1342 
1343  // Allow session resumption for TLS connections
1344  if (m_mapCookies.contains("sessionToken"))
1345  {
1346  QString sessionToken = m_mapCookies["sessionToken"];
1347  MythSessionManager *sessionManager = gCoreContext->GetSessionManager();
1348  MythUserSession session = sessionManager->GetSession(sessionToken);
1349 
1350  if (session.IsValid())
1351  m_userSession = session;
1352  }
1353 
1354  if (IsUrlProtected( m_sBaseUrl ))
1355  {
1356  if (!Authenticated())
1357  {
1358  m_eResponseType = ResponseTypeHTML;
1359  m_nResponseStatus = 401;
1360  m_response.write( GetResponsePage() );
1361  // Since this may not be the first attempt at authentication,
1362  // Authenticated may have set the header with the appropriate
1363  // stale attribute
1364  SetResponseHeader("WWW-Authenticate", GetAuthenticationHeader(false));
1365 
1366  return true;
1367  }
1368 
1369  m_bProtected = true;
1370  }
1371 
1372  bSuccess = true;
1373 
1374  SetContentType( m_mapHeaders[ "content-type" ] );
1375  // Lets load payload if any.
1376  long nPayloadSize = m_mapHeaders[ "content-length" ].toLong();
1377 
1378  if (nPayloadSize > 0)
1379  {
1380  char *pszPayload = new char[ nPayloadSize + 2 ];
1381  long nBytes = 0;
1382 
1383  nBytes = ReadBlock( pszPayload, nPayloadSize, 5000 );
1384  if (nBytes == nPayloadSize )
1385  {
1386  m_sPayload = QString::fromUtf8( pszPayload, nPayloadSize );
1387 
1388  // See if the payload is just data from a form post
1389  if (m_eContentType == ContentType_Urlencoded)
1390  GetParameters( m_sPayload, m_mapParams );
1391  }
1392  else
1393  {
1394  LOG(VB_GENERAL, LOG_ERR,
1395  QString("Unable to read entire payload (read %1 of %2 bytes)")
1396  .arg( nBytes ) .arg( nPayloadSize ) );
1397  bSuccess = false;
1398  }
1399 
1400  delete [] pszPayload;
1401  }
1402 
1403  // Check to see if this is a SOAP encoded message
1404  QString sSOAPAction = GetRequestHeader( "SOAPACTION", "" );
1405 
1406  if (!sSOAPAction.isEmpty())
1407  bSuccess = ProcessSOAPPayload( sSOAPAction );
1408  else
1409  ExtractMethodFromURL();
1410 
1411 #if 0
1412  if (m_sMethod != "*" )
1413  LOG(VB_HTTP, LOG_DEBUG,
1414  QString("HTTPRequest::ParseRequest - Socket (%1) Base (%2) "
1415  "Method (%3) - Bytes in Socket Buffer (%4)")
1416  .arg(getSocketHandle()) .arg(m_sBaseUrl)
1417  .arg(m_sMethod) .arg(BytesAvailable()));
1418 #endif
1419  }
1420  catch(...)
1421  {
1422  LOG(VB_GENERAL, LOG_WARNING,
1423  "Unexpected exception in HTTPRequest::ParseRequest" );
1424  }
1425 
1426  return bSuccess;
1427 }
1428 
1430 //
1432 
1433 void HTTPRequest::ProcessRequestLine( const QString &sLine )
1434 {
1435  m_sRawRequest = sLine;
1436 
1437  QString sToken;
1438  QStringList tokens = sLine.split(m_procReqLineExp, QString::SkipEmptyParts);
1439  int nCount = tokens.count();
1440 
1441  // ----------------------------------------------------------------------
1442 
1443  if ( sLine.startsWith( QString("HTTP/") ))
1444  m_eType = RequestTypeResponse;
1445  else
1446  m_eType = RequestTypeUnknown;
1447 
1448  // ----------------------------------------------------------------------
1449  // if this is actually a response, then sLine's format will be:
1450  // HTTP/m.n <response code> <response text>
1451  // otherwise:
1452  // <method> <Resource URI> HTTP/m.n
1453  // ----------------------------------------------------------------------
1454 
1455  if (m_eType != RequestTypeResponse)
1456  {
1457  // ------------------------------------------------------------------
1458  // Process as a request
1459  // ------------------------------------------------------------------
1460 
1461  if (nCount > 0)
1462  SetRequestType( tokens[0].trimmed() );
1463 
1464  if (nCount > 1)
1465  {
1466  m_sOriginalUrl = tokens[1].toUtf8(); // Used by authorization check
1467  m_sRequestUrl = QUrl::fromPercentEncoding(tokens[1].toUtf8());
1468  m_sBaseUrl = m_sRequestUrl.section( '?', 0, 0).trimmed();
1469 
1470  m_sResourceUrl = m_sBaseUrl; // Save complete url without parameters
1471 
1472  // Process any Query String Parameters
1473  QString sQueryStr = tokens[1].section( '?', 1, 1 );
1474 
1475  if (!sQueryStr.isEmpty())
1476  GetParameters( sQueryStr, m_mapParams );
1477  }
1478 
1479  if (nCount > 2)
1480  SetRequestProtocol( tokens[2].trimmed() );
1481  }
1482  else
1483  {
1484  // ------------------------------------------------------------------
1485  // Process as a Response
1486  // ------------------------------------------------------------------
1487  if (nCount > 0)
1488  SetRequestProtocol( tokens[0].trimmed() );
1489 
1490  if (nCount > 1)
1491  m_nResponseStatus = tokens[1].toInt();
1492  }
1493 
1494 
1495 }
1496 
1498 //
1500 
1501 bool HTTPRequest::ParseRange( QString sRange,
1502  long long llSize,
1503  long long *pllStart,
1504  long long *pllEnd )
1505 {
1506  // ----------------------------------------------------------------------
1507  // -=>TODO: Only handle 1 range at this time...
1508  // should make work with full spec.
1509  // ----------------------------------------------------------------------
1510 
1511  if (sRange.isEmpty())
1512  return false;
1513 
1514  // ----------------------------------------------------------------------
1515  // remove any "bytes="
1516  // ----------------------------------------------------------------------
1517  int nIdx = sRange.indexOf(m_parseRangeExp);
1518 
1519  if (nIdx < 0)
1520  return false;
1521 
1522  if (nIdx > 0)
1523  sRange.remove( 0, nIdx );
1524 
1525  // ----------------------------------------------------------------------
1526  // Split multiple ranges
1527  // ----------------------------------------------------------------------
1528 
1529  QStringList ranges = sRange.split(',', QString::SkipEmptyParts);
1530 
1531  if (ranges.count() == 0)
1532  return false;
1533 
1534  // ----------------------------------------------------------------------
1535  // Split first range into its components
1536  // ----------------------------------------------------------------------
1537 
1538  QStringList parts = ranges[0].split('-');
1539 
1540  if (parts.count() != 2)
1541  return false;
1542 
1543  if (parts[0].isEmpty() && parts[1].isEmpty())
1544  return false;
1545 
1546  // ----------------------------------------------------------------------
1547  //
1548  // ----------------------------------------------------------------------
1549 
1550  bool conv_ok = false;
1551  if (parts[0].isEmpty())
1552  {
1553  // ------------------------------------------------------------------
1554  // Does it match "-####"
1555  // ------------------------------------------------------------------
1556 
1557  long long llValue = parts[1].toLongLong(&conv_ok);
1558  if (!conv_ok) return false;
1559 
1560  *pllStart = llSize - llValue;
1561  *pllEnd = llSize - 1;
1562  }
1563  else if (parts[1].isEmpty())
1564  {
1565  // ------------------------------------------------------------------
1566  // Does it match "####-"
1567  // ------------------------------------------------------------------
1568 
1569  *pllStart = parts[0].toLongLong(&conv_ok);
1570 
1571  if (!conv_ok)
1572  return false;
1573 
1574  *pllEnd = llSize - 1;
1575  }
1576  else
1577  {
1578  // ------------------------------------------------------------------
1579  // Must be "####-####"
1580  // ------------------------------------------------------------------
1581 
1582  *pllStart = parts[0].toLongLong(&conv_ok);
1583  if (!conv_ok) return false;
1584  *pllEnd = parts[1].toLongLong(&conv_ok);
1585  if (!conv_ok) return false;
1586 
1587  if (*pllStart > *pllEnd)
1588  return false;
1589  }
1590 
1591  LOG(VB_HTTP, LOG_DEBUG, QString("%1 Range Requested %2 - %3")
1592  .arg(getSocketHandle()) .arg(*pllStart) .arg(*pllEnd));
1593 
1594  return true;
1595 }
1596 
1598 //
1600 
1602 {
1603  // Strip out leading http://192.168.1.1:6544/ -> /
1604  // Should fix #8678
1605  // FIXME what about https?
1606  QRegExp sRegex("^http://.*/");
1607  sRegex.setMinimal(true);
1608  m_sBaseUrl.replace(sRegex, "/");
1609 
1610  QStringList sList = m_sBaseUrl.split('/', QString::SkipEmptyParts);
1611 
1612  m_sMethod = "";
1613 
1614  if (!sList.isEmpty())
1615  {
1616  m_sMethod = sList.last();
1617  sList.pop_back();
1618  }
1619 
1620  m_sBaseUrl = '/' + sList.join( "/" );
1621  LOG(VB_HTTP, LOG_INFO, QString("ExtractMethodFromURL(end) : %1 : %2")
1622  .arg(m_sMethod).arg(m_sBaseUrl));
1623 }
1624 
1626 //
1628 
1629 bool HTTPRequest::ProcessSOAPPayload( const QString &sSOAPAction )
1630 {
1631  bool bSuccess = false;
1632 
1633  // ----------------------------------------------------------------------
1634  // Open Supplied XML uPnp Description file.
1635  // ----------------------------------------------------------------------
1636 
1637  LOG(VB_HTTP, LOG_INFO,
1638  QString("HTTPRequest::ProcessSOAPPayload : %1 : ").arg(sSOAPAction));
1639  QDomDocument doc ( "request" );
1640 
1641  QString sErrMsg;
1642  int nErrLine = 0;
1643  int nErrCol = 0;
1644 
1645  if (!doc.setContent( m_sPayload, true, &sErrMsg, &nErrLine, &nErrCol ))
1646  {
1647  LOG(VB_GENERAL, LOG_ERR,
1648  QString( "Error parsing request at line: %1 column: %2 : %3" )
1649  .arg(nErrLine) .arg(nErrCol) .arg(sErrMsg));
1650  return( false );
1651  }
1652 
1653  // --------------------------------------------------------------
1654  // XML Document Loaded... now parse it
1655  // --------------------------------------------------------------
1656 
1657  QString sService;
1658 
1659  if (sSOAPAction.contains( '#' ))
1660  {
1661  m_sNameSpace = sSOAPAction.section( '#', 0, 0).remove( 0, 1);
1662  m_sMethod = sSOAPAction.section( '#', 1 );
1663  m_sMethod.remove( m_sMethod.length()-1, 1 );
1664  }
1665  else
1666  {
1667  if (sSOAPAction.contains( '/' ))
1668  {
1669  int nPos = sSOAPAction.lastIndexOf( '/' );
1670  m_sNameSpace = sSOAPAction.mid(1, nPos);
1671  m_sMethod = sSOAPAction.mid(nPos + 1,
1672  sSOAPAction.length() - nPos - 2);
1673 
1674  nPos = m_sNameSpace.lastIndexOf( '/', -2);
1675  sService = m_sNameSpace.mid(nPos + 1,
1676  m_sNameSpace.length() - nPos - 2);
1677  m_sNameSpace = m_sNameSpace.mid( 0, nPos );
1678  }
1679  else
1680  {
1681  m_sNameSpace.clear();
1682  m_sMethod = sSOAPAction;
1683  m_sMethod.remove( QChar( '\"' ) );
1684  }
1685  }
1686 
1687  QDomNodeList oNodeList = doc.elementsByTagNameNS( m_sNameSpace, m_sMethod );
1688 
1689  if (oNodeList.count() == 0)
1690  oNodeList =
1691  doc.elementsByTagNameNS("http://schemas.xmlsoap.org/soap/envelope/",
1692  "Body");
1693 
1694  if (oNodeList.count() > 0)
1695  {
1696  QDomNode oMethod = oNodeList.item(0);
1697 
1698  if (!oMethod.isNull())
1699  {
1700  m_bSOAPRequest = true;
1701 
1702  for ( QDomNode oNode = oMethod.firstChild(); !oNode.isNull();
1703  oNode = oNode.nextSibling() )
1704  {
1705  QDomElement e = oNode.toElement();
1706 
1707  if (!e.isNull())
1708  {
1709  QString sName = e.tagName();
1710  QString sValue = "";
1711 
1712  QDomText oText = oNode.firstChild().toText();
1713 
1714  if (!oText.isNull())
1715  sValue = oText.nodeValue();
1716 
1717  sName = QUrl::fromPercentEncoding(sName.toUtf8());
1718  sValue = QUrl::fromPercentEncoding(sValue.toUtf8());
1719 
1720  m_mapParams.insert( sName.trimmed().toLower(), sValue );
1721  }
1722  }
1723 
1724  bSuccess = true;
1725  }
1726  }
1727 
1728  return bSuccess;
1729 }
1730 
1732 //
1734 
1736 {
1737  Serializer *pSerializer = nullptr;
1738 
1739  if (m_bSOAPRequest)
1740  pSerializer = (Serializer *)new SoapSerializer(&m_response,
1741  m_sNameSpace, m_sMethod);
1742  else
1743  {
1744  QString sAccept = GetRequestHeader( "Accept", "*/*" );
1745 
1746  if (sAccept.contains( "application/json", Qt::CaseInsensitive ) ||
1747  sAccept.contains( "text/javascript", Qt::CaseInsensitive ))
1748  pSerializer = (Serializer *)new JSONSerializer(&m_response,
1749  m_sMethod);
1750  else if (sAccept.contains( "text/x-apple-plist+xml", Qt::CaseInsensitive ))
1751  pSerializer = (Serializer *)new XmlPListSerializer(&m_response);
1752  }
1753 
1754  // Default to XML
1755 
1756  if (pSerializer == nullptr)
1757  pSerializer = (Serializer *)new XmlSerializer(&m_response, m_sMethod);
1758 
1759  return pSerializer;
1760 }
1761 
1763 //
1765 
1766 QString HTTPRequest::Encode(const QString &sIn)
1767 {
1768  QString sStr = sIn;
1769 #if 0
1770  LOG(VB_HTTP, LOG_DEBUG,
1771  QString("HTTPRequest::Encode Input : %1").arg(sStr));
1772 #endif
1773  sStr.replace('&', "&amp;" ); // This _must_ come first
1774  sStr.replace('<', "&lt;" );
1775  sStr.replace('>', "&gt;" );
1776  sStr.replace('"', "&quot;");
1777  sStr.replace("'", "&apos;");
1778 
1779 #if 0
1780  LOG(VB_HTTP, LOG_DEBUG,
1781  QString("HTTPRequest::Encode Output : %1").arg(sStr));
1782 #endif
1783  return sStr;
1784 }
1785 
1787 //
1789 
1790 QString HTTPRequest::Decode(const QString& sIn)
1791 {
1792  QString sStr = sIn;
1793  sStr.replace("&amp;", "&");
1794  sStr.replace("&lt;", "<");
1795  sStr.replace("&gt;", ">");
1796  sStr.replace("&quot;", "\"");
1797  sStr.replace("&apos;", "'");
1798 
1799  return sStr;
1800 }
1801 
1803 //
1805 
1806 QString HTTPRequest::GetETagHash(const QByteArray &data)
1807 {
1808  QByteArray hash = QCryptographicHash::hash( data.data(), QCryptographicHash::Sha1);
1809 
1810  return ("\"" + hash.toHex() + "\"");
1811 }
1812 
1814 //
1816 
1817 bool HTTPRequest::IsUrlProtected( const QString &sBaseUrl )
1818 {
1819  QString sProtected = UPnp::GetConfiguration()->GetValue( "HTTP/Protected/Urls", "/setup;/Config" );
1820 
1821  QStringList oList = sProtected.split( ';' );
1822 
1823  for( int nIdx = 0; nIdx < oList.count(); nIdx++)
1824  {
1825  if (sBaseUrl.startsWith( oList[nIdx], Qt::CaseInsensitive ))
1826  return true;
1827  }
1828 
1829  return false;
1830 }
1831 
1833 //
1835 
1837 {
1838  QString authHeader;
1839 
1840  // For now we support a single realm, that will change
1841  QString realm = "MythTV";
1842 
1843  // Always use digest authentication where supported, it may be available
1844  // with HTTP 1.0 client as an extension, but we can't tell if that's the
1845  // case. It's guaranteed to be available for HTTP 1.1+
1846  if (m_nMajor >= 1 && m_nMinor > 0)
1847  {
1848  QString nonce = CalculateDigestNonce(MythDate::current_iso_string());
1849  QString stale = isStale ? "true" : "false"; // FIXME
1850  authHeader = QString("Digest realm=\"%1\",nonce=\"%2\","
1851  "qop=\"auth\",stale=\"%3\",algorithm=\"MD5\"")
1852  .arg(realm).arg(nonce).arg(stale);
1853  }
1854  else
1855  {
1856  authHeader = QString("Basic realm=\"%1\"").arg(realm);
1857  }
1858 
1859  return authHeader;
1860 }
1861 
1863 //
1865 
1866 QString HTTPRequest::CalculateDigestNonce(const QString& timeStamp)
1867 {
1868  QString uniqueID = QString("%1:%2").arg(timeStamp).arg(m_sPrivateToken);
1869  QString hash = QCryptographicHash::hash( uniqueID.toLatin1(), QCryptographicHash::Sha1).toHex(); // TODO: Change to Sha2 with QT5?
1870  QString nonce = QString("%1%2").arg(timeStamp).arg(hash); // Note: since this is going in a header it should avoid illegal chars
1871  return nonce;
1872 }
1873 
1875 //
1877 
1879 {
1880  LOG(VB_HTTP, LOG_NOTICE, "Attempting HTTP Basic Authentication");
1881  QStringList oList = m_mapHeaders[ "authorization" ].split( ' ' );
1882 
1883  if (m_nMajor == 1 && m_nMinor == 0) // We only support Basic auth for http 1.0 clients
1884  {
1885  LOG(VB_GENERAL, LOG_WARNING, "Basic authentication is only allowed for HTTP 1.0");
1886  return false;
1887  }
1888 
1889  QString sCredentials = QByteArray::fromBase64( oList[1].toUtf8() );
1890 
1891  oList = sCredentials.split( ':' );
1892 
1893  if (oList.count() < 2)
1894  {
1895  LOG(VB_GENERAL, LOG_WARNING, "Authorization attempt with invalid number of tokens");
1896  return false;
1897  }
1898 
1899  QString sUsername = oList[0];
1900  QString sPassword = oList[1];
1901 
1902  if (sUsername == "nouser") // Special logout username
1903  return false;
1904 
1905  MythSessionManager *sessionManager = gCoreContext->GetSessionManager();
1906  if (!MythSessionManager::IsValidUser(sUsername))
1907  {
1908  LOG(VB_GENERAL, LOG_WARNING, "Authorization attempt with invalid username");
1909  return false;
1910  }
1911 
1912  QString client = QString("WebFrontend_%1").arg(GetPeerAddress());
1913  MythUserSession session = sessionManager->LoginUser(sUsername, sPassword,
1914  client);
1915 
1916  if (!session.IsValid())
1917  {
1918  LOG(VB_GENERAL, LOG_WARNING, "Authorization attempt with invalid password");
1919  return false;
1920  }
1921 
1922  LOG(VB_HTTP, LOG_NOTICE, "Valid Authorization received");
1923 
1924  if (IsEncrypted()) // Only set a session cookie for encrypted connections, not safe otherwise
1925  SetCookie("sessionToken", session.GetSessionToken(),
1926  session.GetSessionExpires(), true);
1927 
1928  m_userSession = session;
1929 
1930  return false;
1931 }
1932 
1934 //
1936 
1938 {
1939  LOG(VB_HTTP, LOG_NOTICE, "Attempting HTTP Digest Authentication");
1940  QString realm = "MythTV"; // TODO Check which realm applies for the request path
1941 
1942  QString authMethod = m_mapHeaders[ "authorization" ].section(' ', 0, 0).toLower();
1943 
1944  if (authMethod != "digest")
1945  {
1946  LOG(VB_GENERAL, LOG_WARNING, "Invalid method in Authorization header");
1947  return false;
1948  }
1949 
1950  QString parameterStr = m_mapHeaders[ "authorization" ].section(' ', 1);
1951 
1952  QMap<QString, QString> paramMap;
1953  QStringList paramList = parameterStr.split(',');
1954  QStringList::iterator it;
1955  for (it = paramList.begin(); it != paramList.end(); ++it)
1956  {
1957  QString key = (*it).section('=', 0, 0).toLower().trimmed();
1958  // Since the value may contain '=' return everything after first occurence
1959  QString value = (*it).section('=', 1).trimmed();
1960  // Remove any quotes surrounding the value
1961  value.remove("\"");
1962  paramMap[key] = value;
1963  }
1964 
1965  if (paramMap.size() < 8)
1966  {
1967  LOG(VB_GENERAL, LOG_WARNING, "Invalid number of parameters in Authorization header");
1968  return false;
1969  }
1970 
1971  if (paramMap["nonce"].isEmpty() || paramMap["username"].isEmpty() ||
1972  paramMap["realm"].isEmpty() || paramMap["uri"].isEmpty() ||
1973  paramMap["response"].isEmpty() || paramMap["qop"].isEmpty() ||
1974  paramMap["cnonce"].isEmpty() || paramMap["nc"].isEmpty())
1975  {
1976  LOG(VB_GENERAL, LOG_WARNING, "Missing required parameters in Authorization header");
1977  return false;
1978  }
1979 
1980  if (paramMap["username"] == "nouser") // Special logout username
1981  return false;
1982 
1983  if (paramMap["uri"] != m_sOriginalUrl)
1984  {
1985  LOG(VB_GENERAL, LOG_WARNING, "Authorization URI doesn't match the "
1986  "request URI");
1987  m_nResponseStatus = 400; // Bad Request
1988  return false;
1989  }
1990 
1991  if (paramMap["realm"] != realm)
1992  {
1993  LOG(VB_GENERAL, LOG_WARNING, "Authorization realm doesn't match the "
1994  "realm of the requested content");
1995  return false;
1996  }
1997 
1998  QByteArray nonce = paramMap["nonce"].toLatin1();
1999  if (nonce.length() < 20)
2000  {
2001  LOG(VB_GENERAL, LOG_WARNING, "Authorization nonce is too short");
2002  return false;
2003  }
2004 
2005  QString nonceTimeStampStr = nonce.left(20); // ISO 8601 fixed length
2006  if (nonce != CalculateDigestNonce(nonceTimeStampStr))
2007  {
2008  LOG(VB_GENERAL, LOG_WARNING, "Authorization nonce doesn't match reference");
2009  LOG(VB_HTTP, LOG_DEBUG, QString("%1 vs %2").arg(QString(nonce))
2010  .arg(CalculateDigestNonce(nonceTimeStampStr)));
2011  return false;
2012  }
2013 
2014  const int AUTH_TIMEOUT = 2 * 60; // 2 Minute timeout to login, to reduce replay attack window
2015  QDateTime nonceTimeStamp = MythDate::fromString(nonceTimeStampStr);
2016  if (!nonceTimeStamp.isValid())
2017  {
2018  LOG(VB_GENERAL, LOG_WARNING, "Authorization nonce timestamp is invalid.");
2019  LOG(VB_HTTP, LOG_DEBUG, QString("Timestamp was '%1'").arg(nonceTimeStampStr));
2020  return false;
2021  }
2022 
2023  if (nonceTimeStamp.secsTo(MythDate::current()) > AUTH_TIMEOUT)
2024  {
2025  LOG(VB_HTTP, LOG_NOTICE, "Authorization nonce timestamp is invalid or too old.");
2026  // Tell the client that the submitted nonce has expired at which
2027  // point they may wish to try again with a fresh nonce instead of
2028  // telling the user that their credentials were invalid
2029  SetResponseHeader("WWW-Authenticate", GetAuthenticationHeader(true), true);
2030  return false;
2031  }
2032 
2033  MythSessionManager *sessionManager = gCoreContext->GetSessionManager();
2034  if (!MythSessionManager::IsValidUser(paramMap["username"]))
2035  {
2036  LOG(VB_GENERAL, LOG_WARNING, "Authorization attempt with invalid username");
2037  return false;
2038  }
2039 
2040  if (paramMap["response"].length() != 32)
2041  {
2042  LOG(VB_GENERAL, LOG_WARNING, "Authorization response field is invalid length");
2043  return false;
2044  }
2045 
2046  // If you're still reading this, well done, not far to go now
2047 
2048  QByteArray a1 = MythSessionManager::GetPasswordDigest(paramMap["username"]).toLatin1();
2049  //QByteArray a1 = "bcd911b2ecb15ffbd6d8e6e744d60cf6";
2050  QString methodDigest = QString("%1:%2").arg(GetRequestType()).arg(paramMap["uri"]);
2051  QByteArray a2 = QCryptographicHash::hash(methodDigest.toLatin1(),
2052  QCryptographicHash::Md5).toHex();
2053 
2054  QString responseDigest = QString("%1:%2:%3:%4:%5:%6").arg(QString(a1))
2055  .arg(paramMap["nonce"])
2056  .arg(paramMap["nc"])
2057  .arg(paramMap["cnonce"])
2058  .arg(paramMap["qop"])
2059  .arg(QString(a2));
2060  QByteArray kd = QCryptographicHash::hash(responseDigest.toLatin1(),
2061  QCryptographicHash::Md5).toHex();
2062 
2063  if (paramMap["response"].toLatin1() == kd)
2064  {
2065  LOG(VB_HTTP, LOG_NOTICE, "Valid Authorization received");
2066  QString client = QString("WebFrontend_%1").arg(GetPeerAddress());
2067  MythUserSession session = sessionManager->LoginUser(paramMap["username"],
2068  a1,
2069  client);
2070  if (!session.IsValid())
2071  {
2072  LOG(VB_GENERAL, LOG_ERR, "Valid Authorization received, but we "
2073  "failed to create a valid session");
2074  return false;
2075  }
2076 
2077  if (IsEncrypted()) // Only set a session cookie for encrypted connections, not safe otherwise
2078  SetCookie("sessionToken", session.GetSessionToken(),
2079  session.GetSessionExpires(), true);
2080 
2081  m_userSession = session;
2082 
2083  return true;
2084  }
2085 
2086  LOG(VB_GENERAL, LOG_WARNING, "Authorization attempt with invalid password digest");
2087  LOG(VB_HTTP, LOG_DEBUG, QString("Received hash was '%1', calculated hash was '%2'")
2088  .arg(paramMap["response"])
2089  .arg(QString(kd)));
2090 
2091  return false;
2092 }
2093 
2095 //
2097 
2099 {
2100  // Check if the existing user has permission to access this resource
2101  if (m_userSession.IsValid()) //m_userSession.CheckPermission())
2102  return true;
2103 
2104  QStringList oList = m_mapHeaders[ "authorization" ].split( ' ' );
2105 
2106  if (oList.count() < 2)
2107  return false;
2108 
2109  if (oList[0].compare( "basic", Qt::CaseInsensitive ) == 0)
2110  return BasicAuthentication();
2111  if (oList[0].compare( "digest", Qt::CaseInsensitive ) == 0)
2112  return DigestAuthentication();
2113 
2114  return false;
2115 }
2116 
2118 //
2120 
2121 void HTTPRequest::SetResponseHeader(const QString& sKey, const QString& sValue,
2122  bool replace)
2123 {
2124  if (!replace && m_mapRespHeaders.contains(sKey))
2125  return;
2126 
2127  m_mapRespHeaders[sKey] = sValue;
2128 }
2129 
2131 //
2133 
2134 void HTTPRequest::SetCookie(const QString &sKey, const QString &sValue,
2135  const QDateTime &expiryDate, bool secure)
2136 {
2137  if (secure && !IsEncrypted())
2138  {
2139  LOG(VB_GENERAL, LOG_WARNING, QString("HTTPRequest::SetCookie(%1=%2): "
2140  "A secure cookie cannot be set on an unencrypted connection.")
2141  .arg(sKey).arg(sValue));
2142  return;
2143  }
2144 
2145  QStringList cookieAttributes;
2146 
2147  // Key=Value
2148  cookieAttributes.append(QString("%1=%2").arg(sKey).arg(sValue));
2149 
2150  // Domain - Most browsers have problems with a hostname, so it's better to omit this
2151 // cookieAttributes.append(QString("Domain=%1").arg(GetHostName()));
2152 
2153  // Path - Fix to root, no call for restricting to other paths yet
2154  cookieAttributes.append("Path=/");
2155 
2156  // Expires - Expiry date, always set one, just good practice
2157  QString expires = MythDate::toString(expiryDate, MythDate::kRFC822); // RFC 822
2158  cookieAttributes.append(QString("Expires=%1").arg(expires)); // Cookie Expiry date
2159 
2160  // Secure - Only send this cookie over encrypted connections, it contains
2161  // sensitive info SECURITY
2162  if (secure)
2163  cookieAttributes.append("Secure");
2164 
2165  // HttpOnly - No cookie stealing javascript SECURITY
2166  cookieAttributes.append("HttpOnly");
2167 
2168  SetResponseHeader("Set-Cookie", cookieAttributes.join("; "));
2169 }
2170 
2172 //
2174 
2176 {
2177  // TODO: This only deals with the HTTP 1.1 case, 1.0 should be rare but we
2178  // should probably still handle it
2179 
2180  // RFC 3875 - The is the hostname or ip address in the client request, not
2181  // the name or ip we might otherwise know for this server
2182  QString hostname = m_mapHeaders["host"];
2183  if (!hostname.isEmpty())
2184  {
2185  // Strip the port
2186  if (hostname.contains("]:")) // IPv6 port
2187  {
2188  return hostname.section("]:", 0 , 0);
2189  }
2190  if (hostname.contains(":")) // IPv4 port
2191  {
2192  return hostname.section(":", 0 , 0);
2193  }
2194  return hostname;
2195  }
2196 
2197  return GetHostAddress();
2198 }
2199 
2200 
2202 {
2203  QString type;
2204  switch ( m_eType )
2205  {
2206  case RequestTypeUnknown :
2207  type = "UNKNOWN";
2208  break;
2209  case RequestTypeGet :
2210  type = "GET";
2211  break;
2212  case RequestTypeHead :
2213  type = "HEAD";
2214  break;
2215  case RequestTypePost :
2216  type = "POST";
2217  break;
2218  case RequestTypeOptions:
2219  type = "OPTIONS";
2220  break;
2221  case RequestTypeMSearch:
2222  type = "M-SEARCH";
2223  break;
2224  case RequestTypeNotify:
2225  type = "NOTIFY";
2226  break;
2227  case RequestTypeSubscribe :
2228  type = "SUBSCRIBE";
2229  break;
2230  case RequestTypeUnsubscribe :
2231  type = "UNSUBSCRIBE";
2232  break;
2233  case RequestTypeResponse :
2234  type = "RESPONSE";
2235  break;
2236  }
2237 
2238  return type;
2239 }
2240 
2241 void HTTPRequest::AddCORSHeaders( const QString &sOrigin )
2242 {
2243  // ----------------------------------------------------------------------
2244  // SECURITY: Access-Control-Allow-Origin Wildcard
2245  //
2246  // This is a REALLY bad idea, so bad in fact that I'm including it here but
2247  // commented out in the hope that anyone thinking of adding it in the future
2248  // will see it and then read this comment.
2249  //
2250  // Browsers do not verify that the origin is on the same network. This means
2251  // that a malicious script embedded or included into ANY webpage you visit
2252  // could then access servers on your local network including MythTV. They
2253  // can grab data, delete data including recordings and videos, schedule
2254  // recordings and generally ruin your day.
2255  //
2256  // This might seem paranoid and a remote possibility, but then that's how
2257  // a lot of exploits are born. Do NOT allow wildcards.
2258  //
2259  //m_mapRespHeaders[ "Access-Control-Allow-Origin" ] = "*";
2260  // ----------------------------------------------------------------------
2261 
2262  // ----------------------------------------------------------------------
2263  // SECURITY: Allow the WebFrontend on the Master backend and ONLY this
2264  // machine to access resources on a frontend or slave web server
2265  //
2266  // http://www.w3.org/TR/cors/#introduction
2267  // ----------------------------------------------------------------------
2268 
2269  QStringList allowedOrigins;
2270  char localhostname[1024]; // about HOST_NAME_MAX * 4
2271 
2272  int serverStatusPort = gCoreContext->GetMasterServerStatusPort();
2273  int backendSSLPort = gCoreContext->GetNumSetting( "BackendSSLPort",
2274  serverStatusPort + 10);
2275 
2276  QString masterAddrPort = QString("%1:%2")
2278  .arg(serverStatusPort);
2279  QString masterTLSAddrPort = QString("%1:%2")
2281  .arg(backendSSLPort);
2282 
2283  allowedOrigins << QString("http://%1").arg(masterAddrPort);
2284  allowedOrigins << QString("https://%2").arg(masterTLSAddrPort);
2285 
2286  if (!gethostname(localhostname, 1024))
2287  {
2288  allowedOrigins << QString("http://%1:%2")
2289  .arg(localhostname).arg(serverStatusPort);
2290  allowedOrigins << QString("https://%1:%2")
2291  .arg(localhostname).arg(backendSSLPort);
2292  }
2293 
2294  QStringList allowedOriginsList =
2295  gCoreContext->GetSetting("AllowedOriginsList", QString(
2296  "https://chromecast.mythtv.org,"
2297  "http://chromecast.mythtvcast.com"
2298  )).split(",");
2299 
2300  for (QStringList::const_iterator it = allowedOriginsList.begin();
2301  it != allowedOriginsList.end(); it++)
2302  {
2303  if ((*it).isEmpty())
2304  continue;
2305 
2306  if (*it == "*" || (!(*it).startsWith("http://") &&
2307  !(*it).startsWith("https://")))
2308  LOG(VB_GENERAL, LOG_ERR, QString("Illegal AllowedOriginsList"
2309  " entry '%1'. Must start with http[s]:// and not be *")
2310  .arg(*it));
2311  else
2312  allowedOrigins << *it;
2313  }
2314 
2315  if (VERBOSE_LEVEL_CHECK(VB_HTTP, LOG_DEBUG))
2316  for (QStringList::const_iterator it = allowedOrigins.begin();
2317  it != allowedOrigins.end(); it++)
2318  LOG(VB_HTTP, LOG_DEBUG, QString("Will allow Origin: %1").arg(*it));
2319 
2320  if (allowedOrigins.contains(sOrigin))
2321  {
2322  SetResponseHeader( "Access-Control-Allow-Origin" , sOrigin);
2323  SetResponseHeader( "Access-Control-Allow-Credentials" , "true");
2324  SetResponseHeader( "Access-Control-Allow-Headers" , "Content-Type");
2325  LOG(VB_HTTP, LOG_DEBUG, QString("Allow-Origin: %1)").arg(sOrigin));
2326  }
2327  else
2328  LOG(VB_GENERAL, LOG_CRIT, QString("HTTPRequest: Cross-origin request "
2329  "received with origin (%1)")
2330  .arg(sOrigin));
2331 }
2332 
2335 //
2336 // BufferedSocketDeviceRequest Class Implementation
2337 //
2340 
2342 {
2343  QString sLine;
2344 
2345  if (m_pSocket && m_pSocket->isValid() &&
2346  m_pSocket->state() == QAbstractSocket::ConnectedState)
2347  {
2348  bool timeout = false;
2349  MythTimer timer;
2350  timer.start();
2351  while (!m_pSocket->canReadLine() && !timeout)
2352  {
2353  timeout = !(m_pSocket->waitForReadyRead( msecs ));
2354 
2355  if ( timer.elapsed() >= msecs )
2356  {
2357  timeout = true;
2358  LOG(VB_HTTP, LOG_INFO, "BufferedSocketDeviceRequest::ReadLine() - Exceeded Total Elapsed Wait Time." );
2359  }
2360  }
2361 
2362  if (!timeout)
2363  sLine = m_pSocket->readLine();
2364  }
2365 
2366  return( sLine );
2367 }
2368 
2370 //
2372 
2373 qint64 BufferedSocketDeviceRequest::ReadBlock(char *pData, qint64 nMaxLen,
2374  int msecs)
2375 {
2376  if (m_pSocket && m_pSocket->isValid() &&
2377  m_pSocket->state() == QAbstractSocket::ConnectedState)
2378  {
2379  if (msecs == 0)
2380  return( m_pSocket->read( pData, nMaxLen ));
2381 
2382  bool bTimeout = false;
2383  MythTimer timer;
2384  timer.start();
2385  while ( (m_pSocket->bytesAvailable() < (int)nMaxLen) && !bTimeout ) // This can end up waiting far longer than msecs
2386  {
2387  bTimeout = !(m_pSocket->waitForReadyRead( msecs ));
2388 
2389  if ( timer.elapsed() >= msecs )
2390  {
2391  bTimeout = true;
2392  LOG(VB_HTTP, LOG_INFO, "BufferedSocketDeviceRequest::ReadBlock() - Exceeded Total Elapsed Wait Time." );
2393  }
2394  }
2395 
2396  // Just return what we have even if timed out.
2397 
2398  return( m_pSocket->read( pData, nMaxLen ));
2399  }
2400 
2401  return( -1 );
2402 }
2403 
2405 //
2407 
2408 qint64 BufferedSocketDeviceRequest::WriteBlock(const char *pData, qint64 nLen)
2409 {
2410  qint64 bytesWritten = -1;
2411  if (m_pSocket && m_pSocket->isValid() &&
2412  m_pSocket->state() == QAbstractSocket::ConnectedState)
2413  {
2414  bytesWritten = m_pSocket->write( pData, nLen );
2415  m_pSocket->waitForBytesWritten();
2416  }
2417 
2418  return( bytesWritten );
2419 }
2420 
2422 //
2424 
2426 {
2427  return( m_pSocket->localAddress().toString() );
2428 }
2429 
2431 //
2433 
2435 {
2436  return( m_pSocket->localPort() );
2437 }
2438 
2439 
2441 //
2443 
2445 {
2446  return( m_pSocket->peerAddress().toString() );
2447 }
2448 
HttpRequestType
Definition: httprequest.h:39
QString GetLanguageAndVariant(void)
Returns the user-set language and variant.
QMap< QString, QString > QStringMap
Definition: upnputil.h:40
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
MythSessionManager * GetSessionManager(void)
Serializer * GetSerializer()
#define SOAP_ENVELOPE_BEGIN
Definition: httprequest.h:29
QString GetResponseHeaders(void)
HttpContentType SetContentType(const QString &sType)
QString GetRequestHeader(const QString &sKey, QString sDefault)
HttpContentType
Definition: httprequest.h:61
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.
static const int g_nMIMELength
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:107
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)
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)
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
const QDateTime GetSessionExpires() const
Definition: mythsession.h:43
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()
const char * pszExtension
Definition: httprequest.h:85
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
const char * pszType
Definition: httprequest.h:86
static long GetParameters(QString sParams, QStringMap &mapParams)
qint64 SendData(QIODevice *pDevice, qint64 llStart, qint64 llBytes)
#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)
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
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
static bool IsUrlProtected(const QString &sBaseUrl)
quint16 GetHostPort() override
void SetRequestProtocol(const QString &sLine)