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