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;
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  pSerializer = (Serializer *)new JSONSerializer(&m_response,
1748  m_sMethod);
1749  else if (sAccept.contains( "text/javascript", Qt::CaseInsensitive ))
1750  pSerializer = (Serializer *)new JSONSerializer(&m_response,
1751  m_sMethod);
1752  else if (sAccept.contains( "text/x-apple-plist+xml", Qt::CaseInsensitive ))
1753  pSerializer = (Serializer *)new XmlPListSerializer(&m_response);
1754  }
1755 
1756  // Default to XML
1757 
1758  if (pSerializer == nullptr)
1759  pSerializer = (Serializer *)new XmlSerializer(&m_response, m_sMethod);
1760 
1761  return pSerializer;
1762 }
1763 
1765 //
1767 
1768 QString HTTPRequest::Encode(const QString &sIn)
1769 {
1770  QString sStr = sIn;
1771 #if 0
1772  LOG(VB_HTTP, LOG_DEBUG,
1773  QString("HTTPRequest::Encode Input : %1").arg(sStr));
1774 #endif
1775  sStr.replace('&', "&amp;" ); // This _must_ come first
1776  sStr.replace('<', "&lt;" );
1777  sStr.replace('>', "&gt;" );
1778  sStr.replace('"', "&quot;");
1779  sStr.replace("'", "&apos;");
1780 
1781 #if 0
1782  LOG(VB_HTTP, LOG_DEBUG,
1783  QString("HTTPRequest::Encode Output : %1").arg(sStr));
1784 #endif
1785  return sStr;
1786 }
1787 
1789 //
1791 
1792 QString HTTPRequest::Decode(const QString& sIn)
1793 {
1794  QString sStr = sIn;
1795  sStr.replace("&amp;", "&");
1796  sStr.replace("&lt;", "<");
1797  sStr.replace("&gt;", ">");
1798  sStr.replace("&quot;", "\"");
1799  sStr.replace("&apos;", "'");
1800 
1801  return sStr;
1802 }
1803 
1805 //
1807 
1808 QString HTTPRequest::GetETagHash(const QByteArray &data)
1809 {
1810  QByteArray hash = QCryptographicHash::hash( data.data(), QCryptographicHash::Sha1);
1811 
1812  return ("\"" + hash.toHex() + "\"");
1813 }
1814 
1816 //
1818 
1819 bool HTTPRequest::IsUrlProtected( const QString &sBaseUrl )
1820 {
1821  QString sProtected = UPnp::GetConfiguration()->GetValue( "HTTP/Protected/Urls", "/setup;/Config" );
1822 
1823  QStringList oList = sProtected.split( ';' );
1824 
1825  for( int nIdx = 0; nIdx < oList.count(); nIdx++)
1826  {
1827  if (sBaseUrl.startsWith( oList[nIdx], Qt::CaseInsensitive ))
1828  return true;
1829  }
1830 
1831  return false;
1832 }
1833 
1835 //
1837 
1839 {
1840  QString authHeader;
1841 
1842  // For now we support a single realm, that will change
1843  QString realm = "MythTV";
1844 
1845  // Always use digest authentication where supported, it may be available
1846  // with HTTP 1.0 client as an extension, but we can't tell if that's the
1847  // case. It's guaranteed to be available for HTTP 1.1+
1848  if (m_nMajor >= 1 && m_nMinor > 0)
1849  {
1850  QString nonce = CalculateDigestNonce(MythDate::current_iso_string());
1851  QString stale = isStale ? "true" : "false"; // FIXME
1852  authHeader = QString("Digest realm=\"%1\",nonce=\"%2\","
1853  "qop=\"auth\",stale=\"%3\",algorithm=\"MD5\"")
1854  .arg(realm).arg(nonce).arg(stale);
1855  }
1856  else
1857  {
1858  authHeader = QString("Basic realm=\"%1\"").arg(realm);
1859  }
1860 
1861  return authHeader;
1862 }
1863 
1865 //
1867 
1868 QString HTTPRequest::CalculateDigestNonce(const QString& timeStamp)
1869 {
1870  QString uniqueID = QString("%1:%2").arg(timeStamp).arg(m_sPrivateToken);
1871  QString hash = QCryptographicHash::hash( uniqueID.toLatin1(), QCryptographicHash::Sha1).toHex(); // TODO: Change to Sha2 with QT5?
1872  QString nonce = QString("%1%2").arg(timeStamp).arg(hash); // Note: since this is going in a header it should avoid illegal chars
1873  return nonce;
1874 }
1875 
1877 //
1879 
1881 {
1882  LOG(VB_HTTP, LOG_NOTICE, "Attempting HTTP Basic Authentication");
1883  QStringList oList = m_mapHeaders[ "authorization" ].split( ' ' );
1884 
1885  if (m_nMajor == 1 && m_nMinor == 0) // We only support Basic auth for http 1.0 clients
1886  {
1887  LOG(VB_GENERAL, LOG_WARNING, "Basic authentication is only allowed for HTTP 1.0");
1888  return false;
1889  }
1890 
1891  QString sCredentials = QByteArray::fromBase64( oList[1].toUtf8() );
1892 
1893  oList = sCredentials.split( ':' );
1894 
1895  if (oList.count() < 2)
1896  {
1897  LOG(VB_GENERAL, LOG_WARNING, "Authorization attempt with invalid number of tokens");
1898  return false;
1899  }
1900 
1901  QString sUsername = oList[0];
1902  QString sPassword = oList[1];
1903 
1904  if (sUsername == "nouser") // Special logout username
1905  return false;
1906 
1907  MythSessionManager *sessionManager = gCoreContext->GetSessionManager();
1908  if (!sessionManager->IsValidUser(sUsername))
1909  {
1910  LOG(VB_GENERAL, LOG_WARNING, "Authorization attempt with invalid username");
1911  return false;
1912  }
1913 
1914  QString client = QString("WebFrontend_%1").arg(GetPeerAddress());
1915  MythUserSession session = sessionManager->LoginUser(sUsername, sPassword,
1916  client);
1917 
1918  if (!session.IsValid())
1919  {
1920  LOG(VB_GENERAL, LOG_WARNING, "Authorization attempt with invalid password");
1921  return false;
1922  }
1923 
1924  LOG(VB_HTTP, LOG_NOTICE, "Valid Authorization received");
1925 
1926  if (IsEncrypted()) // Only set a session cookie for encrypted connections, not safe otherwise
1927  SetCookie("sessionToken", session.GetSessionToken(),
1928  session.GetSessionExpires(), true);
1929 
1930  m_userSession = session;
1931 
1932  return false;
1933 }
1934 
1936 //
1938 
1940 {
1941  LOG(VB_HTTP, LOG_NOTICE, "Attempting HTTP Digest Authentication");
1942  QString realm = "MythTV"; // TODO Check which realm applies for the request path
1943 
1944  QString authMethod = m_mapHeaders[ "authorization" ].section(' ', 0, 0).toLower();
1945 
1946  if (authMethod != "digest")
1947  {
1948  LOG(VB_GENERAL, LOG_WARNING, "Invalid method in Authorization header");
1949  return false;
1950  }
1951 
1952  QString parameterStr = m_mapHeaders[ "authorization" ].section(' ', 1);
1953 
1954  QMap<QString, QString> paramMap;
1955  QStringList paramList = parameterStr.split(',');
1956  QStringList::iterator it;
1957  for (it = paramList.begin(); it != paramList.end(); ++it)
1958  {
1959  QString key = (*it).section('=', 0, 0).toLower().trimmed();
1960  // Since the value may contain '=' return everything after first occurence
1961  QString value = (*it).section('=', 1).trimmed();
1962  // Remove any quotes surrounding the value
1963  value.remove("\"");
1964  paramMap[key] = value;
1965  }
1966 
1967  if (paramMap.size() < 8)
1968  {
1969  LOG(VB_GENERAL, LOG_WARNING, "Invalid number of parameters in Authorization header");
1970  return false;
1971  }
1972 
1973  if (paramMap["nonce"].isEmpty() || paramMap["username"].isEmpty() ||
1974  paramMap["realm"].isEmpty() || paramMap["uri"].isEmpty() ||
1975  paramMap["response"].isEmpty() || paramMap["qop"].isEmpty() ||
1976  paramMap["cnonce"].isEmpty() || paramMap["nc"].isEmpty())
1977  {
1978  LOG(VB_GENERAL, LOG_WARNING, "Missing required parameters in Authorization header");
1979  return false;
1980  }
1981 
1982  if (paramMap["username"] == "nouser") // Special logout username
1983  return false;
1984 
1985  if (paramMap["uri"] != m_sOriginalUrl)
1986  {
1987  LOG(VB_GENERAL, LOG_WARNING, "Authorization URI doesn't match the "
1988  "request URI");
1989  m_nResponseStatus = 400; // Bad Request
1990  return false;
1991  }
1992 
1993  if (paramMap["realm"] != realm)
1994  {
1995  LOG(VB_GENERAL, LOG_WARNING, "Authorization realm doesn't match the "
1996  "realm of the requested content");
1997  return false;
1998  }
1999 
2000  QByteArray nonce = paramMap["nonce"].toLatin1();
2001  if (nonce.length() < 20)
2002  {
2003  LOG(VB_GENERAL, LOG_WARNING, "Authorization nonce is too short");
2004  return false;
2005  }
2006 
2007  QString nonceTimeStampStr = nonce.left(20); // ISO 8601 fixed length
2008  if (nonce != CalculateDigestNonce(nonceTimeStampStr))
2009  {
2010  LOG(VB_GENERAL, LOG_WARNING, "Authorization nonce doesn't match reference");
2011  LOG(VB_HTTP, LOG_DEBUG, QString("%1 vs %2").arg(QString(nonce))
2012  .arg(CalculateDigestNonce(nonceTimeStampStr)));
2013  return false;
2014  }
2015 
2016  const int AUTH_TIMEOUT = 2 * 60; // 2 Minute timeout to login, to reduce replay attack window
2017  QDateTime nonceTimeStamp = MythDate::fromString(nonceTimeStampStr);
2018  if (!nonceTimeStamp.isValid())
2019  {
2020  LOG(VB_GENERAL, LOG_WARNING, "Authorization nonce timestamp is invalid.");
2021  LOG(VB_HTTP, LOG_DEBUG, QString("Timestamp was '%1'").arg(nonceTimeStampStr));
2022  return false;
2023  }
2024 
2025  if (nonceTimeStamp.secsTo(MythDate::current()) > AUTH_TIMEOUT)
2026  {
2027  LOG(VB_HTTP, LOG_NOTICE, "Authorization nonce timestamp is invalid or too old.");
2028  // Tell the client that the submitted nonce has expired at which
2029  // point they may wish to try again with a fresh nonce instead of
2030  // telling the user that their credentials were invalid
2031  SetResponseHeader("WWW-Authenticate", GetAuthenticationHeader(true), true);
2032  return false;
2033  }
2034 
2035  MythSessionManager *sessionManager = gCoreContext->GetSessionManager();
2036  if (!sessionManager->IsValidUser(paramMap["username"]))
2037  {
2038  LOG(VB_GENERAL, LOG_WARNING, "Authorization attempt with invalid username");
2039  return false;
2040  }
2041 
2042  if (paramMap["response"].length() != 32)
2043  {
2044  LOG(VB_GENERAL, LOG_WARNING, "Authorization response field is invalid length");
2045  return false;
2046  }
2047 
2048  // If you're still reading this, well done, not far to go now
2049 
2050  QByteArray a1 = sessionManager->GetPasswordDigest(paramMap["username"]).toLatin1();
2051  //QByteArray a1 = "bcd911b2ecb15ffbd6d8e6e744d60cf6";
2052  QString methodDigest = QString("%1:%2").arg(GetRequestType()).arg(paramMap["uri"]);
2053  QByteArray a2 = QCryptographicHash::hash(methodDigest.toLatin1(),
2054  QCryptographicHash::Md5).toHex();
2055 
2056  QString responseDigest = QString("%1:%2:%3:%4:%5:%6").arg(QString(a1))
2057  .arg(paramMap["nonce"])
2058  .arg(paramMap["nc"])
2059  .arg(paramMap["cnonce"])
2060  .arg(paramMap["qop"])
2061  .arg(QString(a2));
2062  QByteArray kd = QCryptographicHash::hash(responseDigest.toLatin1(),
2063  QCryptographicHash::Md5).toHex();
2064 
2065  if (paramMap["response"].toLatin1() == kd)
2066  {
2067  LOG(VB_HTTP, LOG_NOTICE, "Valid Authorization received");
2068  QString client = QString("WebFrontend_%1").arg(GetPeerAddress());
2069  MythUserSession session = sessionManager->LoginUser(paramMap["username"],
2070  a1,
2071  client);
2072  if (!session.IsValid())
2073  {
2074  LOG(VB_GENERAL, LOG_ERR, "Valid Authorization received, but we "
2075  "failed to create a valid session");
2076  return false;
2077  }
2078 
2079  if (IsEncrypted()) // Only set a session cookie for encrypted connections, not safe otherwise
2080  SetCookie("sessionToken", session.GetSessionToken(),
2081  session.GetSessionExpires(), true);
2082 
2083  m_userSession = session;
2084 
2085  return true;
2086  }
2087 
2088  LOG(VB_GENERAL, LOG_WARNING, "Authorization attempt with invalid password digest");
2089  LOG(VB_HTTP, LOG_DEBUG, QString("Received hash was '%1', calculated hash was '%2'")
2090  .arg(paramMap["response"])
2091  .arg(QString(kd)));
2092 
2093  return false;
2094 }
2095 
2097 //
2099 
2101 {
2102  // Check if the existing user has permission to access this resource
2103  if (m_userSession.IsValid()) //m_userSession.CheckPermission())
2104  return true;
2105 
2106  QStringList oList = m_mapHeaders[ "authorization" ].split( ' ' );
2107 
2108  if (oList.count() < 2)
2109  return false;
2110 
2111  if (oList[0].compare( "basic", Qt::CaseInsensitive ) == 0)
2112  return BasicAuthentication();
2113  if (oList[0].compare( "digest", Qt::CaseInsensitive ) == 0)
2114  return DigestAuthentication();
2115 
2116  return false;
2117 }
2118 
2120 //
2122 
2123 void HTTPRequest::SetResponseHeader(const QString& sKey, const QString& sValue,
2124  bool replace)
2125 {
2126  if (!replace && m_mapRespHeaders.contains(sKey))
2127  return;
2128 
2129  m_mapRespHeaders[sKey] = sValue;
2130 }
2131 
2133 //
2135 
2136 void HTTPRequest::SetCookie(const QString &sKey, const QString &sValue,
2137  const QDateTime &expiryDate, bool secure)
2138 {
2139  if (secure && !IsEncrypted())
2140  {
2141  LOG(VB_GENERAL, LOG_WARNING, QString("HTTPRequest::SetCookie(%1=%2): "
2142  "A secure cookie cannot be set on an unencrypted connection.")
2143  .arg(sKey).arg(sValue));
2144  return;
2145  }
2146 
2147  QStringList cookieAttributes;
2148 
2149  // Key=Value
2150  cookieAttributes.append(QString("%1=%2").arg(sKey).arg(sValue));
2151 
2152  // Domain - Most browsers have problems with a hostname, so it's better to omit this
2153 // cookieAttributes.append(QString("Domain=%1").arg(GetHostName()));
2154 
2155  // Path - Fix to root, no call for restricting to other paths yet
2156  cookieAttributes.append("Path=/");
2157 
2158  // Expires - Expiry date, always set one, just good practice
2159  QString expires = MythDate::toString(expiryDate, MythDate::kRFC822); // RFC 822
2160  cookieAttributes.append(QString("Expires=%1").arg(expires)); // Cookie Expiry date
2161 
2162  // Secure - Only send this cookie over encrypted connections, it contains
2163  // sensitive info SECURITY
2164  if (secure)
2165  cookieAttributes.append("Secure");
2166 
2167  // HttpOnly - No cookie stealing javascript SECURITY
2168  cookieAttributes.append("HttpOnly");
2169 
2170  SetResponseHeader("Set-Cookie", cookieAttributes.join("; "));
2171 }
2172 
2174 //
2176 
2178 {
2179  // TODO: This only deals with the HTTP 1.1 case, 1.0 should be rare but we
2180  // should probably still handle it
2181 
2182  // RFC 3875 - The is the hostname or ip address in the client request, not
2183  // the name or ip we might otherwise know for this server
2184  QString hostname = m_mapHeaders["host"];
2185  if (!hostname.isEmpty())
2186  {
2187  // Strip the port
2188  if (hostname.contains("]:")) // IPv6 port
2189  {
2190  return hostname.section("]:", 0 , 0);
2191  }
2192  if (hostname.contains(":")) // IPv4 port
2193  {
2194  return hostname.section(":", 0 , 0);
2195  }
2196  return hostname;
2197  }
2198 
2199  return GetHostAddress();
2200 }
2201 
2202 
2204 {
2205  QString type;
2206  switch ( m_eType )
2207  {
2208  case RequestTypeUnknown :
2209  type = "UNKNOWN";
2210  break;
2211  case RequestTypeGet :
2212  type = "GET";
2213  break;
2214  case RequestTypeHead :
2215  type = "HEAD";
2216  break;
2217  case RequestTypePost :
2218  type = "POST";
2219  break;
2220  case RequestTypeOptions:
2221  type = "OPTIONS";
2222  break;
2223  case RequestTypeMSearch:
2224  type = "M-SEARCH";
2225  break;
2226  case RequestTypeNotify:
2227  type = "NOTIFY";
2228  break;
2229  case RequestTypeSubscribe :
2230  type = "SUBSCRIBE";
2231  break;
2232  case RequestTypeUnsubscribe :
2233  type = "UNSUBSCRIBE";
2234  break;
2235  case RequestTypeResponse :
2236  type = "RESPONSE";
2237  break;
2238  }
2239 
2240  return type;
2241 }
2242 
2243 void HTTPRequest::AddCORSHeaders( const QString &sOrigin )
2244 {
2245  // ----------------------------------------------------------------------
2246  // SECURITY: Access-Control-Allow-Origin Wildcard
2247  //
2248  // This is a REALLY bad idea, so bad in fact that I'm including it here but
2249  // commented out in the hope that anyone thinking of adding it in the future
2250  // will see it and then read this comment.
2251  //
2252  // Browsers do not verify that the origin is on the same network. This means
2253  // that a malicious script embedded or included into ANY webpage you visit
2254  // could then access servers on your local network including MythTV. They
2255  // can grab data, delete data including recordings and videos, schedule
2256  // recordings and generally ruin your day.
2257  //
2258  // This might seem paranoid and a remote possibility, but then that's how
2259  // a lot of exploits are born. Do NOT allow wildcards.
2260  //
2261  //m_mapRespHeaders[ "Access-Control-Allow-Origin" ] = "*";
2262  // ----------------------------------------------------------------------
2263 
2264  // ----------------------------------------------------------------------
2265  // SECURITY: Allow the WebFrontend on the Master backend and ONLY this
2266  // machine to access resources on a frontend or slave web server
2267  //
2268  // http://www.w3.org/TR/cors/#introduction
2269  // ----------------------------------------------------------------------
2270 
2271  QStringList allowedOrigins;
2272  char localhostname[1024]; // about HOST_NAME_MAX * 4
2273 
2274  int serverStatusPort = gCoreContext->GetMasterServerStatusPort();
2275  int backendSSLPort = gCoreContext->GetNumSetting( "BackendSSLPort",
2276  serverStatusPort + 10);
2277 
2278  QString masterAddrPort = QString("%1:%2")
2280  .arg(serverStatusPort);
2281  QString masterTLSAddrPort = QString("%1:%2")
2283  .arg(backendSSLPort);
2284 
2285  allowedOrigins << QString("http://%1").arg(masterAddrPort);
2286  allowedOrigins << QString("https://%2").arg(masterTLSAddrPort);
2287 
2288  if (!gethostname(localhostname, 1024))
2289  {
2290  allowedOrigins << QString("http://%1:%2")
2291  .arg(localhostname).arg(serverStatusPort);
2292  allowedOrigins << QString("https://%1:%2")
2293  .arg(localhostname).arg(backendSSLPort);
2294  }
2295 
2296  QStringList allowedOriginsList =
2297  gCoreContext->GetSetting("AllowedOriginsList", QString(
2298  "https://chromecast.mythtv.org,"
2299  "http://chromecast.mythtvcast.com"
2300  )).split(",");
2301 
2302  for (QStringList::const_iterator it = allowedOriginsList.begin();
2303  it != allowedOriginsList.end(); it++)
2304  {
2305  if ((*it).isEmpty())
2306  continue;
2307 
2308  if (*it == "*" || (!(*it).startsWith("http://") &&
2309  !(*it).startsWith("https://")))
2310  LOG(VB_GENERAL, LOG_ERR, QString("Illegal AllowedOriginsList"
2311  " entry '%1'. Must start with http[s]:// and not be *")
2312  .arg(*it));
2313  else
2314  allowedOrigins << *it;
2315  }
2316 
2317  if (VERBOSE_LEVEL_CHECK(VB_HTTP, LOG_DEBUG))
2318  for (QStringList::const_iterator it = allowedOrigins.begin();
2319  it != allowedOrigins.end(); it++)
2320  LOG(VB_HTTP, LOG_DEBUG, QString("Will allow Origin: %1").arg(*it));
2321 
2322  if (allowedOrigins.contains(sOrigin))
2323  {
2324  SetResponseHeader( "Access-Control-Allow-Origin" , sOrigin);
2325  SetResponseHeader( "Access-Control-Allow-Credentials" , "true");
2326  SetResponseHeader( "Access-Control-Allow-Headers" , "Content-Type");
2327  LOG(VB_HTTP, LOG_DEBUG, QString("Allow-Origin: %1)").arg(sOrigin));
2328  }
2329  else
2330  LOG(VB_GENERAL, LOG_CRIT, QString("HTTPRequest: Cross-origin request "
2331  "received with origin (%1)")
2332  .arg(sOrigin));
2333 }
2334 
2337 //
2338 // BufferedSocketDeviceRequest Class Implementation
2339 //
2342 
2344 {
2345  QString sLine;
2346 
2347  if (m_pSocket && m_pSocket->isValid() &&
2348  m_pSocket->state() == QAbstractSocket::ConnectedState)
2349  {
2350  bool timeout = false;
2351  MythTimer timer;
2352  timer.start();
2353  while (!m_pSocket->canReadLine() && !timeout)
2354  {
2355  timeout = !(m_pSocket->waitForReadyRead( msecs ));
2356 
2357  if ( timer.elapsed() >= msecs )
2358  {
2359  timeout = true;
2360  LOG(VB_HTTP, LOG_INFO, "BufferedSocketDeviceRequest::ReadLine() - Exceeded Total Elapsed Wait Time." );
2361  }
2362  }
2363 
2364  if (!timeout)
2365  sLine = m_pSocket->readLine();
2366  }
2367 
2368  return( sLine );
2369 }
2370 
2372 //
2374 
2375 qint64 BufferedSocketDeviceRequest::ReadBlock(char *pData, qint64 nMaxLen,
2376  int msecs)
2377 {
2378  if (m_pSocket && m_pSocket->isValid() &&
2379  m_pSocket->state() == QAbstractSocket::ConnectedState)
2380  {
2381  if (msecs == 0)
2382  return( m_pSocket->read( pData, nMaxLen ));
2383 
2384  bool bTimeout = false;
2385  MythTimer timer;
2386  timer.start();
2387  while ( (m_pSocket->bytesAvailable() < (int)nMaxLen) && !bTimeout ) // This can end up waiting far longer than msecs
2388  {
2389  bTimeout = !(m_pSocket->waitForReadyRead( msecs ));
2390 
2391  if ( timer.elapsed() >= msecs )
2392  {
2393  bTimeout = true;
2394  LOG(VB_HTTP, LOG_INFO, "BufferedSocketDeviceRequest::ReadBlock() - Exceeded Total Elapsed Wait Time." );
2395  }
2396  }
2397 
2398  // Just return what we have even if timed out.
2399 
2400  return( m_pSocket->read( pData, nMaxLen ));
2401  }
2402 
2403  return( -1 );
2404 }
2405 
2407 //
2409 
2410 qint64 BufferedSocketDeviceRequest::WriteBlock(const char *pData, qint64 nLen)
2411 {
2412  qint64 bytesWritten = -1;
2413  if (m_pSocket && m_pSocket->isValid() &&
2414  m_pSocket->state() == QAbstractSocket::ConnectedState)
2415  {
2416  bytesWritten = m_pSocket->write( pData, nLen );
2417  m_pSocket->waitForBytesWritten();
2418  }
2419 
2420  return( bytesWritten );
2421 }
2422 
2424 //
2426 
2428 {
2429  return( m_pSocket->localAddress().toString() );
2430 }
2431 
2433 //
2435 
2437 {
2438  return( m_pSocket->localPort() );
2439 }
2440 
2441 
2443 //
2445 
2447 {
2448  return( m_pSocket->peerAddress().toString() );
2449 }
2450 
HttpRequestType
Definition: httprequest.h:39
QString GetLanguageAndVariant(void)
Returns the user-set language and variant.
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)
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
static const int g_nMIMELength
bool ParseRequest()
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:109
virtual QString GetContentType()=0
static QStringList GetSupportedMimeTypes()
QString GetMasterServerIP(void)
Returns the Master Backend IP address If the address is an IPv6 address, the scope Id is removed.
QString GetResponseType(void)
QByteArray GetResponsePage(void)
QMap< QString, QString > QStringMap
Definition: upnputil.h:40
void ExtractMethodFromURL()
bool IsValidUser(const QString &username)
Check if the given user exists but not whether there is a valid session open for them!
We use digest authentication because it protects the password over unprotected networks.
Definition: mythsession.h:98
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
Present date/time in UTC.
Definition: mythdate.h:28
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)
HTTP Date format.
Definition: mythdate.h:27
int elapsed(void) const
Returns milliseconds elapsed since last start() or restart()
Definition: mythtimer.cpp:90
static QString StaticPage
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:87
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)
const char * pszType
Definition: httprequest.h:88
static long GetParameters(QString sParams, QStringMap &mapParams)
qint64 SendData(QIODevice *pDevice, qint64 llStart, qint64 llBytes)
QString GetResponseProtocol() const
#define SOAP_ENVELOPE_END
Definition: httprequest.h:32
QString GetRequestType() const
int GetMasterServerStatusPort(void)
Returns the Master Backend status port If no master server status port has been defined in the databa...
qint64 SendResponse(void)
static QString GetServerVersion(void)
Definition: httpserver.cpp:292
QString GetAuthenticationHeader(bool isStale=false)
#define SENDFILE_BUFFER_SIZE
QString GetRequestProtocol() const
void FormatErrorResponse(bool bServerError, const QString &sFaultString, const QString &sDetails)
static MIMETypes g_MIMETypes[]
Definition: httprequest.cpp:63
void FormatFileResponse(const QString &sFileName)
QString CalculateDigestNonce(const QString &timeStamp)
bool ParseKeepAlive(void)
QDateTime fromString(const QString &dtstr)
Converts kFilename && kISODate formats to QDateTime.
Definition: mythdate.cpp:30
bool ProcessSOAPPayload(const QString &sSOAPAction)
void start(void)
starts measuring elapsed time.
Definition: mythtimer.cpp:47
qint64 ReadBlock(char *pData, qint64 nMaxLen, int msecs=0) override
QString ReadLine(int msecs) override
bool IsUrlProtected(const QString &sBaseUrl)
quint16 GetHostPort() override
void SetRequestProtocol(const QString &sLine)