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
66 // Build a list of directories. Start the list with enough entries
67 // to handle a current install, preventing multiple allocations.
68 static constexpr int approximate_count {60};
69 QStringList dirs;
70 dirs.reserve(approximate_count);
71 for (const auto & dir : s_dirs)
72 {
73 dirs.append(dir);
74 QDirIterator it(m_config.m_rootDir + dir, QDir::Dirs | QDir::Readable | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
75 while (it.hasNext())
76 dirs.append(it.next().remove(m_config.m_rootDir) + "/");
77 }
78
79 // And finally the root handler
80 dirs.append("/");
81 NewPaths(dirs);
82}
83
85{
86 Stopped();
87}
88
90{
91 if (Enable && !isListening())
92 {
93 Init();
94 bool tcp = m_config.m_port != 0;
95 bool ssl = m_config.m_sslPort != 0;
96
97 if (tcp)
98 {
99 tcp = listen(m_config.m_port);
100 // ServerPool as written will overwrite the port setting if we listen
101 // on an additional port (i.e. SSL). So check which port is in use before
102 // continuing
103 if (m_config.m_port != serverPort())
104 {
105 LOG(VB_GENERAL, LOG_WARNING, LOC + QString("Server is using port %1 - expected %2")
106 .arg(serverPort()).arg(m_config.m_port));
108 }
109 if (m_config.m_port_2)
111 }
112
113 if (ssl)
114 {
115 ssl = listen(m_config.m_sslPort, true, kSSLServer);
117 {
118 LOG(VB_GENERAL, LOG_WARNING, LOC + QString("Server is using port %1 - expected %2")
119 .arg(serverPort()).arg(m_config.m_sslPort));
121 }
122 }
123
124 Started(tcp, ssl);
125 }
126 else if (!Enable && isListening())
127 {
128 close();
129 Stopped();
130 }
131}
132
139{
140 // Just in case we get in a mess
141 Stopped();
142
143 // Decide on the ports to use
145 {
146 m_config.m_port = XmlConfiguration().GetValue("UPnP/MythFrontend/ServicePort", 6547);
147 // I don't think there is an existing setting for this
148 m_config.m_sslPort = static_cast<uint16_t>(gCoreContext->GetNumSetting("FrontendSSLPort", m_config.m_port + 10));
149
150 }
151 else if (gCoreContext->IsBackend())
152 {
154 // Additional port, may be removed later
156 m_config.m_sslPort = static_cast<uint16_t>(gCoreContext->GetNumSetting("BackendSSLPort", m_config.m_port + 10));
157 }
158 else
159 {
160 // N.B. This assumes only the frontend and backend use HTTP...
161 m_config.m_port = 0;
163 }
164
165 // If this fails, unset the SSL port
166#ifndef QT_NO_OPENSSL
168#endif
169 {
171 }
172
173 if (m_config.m_sslPort == 0)
174 LOG(VB_HTTP, LOG_INFO, LOC + "SSL is disabled");
175
176 // Set the server ident
177 QString version = GetMythSourceVersion();
178 if (version.startsWith("v"))
179 version = version.right(version.length() - 1);
180
181#ifdef Q_OS_WINDOWS
182 QString server = QStringLiteral("Windows");
183#else
184 struct utsname uname_info {};
185 uname(&uname_info);
186 QString server = QStringLiteral("%1/%2").arg(uname_info.sysname, uname_info.release);
187#endif
188 m_config.m_serverName = QString("MythTV/%1 %2 UPnP/1.0").arg(version, server);
189
190 // Retrieve language
192
193 // Get master backend details for Origin checks
197
198 // Get keep alive timeout
199 auto timeout = gCoreContext->GetNumSetting("HTTP/KeepAliveTimeoutSecs", HTTP_SOCKET_TIMEOUT_MS / 1000);
200 m_config.m_timeout = static_cast<std::chrono::milliseconds>(timeout * 1000);
201}
202
203void MythHTTPServer::Started([[maybe_unused]] bool Tcp,
204 [[maybe_unused]] bool Ssl)
205{
206#if CONFIG_LIBDNS_SD
207 // Advertise our webserver
208 delete m_bonjour;
209 delete m_bonjourSSL;
210 m_bonjour = nullptr;
211 m_bonjourSSL = nullptr;
212 if (!(Tcp || Ssl))
213 return;
214
215 auto host = QHostInfo::localHostName();
216 if (host.isEmpty())
217 host = tr("Unknown");
218
219 if (Tcp)
220 {
221 m_bonjour = new BonjourRegister();
222 m_bonjour->Register(m_config.m_port, QByteArrayLiteral("_http._tcp"),
223 QStringLiteral("%1 on %2").arg(QCoreApplication::applicationName(), host).toLatin1().constData(), {});
224 }
225
226 if (Ssl)
227 {
228 m_bonjourSSL = new BonjourRegister();
229 m_bonjourSSL->Register(m_config.m_sslPort, QByteArrayLiteral("_https._tcp"),
230 QStringLiteral("%1 on %2").arg(QCoreApplication::applicationName(), host).toLatin1().constData(), {});
231 }
232#endif
233
234 // Build our list of hosts and allowed origins.
235 BuildHosts();
236 BuildOrigins();
237}
238
240{
241 // Clear allowed origins
242 m_config.m_hosts.clear();
244
245#if CONFIG_LIBDNS_SD
246 // Stop advertising
247 delete m_bonjour;
248 delete m_bonjourSSL;
249 m_bonjour = nullptr;
250 m_bonjourSSL = nullptr;
251#endif
252}
253
255{
256 if (!m_connectionQueue.empty())
257 {
258 emit ProcessTCPQueue();
259 }
260}
261
263{
264 if (AvailableThreads() > 0)
265 {
266 auto entry = m_connectionQueue.dequeue();
268 auto name = QString("HTTP%1%2").arg(entry.m_ssl ? "S" : "").arg(m_threadNum++);
269 auto * newthread = new MythHTTPThread(this, m_config, name, entry.m_socketFD, entry.m_ssl);
270 AddThread(newthread);
271 connect(newthread->qthread(), &QThread::finished, this, &MythHTTPThreadPool::ThreadFinished);
272 connect(newthread->qthread(), &QThread::finished, this, &MythHTTPServer::ThreadFinished);
273 newthread->start();
274 return;
275 }
276}
277
279{
280 if (!Socket)
281 return;
282 auto * server = qobject_cast<PrivTcpServer*>(QObject::sender());
283 MythTcpQueueEntry entry;
284 entry.m_socketFD = Socket;
285 entry.m_ssl = (server->GetServerType() == kSSLServer);
286 m_connectionQueue.enqueue(entry);
287 emit ProcessTCPQueue();
288}
289
290bool MythHTTPServer::ReservedPath(const QString& Path)
291{
292 static const QStringList s_reservedPaths { HTTP_SERVICES_DIR };
293 if (s_reservedPaths.contains(Path, Qt::CaseInsensitive))
294 {
295 LOG(VB_GENERAL, LOG_WARNING, LOC + QString("Server path '%1' is reserved - ignoring").arg(Path));
296 return true;
297 }
298 return false;
299}
300
322void MythHTTPServer::NewPaths(const QStringList &Paths)
323{
324 if (Paths.isEmpty())
325 return;
326 for (const auto & path : std::as_const(Paths))
327 {
328 if (ReservedPath(path))
329 continue;
330 if (m_config.m_filePaths.contains(path))
331 LOG(VB_GENERAL, LOG_WARNING, LOC + QString("'%1' is already registered").arg(path));
332 else
333 LOG(VB_HTTP, LOG_INFO, LOC + QString("Adding path: '%1'").arg(path));
334 m_config.m_filePaths.append(path);
335 }
337}
338
339void MythHTTPServer::StalePaths(const QStringList& Paths)
340{
341 if (Paths.isEmpty())
342 return;
343 for (const auto & path : std::as_const(Paths))
344 {
345 if (m_config.m_filePaths.contains(path))
346 {
347 LOG(VB_HTTP, LOG_INFO, LOC + QString("Removing path: '%1'").arg(path));
348 m_config.m_filePaths.removeOne(path);
349 }
350 }
352}
353
376{
377 bool newhandlers = false;
378 for (const auto & handler : std::as_const(Handlers))
379 {
380 if (ReservedPath(handler.first))
381 continue;
382 if (!std::ranges::any_of(m_config.m_handlers,
383 [&handler](const HTTPHandler& Handler) { return Handler.first == handler.first; }))
384 {
385 LOG(VB_HTTP, LOG_INFO, LOC + QString("Adding handler for '%1'").arg(handler.first));
386 m_config.m_handlers.push_back(handler);
387 newhandlers = true;
388 }
389 else
390 {
391 LOG(VB_GENERAL, LOG_WARNING, LOC + QString("Handler '%1' already registered - ignoring")
392 .arg(handler.first));
393 }
394 }
395 if (newhandlers)
397}
398
400{
401 bool stalehandlers = false;
402 for (const auto & handler : std::as_const(Handlers))
403 {
404 auto found = std::ranges::find(m_config.m_handlers, handler.first,
405 &HTTPHandler::first);
406 if (found != m_config.m_handlers.end())
407 {
408 m_config.m_handlers.erase(found);
409 stalehandlers = true;
410 }
411 }
412 if (stalehandlers)
414}
415
417{
418 bool newservices = false;
419 for (const auto & service : std::as_const(Services))
420 {
421 if (ReservedPath(service.first))
422 continue;
423 if (!std::ranges::any_of(m_config.m_services,
424 [&service](const HTTPService& Service) { return Service.first == service.first; }))
425 {
426 LOG(VB_HTTP, LOG_INFO, LOC + QString("Adding service for '%1'").arg(service.first));
427 m_config.m_services.push_back(service);
428 newservices = true;
429 }
430 else
431 {
432 LOG(VB_GENERAL, LOG_WARNING, LOC + QString("Service '%1' already registered - ignoring")
433 .arg(service.first));
434 }
435 }
436 if (newservices)
438}
439
441{
442 bool staleservices = false;
443 for (const auto & service : std::as_const(Services))
444 {
445 auto found = std::ranges::find(m_config.m_services, service.first,
446 &HTTPService::first);
447 if (found != m_config.m_services.end())
448 {
449 m_config.m_services.erase(found);
450 staleservices = true;
451 }
452 }
453 if (staleservices)
455}
456
465{
466 LOG(VB_HTTP, LOG_INFO, LOC + QString("Adding error page handler"));
469}
470
472{
473 m_config.m_hosts.clear();
474
475 // Iterate over the addresses ServerPool was asked to listen on. This should
476 // pick up all variations of localhost and external IP etc
477 QStringList lookups;
478 auto defaults = DefaultListen();
479 bool allipv4 = false;
480 bool allipv6 = false;
481 for (const auto & address : std::as_const(defaults))
482 {
483 if (address == QHostAddress::AnyIPv4)
484 allipv4 |= true;
485 else if (address == QHostAddress::AnyIPv6)
486 allipv6 |= true;
487 else
488 lookups.append(address.toString());
489 }
490
491 // 'Any address' (0.0.0.0) is in use. Retrieve the complete list of avaible
492 // addresses and filter as required.
493 if (allipv4 || allipv6)
494 {
495 auto addresses = QNetworkInterface::allAddresses();
496 for (const auto & address : std::as_const(addresses))
497 {
498 if ((allipv4 && address.protocol() == QAbstractSocket::IPv4Protocol) ||
499 (allipv6 && address.protocol() == QAbstractSocket::IPv6Protocol))
500 {
501 lookups.append(address.toString());
502 }
503 }
504 }
505
506 lookups.removeDuplicates();
507
508 // Trigger reverse lookups
509 for (const auto & address : lookups)
510 {
512 QHostInfo::lookupHost(address, this, &MythHTTPServer::HostResolved);
513 }
514}
515
528{
530
531 // Add master backend. We need to resolve this separately to handle both status
532 // and SSL ports
534 QHostInfo::lookupHost(m_masterIPAddress, this, &MythHTTPServer::MasterResolved);
535
536 // Add configured overrides - are these still needed?
537 QStringList extras = gCoreContext->GetSetting("AllowedOriginsList", QString(
538 "https://chromecast.mythtv.org"
539 )).split(",");
540 for (const auto & extra : std::as_const(extras))
541 {
542 QString clean = extra.trimmed();
543 if (clean.startsWith("http://") || clean.startsWith("https://"))
544 m_config.m_allowedOrigins.append(clean);
545 }
546}
547
548QStringList MythHTTPServer::BuildAddressList(QHostInfo& Info)
549{
550 bool addhostname = true;
551 QString hostname = Info.hostName();
552 QStringList results;
553 auto ipaddresses = Info.addresses();
554 results.reserve(ipaddresses.count() + 1);
555 for(auto & address : ipaddresses)
556 {
557 QString result = MythHTTP::AddressToString(address);
558 // This filters out IPv6 addresses that are passed back as host names
559 if (result.contains(hostname))
560 addhostname = false;
561 results.append(result.toLower());
562 }
563 if (addhostname)
564 results.append(hostname.toLower());
565 return results;
566}
567
574{
575 auto addresses = BuildAddressList(Info);
576
577 // Add status and SSL addressed for each
578 for (const auto & address : std::as_const(addresses))
579 {
580 m_config.m_allowedOrigins.append(QString("http://%1").arg(address));
581 m_config.m_allowedOrigins.append(QString("http://%1:%2").arg(address).arg(m_masterStatusPort));
582 if (m_masterSSLPort != 0)
583 {
584 m_config.m_allowedOrigins.append(QString("https://%1").arg(address));
585 m_config.m_allowedOrigins.append(QString("https://%1:%2").arg(address).arg(m_masterSSLPort));
586 }
587 }
588 m_config.m_allowedOrigins.removeDuplicates();
589 if (--m_originLookups == 0)
590 DebugOrigins();
592}
593
595{
596 if (VERBOSE_LEVEL_CHECK(VB_HTTP, LOG_INFO))
597 {
598 LOG(VB_GENERAL, LOG_INFO, LOC +
599 QString("Name resolution complete: %1 'Origins' found").arg(m_config.m_allowedOrigins.size()));
600 for (const auto & address : std::as_const(m_config.m_allowedOrigins))
601 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Allowed origin: %1").arg(address));
602 }
603}
604
607void MythHTTPServer::ResolveHost(QHostInfo Info)
608{
609 auto addresses = BuildAddressList(Info);
610 for (const auto & address : std::as_const(addresses))
611 {
612 // The port is optional - so just add both to our list to simplify the
613 // checks when a request is received
614 m_config.m_hosts.append(QString("%1").arg(address));
615 if (m_config.m_port != 0)
616 m_config.m_hosts.append(QString("%1:%2").arg(address).arg(m_config.m_port));
617 if (m_config.m_sslPort != 0)
618 m_config.m_hosts.append(QString("%1:%2").arg(address).arg(m_config.m_sslPort));
619 }
620 m_config.m_hosts.removeDuplicates();
621 if (--m_hostLookups == 0)
622 DebugHosts();
624}
625
627{
628 if (VERBOSE_LEVEL_CHECK(VB_HTTP, LOG_INFO))
629 {
630 LOG(VB_GENERAL, LOG_INFO, LOC +
631 QString("Name resolution complete: %1 'Hosts' found").arg(m_config.m_hosts.size()));
632 for (const auto & address : std::as_const(m_config.m_hosts))
633 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Host: %1").arg(address));
634 }
635}
636
637#include "moc_mythhttpserver.cpp"
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:306
bool listen(QList< QHostAddress > addrs, quint16 port, bool requireall=true, PoolServerType type=kTCPServer)
Definition: serverpool.cpp:396
void close(void)
Definition: serverpool.cpp:375
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:280
#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