MythTV master
mythhttpserver.cpp
Go to the documentation of this file.
1// C++ headers
2#include <algorithm>
3
4// Qt
5#include <QtGlobal>
6#if QT_VERSION >= QT_VERSION_CHECK(6,5,0)
7#include <QtSystemDetection>
8#endif
9#include <QDirIterator>
10#include <QNetworkInterface>
11#include <QCoreApplication>
12#include <QSslKey>
13#include <QSslCipher>
14#include <QSslCertificate>
15
16// MythTV
17#include "mythconfig.h"
18#include "mythversion.h"
19#include "mythdirs.h"
20#include "mythcorecontext.h"
21#include "mythlogging.h"
23#if CONFIG_LIBDNS_SD
24#include "bonjourregister.h"
25#endif
26#include "http/mythhttpsocket.h"
28#include "http/mythhttpthread.h"
29#include "http/mythhttps.h"
30#include "http/mythhttpserver.h"
31
32// Std
33#ifndef Q_OS_WINDOWS
34#include <sys/utsname.h>
35#endif
36
37#define LOC QString("HTTPServer: ")
38
40{
41 // Join the dots
53
54 // Find our static content
56 while (m_config.m_rootDir.endsWith("/"))
57 m_config.m_rootDir.chop(1);
58 m_config.m_rootDir.append(QStringLiteral("/html"));
59
60 // Add our default paths (mostly static js, css, images etc).
61 // We need to pass individual directories to the threads, so inspect the
62 // the paths we want for sub-directories
63 static const QStringList s_dirs = { "/assets/", "/3rdParty/", "/css/", "/images/", "/apps/", "/xslt/" };
64 m_config.m_filePaths.clear();
65 QStringList dirs;
66 for (const auto & dir : s_dirs)
67 {
68 dirs.append(dir);
69 QDirIterator it(m_config.m_rootDir + dir, QDir::Dirs | QDir::Readable | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
70 while (it.hasNext())
71 dirs.append(it.next().remove(m_config.m_rootDir) + "/");
72 }
73
74 // And finally the root handler
75 dirs.append("/");
76 NewPaths(dirs);
77}
78
80{
81 Stopped();
82}
83
85{
86 if (Enable && !isListening())
87 {
88 Init();
89 bool tcp = m_config.m_port != 0;
90 bool ssl = m_config.m_sslPort != 0;
91
92 if (tcp)
93 {
94 tcp = listen(m_config.m_port);
95 // ServerPool as written will overwrite the port setting if we listen
96 // on an additional port (i.e. SSL). So check which port is in use before
97 // continuing
98 if (m_config.m_port != serverPort())
99 {
100 LOG(VB_GENERAL, LOG_WARNING, LOC + QString("Server is using port %1 - expected %2")
101 .arg(serverPort()).arg(m_config.m_port));
103 }
104 if (m_config.m_port_2)
106 }
107
108 if (ssl)
109 {
110 ssl = listen(m_config.m_sslPort, true, kSSLServer);
112 {
113 LOG(VB_GENERAL, LOG_WARNING, LOC + QString("Server is using port %1 - expected %2")
114 .arg(serverPort()).arg(m_config.m_sslPort));
116 }
117 }
118
119 Started(tcp, ssl);
120 }
121 else if (!Enable && isListening())
122 {
123 close();
124 Stopped();
125 }
126}
127
134{
135 // Just in case we get in a mess
136 Stopped();
137
138 // Decide on the ports to use
140 {
141 m_config.m_port = XmlConfiguration().GetValue("UPnP/MythFrontend/ServicePort", 6547);
142 // I don't think there is an existing setting for this
143 m_config.m_sslPort = static_cast<uint16_t>(gCoreContext->GetNumSetting("FrontendSSLPort", m_config.m_port + 10));
144
145 }
146 else if (gCoreContext->IsBackend())
147 {
149 // Additional port, may be removed later
151 m_config.m_sslPort = static_cast<uint16_t>(gCoreContext->GetNumSetting("BackendSSLPort", m_config.m_port + 10));
152 }
153 else
154 {
155 // N.B. This assumes only the frontend and backend use HTTP...
156 m_config.m_port = 0;
158 }
159
160 // If this fails, unset the SSL port
161#ifndef QT_NO_OPENSSL
163#endif
164 {
166 }
167
168 if (m_config.m_sslPort == 0)
169 LOG(VB_HTTP, LOG_INFO, LOC + "SSL is disabled");
170
171 // Set the server ident
172 QString version = GetMythSourceVersion();
173 if (version.startsWith("v"))
174 version = version.right(version.length() - 1);
175
176#ifdef Q_OS_WINDOWS
177 QString server = QStringLiteral("Windows");
178#else
179 struct utsname uname_info {};
180 uname(&uname_info);
181 QString server = QStringLiteral("%1/%2").arg(uname_info.sysname, uname_info.release);
182#endif
183 m_config.m_serverName = QString("MythTV/%1 %2 UPnP/1.0").arg(version, server);
184
185 // Retrieve language
187
188 // Get master backend details for Origin checks
192
193 // Get keep alive timeout
194 auto timeout = gCoreContext->GetNumSetting("HTTP/KeepAliveTimeoutSecs", HTTP_SOCKET_TIMEOUT_MS / 1000);
195 m_config.m_timeout = static_cast<std::chrono::milliseconds>(timeout * 1000);
196}
197
198void MythHTTPServer::Started([[maybe_unused]] bool Tcp,
199 [[maybe_unused]] bool Ssl)
200{
201#if CONFIG_LIBDNS_SD
202 // Advertise our webserver
203 delete m_bonjour;
204 delete m_bonjourSSL;
205 m_bonjour = nullptr;
206 m_bonjourSSL = nullptr;
207 if (!(Tcp || Ssl))
208 return;
209
210 auto host = QHostInfo::localHostName();
211 if (host.isEmpty())
212 host = tr("Unknown");
213
214 if (Tcp)
215 {
216 m_bonjour = new BonjourRegister();
217 m_bonjour->Register(m_config.m_port, QByteArrayLiteral("_http._tcp"),
218 QStringLiteral("%1 on %2").arg(QCoreApplication::applicationName(), host).toLatin1().constData(), {});
219 }
220
221 if (Ssl)
222 {
223 m_bonjourSSL = new BonjourRegister();
224 m_bonjourSSL->Register(m_config.m_sslPort, QByteArrayLiteral("_https._tcp"),
225 QStringLiteral("%1 on %2").arg(QCoreApplication::applicationName(), host).toLatin1().constData(), {});
226 }
227#endif
228
229 // Build our list of hosts and allowed origins.
230 BuildHosts();
231 BuildOrigins();
232}
233
235{
236 // Clear allowed origins
237 m_config.m_hosts.clear();
239
240#if CONFIG_LIBDNS_SD
241 // Stop advertising
242 delete m_bonjour;
243 delete m_bonjourSSL;
244 m_bonjour = nullptr;
245 m_bonjourSSL = nullptr;
246#endif
247}
248
250{
251 if (!m_connectionQueue.empty())
252 {
253 emit ProcessTCPQueue();
254 }
255}
256
258{
259 if (AvailableThreads() > 0)
260 {
261 auto entry = m_connectionQueue.dequeue();
263 auto name = QString("HTTP%1%2").arg(entry.m_ssl ? "S" : "").arg(m_threadNum++);
264 auto * newthread = new MythHTTPThread(this, m_config, name, entry.m_socketFD, entry.m_ssl);
265 AddThread(newthread);
266 connect(newthread->qthread(), &QThread::finished, this, &MythHTTPThreadPool::ThreadFinished);
267 connect(newthread->qthread(), &QThread::finished, this, &MythHTTPServer::ThreadFinished);
268 newthread->start();
269 return;
270 }
271}
272
274{
275 if (!Socket)
276 return;
277 auto * server = qobject_cast<PrivTcpServer*>(QObject::sender());
278 MythTcpQueueEntry entry;
279 entry.m_socketFD = Socket;
280 entry.m_ssl = (server->GetServerType() == kSSLServer);
281 m_connectionQueue.enqueue(entry);
282 emit ProcessTCPQueue();
283}
284
285bool MythHTTPServer::ReservedPath(const QString& Path)
286{
287 static const QStringList s_reservedPaths { HTTP_SERVICES_DIR };
288 if (s_reservedPaths.contains(Path, Qt::CaseInsensitive))
289 {
290 LOG(VB_GENERAL, LOG_WARNING, LOC + QString("Server path '%1' is reserved - ignoring").arg(Path));
291 return true;
292 }
293 return false;
294}
295
317void MythHTTPServer::NewPaths(const QStringList &Paths)
318{
319 if (Paths.isEmpty())
320 return;
321 for (const auto & path : std::as_const(Paths))
322 {
323 if (ReservedPath(path))
324 continue;
325 if (m_config.m_filePaths.contains(path))
326 LOG(VB_GENERAL, LOG_WARNING, LOC + QString("'%1' is already registered").arg(path));
327 else
328 LOG(VB_HTTP, LOG_INFO, LOC + QString("Adding path: '%1'").arg(path));
329 m_config.m_filePaths.append(path);
330 }
332}
333
334void MythHTTPServer::StalePaths(const QStringList& Paths)
335{
336 if (Paths.isEmpty())
337 return;
338 for (const auto & path : std::as_const(Paths))
339 {
340 if (m_config.m_filePaths.contains(path))
341 {
342 LOG(VB_HTTP, LOG_INFO, LOC + QString("Removing path: '%1'").arg(path));
343 m_config.m_filePaths.removeOne(path);
344 }
345 }
347}
348
371{
372 bool newhandlers = false;
373 for (const auto & handler : std::as_const(Handlers))
374 {
375 if (ReservedPath(handler.first))
376 continue;
377 if (!std::ranges::any_of(m_config.m_handlers,
378 [&handler](const HTTPHandler& Handler) { return Handler.first == handler.first; }))
379 {
380 LOG(VB_HTTP, LOG_INFO, LOC + QString("Adding handler for '%1'").arg(handler.first));
381 m_config.m_handlers.push_back(handler);
382 newhandlers = true;
383 }
384 else
385 {
386 LOG(VB_GENERAL, LOG_WARNING, LOC + QString("Handler '%1' already registered - ignoring")
387 .arg(handler.first));
388 }
389 }
390 if (newhandlers)
392}
393
395{
396 bool stalehandlers = false;
397 for (const auto & handler : std::as_const(Handlers))
398 {
399 auto found = std::ranges::find(m_config.m_handlers, handler.first,
400 &HTTPHandler::first);
401 if (found != m_config.m_handlers.end())
402 {
403 m_config.m_handlers.erase(found);
404 stalehandlers = true;
405 }
406 }
407 if (stalehandlers)
409}
410
412{
413 bool newservices = false;
414 for (const auto & service : std::as_const(Services))
415 {
416 if (ReservedPath(service.first))
417 continue;
418 if (!std::ranges::any_of(m_config.m_services,
419 [&service](const HTTPService& Service) { return Service.first == service.first; }))
420 {
421 LOG(VB_HTTP, LOG_INFO, LOC + QString("Adding service for '%1'").arg(service.first));
422 m_config.m_services.push_back(service);
423 newservices = true;
424 }
425 else
426 {
427 LOG(VB_GENERAL, LOG_WARNING, LOC + QString("Service '%1' already registered - ignoring")
428 .arg(service.first));
429 }
430 }
431 if (newservices)
433}
434
436{
437 bool staleservices = false;
438 for (const auto & service : std::as_const(Services))
439 {
440 auto found = std::ranges::find(m_config.m_services, service.first,
441 &HTTPService::first);
442 if (found != m_config.m_services.end())
443 {
444 m_config.m_services.erase(found);
445 staleservices = true;
446 }
447 }
448 if (staleservices)
450}
451
460{
461 LOG(VB_HTTP, LOG_INFO, LOC + QString("Adding error page handler"));
464}
465
467{
468 m_config.m_hosts.clear();
469
470 // Iterate over the addresses ServerPool was asked to listen on. This should
471 // pick up all variations of localhost and external IP etc
472 QStringList lookups;
473 auto defaults = DefaultListen();
474 bool allipv4 = false;
475 bool allipv6 = false;
476 for (const auto & address : std::as_const(defaults))
477 {
478 if (address == QHostAddress::AnyIPv4)
479 allipv4 |= true;
480 else if (address == QHostAddress::AnyIPv6)
481 allipv6 |= true;
482 else
483 lookups.append(address.toString());
484 }
485
486 // 'Any address' (0.0.0.0) is in use. Retrieve the complete list of avaible
487 // addresses and filter as required.
488 if (allipv4 || allipv6)
489 {
490 auto addresses = QNetworkInterface::allAddresses();
491 for (const auto & address : std::as_const(addresses))
492 {
493 if ((allipv4 && address.protocol() == QAbstractSocket::IPv4Protocol) ||
494 (allipv6 && address.protocol() == QAbstractSocket::IPv6Protocol))
495 {
496 lookups.append(address.toString());
497 }
498 }
499 }
500
501 lookups.removeDuplicates();
502
503 // Trigger reverse lookups
504 for (const auto & address : lookups)
505 {
507 QHostInfo::lookupHost(address, this, &MythHTTPServer::HostResolved);
508 }
509}
510
523{
525
526 // Add master backend. We need to resolve this separately to handle both status
527 // and SSL ports
529 QHostInfo::lookupHost(m_masterIPAddress, this, &MythHTTPServer::MasterResolved);
530
531 // Add configured overrides - are these still needed?
532 QStringList extras = gCoreContext->GetSetting("AllowedOriginsList", QString(
533 "https://chromecast.mythtv.org"
534 )).split(",");
535 for (const auto & extra : std::as_const(extras))
536 {
537 QString clean = extra.trimmed();
538 if (clean.startsWith("http://") || clean.startsWith("https://"))
539 m_config.m_allowedOrigins.append(clean);
540 }
541}
542
543QStringList MythHTTPServer::BuildAddressList(QHostInfo& Info)
544{
545 bool addhostname = true;
546 QString hostname = Info.hostName();
547 QStringList results;
548 auto ipaddresses = Info.addresses();
549 for(auto & address : ipaddresses)
550 {
551 QString result = MythHTTP::AddressToString(address);
552 // This filters out IPv6 addresses that are passed back as host names
553 if (result.contains(hostname))
554 addhostname = false;
555 results.append(result.toLower());
556 }
557 if (addhostname)
558 results.append(hostname.toLower());
559 return results;
560}
561
568{
569 auto addresses = BuildAddressList(Info);
570
571 // Add status and SSL addressed for each
572 for (const auto & address : std::as_const(addresses))
573 {
574 m_config.m_allowedOrigins.append(QString("http://%1").arg(address));
575 m_config.m_allowedOrigins.append(QString("http://%1:%2").arg(address).arg(m_masterStatusPort));
576 if (m_masterSSLPort != 0)
577 {
578 m_config.m_allowedOrigins.append(QString("https://%1").arg(address));
579 m_config.m_allowedOrigins.append(QString("https://%1:%2").arg(address).arg(m_masterSSLPort));
580 }
581 }
582 m_config.m_allowedOrigins.removeDuplicates();
583 if (--m_originLookups == 0)
584 DebugOrigins();
586}
587
589{
590 if (VERBOSE_LEVEL_CHECK(VB_HTTP, LOG_INFO))
591 {
592 LOG(VB_GENERAL, LOG_INFO, LOC +
593 QString("Name resolution complete: %1 'Origins' found").arg(m_config.m_allowedOrigins.size()));
594 for (const auto & address : std::as_const(m_config.m_allowedOrigins))
595 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Allowed origin: %1").arg(address));
596 }
597}
598
601void MythHTTPServer::ResolveHost(QHostInfo Info)
602{
603 auto addresses = BuildAddressList(Info);
604 for (const auto & address : std::as_const(addresses))
605 {
606 // The port is optional - so just add both to our list to simplify the
607 // checks when a request is received
608 m_config.m_hosts.append(QString("%1").arg(address));
609 if (m_config.m_port != 0)
610 m_config.m_hosts.append(QString("%1:%2").arg(address).arg(m_config.m_port));
611 if (m_config.m_sslPort != 0)
612 m_config.m_hosts.append(QString("%1:%2").arg(address).arg(m_config.m_sslPort));
613 }
614 m_config.m_hosts.removeDuplicates();
615 if (--m_hostLookups == 0)
616 DebugHosts();
618}
619
621{
622 if (VERBOSE_LEVEL_CHECK(VB_HTTP, LOG_INFO))
623 {
624 LOG(VB_GENERAL, LOG_INFO, LOC +
625 QString("Name resolution complete: %1 'Hosts' found").arg(m_config.m_hosts.size()));
626 for (const auto & address : std::as_const(m_config.m_hosts))
627 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Host: %1").arg(address));
628 }
629}
bool IsFrontend(void) const
is this process a frontend process
QString GetMasterServerIP(void)
Returns the Master Backend IP address If the address is an IPv6 address, the scope Id is removed.
QString GetSetting(const QString &key, const QString &defaultval="")
int GetBackendStatusPort(void)
Returns the locally defined backend status port.
bool IsBackend(void) const
is this process a backend process
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.
QSslConfiguration m_sslConfig
Definition: mythhttptypes.h:78
std::chrono::milliseconds m_timeout
Definition: mythhttptypes.h:69
QStringList m_filePaths
Definition: mythhttptypes.h:73
HTTPHandler m_errorPageHandler
Definition: mythhttptypes.h:76
HTTPHandlers m_handlers
Definition: mythhttptypes.h:74
quint16 m_port_2
Definition: mythhttptypes.h:65
QString m_rootDir
Definition: mythhttptypes.h:67
QStringList m_allowedOrigins
Definition: mythhttptypes.h:72
QStringList m_hosts
Definition: mythhttptypes.h:71
HTTPServices m_services
Definition: mythhttptypes.h:75
QString m_serverName
Definition: mythhttptypes.h:68
QString m_language
Definition: mythhttptypes.h:70
quint16 m_sslPort
Definition: mythhttptypes.h:66
static bool InitSSLServer(QSslConfiguration &Config)
Definition: mythhttps.cpp:13
void EnableDisable(bool Enable)
void RemoveServices(const HTTPServices &Services)
void NewErrorPageHandler(const HTTPHandler &Handler)
Add new error page handler.
~MythHTTPServer() override
void ServicesChanged(const HTTPServices &Services)
void MasterResolved(QHostInfo Info)
static bool ReservedPath(const QString &Path)
void Started(bool Tcp, bool Ssl)
void RemovePaths(const QStringList &Paths)
QQueue< MythTcpQueueEntry > m_connectionQueue
void Init()
Initialise server configuration.
void BuildOrigins()
Generate a list of allowed 'Origins' for validating CORS requests.
void StalePaths(const QStringList &Paths)
void StaleHandlers(const HTTPHandlers &Handlers)
void EnableHTTP(bool Enable)
void HandlersChanged(const HTTPHandlers &Handlers)
void StaleServices(const HTTPServices &Services)
void ErrorHandlerChanged(const HTTPHandler &Handler)
void AddHandlers(const HTTPHandlers &Handlers)
void OriginsChanged(const QStringList &Origins)
void AddServices(const HTTPServices &Services)
void AddErrorPageHandler(const HTTPHandler &Handler)
static QStringList BuildAddressList(QHostInfo &Info)
MythHTTPConfig m_config
void newTcpConnection(qintptr socket) override
void NewPaths(const QStringList &Paths)
Add new paths that will serve simple files.
void ResolveMaster(QHostInfo Info)
Add master backend addresses to the allowed Origins list.
void RemoveHandlers(const HTTPHandlers &Handlers)
void NewServices(const HTTPServices &Services)
void PathsChanged(const QStringList &Paths)
void ResolveHost(QHostInfo Info)
Add the results of a reverse lookup to our allowed list.
void ProcessTCPQueueHandler()
void AddPaths(const QStringList &Paths)
void HostResolved(QHostInfo Info)
QString m_masterIPAddress
void HostsChanged(const QStringList &Hosts)
void ProcessTCPQueue()
void NewHandlers(const HTTPHandlers &Handlers)
Add new handlers.
size_t MaxThreads() const
size_t AvailableThreads() const
void AddThread(MythHTTPThread *Thread)
static QString AddressToString(QHostAddress &Address)
static QList< QHostAddress > DefaultListen(void)
Definition: serverpool.cpp:305
bool listen(QList< QHostAddress > addrs, quint16 port, bool requireall=true, PoolServerType type=kTCPServer)
Definition: serverpool.cpp:395
void close(void)
Definition: serverpool.cpp:374
bool isListening(void) const
Definition: serverpool.h:92
quint16 serverPort(void) const
Definition: serverpool.h:95
QString GetValue(const QString &setting)
static pid_list_t::iterator find(const PIDInfoMap &map, pid_list_t &list, pid_list_t::iterator begin, pid_list_t::iterator end, bool find_open)
unsigned short uint16_t
Definition: iso6937tables.h:3
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
QString GetShareDir(void)
Definition: mythdirs.cpp:283
#define LOC
static constexpr int HTTP_SOCKET_TIMEOUT_MS
Definition: mythhttptypes.h:25
std::pair< QString, HTTPServiceCtor > HTTPService
Definition: mythhttptypes.h:54
std::vector< HTTPHandler > HTTPHandlers
Definition: mythhttptypes.h:48
std::vector< HTTPService > HTTPServices
Definition: mythhttptypes.h:55
std::pair< QString, HTTPFunction > HTTPHandler
Definition: mythhttptypes.h:47
#define HTTP_SERVICES_DIR
Definition: mythhttptypes.h:26
static bool VERBOSE_LEVEL_CHECK(uint64_t mask, LogLevel_t level)
Definition: mythlogging.h:29
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
const char * GetMythSourceVersion()
Definition: mythversion.cpp:7
string version
Definition: giantbomb.py:185
string hostname
Definition: caa.py:17
@ kSSLServer
Definition: serverpool.h:33