MythTV master
mythhttpsocket.cpp
Go to the documentation of this file.
1// Qt
2#include <QThread>
3#include <QTcpSocket>
4#ifndef QT_NO_OPENSSL
5#include <QSslSocket>
6#endif
7
8// MythTV
9#include "mythlogging.h"
10#include "mythcorecontext.h"
11#include "http/mythwebsocket.h"
12#include "http/mythhttps.h"
13#include "http/mythhttpsocket.h"
14#include "http/mythhttpdata.h"
15#include "http/mythhttpfile.h"
18#include "http/mythhttpranges.h"
21
22// Std
23#include <chrono>
24using namespace std::chrono_literals;
25
26#define LOC QString(m_peer + ": ")
27
28MythHTTPSocket::MythHTTPSocket(qintptr Socket, bool SSL, MythHTTPConfig Config)
29 : m_socketFD(Socket),
30 m_config(std::move(Config))
31{
32 // Connect Finish signal to Stop
33 connect(this, &MythHTTPSocket::Finish, this, &MythHTTPSocket::Stop);
34
35 // Create socket
36 QSslSocket* sslsocket = nullptr;
37 if (SSL)
38 {
39#ifndef QT_NO_OPENSSL
40 sslsocket = new QSslSocket(this);
41 m_socket = sslsocket;
42#endif
43 }
44 else
45 {
46 m_socket = new QTcpSocket(this);
47 }
48
49 if (!m_socket)
50 {
51 Stop();
52 return;
53 }
54
55 connect(m_socket, &QTcpSocket::readyRead, this, &MythHTTPSocket::Read);
56 connect(m_socket, &QTcpSocket::bytesWritten, this, &MythHTTPSocket::Write);
57 connect(m_socket, &QTcpSocket::disconnected, this, &MythHTTPSocket::Disconnected);
58 connect(m_socket, &QTcpSocket::disconnected, QThread::currentThread(), &QThread::quit);
59 connect(m_socket, &QTcpSocket::errorOccurred, this, &MythHTTPSocket::Error);
60 m_socket->setSocketDescriptor(m_socketFD);
61
62#ifndef QT_NO_OPENSSL
63 if (SSL && sslsocket)
65#endif
66
67 m_peer = QString("%1:%2").arg(m_socket->peerAddress().toString()).arg(m_socket->peerPort());
68 LOG(VB_HTTP, LOG_INFO, LOC + "New connection");
69
70 // Setup up timeout timer
71 m_timer.setTimerType(Qt::PreciseTimer);
74
76 Stop();
77
78 // Setup root services handler - a service for services
79 // Note: the services service is not self aware - i.e. it doesn't list itself
80 // as a service. This is intentional; a client needs to know of the existence
81 // of /services/ already - otherwise it wouldn't be calling it and the 'services'
82 // service would appear again as /services/services/
83 auto service = MythHTTPService::Create<MythHTTPServices>();
84 m_activeServices.emplace_back(service);
85 auto * services = dynamic_cast<MythHTTPServices*>(service.get());
86 if (services == nullptr)
87 {
88 LOG(VB_HTTP, LOG_ERR, LOC + "Failed to get root services handler.");
89 return;
90 }
92 services->UpdateServices(m_config.m_services);
93}
94
96{
97 delete m_websocketevent;
98 delete m_websocket;
99 if (m_socket)
100 m_socket->close();
101 delete m_socket;
102}
103
109void MythHTTPSocket::PathsChanged(const QStringList& Paths)
110{
111 m_config.m_filePaths = Paths;
112}
113
115{
116 m_config.m_handlers = Handlers;
117}
118
120{
121 m_config.m_services = Services;
123}
124
130void MythHTTPSocket::OriginsChanged(const QStringList& Origins)
131{
132 m_config.m_allowedOrigins = Origins;
133}
134
135void MythHTTPSocket::HostsChanged(const QStringList& Hosts)
136{
137 m_config.m_hosts = Hosts;
138}
139
146{
147 LOG(VB_HTTP, LOG_INFO, LOC + "Disconnected");
148}
149
150void MythHTTPSocket::Error(QAbstractSocket::SocketError Error)
151{
152 if (Error != QAbstractSocket::RemoteHostClosedError)
153 LOG(VB_HTTP, LOG_INFO, LOC + QString("Error %1 (%2)").arg(Error).arg(m_socket->errorString()));
154}
155
161{
162 LOG(VB_HTTP, LOG_DEBUG, LOC + "Timed out waiting for activity");
163 // Send '408 Request Timeout' - Respond will close the socket.
164 // Note: some clients will close the socket themselves at the correct time -
165 // in which case the disconnect may preceed the timeout here - so avoid
166 // misleading write errors...
167 if (m_socket->state() == QAbstractSocket::ConnectedState)
168 {
169 // If this has timed out while a write is in progress, then the socket
170 // buffers are not empty and we cannot send a response - so just close.
171 if (m_socket->bytesToWrite() || !m_queue.empty())
172 {
173 m_queue.clear();
174 LOG(VB_HTTP, LOG_INFO, LOC + "Timeout during write. Quitting.");
175 Stop();
176 return;
177 }
179 Stop();
180 }
181}
182
190{
191 LOG(VB_HTTP, LOG_INFO, LOC + "Stop");
192 if (m_websocket)
194 m_timer.stop();
195 m_stopping = true;
196 // Note: previously this called QTcpSocket::disconnectFromHost - but when
197 // an interrupt is received (i.e. quit the app), there is an intermittent
198 // race condition where the socket is disconnected before the thread is told
199 // to quit and the call to QThread::quit is not triggered (i.e. the connection
200 // between QTcpSocket::disconnected and QThread::quit does not work). So
201 // just quit the thread.
202 QThread::currentThread()->quit();
203}
204
210{
211 if (m_stopping)
212 return;
213
214 // Warn if we haven't sent the last response
215 if (!m_queue.empty())
216 LOG(VB_GENERAL, LOG_WARNING, LOC + "New request but queue is not empty");
217
218 // reset the idle timer
220
221 bool ready = false;
222 if (!m_parser.Read(m_socket, ready))
223 {
224 LOG(VB_HTTP, LOG_WARNING, LOC + "HTTP error");
225 Stop();
226 return;
227 }
228
229 if (!ready)
230 return;
231
232 // We have a completed request
234 HTTPResponse response = nullptr;
235
236 // Request should have initial OK status if valid
237 if (request->m_status != HTTPOK)
238 {
239 response = MythHTTPResponse::ErrorResponse(request);
240 Respond(response);
241 return;
242 }
243
244 // Websocket upgrade request...
245 // This will have a connection type of HTTPConnectionUpgrade on success.
246 // Once the response is sent, we will create a WebSocket instance to handle
247 // further read/write signals.
248 if (!MythHTTP::GetHeader(request->m_headers, "upgrade").isEmpty())
250
251 // Try (possibly file specific) handlers
252 if (response == nullptr)
253 {
254 // cppcheck-suppress unassignedVariable
255 for (const auto& [path, function] : m_config.m_handlers)
256 {
257 if (path == request->m_url.toString())
258 {
259 response = std::invoke(function, request);
260 if (response)
261 break;
262 }
263 }
264 }
265
266 const QString& rpath = request->m_path;
267
268 // Try active services first - this is currently the services root only
269 if (response == nullptr)
270 {
271 LOG(VB_HTTP, LOG_INFO, LOC + QString("Processing: path '%1' file '%2'")
272 .arg(rpath, request->m_fileName));
273
274 for (auto & service : m_activeServices)
275 {
276 if (service->Name() == rpath)
277 {
278 response = service->HTTPRequest(request);
279 if (response)
280 break;
281 }
282 }
283 }
284
285 // Then 'inactive' services
286 if (response == nullptr)
287 {
288 // cppcheck-suppress unassignedVariable
289 for (const auto & [path, constructor] : m_config.m_services)
290 {
291 if (path == rpath)
292 {
293 auto instance = std::invoke(constructor);
294 response = instance->HTTPRequest(request);
295 if (response)
296 break;
297 // the service object will be deleted here as it goes out of scope
298 // but we could retain a cache...
299 }
300 }
301 }
302
303 // Try (dynamic) handlers
304 if (response == nullptr)
305 {
306 // cppcheck-suppress unassignedVariable
307 for (const auto& [path, function] : m_config.m_handlers)
308 {
309 if (path == rpath)
310 {
311 response = std::invoke(function, request);
312 if (response)
313 break;
314 }
315 }
316 }
317
318 // then simple file path handlers
319 if (response == nullptr)
320 {
321 for (const auto & path : std::as_const(m_config.m_filePaths))
322 {
323 if (path == rpath)
324 {
325 response = MythHTTPFile::ProcessFile(request);
326 if (response)
327 break;
328 }
329 }
330 }
331
332 // Try error page handler
333 if (response == nullptr || response->m_status == HTTPNotFound)
334 {
335 if(m_config.m_errorPageHandler.first.length() > 0)
336 {
337 auto function = m_config.m_errorPageHandler.second;
338 response = std::invoke(function, request);
339 }
340 }
341
342 // nothing to see
343 if (response == nullptr)
344 {
345 request->m_status = HTTPNotFound;
346 response = MythHTTPResponse::ErrorResponse(request);
347 }
348
349 // Send the response
350 Respond(response);
351}
352
359{
360 if (!Response || (m_socket->state() != QAbstractSocket::ConnectedState))
361 return;
362
363 // Warn if the last response has not been completed
364 if (!m_queue.empty())
365 LOG(VB_GENERAL, LOG_WARNING, LOC + "Responding but queue is not empty");
366
367 // Reset the write tracker
368 m_writeTime.start();
369 m_totalToSend = 0;
370 m_totalWritten = 0;
371 m_totalSent = 0;
372 m_writeBuffer = nullptr;
373
374 // Finalise the response
375 Response->Finalise(m_config);
376
377 // Queue headers
378 for (const auto & header : std::as_const(Response->m_responseHeaders))
379 {
380 LOG(VB_HTTP, LOG_DEBUG, header->trimmed().constData());
381 m_queue.emplace_back(header);
382 }
383
384 // Queue in memory response content or file
385 if (!std::get_if<std::monostate>(&Response->m_response))
386 m_queue.push_back(Response->m_response);
387 else
388 LOG(VB_HTTP, LOG_DEBUG, LOC + "No content in response");
389
390 // Sum the expected number of bytes to be written. This is the size of the
391 // data or file OR the total size of the range request. For multipart range
392 // requests, add the total size of the additional headers.
393 for (const auto & content : std::as_const(m_queue))
394 {
395 if (const auto * data = std::get_if<HTTPData>(&content))
396 {
397 m_totalToSend += (*data)->m_partialSize > 0 ? (*data)->m_partialSize : (*data)->size();
398 m_totalToSend += (*data)->m_multipartHeaderSize;
399 }
400 else if (const auto * file = std::get_if<HTTPFile>(&content))
401 {
402 m_totalToSend += (*file)->m_partialSize > 0 ? (*file)->m_partialSize : (*file)->size();
403 m_totalToSend += (*file)->m_multipartHeaderSize;
404 }
405 }
406
407 // Signal what to do once the response is sent (close, keep alive or upgrade)
408 m_nextConnection = Response->m_connection;
409
410 // Get the ball rolling
411 Write();
412}
413
418void MythHTTPSocket::RespondDirect(qintptr Socket, const HTTPResponse& Response, const MythHTTPConfig &Config)
419{
420 if (!(Socket && Response))
421 return;
422
423 Response->Finalise(Config);
424 auto * socket = new QTcpSocket();
425 socket->setSocketDescriptor(Socket);
426 for (const auto & header : std::as_const(Response->m_responseHeaders))
427 socket->write(*header);
428 socket->flush();
429 socket->disconnectFromHost();
430 delete socket;
431}
432
433void MythHTTPSocket::Write(int64_t Written)
434{
435 auto chunkheader = [&](const QByteArray& Data)
436 {
437 int64_t wrote = m_socket->write(Data);
438 if (wrote < 0)
439 {
440 LOG(VB_GENERAL, LOG_ERR, "Error writing chunk header");
441 return;
442 }
443 m_totalToSend += wrote;
444 m_totalWritten += wrote;
445 };
446
447 if (m_stopping)
448 return;
449
450 // Naughty clients have a habit of stopping reading but leaving the socket open.
451 // Instead of stopping the timer when a request has been received, we leave
452 // it running and update when we see any read OR write activity
453 // TODO perhaps worth using a larger timeout (e.g. *2) for writes - for clients
454 // that are streaming video and have buffered a large amount of data - but with range
455 // request support this probably shouldn't matter - as they can just reconnect
456 // and pick up where they left off.
458
459 // Update write tracker
460 m_totalSent += Written;
461 int64_t buffered = m_socket->bytesToWrite();
462 LOG(VB_HTTP, LOG_DEBUG, LOC + QString("Socket sent %1bytes (still to send %2)")
463 .arg(Written).arg(buffered));
464
465 // If the client cannot consume data as fast as we are sending it, we just
466 // buffer more and more data in the Qt socket code. So return and wait for
467 // the next write.
468 if (buffered > (HTTP_CHUNKSIZE << 1))
469 {
470 LOG(VB_HTTP, LOG_DEBUG, LOC + "Draining buffers");
471 return;
472 }
473
475 {
476 auto seconds = static_cast<double>(m_writeTime.nsecsElapsed()) / 1000000000.0;
477 auto rate = static_cast<uint64_t>(static_cast<double>(m_totalSent) / seconds);
478 LOG(VB_HTTP, LOG_INFO, LOC + QString("Wrote %1bytes in %2seconds (%3)")
479 .arg(m_totalSent).arg(seconds, 8, 'f', 6, '0')
480 .arg(MythHTTPWS::BitrateToString(rate)));
481
482 if (m_queue.empty())
483 {
485 Stop();
488 return;
489 }
490 // This is going to be unrecoverable
491 LOG(VB_GENERAL, LOG_ERR, LOC + "Write complete but queue not empty");
492 Stop();
493 return;
494 }
495
496 // Fill them buffers
497 int64_t available = HTTP_CHUNKSIZE;
498 while ((available > 0) && !m_queue.empty())
499 {
500 int64_t written = 0;
501 int64_t read = 0;
502 int64_t wrote = 0;
503 int64_t itemsize = 0;
504 int64_t towrite = 0;
505 bool chunk = false;
506 auto * data = std::get_if<HTTPData>(&m_queue.front());
507 auto * file = std::get_if<HTTPFile>(&m_queue.front());
508
509 if (data)
510 {
511 chunk = (*data)->m_encoding == HTTPChunked;
512 written = (*data)->m_written;
513 itemsize = (*data)->m_partialSize > 0 ? (*data)->m_partialSize : static_cast<int64_t>((*data)->size());
514 towrite = std::min(itemsize - written, available);
515 int64_t offset = 0;
516 HTTPMulti multipart { nullptr, nullptr };
517 if (!(*data)->m_ranges.empty())
518 multipart = MythHTTPRanges::HandleRangeWrite(*data, available, towrite, offset);
519 if (chunk)
520 chunkheader(QStringLiteral("%1\r\n").arg(towrite, 0, 16).toLatin1().constData());
521 if (multipart.first)
522 wrote += m_socket->write(multipart.first->constData());
523 wrote += m_socket->write((*data)->constData() + written + offset, towrite);
524 if (multipart.second)
525 wrote += m_socket->write(multipart.second->constData());
526 if (chunk)
527 chunkheader("\r\n");
528 }
529 else if (file)
530 {
531 chunk = (*file)->m_encoding == HTTPChunked;
532 written = (*file)->m_written;
533 itemsize = (*file)->m_partialSize > 0 ? (*file)->m_partialSize : static_cast<int64_t>((*file)->size());
534 towrite = std::min(itemsize - written, available);
535 HTTPMulti multipart { nullptr, nullptr };
536 if (!(*file)->m_ranges.empty())
537 {
538 int64_t offset = 0;
539 multipart = MythHTTPRanges::HandleRangeWrite(*file, available, towrite, offset);
540 if (offset != (*file)->pos())
541 (*file)->seek(offset);
542 }
543 int64_t writebufsize = std::min(itemsize, static_cast<int64_t>(HTTP_CHUNKSIZE));
544 if (m_writeBuffer && (m_writeBuffer->size() < writebufsize))
545 m_writeBuffer = nullptr;
546 if (!m_writeBuffer)
547 m_writeBuffer = MythHTTPData::Create(static_cast<int>(writebufsize), '\0');
548 read = (*file)->read(m_writeBuffer->data(), towrite);
549 if (read > 0)
550 {
551 if (chunk)
552 chunkheader(QStringLiteral("%1\r\n").arg(read, 0, 16).toLatin1().constData());
553 if (multipart.first)
554 wrote += m_socket->write(multipart.first->constData());
555 wrote += m_socket->write(m_writeBuffer->data(), read);
556 if (multipart.second)
557 wrote += m_socket->write(multipart.second->constData());
558 if (chunk)
559 chunkheader("\r\n");
560 }
561 }
562
563 if (wrote < 0 || read < 0)
564 {
565 LOG(VB_GENERAL, LOG_ERR, LOC + QString("Write error (Read: %1 Write: %2)")
566 .arg(read).arg(wrote));
567 Stop();
568 return;
569 }
570
571 written += wrote;
572 if (data) (*data)->m_written = written;
573 if (file) (*file)->m_written = written;
574 m_totalWritten += wrote;
575 available -= wrote;
576
577#if 0
578 LOG(VB_HTTP, LOG_DEBUG, LOC + QString("Sent %1 bytes (cum: %2 of %3)")
579 .arg(wrote).arg(m_totalWritten).arg(m_totalToSend));
580#endif
581
582 // Move on to the next buffer?
583 if (written >= itemsize)
584 {
585 if (chunk)
586 chunkheader("0\r\n\r\n");
587 m_queue.pop_front();
588 m_writeBuffer = nullptr;
589 }
590 }
591}
592
596{
597 // Disconnect and stop our read/write timer
598 m_timer.disconnect(this);
599 m_timer.stop();
600
601 // Disconnect our socket read/writes
602 disconnect(m_socket, &QTcpSocket::readyRead, this, &MythHTTPSocket::Read);
603 disconnect(m_socket, &QTcpSocket::bytesWritten, this, &MythHTTPSocket::Write);
604
605 // Create server websocket instance
607 if (!m_websocket)
608 {
609 LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to create websocket");
610 Stop();
611 return;
612 }
613 // Create event listener
615 if (!m_websocketevent)
616 {
617 LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to create websocket event listener");
618 Stop();
619 return;
620 }
621
622 // Listen for messages
626
627 // Sending messages
629
630 // Add this thread to the list of upgraded threads, so we free up slots
631 // for regular HTTP sockets.
632 emit ThreadUpgraded(QThread::currentThread());
633}
634
636{
637 if (!Text)
638 return;
640}
641
643{
644 if (Payloads.empty())
645 return;
647}
648
650{
651 if (Payloads.empty())
652 return;
653}
bool CheckSubnet(const QAbstractSocket *socket)
Check if a socket is connected to an approved peer.
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
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
static HTTPData Create()
Definition: mythhttpdata.cpp:4
static HTTPResponse ProcessFile(const HTTPRequest2 &Request)
bool Read(QTcpSocket *Socket, bool &Ready)
HTTPRequest2 GetRequest(const MythHTTPConfig &Config, QTcpSocket *Socket)
static HTTPMulti HandleRangeWrite(HTTPVariant Data, int64_t Available, int64_t &ToWrite, int64_t &Offset)
static HTTPResponse UpgradeResponse(const HTTPRequest2 &Request, MythSocketProtocol &Protocol, bool &Testing)
static HTTPResponse ErrorResponse(MythHTTPStatus Status, const QString &ServerName)
static void InitSSLSocket(QSslSocket *Socket, QSslConfiguration &Config)
Definition: mythhttps.cpp:118
void UpdateServices(const HTTPServices &Services)
HTTPQueue m_queue
QElapsedTimer m_writeTime
void Write(int64_t Written=0)
int64_t m_totalSent
void NewRawTextMessage(const DataPayloads &Payloads)
void NewTextMessage(const StringPayload &Text)
~MythHTTPSocket() override
static void RespondDirect(qintptr Socket, const HTTPResponse &Response, const MythHTTPConfig &Config)
Send an (error) response directly without creating a thread.
HTTPData m_writeBuffer
void Respond(const HTTPResponse &Response)
Send response to client.
void ThreadUpgraded(QThread *Thread)
void Disconnected()
The socket was disconnected.
QTcpSocket * m_socket
MythHTTPParser m_parser
void Timeout()
Close the socket after a period of inactivity.
MythHTTPConfig m_config
void Stop()
Close the socket and quit the thread.
void PathsChanged(const QStringList &Paths)
Update our list of recognised file paths.
static void NewBinaryMessage(const DataPayloads &Payloads)
HTTPServicePtrs m_activeServices
void UpdateServices(const HTTPServices &Services)
void HandlersChanged(const HTTPHandlers &Handlers)
void OriginsChanged(const QStringList &Origins)
Update the list of allowed Origins.
void Read()
Read data from the socket which is parsed by MythHTTPParser.
int64_t m_totalToSend
MythHTTPConnection m_nextConnection
MythWebSocket * m_websocket
qintptr m_socketFD
MythWebSocketEvent * m_websocketevent
void Error(QAbstractSocket::SocketError Error)
MythHTTPSocket(qintptr Socket, bool SSL, MythHTTPConfig Config)
void ServicesChanged(const HTTPServices &Services)
int64_t m_totalWritten
void SetupWebSocket()
Transition socket to a WebSocket.
void HostsChanged(const QStringList &Hosts)
MythSocketProtocol m_protocol
static QString BitrateToString(uint64_t Rate)
static QString GetHeader(const HTTPHeaders &Headers, const QString &Value, const QString &Default="")
bool HandleTextMessage(const StringPayload &Text)
bool HandleRawTextMessage(const DataPayloads &Payloads)
void SendTextMessage(const QString &)
void Close()
Initiate the close handshake when we are exiting.
void SendTextFrame(const QString &Text)
static MythWebSocket * CreateWebsocket(bool Server, QTcpSocket *Socket, MythSocketProtocol Protocol, bool Testing)
void NewRawTextMessage(const DataPayloads &Payloads)
void NewBinaryMessage(const DataPayloads &Payloads)
void NewTextMessage(const StringPayload &Text)
@ quit
Definition: lirc_client.h:30
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
#define HTTP_CHUNKSIZE
A group of functions shared between HTTP and WebSocket code.
std::vector< DataPayload > DataPayloads
std::shared_ptr< MythSharedString > StringPayload
#define LOC
@ HTTPOK
@ HTTPRequestTimeout
@ HTTPNotFound
@ HTTPConnectionClose
@ HTTPConnectionUpgrade
std::shared_ptr< MythHTTPRequest > HTTPRequest2
Definition: mythhttptypes.h:39
std::vector< HTTPHandler > HTTPHandlers
Definition: mythhttptypes.h:48
std::shared_ptr< MythHTTPResponse > HTTPResponse
Definition: mythhttptypes.h:40
std::vector< HTTPService > HTTPServices
Definition: mythhttptypes.h:55
@ HTTPChunked
std::pair< HTTPData, HTTPData > HTTPMulti
Definition: mythhttptypes.h:49
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
def read(device=None, features=[])
Definition: disc.py:35
STL namespace.