15#include <QCoreApplication>
16#include <QDesktopServices>
17#include <QElapsedTimer>
21#include <QMutexLocker>
22#include <QNetworkAccessManager>
23#include <QNetworkDiskCache>
24#include <QNetworkInterface>
25#include <QNetworkProxy>
26#include <QNetworkReply>
27#include <QNetworkRequest>
32#include <QSslConfiguration>
46static const QString
LOC { QStringLiteral(
"[netstream] ") };
64 static const QEvent::Type
kType = QEvent::User;
80 static const QEvent::Type
kType =
static_cast< QEvent::Type
>(QEvent::User + 1);
100 m_cert(std::move(cert))
102 setObjectName(
"NetStream " + url.toString());
104 QNetworkRequest::CacheLoadControl attr {QNetworkRequest::PreferNetwork};
106 attr = QNetworkRequest::AlwaysCache;
108 attr = QNetworkRequest::PreferCache;
110 attr = QNetworkRequest::AlwaysNetwork;
111 m_request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, attr);
139static inline QString
Source(
const QNetworkRequest &request)
141 switch (request.attribute(QNetworkRequest::CacheLoadControlAttribute).toInt())
143 case QNetworkRequest::AlwaysCache:
return "cache";
144 case QNetworkRequest::PreferCache:
return "cache-preferred";
145 case QNetworkRequest::PreferNetwork:
return "net-preferred";
146 case QNetworkRequest::AlwaysNetwork:
return "net";
151static inline QString
Source(
const QNetworkReply* reply)
153 return reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool() ?
163 LOG(VB_GENERAL, LOG_WARNING,
LOC +
164 QString(
"(%1) Request unsupported URL: %2")
165 .arg(
m_id).arg(url.toString()) );
188 const QByteArray ua(
"User-Agent");
190 m_request.setRawHeader(ua,
"UK-MHEG/2 MYT001/001 MHGGNU/001");
193 m_request.setRawHeader(
"Range", QString(
"bytes=%1-").arg(
m_pos).toLatin1());
198 QSslConfiguration ssl(QSslConfiguration::defaultConfiguration());
200 QList<QSslCertificate> clist;
203 clist = QSslCertificate::fromData(
m_cert, QSsl::Der);
205 LOG(VB_GENERAL, LOG_WARNING,
LOC + QString(
"Invalid certificate: %1")
206 .arg(
m_cert.toPercentEncoding().constData()) );
212 ssl.setPeerVerifyMode(QSslSocket::VerifyNone);
216 ssl.setCaCertificates(clist);
223 if (!fname.isEmpty())
225 QSslCertificate cert;
227 if (f1.open(QIODevice::ReadOnly))
229 cert = QSslCertificate(&f1, QSsl::Pem);
231 ssl.setLocalCertificate(cert);
233 LOG(VB_GENERAL, LOG_WARNING,
LOC +
234 QString(
"'%1' is an invalid certificate").arg(f1.fileName()) );
238 LOG(VB_GENERAL, LOG_WARNING,
LOC +
239 QString(
"Opening client certificate '%1': %2")
240 .arg(f1.fileName(), f1.errorString()) );
245 if (!fname.isEmpty())
248 if (f2.open(QIODevice::ReadOnly))
250 auto keyAlgo = cert.isNull() ? QSsl::Rsa : cert.publicKey().algorithm();
251 QSslKey key(&f2, keyAlgo, QSsl::Pem, QSsl::PrivateKey,
254 ssl.setPrivateKey(key);
256 LOG(VB_GENERAL, LOG_WARNING,
LOC +
257 QString(
"'%1' is an invalid key").arg(f2.fileName()) );
261 LOG(VB_GENERAL, LOG_WARNING,
LOC +
262 QString(
"Opening private key '%1': %2")
263 .arg(f2.fileName(), f2.errorString()) );
272 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"(%1) Request %2 bytes=%3- from %4")
292 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"(%1) Started 0x%2")
293 .arg(
m_id).arg(quintptr(reply),0,16) );
306 connect(reply, &QNetworkReply::sslErrors,
this,
314 LOG(VB_GENERAL, LOG_ERR,
LOC +
315 QString(
"(%1) Started but m_reply not NULL").arg(
m_id));
322 qlonglong len = reply->header(QNetworkRequest::ContentLengthHeader)
324 return ok ? len : -1;
328 qulonglong &first, qulonglong &last)
330 QByteArray range = reply->rawHeader(
"Content-Range");
336 const char *fmt =
" bytes %20" SCNd64
" - %20" SCNd64
" / %20" SCNd64;
337 if (3 != std::sscanf(range.constData(), fmt, &first, &last, &len))
339 LOG(VB_GENERAL, LOG_ERR,
LOC + QString(
"Invalid Content-Range:'%1'")
340 .arg(range.constData()) );
344 return static_cast<qlonglong
>(len);
348static bool inline RequestRange(
const QNetworkRequest &request,
349 qlonglong &first, qlonglong &last)
353 QByteArray range = request.rawHeader(
"Range");
357 if (1 > std::sscanf(range.constData(),
" bytes %20lld - %20lld", &first, &last))
359 LOG(VB_GENERAL, LOG_ERR,
LOC + QString(
"Invalid Range:'%1'")
360 .arg(range.constData()) );
375 qint64 avail =
m_reply->bytesAvailable();
377 (avail <= 4 *
kMaxBuffer) ? LOG_INFO : LOG_WARNING,
378 LOC + QString(
"(%1) Ready 0x%2, %3 bytes available").arg(
m_id)
379 .arg(quintptr(
m_reply),0,16).arg(avail) );
383 qulonglong first = 0;
389 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"(%1) Ready 0x%2, range %3-%4/%5")
390 .arg(
m_id).arg(quintptr(
m_reply),0,16).arg(first).arg(last).arg(len) );
395 if (m_state < kReady || m_size >= 0)
397 LOG(VB_FILE, LOG_INFO,
LOC +
398 QString(
"(%1) Ready 0x%2, content length %3")
404 m_state = std::max(m_state,
kReady);
414 LOG(VB_GENERAL, LOG_ERR,
LOC +
415 QString(
"(%1) ReadyRead but m_reply = NULL").arg(
m_id));
427 if (QNetworkReply::NoError ==
error)
431 QNetworkRequest::RedirectionTargetAttribute).toUrl();
438 LOG(VB_FILE, LOG_WARNING,
LOC + QString(
"(%1) Too many redirections")
447 LOG(VB_FILE, LOG_WARNING,
LOC + QString(
"(%1) Redirection loop to %2")
448 .arg(
m_id).arg(url.toString()) );
453 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"(%1) Redirecting").arg(
m_id));
460 LOG(VB_FILE, LOG_WARNING,
LOC + QString(
"(%1): %2")
470 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"(%1) Finished 0x%2 %3/%4 bytes from %5")
482 LOG(VB_GENERAL, LOG_ERR,
LOC + QString(
"(%1) Finished but m_reply = NULL")
496 for (
const auto& e : std::as_const(errors))
498 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"(%1) SSL error %2: ")
499 .arg(
m_id).arg(e.error()) + e.errorString() );
503 case QSslError::SelfSignedCertificateInChain:
514 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"(%1) SSL errors ignored").arg(
m_id));
515 m_reply->ignoreSslErrors(errors);
520 LOG(VB_GENERAL, LOG_ERR,
LOC +
521 QString(
"(%1) SSL error but m_reply = NULL").arg(
m_id) );
533 return url.isValid() &&
534 (url.scheme() ==
"http" || url.scheme() ==
"https") &&
535 !url.authority().isEmpty() &&
536 !url.path().isEmpty();
551 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"(%1) Cancelled").arg(
m_id) );
559 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"(%1) Abort 0x%2")
572 QElapsedTimer
t;
t.start();
580 unsigned elapsed =
t.elapsed();
581 if (elapsed >= millisecs)
592 qint64 avail =
m_reply->read(
reinterpret_cast< char*
>(data), sz);
596 LOG(VB_FILE, LOG_DEBUG,
LOC + QString(
"(%1) safe_read @ %4 => %2/%3, %5 mS")
597 .arg(
m_id).arg(avail).arg(sz).arg(
m_pos).arg(
t.elapsed()) );
611 LOG(VB_GENERAL, LOG_ERR,
LOC +
612 QString(
"(%1) Seek(%2) out of range [0..%3]")
617 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"(%1) Seek(%2) curr %3 end %4")
645 QElapsedTimer
t;
t.start();
648 auto elapsed = std::chrono::milliseconds(
t.elapsed());
662 QElapsedTimer
t;
t.start();
665 auto elapsed = std::chrono::milliseconds(
t.elapsed());
678 return !
m_reply ? QNetworkReply::OperationCanceledError :
m_reply->error();
700 QByteArray data =
m_reply->readAll();
701 m_pos += data.size();
749 QMutexLocker locker(&
s_mtx);
759 setObjectName(
"NAMThread");
764 qRegisterMetaType< QList<QSslError> >();
778 LOG(VB_FILE, LOG_INFO,
LOC +
"NAMThread starting");
780 m_nam =
new QNetworkAccessManager();
781 m_nam->setObjectName(
"NetStream NAM");
784 std::unique_ptr<QNetworkDiskCache> cache = std::make_unique<QNetworkDiskCache>();
786 cache->setCacheDirectory(
GetConfDir() +
"/cache/netstream-" +
789 m_nam->setCache(cache.release());
793 QString proxy(qEnvironmentVariable(
"MYTHMHEG_PROXY"));
794 if (!proxy.isEmpty())
796 QUrl url(proxy, QUrl::TolerantMode);
797 QNetworkProxy::ProxyType
type {QNetworkProxy::NoProxy};
798 if (url.scheme().isEmpty()
799 || (url.scheme() ==
"http")
800 || (url.scheme() ==
"https"))
801 type = QNetworkProxy::HttpProxy;
802 else if (url.scheme() ==
"socks")
803 type = QNetworkProxy::Socks5Proxy;
804 else if (url.scheme() ==
"cache")
805 type = QNetworkProxy::HttpCachingProxy;
806 else if (url.scheme() ==
"ftp")
807 type = QNetworkProxy::FtpCachingProxy;
809 if (QNetworkProxy::NoProxy !=
type)
811 LOG(VB_GENERAL, LOG_INFO,
LOC +
"Using proxy: " + proxy);
812 m_nam->setProxy(QNetworkProxy(
813 type, url.host(), url.port(), url.userName(), url.password() ));
817 LOG(VB_MHEG, LOG_ERR,
LOC + QString(
"Unknown proxy type %1")
818 .arg(url.scheme()) );
823 connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit,
832 QCoreApplication::processEvents();
843 QScopedPointer< QEvent > ev(
m_workQ.dequeue());
855 LOG(VB_FILE, LOG_INFO,
LOC +
"NAMThread stopped");
873 switch (event->type())
877#pragma GCC diagnostic push
878#pragma GCC diagnostic ignored "-Wswitch"
881#pragma GCC diagnostic pop
892 LOG(VB_GENERAL, LOG_ERR,
LOC +
"Invalid NetStreamRequest");
896 if (!
p->m_bCancelled)
898 QNetworkReply *reply =
m_nam->get(
p->m_req);
899 LOG(VB_FILE, LOG_DEBUG,
LOC + QString(
"(%1) StartRequest 0x%2")
900 .arg(
p->m_id).arg(quintptr(reply),0,16) );
905 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"(%1) NetStreamRequest cancelled").arg(
p->m_id) );
914 LOG(VB_GENERAL, LOG_ERR,
LOC +
"Invalid NetStreamAbort");
918 LOG(VB_FILE, LOG_DEBUG,
LOC + QString(
"(%1) AbortRequest 0x%2").arg(
p->m_id)
919 .arg(quintptr(
p->m_reply),0,16) );
921 p->m_reply->disconnect();
929 auto interfaces = QNetworkInterface::allInterfaces();
930 return std::ranges::any_of(interfaces,
931 [](
const QNetworkInterface& iface)
933 auto f = iface.flags();
934 if (f.testFlag(QNetworkInterface::IsLoopBack))
936 return f.testFlag(QNetworkInterface::IsRunning);
946 QMutexLocker locker(&m.
m_mutex);
951 QAbstractNetworkCache *cache = m.
m_nam->cache();
955 QNetworkCacheMetaData meta = cache->metaData(url);
958 LOG(VB_FILE, LOG_DEBUG,
LOC + QString(
"GetLastModified('%1') not in cache")
959 .arg(url.toString()));
964 QDateTime
const now(QDateTime::currentDateTime());
965 QDateTime expire = meta.expirationDate();
966 if (expire.isValid() && expire.toLocalTime() < now)
968 LOG(VB_FILE, LOG_INFO,
LOC + QString(
"GetLastModified('%1') past expiration %2")
969 .arg(url.toString(), expire.toString()));
974 QDateTime lastMod = meta.lastModified();
976 QNetworkCacheMetaData::RawHeaderList
headers = meta.rawHeaders();
977 for (
const auto& h : std::as_const(
headers))
980 static const QString kSzFormat {
"ddd, dd MMM yyyy HH:mm:ss 'GMT'" };
982 QString
const first(h.first.toLower());
983 if (first ==
"cache-control")
985 QString
const second(h.second.toLower());
986 if (second ==
"no-cache" || second ==
"no-store")
988 LOG(VB_FILE, LOG_INFO,
LOC +
989 QString(
"GetLastModified('%1') Cache-Control disabled")
990 .arg(url.toString()) );
995 else if (first ==
"date")
1000 LOG(VB_GENERAL, LOG_WARNING,
LOC +
1001 QString(
"GetLastModified invalid Date header '%1'")
1002 .arg(h.second.constData()));
1005#if QT_VERSION < QT_VERSION_CHECK(6,5,0)
1006 d.setTimeSpec(Qt::UTC);
1008 d.setTimeZone(QTimeZone(QTimeZone::UTC));
1014 LOG(VB_FILE, LOG_DEBUG,
LOC + QString(
"GetLastModified('%1') last modified %2")
1015 .arg(url.toString(), lastMod.toString()));
QString GetHostName(void)
QString GetSetting(const QString &key, const QString &defaultval="")
Thread to process NetStream requests.
static bool isAvailable()
static void PostEvent(QEvent *e)
QRecursiveMutex m_mutexNAM
QNetworkAccessManager * m_nam
bool StartRequest(NetStreamRequest *p)
static QRecursiveMutex * GetMutex()
QQueue< QEvent * > m_workQ
bool NewRequest(QEvent *event)
static bool AbortRequest(NetStreamAbort *p)
static NAMThread & manager()
NetworkAccessManager event loop thread.
void requestStarted(int, QNetworkReply *)
static QDateTime GetLastModified(const QUrl &url)
static const QEvent::Type kType
NetStreamAbort(int id, QNetworkReply *reply)
QNetworkReply *const m_reply
volatile bool m_bCancelled
static const QEvent::Type kType
NetStreamRequest(int id, const QNetworkRequest &req)
const QNetworkRequest m_req
qlonglong BytesAvailable() const
qlonglong GetSize() const
void slotRequestStarted(int id, QNetworkReply *reply)
qlonglong Seek(qlonglong pos)
static bool isAvailable()
Public helpers.
int safe_read(void *data, unsigned sz, unsigned millisecs=0)
NetStreamRequest * m_pending
QNetworkReply::NetworkError GetError() const
static QDateTime GetLastModified(const QUrl &url)
enum NetStream::@17 kClosed
void slotSslErrors(const QList< QSslError > &errors)
bool WaitTillReady(std::chrono::milliseconds timeout)
Synchronous interface.
bool isStarted() const
Asynchronous interface.
static bool IsSupported(const QUrl &url)
RingBuffer interface.
NetStream(const QUrl &url, EMode mode=kPreferCache, QByteArray cert=QByteArray())
Network streaming request.
bool Request(const QUrl &url)
void ReadyRead(QObject *)
qlonglong GetReadPosition() const
QNetworkRequest m_request
QWaitCondition m_finished
QString GetErrorString() const
bool WaitTillFinished(std::chrono::milliseconds timeout)
static const iso6937table * d
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
QString GetShareDir(void)
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
QDateTime fromString(const QString &dtstr)
Converts kFilename && kISODate formats to QDateTime.
static qlonglong ContentLength(const QNetworkReply *reply)
static QAtomicInt s_nRequest(1)
static qlonglong ContentRange(const QNetworkReply *reply, qulonglong &first, qulonglong &last)
static QString Source(const QNetworkRequest &request)
static constexpr qint64 kMaxBuffer