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 <QtGlobal>
16#if QT_VERSION >= QT_VERSION_CHECK(6,5,0)
17#include <QtSystemDetection>
18#endif
19#include <QFile>
20#include <QFileInfo>
21#include <QHostInfo>
22#include <QStringList>
23#include <QCryptographicHash>
24#include <QDateTime>
25#include <Qt>
26#include <QUrl>
27
28#include <algorithm>
29#include <cerrno>
30#include <cstdlib>
31#include <fcntl.h>
32#include <sys/stat.h>
33#include <sys/types.h>
34#include <unistd.h> // for gethostname
35// FOR DEBUGGING
36#include <iostream>
37
38#ifndef Q_OS_WINDOWS
39#include <netinet/tcp.h>
40#endif
41
42#include "libmythbase/compat.h"
48#include "libmythbase/mythversion.h"
50
55
56#include "httpserver.h"
57#include "upnpresultcode.h"
58
59#ifndef O_LARGEFILE
60#define O_LARGEFILE 0
61#endif
62
64{
65 switch( eCode )
66 {
67 case UPnPResult_Success : return "Success";
68 case UPnPResult_InvalidAction : return "Invalid Action";
69 case UPnPResult_InvalidArgs : return "Invalid Args";
70 case UPnPResult_ActionFailed : return "Action Failed";
71 case UPnPResult_ArgumentValueInvalid : return "Argument Value Invalid";
72 case UPnPResult_ArgumentValueOutOfRange : return "Argument Value Out Of Range";
73 case UPnPResult_OptionalActionNotImplemented: return "Optional Action Not Implemented";
74 case UPnPResult_OutOfMemory : return "Out Of Memory";
75 case UPnPResult_HumanInterventionRequired : return "Human Intervention Required";
76 case UPnPResult_StringArgumentTooLong : return "String Argument Too Long";
77 case UPnPResult_ActionNotAuthorized : return "Action Not Authorized";
78 case UPnPResult_SignatureFailure : return "Signature Failure";
79 case UPnPResult_SignatureMissing : return "Signature Missing";
80 case UPnPResult_NotEncrypted : return "Not Encrypted";
81 case UPnPResult_InvalidSequence : return "Invalid Sequence";
82 case UPnPResult_InvalidControlURL : return "Invalid Control URL";
83 case UPnPResult_NoSuchSession : return "No Such Session";
84 case UPnPResult_MS_AccessDenied : return "Access Denied";
85
86 case UPnPResult_CDS_NoSuchObject : return "No Such Object";
87 case UPnPResult_CDS_InvalidCurrentTagValue : return "Invalid CurrentTagValue";
88 case UPnPResult_CDS_InvalidNewTagValue : return "Invalid NewTagValue";
89 case UPnPResult_CDS_RequiredTag : return "Required Tag";
90 case UPnPResult_CDS_ReadOnlyTag : return "Read Only Tag";
91 case UPnPResult_CDS_ParameterMismatch : return "Parameter Mismatch";
92 case UPnPResult_CDS_InvalidSearchCriteria : return "Invalid Search Criteria";
93 case UPnPResult_CDS_InvalidSortCriteria : return "Invalid Sort Criteria";
94 case UPnPResult_CDS_NoSuchContainer : return "No Such Container";
95 case UPnPResult_CDS_RestrictedObject : return "Restricted Object";
96 case UPnPResult_CDS_BadMetadata : return "Bad Metadata";
97 case UPnPResult_CDS_ResrtictedParentObject : return "Resrticted Parent Object";
98 case UPnPResult_CDS_NoSuchSourceResource : return "No Such Source Resource";
99 case UPnPResult_CDS_ResourceAccessDenied : return "Resource Access Denied";
100 case UPnPResult_CDS_TransferBusy : return "Transfer Busy";
101 case UPnPResult_CDS_NoSuchFileTransfer : return "No Such File Transfer";
102 case UPnPResult_CDS_NoSuchDestRes : return "No Such Destination Resource";
103 case UPnPResult_CDS_DestResAccessDenied : return "Destination Resource Access Denied";
104 case UPnPResult_CDS_CannotProcessRequest : return "Cannot Process The Request";
105
106 //case UPnPResult_CMGR_IncompatibleProtocol = 701,
107 //case UPnPResult_CMGR_IncompatibleDirections = 702,
108 //case UPnPResult_CMGR_InsufficientNetResources = 703,
109 //case UPnPResult_CMGR_LocalRestrictions = 704,
110 //case UPnPResult_CMGR_AccessDenied = 705,
111 //case UPnPResult_CMGR_InvalidConnectionRef = 706,
112 case UPnPResult_CMGR_NotInNetwork : return "Not In Network";
113 case UPnPResult_MythTV_NoNamespaceGiven: return "No Namespace Given";
114 case UPnPResult_MythTV_XmlParseError : return "XML Parse Error";
115 }
116
117 return "Unknown";
118}
119
120static std::array<const MIMETypes,66> g_MIMETypes
121{{
122 // Image Mime Types
123 { .pszExtension="gif" , .pszType="image/gif" },
124 { .pszExtension="ico" , .pszType="image/x-icon" },
125 { .pszExtension="jpeg", .pszType="image/jpeg" },
126 { .pszExtension="jpg" , .pszType="image/jpeg" },
127 { .pszExtension="mng" , .pszType="image/x-mng" },
128 { .pszExtension="png" , .pszType="image/png" },
129 { .pszExtension="svg" , .pszType="image/svg+xml" },
130 { .pszExtension="svgz", .pszType="image/svg+xml" },
131 { .pszExtension="tif" , .pszType="image/tiff" },
132 { .pszExtension="tiff", .pszType="image/tiff" },
133 // Text Mime Types
134 { .pszExtension="htm" , .pszType="text/html" },
135 { .pszExtension="html", .pszType="text/html" },
136 { .pszExtension="qsp" , .pszType="text/html" },
137 { .pszExtension="txt" , .pszType="text/plain" },
138 { .pszExtension="xml" , .pszType="text/xml" },
139 { .pszExtension="qxml", .pszType="text/xml" },
140 { .pszExtension="xslt", .pszType="text/xml" },
141 { .pszExtension="css" , .pszType="text/css" },
142 // Application Mime Types
143 { .pszExtension="crt" , .pszType="application/x-x509-ca-cert" },
144 { .pszExtension="doc" , .pszType="application/vnd.ms-word" },
145 { .pszExtension="gz" , .pszType="application/x-tar" },
146 { .pszExtension="js" , .pszType="application/javascript" },
147 { .pszExtension="m3u" , .pszType="application/x-mpegurl" }, // HTTP Live Streaming
148 { .pszExtension="m3u8", .pszType="application/x-mpegurl" }, // HTTP Live Streaming
149 { .pszExtension="ogx" , .pszType="application/ogg" }, // http://wiki.xiph.org/index.php/MIME_Types_and_File_Extensions
150 { .pszExtension="pdf" , .pszType="application/pdf" },
151 { .pszExtension="pem" , .pszType="application/x-x509-ca-cert" },
152 { .pszExtension="qjs" , .pszType="application/javascript" },
153 { .pszExtension="rm" , .pszType="application/vnd.rn-realmedia" },
154 { .pszExtension="swf" , .pszType="application/x-shockwave-flash" },
155 { .pszExtension="xls" , .pszType="application/vnd.ms-excel" },
156 { .pszExtension="zip" , .pszType="application/x-tar" },
157 // Audio Mime Types:
158 { .pszExtension="aac" , .pszType="audio/mp4" },
159 { .pszExtension="ac3" , .pszType="audio/vnd.dolby.dd-raw" }, // DLNA?
160 { .pszExtension="flac", .pszType="audio/x-flac" }, // This may become audio/flac in the future
161 { .pszExtension="m4a" , .pszType="audio/x-m4a" },
162 { .pszExtension="mid" , .pszType="audio/midi" },
163 { .pszExtension="mka" , .pszType="audio/x-matroska" },
164 { .pszExtension="mp3" , .pszType="audio/mpeg" },
165 { .pszExtension="oga" , .pszType="audio/ogg" }, // Defined: http://wiki.xiph.org/index.php/MIME_Types_and_File_Extensions
166 { .pszExtension="ogg" , .pszType="audio/ogg" }, // Defined: http://wiki.xiph.org/index.php/MIME_Types_and_File_Extensions
167 { .pszExtension="wav" , .pszType="audio/wav" },
168 { .pszExtension="wma" , .pszType="audio/x-ms-wma" },
169 // Video Mime Types
170 { .pszExtension="3gp" , .pszType="video/3gpp" }, // Also audio/3gpp
171 { .pszExtension="3g2" , .pszType="video/3gpp2" }, // Also audio/3gpp2
172 { .pszExtension="asx" , .pszType="video/x-ms-asf" },
173 { .pszExtension="asf" , .pszType="video/x-ms-asf" },
174 { .pszExtension="avi" , .pszType="video/x-msvideo" }, // Also video/avi
175 { .pszExtension="m2p" , .pszType="video/mp2p" }, // RFC 3555
176 { .pszExtension="m4v" , .pszType="video/mp4" },
177 { .pszExtension="mpeg", .pszType="video/mp2p" }, // RFC 3555
178 { .pszExtension="mpeg2",.pszType="video/mp2p" }, // RFC 3555
179 { .pszExtension="mpg" , .pszType="video/mp2p" }, // RFC 3555
180 { .pszExtension="mpg2", .pszType="video/mp2p" }, // RFC 3555
181 { .pszExtension="mov" , .pszType="video/quicktime" },
182 { .pszExtension="mp4" , .pszType="video/mp4" },
183 { .pszExtension="mkv" , .pszType="video/x-matroska" }, // See http://matroska.org/technical/specs/notes.html#MIME (See NOTE 1)
184 { .pszExtension="nuv" , .pszType="video/nupplevideo" },
185 { .pszExtension="ogv" , .pszType="video/ogg" }, // Defined: http://wiki.xiph.org/index.php/MIME_Types_and_File_Extensions
186 { .pszExtension="ps" , .pszType="video/mp2p" }, // RFC 3555
187 { .pszExtension="ts" , .pszType="video/mp2t" }, // RFC 3555
188 { .pszExtension="vob" , .pszType="video/mpeg" }, // Also video/dvd
189 { .pszExtension="wmv" , .pszType="video/x-ms-wmv" },
190 // Font Mime Types
191 { .pszExtension="ttf" , .pszType="font/ttf" },
192 { .pszExtension="woff" , .pszType="font/woff" },
193 { .pszExtension="woff2", .pszType="font/woff2" }
194}};
195
196// NOTE 1
197// This formerly was video/x-matroska, but got changed due to #8643
198// This was reverted from video/x-mkv, due to #10980
199// See http://matroska.org/technical/specs/notes.html#MIME
200// If you can't please everyone, may as well be correct as you piss some off
201
202static QString StaticPage =
203 "<!DOCTYPE html>"
204 "<HTML>"
205 "<HEAD>"
206 "<TITLE>Error %1</TITLE>"
207 "<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=ISO-8859-1\">"
208 "</HEAD>"
209 "<BODY><H1>%2.</H1></BODY>"
210 "</HTML>";
211
212const char *HTTPRequest::s_szServerHeaders = "Accept-Ranges: bytes\r\n";
213
215//
217
218QString HTTPRequest::GetLastHeader( const QString &sType ) const
219{
220 QStringList values = m_mapHeaders.values( sType );
221 if (!values.isEmpty())
222 return values.last();
223 return {};
224}
225
227//
229
231{
232 // HTTP
233 if (sType == "GET" ) return( m_eType = RequestTypeGet );
234 if (sType == "HEAD" ) return( m_eType = RequestTypeHead );
235 if (sType == "POST" ) return( m_eType = RequestTypePost );
236 if (sType == "OPTIONS" ) return( m_eType = RequestTypeOptions );
237
238 // UPnP
239 if (sType == "M-SEARCH" ) return( m_eType = RequestTypeMSearch );
240 if (sType == "NOTIFY" ) return( m_eType = RequestTypeNotify );
241 if (sType == "SUBSCRIBE" ) return( m_eType = RequestTypeSubscribe );
242 if (sType == "UNSUBSCRIBE") return( m_eType = RequestTypeUnsubscribe );
243
244 if (sType.startsWith( QString("HTTP/") )) return( m_eType = RequestTypeResponse );
245
246 LOG(VB_HTTP, LOG_INFO,
247 QString("HTTPRequest::SentRequestType( %1 ) - returning Unknown.")
248 .arg(sType));
249
250 return( m_eType = RequestTypeUnknown);
251}
252
254//
256
257QString HTTPRequest::BuildResponseHeader( long long nSize )
258{
259 QString sHeader;
260 QString sContentType = (m_eResponseType == ResponseTypeOther) ?
262 //-----------------------------------------------------------------------
263 // Headers describing the connection
264 //-----------------------------------------------------------------------
265
266 // The protocol string
267 sHeader = QString( "%1 %2\r\n" ).arg(GetResponseProtocol(),
269
272
273 SetResponseHeader("Connection", m_bKeepAlive ? "Keep-Alive" : "Close" );
274 if (m_bKeepAlive)
275 {
276 if (m_nKeepAliveTimeout == 0s) // Value wasn't passed in by the server, so go with the configured value
277 m_nKeepAliveTimeout = gCoreContext->GetDurSetting<std::chrono::seconds>("HTTP/KeepAliveTimeoutSecs", 10s);
278 SetResponseHeader("Keep-Alive", QString("timeout=%1").arg(m_nKeepAliveTimeout.count()));
279 }
280
281 //-----------------------------------------------------------------------
282 // Entity Headers - Describe the content and allowed methods
283 // RFC 2616 Section 7.1
284 //-----------------------------------------------------------------------
285 if (m_eResponseType != ResponseTypeHeader) // No entity headers
286 {
287 SetResponseHeader("Content-Language", gCoreContext->GetLanguageAndVariant().replace("_", "-"));
288 SetResponseHeader("Content-Type", sContentType);
289
290 // Default to 'inline' but we should support 'attachment' when it would
291 // be appropriate i.e. not when streaming a file to a upnp player or browser
292 // that can support it natively
293 if (!m_sFileName.isEmpty())
294 {
295 // TODO: Add support for utf8 encoding - RFC 5987
296 QString filename = QFileInfo(m_sFileName).fileName(); // Strip any path
297 SetResponseHeader("Content-Disposition", QString("inline; filename=\"%2\"").arg(QString(filename.toLatin1())));
298 }
299
300 SetResponseHeader("Content-Length", QString::number(nSize));
301
302 // See DLNA 7.4.1.3.11.4.3 Tolerance to unavailable contentFeatures.dlna.org header
303 //
304 // It is better not to return this header, than to return it containing
305 // invalid or incomplete information. We are unable to currently determine
306 // this information at this stage, so do not return it. Only older devices
307 // look for it. Newer devices use the information provided in the UPnP
308 // response
309
310// QString sValue = GetHeaderValue( "getContentFeatures.dlna.org", "0" );
311//
312// if (sValue == "1")
313// sHeader += "contentFeatures.dlna.org: DLNA.ORG_OP=01;DLNA.ORG_CI=0;"
314// "DLNA.ORG_FLAGS=01500000000000000000000000000000\r\n";
315
316
317 // DLNA 7.5.4.3.2.33 MT transfer mode indication
318 QString sTransferMode = GetRequestHeader( "transferMode.dlna.org", "" );
319
320 if (sTransferMode.isEmpty())
321 {
322 if (m_sResponseTypeText.startsWith("video/") ||
323 m_sResponseTypeText.startsWith("audio/"))
324 sTransferMode = "Streaming";
325 else
326 sTransferMode = "Interactive";
327 }
328
329 if (sTransferMode == "Streaming")
330 SetResponseHeader("transferMode.dlna.org", "Streaming");
331 else if (sTransferMode == "Background")
332 SetResponseHeader("transferMode.dlna.org", "Background");
333 else if (sTransferMode == "Interactive")
334 SetResponseHeader("transferMode.dlna.org", "Interactive");
335
336 // HACK Temporary hack for Samsung TVs - Needs to be moved later as it's not entirely DLNA compliant
337 if (!GetRequestHeader( "getcontentFeatures.dlna.org", "" ).isEmpty())
338 SetResponseHeader("contentFeatures.dlna.org", "DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01500000000000000000000000000000");
339 }
340
341 auto values = m_mapHeaders.values("origin");
342 for (const auto & value : std::as_const(values))
343 AddCORSHeaders(value);
344
345 if (qEnvironmentVariableIsSet("HTTPREQUEST_DEBUG"))
346 {
347 // Dump response header
348 QMap<QString, QString>::iterator it;
349 for ( it = m_mapRespHeaders.begin(); it != m_mapRespHeaders.end(); ++it )
350 {
351 LOG(VB_HTTP, LOG_INFO, QString("(Response Header) %1: %2").arg(it.key(), it.value()));
352 }
353 }
354
355 sHeader += GetResponseHeaders();
356 sHeader += "\r\n";
357
358 return sHeader;
359}
360
362//
364
366{
367 qint64 nBytes = 0;
368
369 switch( m_eResponseType )
370 {
371 // The following are all eligable for gzip compression
373 case ResponseTypeNone:
374 LOG(VB_HTTP, LOG_INFO,
375 QString("HTTPRequest::SendResponse( None ) :%1 -> %2:")
377 return( -1 );
378 case ResponseTypeJS:
379 case ResponseTypeCSS:
380 case ResponseTypeText:
381 case ResponseTypeSVG:
382 case ResponseTypeXML:
383 case ResponseTypeHTML:
384 // If the reponse isn't already in the buffer, then load it
385 if (m_sFileName.isEmpty() || !m_response.buffer().isEmpty())
386 break;
387 {
388 QFile file(m_sFileName);
389 if (file.exists() && file.size() < (2LL * 1024 * 1024) && // For security/stability, limit size of files read into buffer to 2MiB
390 file.open(QIODevice::ReadOnly | QIODevice::Text))
391 m_response.buffer() = file.readAll();
392
393 if (!m_response.buffer().isEmpty())
394 break;
395
396 // Let SendResponseFile try or send a 404
398 }
399 [[fallthrough]];
400 case ResponseTypeFile: // Binary files
401 LOG(VB_HTTP, LOG_INFO,
402 QString("HTTPRequest::SendResponse( File ) :%1 -> %2:")
404 return( SendResponseFile( m_sFileName ));
407 default:
408 break;
409 }
410
411 LOG(VB_HTTP, LOG_INFO,
412 QString("HTTPRequest::SendResponse(xml/html) (%1) :%2 -> %3: %4")
414 QString::number(m_eResponseType)));
415
416 // ----------------------------------------------------------------------
417 // Check for ETag match...
418 // ----------------------------------------------------------------------
419
420 QString sETag = GetRequestHeader( "If-None-Match", "" );
421
422 if ( !sETag.isEmpty() && sETag == m_mapRespHeaders[ "ETag" ] )
423 {
424 LOG(VB_HTTP, LOG_INFO,
425 QString("HTTPRequest::SendResponse(%1) - Cached")
426 .arg(sETag));
427
428 m_nResponseStatus = 304;
429 m_eResponseType = ResponseTypeHeader; // No entity headers
430
431 // no content can be returned.
432 m_response.buffer().clear();
433 }
434
435 // ----------------------------------------------------------------------
436
437 int nContentLen = m_response.buffer().length();
438
439 QBuffer *pBuffer = &m_response;
440
441 // ----------------------------------------------------------------------
442 // DEBUGGING
443 if (qEnvironmentVariableIsSet("HTTPREQUEST_DEBUG"))
444 std::cout << m_response.buffer().constData() << '\n';
445 // ----------------------------------------------------------------------
446
447 LOG(VB_HTTP, LOG_DEBUG, QString("Reponse Content Length: %1").arg(nContentLen));
448
449 // ----------------------------------------------------------------------
450 // Should we try to return data gzip'd?
451 // ----------------------------------------------------------------------
452
453 QBuffer compBuffer;
454
455 auto values = m_mapHeaders.values("accept-encoding");
456 bool gzip_found = std::ranges::any_of(std::as_const(values),
457 [](const auto & value)
458 {return value.contains( "gzip" ); });
459
460 if (( nContentLen > 0 ) && gzip_found)
461 {
462 QByteArray compressed = gzipCompress( m_response.buffer() );
463 compBuffer.setData( compressed );
464
465 if (!compBuffer.buffer().isEmpty())
466 {
467 pBuffer = &compBuffer;
468
469 SetResponseHeader( "Content-Encoding", "gzip" );
470 LOG(VB_HTTP, LOG_DEBUG, QString("Reponse Compressed Content Length: %1").arg(compBuffer.buffer().length()));
471 }
472 }
473
474 // ----------------------------------------------------------------------
475 // Write out Header.
476 // ----------------------------------------------------------------------
477
478 nContentLen = pBuffer->buffer().length();
479
480 QString rHeader = BuildResponseHeader( nContentLen );
481
482 QByteArray sHeader = rHeader.toUtf8();
483 LOG(VB_HTTP, LOG_DEBUG, QString("Response header size: %1 bytes").arg(sHeader.length()));
484 nBytes = WriteBlock( sHeader.constData(), sHeader.length() );
485
486 if (nBytes < sHeader.length())
487 {
488 LOG( VB_HTTP, LOG_ERR, QString("HttpRequest::SendResponse(): "
489 "Incomplete write of header, "
490 "%1 written of %2")
491 .arg(nBytes).arg(sHeader.length()));
492 }
493
494 // ----------------------------------------------------------------------
495 // Write out Response buffer.
496 // ----------------------------------------------------------------------
497
498 if (( m_eType != RequestTypeHead ) &&
499 ( nContentLen > 0 ))
500 {
501 qint64 bytesWritten = SendData( pBuffer, 0, nContentLen );
502 //qint64 bytesWritten = WriteBlock( pBuffer->buffer(), pBuffer->buffer().length() );
503
504 if (bytesWritten != nContentLen)
505 LOG(VB_HTTP, LOG_ERR, "HttpRequest::SendResponse(): Error occurred while writing response body.");
506 else
507 nBytes += bytesWritten;
508 }
509
510 return nBytes;
511}
512
513void HTTPRequest::SendResponseRedirect(const QString &hostName)
514{
516 m_nResponseStatus = 301;
517
518 QStringList sItems = m_sRawRequest.split( ' ' );
519 QString sUrl = "http://" + GetLastHeader( "host" ) + sItems[1];
520 QUrl url( sUrl );
521 QString ipAddress = gCoreContext->GetSettingOnHost
522 ("BackendServerAddr",hostName,hostName);
523 url.setHost( ipAddress );
524
525 m_mapRespHeaders[ "Location" ] = url.toString();
526
527 LOG(VB_UPNP, LOG_INFO, QString("Sending http redirect to: %1")
528 .arg(url.toString()));
529
530 SendResponse();
531}
532
534//
536
537qint64 HTTPRequest::SendResponseFile( const QString& sFileName )
538{
539 qint64 nBytes = 0;
540 long long llSize = 0;
541 long long llStart = 0;
542 long long llEnd = 0;
543
544 LOG(VB_HTTP, LOG_INFO, QString("SendResponseFile ( %1 )").arg(sFileName));
545
547 m_sResponseTypeText = "text/plain";
548
549 QFile tmpFile( sFileName );
550 if (tmpFile.exists( ) && tmpFile.open( QIODevice::ReadOnly ))
551 {
552
553 m_sResponseTypeText = TestMimeType( sFileName );
554
555 // ------------------------------------------------------------------
556 // Get File size
557 // ------------------------------------------------------------------
558
559 llSize = llEnd = tmpFile.size( );
560
561 m_nResponseStatus = 200;
562
563 // ------------------------------------------------------------------
564 // Process any Range Header
565 // ------------------------------------------------------------------
566
567 bool bRange = false;
568 QString sRange = GetRequestHeader( "range", "" );
569
570 if (!sRange.isEmpty())
571 {
572 bRange = ParseRange( sRange, llSize, &llStart, &llEnd );
573
574 // Adjust ranges that are too long.
575
576 if (llEnd >= llSize)
577 llEnd = llSize-1;
578
579 if ((llSize > llStart) && (llSize > llEnd) && (llEnd > llStart))
580 {
581 if (bRange)
582 {
583 m_nResponseStatus = 206;
584 m_mapRespHeaders[ "Content-Range" ] = QString("bytes %1-%2/%3")
585 .arg( llStart )
586 .arg( llEnd )
587 .arg( llSize );
588 llSize = (llEnd - llStart) + 1;
589 }
590 }
591 else
592 {
593 m_nResponseStatus = 416;
594 // RFC 7233 - A server generating a 416 (Range Not Satisfiable)
595 // response to a byte-range request SHOULD send a Content-Range
596 // header field with an unsatisfied-range value
597 m_mapRespHeaders[ "Content-Range" ] = QString("bytes */%3")
598 .arg( llSize );
599 llSize = 0;
600 LOG(VB_HTTP, LOG_INFO,
601 QString("HTTPRequest::SendResponseFile(%1) - "
602 "invalid byte range %2-%3/%4")
603 .arg(sFileName) .arg(llStart) .arg(llEnd)
604 .arg(llSize));
605 }
606 }
607
608 // HACK: D-Link DSM-320
609 // The following headers are only required by servers which don't support
610 // http keep alive. We do support it, so we don't need it. Keeping it in
611 // place to prevent someone re-adding it in future
612 //m_mapRespHeaders[ "X-User-Agent" ] = "redsonic";
613
614 // ------------------------------------------------------------------
615 //
616 // ------------------------------------------------------------------
617
618 }
619 else
620 {
621 LOG(VB_HTTP, LOG_INFO,
622 QString("HTTPRequest::SendResponseFile(%1) - cannot find file!")
623 .arg(sFileName));
624 m_nResponseStatus = 404;
625 m_response.write( GetResponsePage() );
626 }
627
628 // -=>TODO: Should set "Content-Length: *" if file is still recording
629
630 // ----------------------------------------------------------------------
631 // Write out Header.
632 // ----------------------------------------------------------------------
633
634 QString rHeader = BuildResponseHeader( llSize );
635 QByteArray sHeader = rHeader.toUtf8();
636 LOG(VB_HTTP, LOG_DEBUG, QString("Response header size: %1 bytes").arg(sHeader.length()));
637 nBytes = WriteBlock( sHeader.constData(), sHeader.length() );
638
639 if (nBytes < sHeader.length())
640 {
641 LOG( VB_HTTP, LOG_ERR, QString("HttpRequest::SendResponseFile(): "
642 "Incomplete write of header, "
643 "%1 written of %2")
644 .arg(nBytes).arg(sHeader.length()));
645 }
646
647 // ----------------------------------------------------------------------
648 // Write out File.
649 // ----------------------------------------------------------------------
650
651#if 0
652 LOG(VB_HTTP, LOG_DEBUG,
653 QString("SendResponseFile : size = %1, start = %2, end = %3")
654 .arg(llSize).arg(llStart).arg(llEnd));
655#endif
656 if (( m_eType != RequestTypeHead ) && (llSize != 0))
657 {
658 long long sent = SendFile( tmpFile, llStart, llSize );
659
660 if (sent == -1)
661 {
662 LOG(VB_HTTP, LOG_INFO,
663 QString("SendResponseFile( %1 ) Error: %2 [%3]" )
664 .arg(sFileName) .arg(errno) .arg(strerror(errno)));
665
666 nBytes = -1;
667 }
668 }
669
670 // -=>TODO: Only returns header length...
671 // should we change to return total bytes?
672
673 return nBytes;
674}
675
677//
679
680static constexpr size_t SENDFILE_BUFFER_SIZE { 65536 };
681
682qint64 HTTPRequest::SendData( QIODevice *pDevice, qint64 llStart, qint64 llBytes )
683{
684 bool bShouldClose = false;
685 qint64 sent = 0;
686
687 if (!pDevice->isOpen())
688 {
689 pDevice->open( QIODevice::ReadOnly );
690 bShouldClose = true;
691 }
692
693 // ----------------------------------------------------------------------
694 // Set out file position to requested start location.
695 // ----------------------------------------------------------------------
696
697 if ( !pDevice->seek( llStart ))
698 return -1;
699
700 std::array<char,SENDFILE_BUFFER_SIZE> aBuffer {};
701
702 qint64 llBytesRemaining = llBytes;
703 qint64 llBytesToRead = 0;
704 qint64 llBytesRead = 0;
705
706 while ((sent < llBytes) && !pDevice->atEnd())
707 {
708 llBytesToRead = std::min( (qint64)SENDFILE_BUFFER_SIZE, llBytesRemaining );
709 llBytesRead = pDevice->read( aBuffer.data(), llBytesToRead );
710 if ( llBytesRead != -1 )
711 {
712 if ( WriteBlock( aBuffer.data(), llBytesRead ) == -1)
713 return -1;
714
715 // -=>TODO: We don't handle the situation where we read more than was sent.
716
717 sent += llBytesRead;
718 llBytesRemaining -= llBytesRead;
719 }
720 }
721
722 if (bShouldClose)
723 pDevice->close();
724
725 return sent;
726}
727
729//
731
732qint64 HTTPRequest::SendFile( QFile &file, qint64 llStart, qint64 llBytes )
733{
734 qint64 sent = SendData( (QIODevice *)(&file), llStart, llBytes );
735
736 return sent;
737}
738
739
741//
743
744void HTTPRequest::FormatErrorResponse( bool bServerError,
745 const QString &sFaultString,
746 const QString &sDetails )
747{
749 m_nResponseStatus = 500;
750
751 QTextStream stream( &m_response );
752
753 stream << R"(<?xml version="1.0" encoding="utf-8"?>)";
754
755 QString sWhere = bServerError ? "s:Server" : "s:Client";
756
757 if (m_bSOAPRequest)
758 {
759 m_mapRespHeaders[ "EXT" ] = "";
760
761 stream << SOAP_ENVELOPE_BEGIN
762 << "<s:Fault>"
763 << "<faultcode>" << sWhere << "</faultcode>"
764 << "<faultstring>" << sFaultString << "</faultstring>";
765 }
766
767 if (!sDetails.isEmpty())
768 {
769 stream << "<detail>" << sDetails << "</detail>";
770 }
771
772 if (m_bSOAPRequest)
773 {
774 stream << "</s:Fault>" << SOAP_ENVELOPE_END;
775 }
776
777 stream.flush();
778}
779
781{
782 QString sMsg( msg );
783 QString sDetails = "";
784
785 if (m_bSOAPRequest)
786 sDetails = "<UPnPResult xmlns=\"urn:schemas-upnp-org:control-1-0\">";
787
788 if (sMsg.length() == 0)
789 sMsg = UPnPResultCodeDesc(eCode);
790
791 sDetails += QString( "<errorCode>%1</errorCode>"
792 "<errorDescription>%2</errorDescription>" )
793 .arg( eCode )
794 .arg(QString::fromUtf8(QUrl::toPercentEncoding(sMsg)));
795
796 if (m_bSOAPRequest)
797 sDetails += "</UPnPResult>";
798
799 FormatErrorResponse(true, // -=>TODO: Should make this dynamic
800 "UPnPResult", sDetails);
801}
802
804//
806
808{
811 m_nResponseStatus = 200;
812
814
815 //m_response << pFormatter->ToString();
816}
817
819//
821
823{
825 m_nResponseStatus = 200;
826
827 QTextStream stream( &m_response );
828
829 stream << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n";
830
831 if (m_bSOAPRequest)
832 {
833 m_mapRespHeaders[ "EXT" ] = "";
834
835 stream << SOAP_ENVELOPE_BEGIN
836 << "<u:" << m_sMethod << "Response xmlns:u=\""
837 << m_sNameSpace << "\">\r\n";
838 }
839 else
840 {
841 stream << "<" << m_sMethod << "Response>\r\n";
842 }
843
844 for (const auto & arg : std::as_const(args))
845 {
846 stream << "<" << arg.m_sName;
847
848 if (arg.m_pAttributes)
849 {
850 for (const auto & attr : std::as_const(*arg.m_pAttributes))
851 {
852 stream << " " << attr.m_sName << "='"
853 << QUrl::toPercentEncoding(attr.m_sValue) << "'";
854 }
855 }
856
857 stream << ">";
858
859 if (m_bSOAPRequest)
860 stream << QUrl::toPercentEncoding(arg.m_sValue);
861 else
862 stream << arg.m_sValue;
863
864 stream << "</" << arg.m_sName << ">\r\n";
865 }
866
867 if (m_bSOAPRequest)
868 {
869 stream << "</u:" << m_sMethod << "Response>\r\n"
871 }
872 else
873 {
874 stream << "</" << m_sMethod << "Response>\r\n";
875 }
876
877 stream.flush();
878}
879
881//
883
884void HTTPRequest::FormatRawResponse(const QString &sXML)
885{
887 m_nResponseStatus = 200;
888
889 QTextStream stream( &m_response );
890
891 stream << sXML;
892
893 stream.flush();
894}
896//
898
899void HTTPRequest::FormatFileResponse( const QString &sFileName )
900{
901 m_sFileName = sFileName;
902 QFileInfo file(m_sFileName);
903
904 if (!m_sFileName.isEmpty() && file.exists())
905 {
906 QDateTime ims = QDateTime::fromString(GetRequestHeader("if-modified-since", ""), Qt::RFC2822Date);
907#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
908 ims.setTimeSpec(Qt::UTC);
909#else
910 ims.setTimeZone(QTimeZone(QTimeZone::UTC));
911#endif
912 if (ims.isValid() && ims <= file.lastModified()) // Strong validator
913 {
915 m_nResponseStatus = 304; // Not Modified
916 }
917 else
918 {
921 m_nResponseStatus = 200; // OK
922 SetResponseHeader("Last-Modified", MythDate::toString(file.lastModified(),
924 MythDate::kRFC822))); // RFC 822
925 SetResponseHeader("Cache-Control", "no-cache=\"Ext\", max-age = 7200"); // 2 Hours
926 }
927 }
928 else
929 {
931 m_nResponseStatus = 404; // Resource not found
932 m_response.write( GetResponsePage() );
933 LOG(VB_HTTP, LOG_INFO,
934 QString("HTTPRequest::FormatFileResponse('%1') - cannot find file")
935 .arg(sFileName));
936 }
937}
938
940//
942
943void HTTPRequest::SetRequestProtocol( const QString &sLine )
944{
945 m_sProtocol = sLine.section( '/', 0, 0 ).trimmed();
946 QString sVersion = sLine.section( '/', 1 ).trimmed();
947
948 m_nMajor = sVersion.section( '.', 0, 0 ).toInt();
949 m_nMinor = sVersion.section( '.', 1 ).toInt();
950}
951
953//
955
957{
958 return QString("%1/%2.%3").arg(m_sProtocol,
959 QString::number(m_nMajor),
960 QString::number(m_nMinor));
961}
962
964//
966
968{
969 // RFC 2145
970 //
971 // An HTTP server SHOULD send a response version equal to the highest
972 // version for which the server is at least conditionally compliant, and
973 // whose major version is less than or equal to the one received in the
974 // request.
975
976// if (m_nMajor == 1)
977// QString("HTTP/1.1");
978// else if (m_nMajor == 2)
979// QString("HTTP/2.0");
980
981 return {"HTTP/1.1"};
982}
983
985//
987
989{
990 if ((sType == "application/x-www-form-urlencoded" ) ||
991 (sType.startsWith("application/x-www-form-urlencoded;")))
993
994 if ((sType == "text/xml" ) ||
995 (sType.startsWith("text/xml;") ))
997
998 if ((sType == "application/json") ||
999 sType.startsWith("application/json;"))
1001
1003}
1004
1005
1007//
1009
1011{
1012 switch( m_nResponseStatus )
1013 {
1014 case 200: return "200 OK";
1015 case 201: return "201 Created";
1016 case 202: return "202 Accepted";
1017 case 204: return "204 No Content";
1018 case 205: return "205 Reset Content";
1019 case 206: return "206 Partial Content";
1020 case 300: return "300 Multiple Choices";
1021 case 301: return "301 Moved Permanently";
1022 case 302: return "302 Found";
1023 case 303: return "303 See Other";
1024 case 304: return "304 Not Modified";
1025 case 305: return "305 Use Proxy";
1026 case 307: return "307 Temporary Redirect";
1027 case 308: return "308 Permanent Redirect";
1028 case 400: return "400 Bad Request";
1029 case 401: return "401 Unauthorized";
1030 case 403: return "403 Forbidden";
1031 case 404: return "404 Not Found";
1032 case 405: return "405 Method Not Allowed";
1033 case 406: return "406 Not Acceptable";
1034 case 408: return "408 Request Timeout";
1035 case 410: return "410 Gone";
1036 case 411: return "411 Length Required";
1037 case 412: return "412 Precondition Failed";
1038 case 413: return "413 Request Entity Too Large";
1039 case 414: return "414 Request-URI Too Long";
1040 case 415: return "415 Unsupported Media Type";
1041 case 416: return "416 Requested Range Not Satisfiable";
1042 case 417: return "417 Expectation Failed";
1043 // I'm a teapot
1044 case 428: return "428 Precondition Required"; // RFC 6585
1045 case 429: return "429 Too Many Requests"; // RFC 6585
1046 case 431: return "431 Request Header Fields Too Large"; // RFC 6585
1047 case 500: return "500 Internal Server Error";
1048 case 501: return "501 Not Implemented";
1049 case 502: return "502 Bad Gateway";
1050 case 503: return "503 Service Unavailable";
1051 case 504: return "504 Gateway Timeout";
1052 case 505: return "505 HTTP Version Not Supported";
1053 case 510: return "510 Not Extended";
1054 case 511: return "511 Network Authentication Required"; // RFC 6585
1055 }
1056
1057 return( QString( "%1 Unknown" ).arg( m_nResponseStatus ));
1058}
1059
1061//
1063
1065{
1066 return StaticPage.arg(QString::number(m_nResponseStatus), GetResponseStatus()).toUtf8();
1067}
1068
1070//
1072
1073QString HTTPRequest::GetResponseType( void ) const
1074{
1075 switch( m_eResponseType )
1076 {
1077 case ResponseTypeXML : return "text/xml; charset=\"UTF-8\"";
1078 case ResponseTypeHTML : return "text/html; charset=\"UTF-8\"";
1079 case ResponseTypeCSS : return "text/css; charset=\"UTF-8\"";
1080 case ResponseTypeJS : return "application/javascript";
1081 case ResponseTypeText : return "text/plain; charset=\"UTF-8\"";
1082 case ResponseTypeSVG : return "image/svg+xml";
1083 default: break;
1084 }
1085
1086 return "text/plain";
1087}
1088
1090//
1092
1093QString HTTPRequest::GetMimeType( const QString &sFileExtension )
1094{
1095 QString ext;
1096
1097 for (const auto & type : g_MIMETypes)
1098 {
1099 ext = type.pszExtension;
1100
1101 if ( sFileExtension.compare(ext, Qt::CaseInsensitive) == 0 )
1102 return( type.pszType );
1103 }
1104
1105 return "text/plain";
1106}
1107
1109//
1111
1113{
1114 QStringList mimeTypes;
1115
1116 for (const auto & type : g_MIMETypes)
1117 {
1118 if (!mimeTypes.contains( type.pszType ))
1119 mimeTypes.append( type.pszType );
1120 }
1121
1122 return mimeTypes;
1123}
1124
1126//
1128
1129QString HTTPRequest::TestMimeType( const QString &sFileName )
1130{
1131 QFileInfo info( sFileName );
1132 QString sLOC = "HTTPRequest::TestMimeType(" + sFileName + ") - ";
1133 QString sSuffix = info.suffix().toLower();
1134 QString sMIME = GetMimeType( sSuffix );
1135
1136 if ( sSuffix == "nuv" ) // If a very old recording, might be an MPEG?
1137 {
1138 // Read the header to find out:
1139 QFile file( sFileName );
1140
1141 if ( file.open(QIODevice::ReadOnly | QIODevice::Text) )
1142 {
1143 QByteArray head = file.read(8);
1144 QString sHex = head.toHex();
1145
1146 LOG(VB_HTTP, LOG_DEBUG, sLOC + "file starts with " + sHex);
1147
1148 if ( sHex == "000001ba44000400" ) // MPEG2 PS
1149 sMIME = "video/mp2p";
1150
1151 if ( head == "MythTVVi" )
1152 {
1153 file.seek(100);
1154 head = file.read(4);
1155
1156 if ( head == "DIVX" )
1157 {
1158 LOG(VB_HTTP, LOG_DEBUG, sLOC + "('MythTVVi...DIVXLAME')");
1159 sMIME = "video/mp4";
1160 }
1161 // NuppelVideo is "RJPG" at byte 612
1162 // We could also check the audio (LAME or RAWA),
1163 // but since most UPnP clients choke on Nuppel, no need
1164 }
1165
1166 file.close();
1167 }
1168 else
1169 {
1170 LOG(VB_GENERAL, LOG_ERR, sLOC + "Could not read file");
1171 }
1172 }
1173
1174 LOG(VB_HTTP, LOG_INFO, sLOC + "type is " + sMIME);
1175 return sMIME;
1176}
1177
1179//
1181
1182long HTTPRequest::GetParameters( QString sParams, QStringMap &mapParams )
1183{
1184 long nCount = 0;
1185
1186 LOG(VB_HTTP, LOG_INFO, QString("sParams: '%1'").arg(sParams));
1187
1188 // This looks odd, but it is here to cope with stupid UPnP clients that
1189 // forget to de-escape the URLs. We can't map %26 here as well, as that
1190 // breaks anything that is trying to pass & as part of a name or value.
1191 sParams.replace( "&amp;", "&" );
1192
1193 if (!sParams.isEmpty())
1194 {
1195 QStringList params = sParams.split('&', Qt::SkipEmptyParts);
1196 for (const auto & param : std::as_const(params))
1197 {
1198 QString sName = param.section( '=', 0, 0 );
1199 QString sValue = param.section( '=', 1 );
1200 sValue.replace("+"," ");
1201
1202 if (!sName.isEmpty())
1203 {
1204 sName = QUrl::fromPercentEncoding(sName.toUtf8());
1205 sValue = QUrl::fromPercentEncoding(sValue.toUtf8());
1206
1207 mapParams.insert( sName.trimmed(), sValue );
1208 nCount++;
1209 }
1210 }
1211 }
1212
1213 return nCount;
1214}
1215
1216
1218//
1220
1221QString HTTPRequest::GetRequestHeader( const QString &sKey, const QString &sDefault )
1222{
1223 auto it = m_mapHeaders.find( sKey.toLower() );
1224
1225 if ( it == m_mapHeaders.end())
1226 return sDefault;
1227
1228 return *it;
1229}
1230
1231
1233//
1235
1237{
1238 QString sHeader = s_szServerHeaders;
1239
1240 for ( QStringMap::iterator it = m_mapRespHeaders.begin();
1241 it != m_mapRespHeaders.end();
1242 ++it )
1243 {
1244 sHeader += it.key() + ": ";
1245 sHeader += *it + "\r\n";
1246 }
1247
1248 return sHeader;
1249}
1250
1252//
1254
1256{
1257 // TODO: Think about whether we should use a longer timeout if the client
1258 // has explicitly specified 'Keep-alive'
1259
1260 // HTTP 1.1 ... server may assume keep-alive
1261 bool bKeepAlive = true;
1262
1263 // if HTTP/1.0... must default to false
1264 if ((m_nMajor == 1) && (m_nMinor == 0))
1265 bKeepAlive = false;
1266
1267 // Read Connection Header to see whether the client has explicitly
1268 // asked for the connection to be kept alive or closed after the response
1269 // is sent
1270 QString sConnection = GetRequestHeader( "connection", "default" ).toLower();
1271
1272 QStringList sValueList = sConnection.split(",");
1273
1274 if ( sValueList.contains("close") )
1275 {
1276 LOG(VB_HTTP, LOG_DEBUG, "Client requested the connection be closed");
1277 bKeepAlive = false;
1278 }
1279 else if (sValueList.contains("keep-alive"))
1280 {
1281 bKeepAlive = true;
1282 }
1283
1284 return bKeepAlive;
1285}
1286
1288//
1290
1292{
1293 QStringList sCookieList = m_mapHeaders.values("cookie");
1294
1295 QStringList::iterator it;
1296 for (it = sCookieList.begin(); it != sCookieList.end(); ++it)
1297 {
1298 QString key = (*it).section('=', 0, 0);
1299 QString value = (*it).section('=', 1);
1300
1301 m_mapCookies.insert(key, value);
1302 }
1303}
1304
1306//
1308
1310{
1311 bool bSuccess = false;
1312
1313 try
1314 {
1315 // Read first line to determine requestType
1316 QString sRequestLine = ReadLine( 2s );
1317
1318 if ( sRequestLine.isEmpty() )
1319 {
1320 LOG(VB_GENERAL, LOG_ERR, "Timeout reading first line of request." );
1321 return false;
1322 }
1323
1324 // -=>TODO: Should read lines until a valid request???
1325 ProcessRequestLine( sRequestLine );
1326
1327 if (m_nMajor > 1 || m_nMajor < 0)
1328 {
1330 m_nResponseStatus = 505;
1331 m_response.write( GetResponsePage() );
1332
1333 return true;
1334 }
1335
1337 {
1339 m_nResponseStatus = 501; // Not Implemented
1340 // Conservative list, we can't really know what methods we
1341 // actually allow for an arbitrary resource without some sort of
1342 // high maintenance database
1343 SetResponseHeader("Allow", "GET, HEAD");
1344 m_response.write( GetResponsePage() );
1345 return true;
1346 }
1347
1348 // Read Header
1349 bool bDone = false;
1350 QString sLine = ReadLine( 2s );
1351
1352 while (( !sLine.isEmpty() ) && !bDone )
1353 {
1354 if (sLine != "\r\n")
1355 {
1356 QString sName = sLine.section( ':', 0, 0 ).trimmed();
1357 QString sValue = sLine.section( ':', 1 );
1358
1359 sValue.truncate( sValue.length() - 2 );
1360
1361 if (!sName.isEmpty() && !sValue.isEmpty())
1362 {
1363 m_mapHeaders.insert(sName.toLower(), sValue.trimmed());
1364 }
1365
1366 sLine = ReadLine( 2s );
1367 }
1368 else
1369 {
1370 bDone = true;
1371 }
1372 }
1373
1374 // Dump request header
1375 for ( auto it = m_mapHeaders.begin(); it != m_mapHeaders.end(); ++it )
1376 {
1377 LOG(VB_HTTP, LOG_INFO, QString("(Request Header) %1: %2")
1378 .arg(it.key(), *it));
1379 }
1380
1381 // Parse Cookies
1382 ParseCookies();
1383
1384 // Parse out keep alive
1386
1387 // Check to see if we found the end of the header or we timed out.
1388 if (!bDone)
1389 {
1390 LOG(VB_GENERAL, LOG_INFO, "Timeout waiting for request header." );
1391 return false;
1392 }
1393
1394 // HTTP/1.1 requires that the Host header be present, even if empty
1395 if ((m_nMinor == 1) && !m_mapHeaders.contains("host"))
1396 {
1398 m_nResponseStatus = 400;
1399 m_response.write( GetResponsePage() );
1400
1401 return true;
1402 }
1403
1404 // Destroy session if requested
1405 if (m_mapHeaders.contains("x-myth-clear-session"))
1406 {
1407 SetCookie("sessionToken", "", MythDate::current().addDays(-2), true);
1408 m_mapCookies.remove("sessionToken");
1409 }
1410
1411 // Allow session resumption for TLS connections
1412 if (m_mapCookies.contains("sessionToken"))
1413 {
1414 QString sessionToken = m_mapCookies["sessionToken"];
1416 MythUserSession session = sessionManager->GetSession(sessionToken);
1417
1418 if (session.IsValid())
1419 m_userSession = session;
1420 }
1421
1422 bSuccess = true;
1423
1424 SetContentType( GetLastHeader( "content-type" ) );
1425 // Lets load payload if any.
1426 long nPayloadSize = GetLastHeader( "content-length" ).toLong();
1427
1428 if (nPayloadSize > 0)
1429 {
1430 char *pszPayload = new char[ nPayloadSize + 2 ];
1431 long nBytes = 0;
1432
1433 nBytes = ReadBlock( pszPayload, nPayloadSize, 5s );
1434 if (nBytes == nPayloadSize )
1435 {
1436 m_sPayload = QString::fromUtf8( pszPayload, nPayloadSize );
1437
1438 // See if the payload is just data from a form post
1442 m_mapParams.insert( "json", m_sPayload );
1443 }
1444 else
1445 {
1446 LOG(VB_GENERAL, LOG_ERR,
1447 QString("Unable to read entire payload (read %1 of %2 bytes)")
1448 .arg( nBytes ) .arg( nPayloadSize ) );
1449 bSuccess = false;
1450 }
1451
1452 delete [] pszPayload;
1453 }
1454
1455 // Check to see if this is a SOAP encoded message
1456 QString sSOAPAction = GetRequestHeader( "SOAPACTION", "" );
1457
1458 if (!sSOAPAction.isEmpty())
1459 bSuccess = ProcessSOAPPayload( sSOAPAction );
1460 else
1462
1463#if 0
1464 if (m_sMethod != "*" )
1465 LOG(VB_HTTP, LOG_DEBUG,
1466 QString("HTTPRequest::ParseRequest - Socket (%1) Base (%2) "
1467 "Method (%3) - Bytes in Socket Buffer (%4)")
1468 .arg(getSocketHandle()) .arg(m_sBaseUrl)
1469 .arg(m_sMethod) .arg(BytesAvailable()));
1470#endif
1471 }
1472 catch(...)
1473 {
1474 LOG(VB_GENERAL, LOG_WARNING,
1475 "Unexpected exception in HTTPRequest::ParseRequest" );
1476 }
1477
1478 return bSuccess;
1479}
1480
1482//
1484
1485void HTTPRequest::ProcessRequestLine( const QString &sLine )
1486{
1487 m_sRawRequest = sLine;
1488
1489 QStringList tokens = sLine.split(m_procReqLineExp, Qt::SkipEmptyParts);
1490 int nCount = tokens.count();
1491
1492 // ----------------------------------------------------------------------
1493
1494 if ( sLine.startsWith( QString("HTTP/") ))
1496 else
1498
1499 // ----------------------------------------------------------------------
1500 // if this is actually a response, then sLine's format will be:
1501 // HTTP/m.n <response code> <response text>
1502 // otherwise:
1503 // <method> <Resource URI> HTTP/m.n
1504 // ----------------------------------------------------------------------
1505
1507 {
1508 // ------------------------------------------------------------------
1509 // Process as a request
1510 // ------------------------------------------------------------------
1511
1512 if (nCount > 0)
1513 SetRequestType( tokens[0].trimmed() );
1514
1515 if (nCount > 1)
1516 {
1517 m_sOriginalUrl = tokens[1].toUtf8(); // Used by authorization check
1518 m_sRequestUrl = QUrl::fromPercentEncoding(tokens[1].toUtf8());
1519 m_sBaseUrl = m_sRequestUrl.section( '?', 0, 0).trimmed();
1520
1521 m_sResourceUrl = m_sBaseUrl; // Save complete url without parameters
1522
1523 // Process any Query String Parameters
1524 QString sQueryStr = tokens[1].section( '?', 1, 1 );
1525
1526 if (!sQueryStr.isEmpty())
1527 GetParameters( sQueryStr, m_mapParams );
1528 }
1529
1530 if (nCount > 2)
1531 SetRequestProtocol( tokens[2].trimmed() );
1532 }
1533 else
1534 {
1535 // ------------------------------------------------------------------
1536 // Process as a Response
1537 // ------------------------------------------------------------------
1538 if (nCount > 0)
1539 SetRequestProtocol( tokens[0].trimmed() );
1540
1541 if (nCount > 1)
1542 m_nResponseStatus = tokens[1].toInt();
1543 }
1544
1545
1546}
1547
1549//
1551
1552bool HTTPRequest::ParseRange( QString sRange,
1553 long long llSize,
1554 long long *pllStart,
1555 long long *pllEnd )
1556{
1557 // ----------------------------------------------------------------------
1558 // -=>TODO: Only handle 1 range at this time...
1559 // should make work with full spec.
1560 // ----------------------------------------------------------------------
1561
1562 if (sRange.isEmpty())
1563 return false;
1564
1565 // ----------------------------------------------------------------------
1566 // remove any "bytes="
1567 // ----------------------------------------------------------------------
1568 int nIdx = sRange.indexOf(m_parseRangeExp);
1569
1570 if (nIdx < 0)
1571 return false;
1572
1573 if (nIdx > 0)
1574 sRange.remove( 0, nIdx );
1575
1576 // ----------------------------------------------------------------------
1577 // Split multiple ranges
1578 // ----------------------------------------------------------------------
1579
1580 QStringList ranges = sRange.split(',', Qt::SkipEmptyParts);
1581 if (ranges.count() == 0)
1582 return false;
1583
1584 // ----------------------------------------------------------------------
1585 // Split first range into its components
1586 // ----------------------------------------------------------------------
1587
1588 QStringList parts = ranges[0].split('-');
1589
1590 if (parts.count() != 2)
1591 return false;
1592
1593 if (parts[0].isEmpty() && parts[1].isEmpty())
1594 return false;
1595
1596 // ----------------------------------------------------------------------
1597 //
1598 // ----------------------------------------------------------------------
1599
1600 bool conv_ok = false;
1601 if (parts[0].isEmpty())
1602 {
1603 // ------------------------------------------------------------------
1604 // Does it match "-####"
1605 // ------------------------------------------------------------------
1606
1607 long long llValue = parts[1].toLongLong(&conv_ok);
1608 if (!conv_ok) return false;
1609
1610 *pllStart = llSize - llValue;
1611 *pllEnd = llSize - 1;
1612 }
1613 else if (parts[1].isEmpty())
1614 {
1615 // ------------------------------------------------------------------
1616 // Does it match "####-"
1617 // ------------------------------------------------------------------
1618
1619 *pllStart = parts[0].toLongLong(&conv_ok);
1620
1621 if (!conv_ok)
1622 return false;
1623
1624 *pllEnd = llSize - 1;
1625 }
1626 else
1627 {
1628 // ------------------------------------------------------------------
1629 // Must be "####-####"
1630 // ------------------------------------------------------------------
1631
1632 *pllStart = parts[0].toLongLong(&conv_ok);
1633 if (!conv_ok) return false;
1634 *pllEnd = parts[1].toLongLong(&conv_ok);
1635 if (!conv_ok) return false;
1636
1637 if (*pllStart > *pllEnd)
1638 return false;
1639 }
1640
1641 LOG(VB_HTTP, LOG_DEBUG, QString("%1 Range Requested %2 - %3")
1642 .arg(getSocketHandle()) .arg(*pllStart) .arg(*pllEnd));
1643
1644 return true;
1645}
1646
1648//
1650
1652{
1653 // Strip out leading http://192.168.1.1:6544/ -> /
1654 // Should fix #8678
1655 // FIXME what about https?
1656 static const QRegularExpression re {"^http[s]?://.*?/"};
1657 m_sBaseUrl.replace(re, "/");
1658
1659 QStringList sList = m_sBaseUrl.split('/', Qt::SkipEmptyParts);
1660 m_sMethod = "";
1661
1662 if (!sList.isEmpty())
1663 {
1664 m_sMethod = sList.last();
1665 sList.pop_back();
1666 }
1667
1668 m_sBaseUrl = '/' + sList.join( "/" );
1669 LOG(VB_HTTP, LOG_INFO, QString("ExtractMethodFromURL(end) : %1 : %2")
1670 .arg(m_sMethod, m_sBaseUrl));
1671}
1672
1674//
1676
1677bool HTTPRequest::ProcessSOAPPayload( const QString &sSOAPAction )
1678{
1679 bool bSuccess = false;
1680
1681 // ----------------------------------------------------------------------
1682 // Open Supplied XML uPnp Description file.
1683 // ----------------------------------------------------------------------
1684
1685 LOG(VB_HTTP, LOG_INFO,
1686 QString("HTTPRequest::ProcessSOAPPayload : %1 : ").arg(sSOAPAction));
1687 QDomDocument doc ( "request" );
1688
1689#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
1690 QString sErrMsg;
1691 int nErrLine = 0;
1692 int nErrCol = 0;
1693
1694 if (!doc.setContent( m_sPayload, true, &sErrMsg, &nErrLine, &nErrCol ))
1695 {
1696 LOG(VB_GENERAL, LOG_ERR,
1697 QString( "Error parsing request at line: %1 column: %2 : %3" )
1698 .arg(nErrLine) .arg(nErrCol) .arg(sErrMsg));
1699 return false;
1700 }
1701#else
1702 auto parseResult =doc.setContent( m_sPayload,
1703 QDomDocument::ParseOption::UseNamespaceProcessing );
1704 if (!parseResult)
1705 {
1706 LOG(VB_GENERAL, LOG_ERR,
1707 QString( "Error parsing request at line: %1 column: %2 : %3" )
1708 .arg(parseResult.errorLine).arg(parseResult.errorColumn)
1709 .arg(parseResult.errorMessage));
1710 return( false );
1711 }
1712#endif
1713
1714 // --------------------------------------------------------------
1715 // XML Document Loaded... now parse it
1716 // --------------------------------------------------------------
1717
1718 QString sService;
1719
1720 if (sSOAPAction.contains( '#' ))
1721 {
1722 m_sNameSpace = sSOAPAction.section( '#', 0, 0).remove( 0, 1);
1723 m_sMethod = sSOAPAction.section( '#', 1 );
1724 m_sMethod.remove( m_sMethod.length()-1, 1 );
1725 }
1726 else
1727 {
1728 if (sSOAPAction.contains( '/' ))
1729 {
1730 int nPos = sSOAPAction.lastIndexOf( '/' );
1731 m_sNameSpace = sSOAPAction.mid(1, nPos);
1732 m_sMethod = sSOAPAction.mid(nPos + 1,
1733 sSOAPAction.length() - nPos - 2);
1734
1735 nPos = m_sNameSpace.lastIndexOf( '/', -2);
1736 sService = m_sNameSpace.mid(nPos + 1,
1737 m_sNameSpace.length() - nPos - 2);
1738 m_sNameSpace = m_sNameSpace.mid( 0, nPos );
1739 }
1740 else
1741 {
1742 m_sNameSpace.clear();
1743 m_sMethod = sSOAPAction;
1744 m_sMethod.remove( QChar( '\"' ) );
1745 }
1746 }
1747
1748 QDomNodeList oNodeList = doc.elementsByTagNameNS( m_sNameSpace, m_sMethod );
1749
1750 if (oNodeList.count() == 0)
1751 {
1752 oNodeList =
1753 doc.elementsByTagNameNS("http://schemas.xmlsoap.org/soap/envelope/",
1754 "Body");
1755 }
1756
1757 if (oNodeList.count() > 0)
1758 {
1759 QDomNode oMethod = oNodeList.item(0);
1760
1761 if (!oMethod.isNull())
1762 {
1763 m_bSOAPRequest = true;
1764
1765 for ( QDomNode oNode = oMethod.firstChild(); !oNode.isNull();
1766 oNode = oNode.nextSibling() )
1767 {
1768 QDomElement e = oNode.toElement();
1769
1770 if (!e.isNull())
1771 {
1772 QString sName = e.tagName();
1773 QString sValue = "";
1774
1775 QDomText oText = oNode.firstChild().toText();
1776
1777 if (!oText.isNull())
1778 sValue = oText.nodeValue();
1779
1780 sName = QUrl::fromPercentEncoding(sName.toUtf8());
1781 sValue = QUrl::fromPercentEncoding(sValue.toUtf8());
1782
1783 m_mapParams.insert( sName.trimmed().toLower(), sValue );
1784 }
1785 }
1786
1787 bSuccess = true;
1788 }
1789 }
1790
1791 return bSuccess;
1792}
1793
1795//
1797
1799{
1800 Serializer *pSerializer = nullptr;
1801
1802 if (m_bSOAPRequest)
1803 {
1804 pSerializer = (Serializer *)new SoapSerializer(&m_response,
1806 }
1807 else
1808 {
1809 QString sAccept = GetRequestHeader( "Accept", "*/*" );
1810
1811 if (sAccept.contains( "application/json", Qt::CaseInsensitive ) ||
1812 sAccept.contains( "text/javascript", Qt::CaseInsensitive ))
1813 {
1814 pSerializer = (Serializer *)new JSONSerializer(&m_response,
1815 m_sMethod);
1816 }
1817 else if (sAccept.contains( "text/x-apple-plist+xml", Qt::CaseInsensitive ))
1818 {
1819 pSerializer = (Serializer *)new XmlPListSerializer(&m_response);
1820 }
1821 }
1822
1823 // Default to XML
1824
1825 if (pSerializer == nullptr)
1826 pSerializer = (Serializer *)new XmlSerializer(&m_response, m_sMethod);
1827
1828 return pSerializer;
1829}
1830
1832//
1834
1835QString HTTPRequest::GetETagHash(const QByteArray &data)
1836{
1837 QByteArray hash = QCryptographicHash::hash( data.data(), QCryptographicHash::Sha1);
1838
1839 return ("\"" + hash.toHex() + "\"");
1840}
1841
1843//
1845
1846void HTTPRequest::SetResponseHeader(const QString& sKey, const QString& sValue,
1847 bool replace)
1848{
1849 if (!replace && m_mapRespHeaders.contains(sKey))
1850 return;
1851
1852 m_mapRespHeaders[sKey] = sValue;
1853}
1854
1856//
1858
1859void HTTPRequest::SetCookie(const QString &sKey, const QString &sValue,
1860 const QDateTime &expiryDate, bool secure)
1861{
1862 if (secure && !IsEncrypted())
1863 {
1864 LOG(VB_GENERAL, LOG_WARNING, QString("HTTPRequest::SetCookie(%1=%2): "
1865 "A secure cookie cannot be set on an unencrypted connection.")
1866 .arg(sKey, sValue));
1867 return;
1868 }
1869
1870 QStringList cookieAttributes;
1871
1872 // Key=Value
1873 cookieAttributes.append(QString("%1=%2").arg(sKey, sValue));
1874
1875 // Domain - Most browsers have problems with a hostname, so it's better to omit this
1876// cookieAttributes.append(QString("Domain=%1").arg(GetHostName()));
1877
1878 // Path - Fix to root, no call for restricting to other paths yet
1879 cookieAttributes.append("Path=/");
1880
1881 // Expires - Expiry date, always set one, just good practice
1882 QString expires = MythDate::toString(expiryDate, MythDate::kRFC822); // RFC 822
1883 cookieAttributes.append(QString("Expires=%1").arg(expires)); // Cookie Expiry date
1884
1885 // Secure - Only send this cookie over encrypted connections, it contains
1886 // sensitive info SECURITY
1887 if (secure)
1888 cookieAttributes.append("Secure");
1889
1890 // HttpOnly - No cookie stealing javascript SECURITY
1891 cookieAttributes.append("HttpOnly");
1892
1893 SetResponseHeader("Set-Cookie", cookieAttributes.join("; "));
1894}
1895
1897//
1899
1901{
1902 // TODO: This only deals with the HTTP 1.1 case, 1.0 should be rare but we
1903 // should probably still handle it
1904
1905 // RFC 3875 - The is the hostname or ip address in the client request, not
1906 // the name or ip we might otherwise know for this server
1907 QString hostname = GetLastHeader("host");
1908 if (!hostname.isEmpty())
1909 {
1910 // Strip the port
1911 if (hostname.contains("]:")) // IPv6 port
1912 {
1913 return hostname.section("]:", 0 , 0);
1914 }
1915 if (hostname.contains(":")) // IPv4 port
1916 {
1917 return hostname.section(":", 0 , 0);
1918 }
1919 return hostname;
1920 }
1921
1922 return GetHostAddress();
1923}
1924
1925
1927{
1928 QString type;
1929 switch ( m_eType )
1930 {
1931 case RequestTypeUnknown :
1932 type = "UNKNOWN";
1933 break;
1934 case RequestTypeGet :
1935 type = "GET";
1936 break;
1937 case RequestTypeHead :
1938 type = "HEAD";
1939 break;
1940 case RequestTypePost :
1941 type = "POST";
1942 break;
1943 case RequestTypeOptions:
1944 type = "OPTIONS";
1945 break;
1946 case RequestTypeMSearch:
1947 type = "M-SEARCH";
1948 break;
1949 case RequestTypeNotify:
1950 type = "NOTIFY";
1951 break;
1953 type = "SUBSCRIBE";
1954 break;
1956 type = "UNSUBSCRIBE";
1957 break;
1958 case RequestTypeResponse :
1959 type = "RESPONSE";
1960 break;
1961 }
1962
1963 return type;
1964}
1965
1966void HTTPRequest::AddCORSHeaders( const QString &sOrigin )
1967{
1968 // ----------------------------------------------------------------------
1969 // SECURITY: Access-Control-Allow-Origin Wildcard
1970 //
1971 // This is a REALLY bad idea, so bad in fact that I'm including it here but
1972 // commented out in the hope that anyone thinking of adding it in the future
1973 // will see it and then read this comment.
1974 //
1975 // Browsers do not verify that the origin is on the same network. This means
1976 // that a malicious script embedded or included into ANY webpage you visit
1977 // could then access servers on your local network including MythTV. They
1978 // can grab data, delete data including recordings and videos, schedule
1979 // recordings and generally ruin your day.
1980 //
1981 // This might seem paranoid and a remote possibility, but then that's how
1982 // a lot of exploits are born. Do NOT allow wildcards.
1983 //
1984 //m_mapRespHeaders[ "Access-Control-Allow-Origin" ] = "*";
1985 // ----------------------------------------------------------------------
1986
1987 // ----------------------------------------------------------------------
1988 // SECURITY: Allow the WebFrontend on the Master backend and ONLY this
1989 // machine to access resources on a frontend or slave web server
1990 //
1991 // http://www.w3.org/TR/cors/#introduction
1992 // ----------------------------------------------------------------------
1993
1994 QStringList allowedOrigins;
1995
1996 int serverStatusPort = gCoreContext->GetMasterServerStatusPort();
1997 int backendSSLPort = gCoreContext->GetNumSetting( "BackendSSLPort",
1998 serverStatusPort + 10);
1999
2000 QString masterAddrPort = QString("%1:%2")
2002 .arg(serverStatusPort);
2003 QString masterTLSAddrPort = QString("%1:%2")
2005 .arg(backendSSLPort);
2006
2007 allowedOrigins << QString("http://%1").arg(masterAddrPort);
2008 allowedOrigins << QString("https://%2").arg(masterTLSAddrPort);
2009
2010 QString localhostname = QHostInfo::localHostName();
2011 if (!localhostname.isEmpty())
2012 {
2013 allowedOrigins << QString("http://%1:%2")
2014 .arg(localhostname).arg(serverStatusPort);
2015 allowedOrigins << QString("https://%1:%2")
2016 .arg(localhostname).arg(backendSSLPort);
2017 }
2018
2019 QStringList allowedOriginsList =
2020 gCoreContext->GetSetting("AllowedOriginsList", QString(
2021 "https://chromecast.mythtv.org")).split(",");
2022
2023 for (const auto & origin : std::as_const(allowedOriginsList))
2024 {
2025 if (origin.isEmpty())
2026 continue;
2027
2028 if (origin == "*" || (!origin.startsWith("http://") &&
2029 !origin.startsWith("https://")))
2030 {
2031 LOG(VB_GENERAL, LOG_ERR, QString("Illegal AllowedOriginsList"
2032 " entry '%1'. Must start with http[s]:// and not be *")
2033 .arg(origin));
2034 }
2035 else
2036 {
2037 allowedOrigins << origin;
2038 }
2039 }
2040
2041 if (VERBOSE_LEVEL_CHECK(VB_HTTP, LOG_DEBUG))
2042 {
2043 for (const auto & origin : std::as_const(allowedOrigins))
2044 LOG(VB_HTTP, LOG_DEBUG, QString("Will allow Origin: %1").arg(origin));
2045 }
2046
2047 if (allowedOrigins.contains(sOrigin))
2048 {
2049 SetResponseHeader( "Access-Control-Allow-Origin" , sOrigin);
2050 SetResponseHeader( "Access-Control-Allow-Credentials" , "true");
2051 SetResponseHeader( "Access-Control-Allow-Headers" , "Content-Type");
2052 LOG(VB_HTTP, LOG_DEBUG, QString("Allow-Origin: %1)").arg(sOrigin));
2053 }
2054 else
2055 {
2056 LOG(VB_GENERAL, LOG_CRIT, QString("HTTPRequest: Cross-origin request "
2057 "received with origin (%1)")
2058 .arg(sOrigin));
2059 }
2060}
2061
2064//
2065// BufferedSocketDeviceRequest Class Implementation
2066//
2069
2070QString BufferedSocketDeviceRequest::ReadLine( std::chrono::milliseconds msecs )
2071{
2072 QString sLine;
2073
2074 if (m_pSocket && m_pSocket->isValid() &&
2075 m_pSocket->state() == QAbstractSocket::ConnectedState)
2076 {
2077 bool timeout = false;
2078 MythTimer timer;
2079 timer.start();
2080 while (!m_pSocket->canReadLine() && !timeout)
2081 {
2082 timeout = !(m_pSocket->waitForReadyRead( msecs.count() ));
2083
2084 if ( timer.elapsed() >= msecs )
2085 {
2086 timeout = true;
2087 LOG(VB_HTTP, LOG_INFO, "BufferedSocketDeviceRequest::ReadLine() - Exceeded Total Elapsed Wait Time." );
2088 }
2089 }
2090
2091 if (!timeout)
2092 sLine = m_pSocket->readLine();
2093 }
2094
2095 return sLine;
2096}
2097
2099//
2101
2102qint64 BufferedSocketDeviceRequest::ReadBlock(char *pData, qint64 nMaxLen,
2103 std::chrono::milliseconds msecs)
2104{
2105 if (m_pSocket && m_pSocket->isValid() &&
2106 m_pSocket->state() == QAbstractSocket::ConnectedState)
2107 {
2108 if (msecs == 0ms)
2109 return( m_pSocket->read( pData, nMaxLen ));
2110
2111 bool bTimeout = false;
2112 MythTimer timer;
2113 timer.start();
2114 while ( (m_pSocket->bytesAvailable() < (int)nMaxLen) && !bTimeout ) // This can end up waiting far longer than msecs
2115 {
2116 bTimeout = !(m_pSocket->waitForReadyRead( msecs.count() ));
2117
2118 if ( timer.elapsed() >= msecs )
2119 {
2120 bTimeout = true;
2121 LOG(VB_HTTP, LOG_INFO, "BufferedSocketDeviceRequest::ReadBlock() - Exceeded Total Elapsed Wait Time." );
2122 }
2123 }
2124
2125 // Just return what we have even if timed out.
2126
2127 return( m_pSocket->read( pData, nMaxLen ));
2128 }
2129
2130 return( -1 );
2131}
2132
2134//
2136
2137qint64 BufferedSocketDeviceRequest::WriteBlock(const char *pData, qint64 nLen)
2138{
2139 qint64 bytesWritten = -1;
2140 if (m_pSocket && m_pSocket->isValid() &&
2141 m_pSocket->state() == QAbstractSocket::ConnectedState)
2142 {
2143 bytesWritten = m_pSocket->write( pData, nLen );
2144 m_pSocket->waitForBytesWritten();
2145 }
2146
2147 return bytesWritten;
2148}
2149
2151//
2153
2155{
2156 return( m_pSocket->localAddress().toString() );
2157}
2158
2160//
2162
2164{
2165 return( m_pSocket->localPort() );
2166}
2167
2168
2170//
2172
2174{
2175 return( m_pSocket->peerAddress().toString() );
2176}
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:198
QString BuildResponseHeader(long long nSize)
HttpResponseType m_eResponseType
Definition: httprequest.h:150
QString m_sFileName
Definition: httprequest.h:156
long m_nResponseStatus
Definition: httprequest.h:153
static QString GetETagHash(const QByteArray &data)
virtual QString GetHostAddress()=0
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:123
QString m_sMethod
Definition: httprequest.h:131
QRegularExpression m_parseRangeExp
Definition: httprequest.h:118
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:128
static QString TestMimeType(const QString &sFileName)
QString GetResponseType(void) const
bool ParseRequest()
void ProcessRequestLine(const QString &sLine)
Serializer * GetSerializer()
void ParseCookies(void)
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:134
static QString GetMimeType(const QString &sFileExtension)
HttpContentType SetContentType(const QString &sType)
virtual QString GetHostName()
HttpRequestType m_eType
Definition: httprequest.h:122
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)
void SendResponseRedirect(const QString &hostName)
QString GetLastHeader(const QString &sType) const
QStringMap m_mapRespHeaders
Definition: httprequest.h:154
QString m_sOriginalUrl
Definition: httprequest.h:127
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:130
QString m_sBaseUrl
Definition: httprequest.h:129
bool ParseKeepAlive(void)
QString m_sProtocol
Definition: httprequest.h:139
QStringMap m_mapCookies
Definition: httprequest.h:135
QString m_sRawRequest
Definition: httprequest.h:125
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:115
QStringMap m_mapParams
Definition: httprequest.h:133
QString m_sPayload
Definition: httprequest.h:137
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
QString GetResponseHeaders(void)
QRegularExpression m_procReqLineExp
Definition: httprequest.h:117
static QString GetServerVersion(void)
Definition: httpserver.cpp:280
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="")
QString GetSettingOnHost(const QString &key, const QString &host, const QString &defaultval="")
T GetDurSetting(const QString &key, T defaultval=T::zero())
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.
We use digest authentication because it protects the password over unprotected networks.
Definition: mythsession.h:106
MythUserSession GetSession(const QString &sessionToken)
Load the session details and return.
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
virtual void AddHeaders(QStringMap &headers)
Definition: serializer.cpp:22
virtual QString GetContentType()=0
static std::array< const MIMETypes, 66 > g_MIMETypes
static constexpr size_t SENDFILE_BUFFER_SIZE
QString UPnPResultCodeDesc(UPnPResultCode eCode)
Definition: httprequest.cpp:63
static QString StaticPage
HttpContentType
Definition: httprequest.h:69
@ ContentType_XML
Definition: httprequest.h:72
@ ContentType_JSON
Definition: httprequest.h:73
@ ContentType_Unknown
Definition: httprequest.h:70
@ ContentType_Urlencoded
Definition: httprequest.h:71
static constexpr const char * SOAP_ENVELOPE_END
Definition: httprequest.h:39
@ ResponseTypeSVG
Definition: httprequest.h:85
@ ResponseTypeCSS
Definition: httprequest.h:83
@ ResponseTypeFile
Definition: httprequest.h:86
@ ResponseTypeHTML
Definition: httprequest.h:81
@ ResponseTypeNone
Definition: httprequest.h:78
@ ResponseTypeUnknown
Definition: httprequest.h:79
@ ResponseTypeXML
Definition: httprequest.h:80
@ ResponseTypeJS
Definition: httprequest.h:82
@ ResponseTypeHeader
Definition: httprequest.h:88
@ ResponseTypeText
Definition: httprequest.h:84
@ ResponseTypeOther
Definition: httprequest.h:87
static constexpr const char * SOAP_ENVELOPE_BEGIN
Definition: httprequest.h:36
HttpRequestType
Definition: httprequest.h:47
@ RequestTypeMSearch
Definition: httprequest.h:59
@ RequestTypeSubscribe
Definition: httprequest.h:60
@ RequestTypeNotify
Definition: httprequest.h:62
@ RequestTypePost
Definition: httprequest.h:52
@ RequestTypeOptions
Definition: httprequest.h:56
@ RequestTypeUnsubscribe
Definition: httprequest.h:61
@ RequestTypeGet
Definition: httprequest.h:50
@ RequestTypeHead
Definition: httprequest.h:51
@ RequestTypeResponse
Definition: httprequest.h:64
@ RequestTypeUnknown
Definition: httprequest.h:48
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 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
UPnPResultCode
Definition: upnpresultcode.h:9
@ UPnPResult_MS_AccessDenied
@ UPnPResult_CDS_NoSuchDestRes
@ UPnPResult_InvalidControlURL
@ UPnPResult_ArgumentValueOutOfRange
@ UPnPResult_CDS_InvalidSortCriteria
@ UPnPResult_CDS_NoSuchSourceResource
@ UPnPResult_CDS_ParameterMismatch
@ UPnPResult_OutOfMemory
@ UPnPResult_CDS_TransferBusy
@ UPnPResult_CDS_InvalidNewTagValue
@ UPnPResult_CDS_InvalidSearchCriteria
@ UPnPResult_NotEncrypted
@ UPnPResult_CDS_DestResAccessDenied
@ UPnPResult_HumanInterventionRequired
@ UPnPResult_Success
@ UPnPResult_OptionalActionNotImplemented
@ UPnPResult_CDS_CannotProcessRequest
@ UPnPResult_ArgumentValueInvalid
@ UPnPResult_MythTV_XmlParseError
@ UPnPResult_InvalidAction
@ UPnPResult_CDS_NoSuchContainer
@ UPnPResult_StringArgumentTooLong
@ UPnPResult_ActionFailed
@ UPnPResult_CDS_NoSuchObject
@ UPnPResult_CDS_RestrictedObject
@ UPnPResult_CDS_BadMetadata
@ UPnPResult_CDS_InvalidCurrentTagValue
@ UPnPResult_CDS_ResrtictedParentObject
@ UPnPResult_ActionNotAuthorized
@ UPnPResult_NoSuchSession
@ UPnPResult_CDS_ResourceAccessDenied
@ UPnPResult_CDS_RequiredTag
@ UPnPResult_CMGR_NotInNetwork
@ UPnPResult_CDS_NoSuchFileTransfer
@ UPnPResult_InvalidSequence
@ UPnPResult_CDS_ReadOnlyTag
@ UPnPResult_SignatureMissing
@ UPnPResult_MythTV_NoNamespaceGiven
@ UPnPResult_SignatureFailure
@ UPnPResult_InvalidArgs
QMap< QString, QString > QStringMap
Definition: upnputil.h:28