47 changes: 47 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpdata.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// MythTV
#include "mythhttpdata.h"

HTTPData MythHTTPData::Create()
{
return std::shared_ptr<MythHTTPData>(new MythHTTPData);
}

HTTPData MythHTTPData::Create(const QString &Name, const char *Buffer)
{
return std::shared_ptr<MythHTTPData>(new MythHTTPData(Name, Buffer));
}

HTTPData MythHTTPData::Create(int Size, char Char)
{
return std::shared_ptr<MythHTTPData>(new MythHTTPData(Size, Char));
}

HTTPData MythHTTPData::Create(const QByteArray& Other)
{
return std::shared_ptr<MythHTTPData>(new MythHTTPData(Other));
}

MythHTTPData::MythHTTPData()
: QByteArray(),
MythHTTPContent("")
{
}

MythHTTPData::MythHTTPData(QString FileName, const char * Buffer)
: QByteArray(Buffer),
MythHTTPContent(FileName)
{
m_cacheType = HTTPETag;
}

MythHTTPData::MythHTTPData(int Size, char Char)
: QByteArray(Size, Char),
MythHTTPContent("")
{
}

MythHTTPData::MythHTTPData(const QByteArray& Other)
: QByteArray(Other),
MythHTTPContent("")
{
}
28 changes: 28 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpdata.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#ifndef MYTHHTTPDATA_H
#define MYTHHTTPDATA_H

// Qt
#include <QByteArray>

// MythTV
#include "http/mythhttptypes.h"

class MBASE_PUBLIC MythHTTPData : public QByteArray, public MythHTTPContent
{
public:
static HTTPData Create();
static HTTPData Create(const QString& Name, const char * Buffer);
static HTTPData Create(int Size, char Char);
static HTTPData Create(const QByteArray& Other);

protected:
MythHTTPData();
MythHTTPData(QString FileName, const char * Buffer);
MythHTTPData(int Size, char Char);
MythHTTPData(const QByteArray& Other);

private:
Q_DISABLE_COPY(MythHTTPData)
};

#endif
257 changes: 257 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpencoding.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
// MythTV
#include "mythlogging.h"
#include "mythcoreutil.h"
#include "http/mythmimedatabase.h"
#include "http/mythhttpdata.h"
#include "http/mythhttpfile.h"
#include "http/mythhttpresponse.h"
#include "http/mythhttpencoding.h"

#define LOC QString("HTTPEnc: ")

/*! \brief Parse the incoming HTTP 'Accept' header and return an ordered list of preferences.
*
* We retain the original MIME types rather than converting to a MythMimeType
* as this potentially converts an alias type to its parent (i.e. QMimeDatabase
* will always return application/xml for xml types).
*
* \note This will not remove wildcard mime specifiers but they will likely be
* ignored elsewhere.
*
*
*/
using MimePair = std::pair<float,QString>;
QStringList MythHTTPEncoding::GetMimeTypes(const QString &Accept)
{
// Split out mime types
#if QT_VERSION < QT_VERSION_CHECK(5,14,0)
auto types = Accept.split(",", QString::SkipEmptyParts);
#else
auto types = Accept.split(",", Qt::SkipEmptyParts);
#endif

std::vector<MimePair> weightings;
for (const auto & type : types)
{
QString mime = type.trimmed();
auto quality = 1.0F;
// Find any quality value (defaults to 1)
if (auto index = type.lastIndexOf(";"); index > -1)
{
mime = type.mid(0, index).trimmed().toLower();
auto qual = type.mid(index + 1).trimmed();
if (auto index2 = qual.lastIndexOf("="); index2 > -1)
{
bool ok = false;
auto newquality = qual.mid(index2 + 1).toFloat(&ok);
if (ok)
quality = newquality;
}
}
weightings.emplace_back(quality, mime);
}

// Sort the list
auto comp = [](const MimePair& First, const MimePair& Second) { return First.first > Second.first; };
std::sort(weightings.begin(), weightings.end(), comp);

// Build the final result. This will pass through invalid types - which should
// be handled by the consumer (e.g. wildcard specifiers are not handled).
QStringList result;
for (const auto & weight : weightings)
result.append(weight.second);

// Default to xml
if (result.empty())
result.append("application/xml");
return result;
}

/*! \brief Parse the incoming Content-Type header for POST/PUT content
*/
void MythHTTPEncoding::GetContentType(MythHTTPRequest* Request)
{
if (!Request || !Request->m_content.get())
return;

auto contenttype = MythHTTP::GetHeader(Request->m_headers, "content-type");

// type is e.g. text/html; charset=UTF-8 or multipart/form-data; boundary=something
#if QT_VERSION < QT_VERSION_CHECK(5,14,0)
auto types = contenttype.split(";", QString::SkipEmptyParts);
#else
auto types = contenttype.split(";", Qt::SkipEmptyParts);
#endif

if (types.isEmpty())
return;

// Note: This can produce an invalid mime type but there is no sensible fallback
if (auto mime = MythMimeDatabase().MimeTypeForName(types[0].trimmed().toLower()); mime.IsValid())
{
Request->m_content->m_mimeType = mime;
if (mime.Name() == "application/x-www-form-urlencoded")
GetURLEncodedParameters(Request);
}
}

void MythHTTPEncoding::GetURLEncodedParameters(MythHTTPRequest* Request)
{
if (!Request || !Request->m_content.get())
return;

auto payload = QString::fromUtf8(Request->m_content->constData(), Request->m_content->size());

// This looks odd, but it is here to cope with stupid UPnP clients that
// forget to de-escape the URLs. We can't map %26 here as well, as that
// breaks anything that is trying to pass & as part of a name or value.
payload.replace("&amp;", "&");
if (!payload.isEmpty())
{
#if QT_VERSION < QT_VERSION_CHECK(5,14,0)
QStringList params = payload.split('&', QString::SkipEmptyParts);
#else
QStringList params = payload.split('&', Qt::SkipEmptyParts);
#endif
for (const auto & param : qAsConst(params))
{
QString name = param.section('=', 0, 0);
QString value = param.section('=', 1);
value.replace("+", " ");
if (!name.isEmpty())
{
name = QUrl::fromPercentEncoding(name.toUtf8());
value = QUrl::fromPercentEncoding(value.toUtf8());
Request->m_queries.insert(name.trimmed(), value);
}
}
}
}

/*! \brief Return a QMimeType that represents Content.
*/
MythMimeType MythHTTPEncoding::GetMimeType(HTTPVariant Content)
{
auto data = std::get_if<HTTPData>(&Content);
auto file = std::get_if<HTTPFile>(&Content);
if (!(data || file))
return MythMimeType();

QString filename = data ? (*data)->m_fileName : file ? (*file)->m_fileName : "";

// Per docs, this is performant...
auto mimedb = MythMimeDatabase();

// Look for unambiguous mime type
auto types = mimedb.MimeTypesForFileName(filename);
if (types.size() == 1)
return types.front();

// Look for an override. QMimeDatabase gets it wrong sometimes when the result
// is ambiguous and it resorts to probing. Add to this list as necessary
static const std::map<QString,QString> s_mimeOverrides =
{
{ "ts", "video/mp2t"}
};

auto suffix = mimedb.SuffixForFileName(filename);
for (const auto & type : s_mimeOverrides)
if (suffix.compare(type.first, Qt::CaseInsensitive) == 0)
return mimedb.MimeTypeForName(type.second);

// Try interrogating content as well
if (data)
if (auto mime = mimedb.MimeTypeForFileNameAndData(filename, *(*data).get()); mime.IsValid())
return mime;
if (file)
if (auto mime = mimedb.MimeTypeForFileNameAndData(filename, (*file).get()); mime.IsValid())
return mime;

// Default to text/plain (possibly use application/octet-stream as well?)
return mimedb.MimeTypeForName("text/plain");
}

/*! \brief Compress the response content under certain circumstances or mark
* the content as 'chunkable'.
*
* This only supports gzip compression. deflate is simple enough to add using
* qCompress but Qt adds a header and a footer to the result, which must be
* removed. deflate does save a handful of bytes but we don't really need to support both.
*/
MythHTTPEncode MythHTTPEncoding::Compress(MythHTTPResponse* Response, int64_t& Size)
{
auto result = HTTPNoEncode;
if (!Response || !Response->m_requestHeaders)
return result;

// We need something to compress/chunk
auto data = std::get_if<HTTPData>(&Response->m_response);
auto file = std::get_if<HTTPFile>(&Response->m_response);
if (!(data || file))
return result;

// Don't touch range requests. They do not work with compression and there
// is no point in chunking gzipped content as the client still has to wait
// for the entire payload before unzipping
// Note: It is permissible to chunk a range request - but ignore for the
// timebeing to keep the code simple.
if ((data && !(*data)->m_ranges.empty()) || (file && !(*file)->m_ranges.empty()))
return result;

// Has the client actually requested compression
bool wantgzip = MythHTTP::GetHeader(Response->m_requestHeaders, "accept-encoding").toLower().contains("gzip");

// Chunking is HTTP/1.1 only - and must be supported
bool chunkable = Response->m_version == HTTPOneDotOne;
// and restrict to 'chunky' files
bool chunky = Size > 102400; // 100KB

// Don't compress anything that is too large. Under normal circumstances this
// should not be a problem as we only compress text based data - but avoid
// potentially memory hungry compression operations.
// On the flip side, don't compress trivial amounts of data
bool gzipsize = Size > 512 && !chunky; // 0.5KB <-> 100KB

// Only consider compressing text based content. No point in compressing audio,
// video and images.
bool compressable = (data ? (*data)->m_mimeType : (*file)->m_mimeType).Inherits("text/plain");

// Decision time
bool gzip = wantgzip && gzipsize && compressable;
bool chunk = chunkable && chunky;

if (!gzip)
{
// Chunking happens as we write to the socket, so flag it as required
if (chunk)
{
// Disabled for now
result = HTTPChunked;
if (data) (*data)->m_encoding = result;
if (file) (*file)->m_encoding = result;
}
return result;
}

// As far as I can tell, Qt's implicit sharing of data should ensure we aren't
// copying data unnecessarily here - but I can't be sure. We could definitely
// improve compressing files by avoiding the copy into a temporary buffer.
HTTPData buffer = MythHTTPData::Create(data ? gzipCompress(*data->get()) : gzipCompress((*file)->readAll()));

// Add the required header
Response->AddHeader("Content-Encoding", "gzip");

LOG(VB_HTTP, LOG_INFO, LOC + QString("'%1' compressed from %2 to %3 bytes")
.arg(data ? (*data)->m_fileName : (*file)->fileName())
.arg(Size).arg(buffer->size()));

// Copy the filename and last modified, set the new buffer and set the content size
buffer->m_lastModified = data ? (*data)->m_lastModified : (*file)->m_lastModified;
buffer->m_etag = data ? (*data)->m_etag : (*file)->m_etag;
buffer->m_fileName = data ? (*data)->m_fileName : (*file)->m_fileName;
buffer->m_cacheType = data ? (*data)->m_cacheType : (*file)->m_cacheType;
buffer->m_encoding = HTTPGzip;
Response->m_response = buffer;
Size = buffer->size();
return HTTPGzip;
}
19 changes: 19 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpencoding.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#ifndef MYTHHTTPENCODING_H
#define MYTHHTTPENCODING_H

// MythTV
#include "http/mythhttptypes.h"

class MythHTTPEncoding
{
public:
static QStringList GetMimeTypes(const QString& Accept);
static void GetContentType(MythHTTPRequest* Request);
static MythMimeType GetMimeType(HTTPVariant Content);
static MythHTTPEncode Compress(MythHTTPResponse* Response, int64_t& Size);

protected:
static void GetURLEncodedParameters(MythHTTPRequest* Request);
};

#endif
73 changes: 73 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpfile.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Qt
#include <QFile>
#include <QFileInfo>

// MythTV
#include "mythlogging.h"
#include "mythdate.h"
#include "http/mythhttpresponse.h"
#include "http/mythhttprequest.h"
#include "http/mythhttpfile.h"

#define LOC QString("HTTPFile: ")

HTTPFile MythHTTPFile::Create(const QString &ShortName, const QString& FullName)
{
return std::shared_ptr<MythHTTPFile>(new MythHTTPFile(ShortName, FullName));
}

/*! \class MythHTTPfile
* \brief A simple wrapper around QFile
*/

/*! \brief Default constructor
*
* \param ShortName The filename will be shown in the 'Content-Disposition' header
* (i.e. just the name and extension with all path elements removed).
* \param FullName The full path to the file on disk.
*/
MythHTTPFile::MythHTTPFile(const QString& ShortName, const QString& FullName)
: QFile(FullName),
MythHTTPContent(ShortName)
{
}

HTTPResponse MythHTTPFile::ProcessFile(HTTPRequest2 Request)
{
// Build full path
QString file = Request->m_root + Request->m_path + Request->m_fileName;

// Process options requests
auto response = MythHTTPResponse::HandleOptions(Request);
if (response)
return response;

// Ensure the file exists
LOG(VB_HTTP, LOG_INFO, LOC + QString("Looking for '%1'").arg(file));

if (!QFileInfo::exists(file))
{
LOG(VB_GENERAL, LOG_WARNING, LOC + QString("Failed to find '%1'").arg(file));
Request->m_status = HTTPNotFound;
return MythHTTPResponse::ErrorResponse(Request);
}

// Try and open
auto httpfile = MythHTTPFile::Create(Request->m_fileName, file);
if (!httpfile->open(QIODevice::ReadOnly))
{
LOG(VB_GENERAL, LOG_WARNING, LOC + QString("Failed to open '%1'").arg(file));
Request->m_status = HTTPNotFound;
return MythHTTPResponse::ErrorResponse(Request);
}
httpfile->m_lastModified = QFileInfo(file).lastModified();
httpfile->m_cacheType = HTTPLastModified | HTTPLongLife;

LOG(VB_HTTP, LOG_DEBUG, LOC + QString("Last modified: %2")
.arg(MythDate::toString(httpfile->m_lastModified, MythDate::kOverrideUTC | MythDate::kRFC822)));

// Create our response
response = MythHTTPResponse::FileResponse(Request, httpfile);
// Assume static content
return response;
}
24 changes: 24 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpfile.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#ifndef MYTHHTTPFILE_H
#define MYTHHTTPFILE_H

// Qt
#include <QFile>

// MythTV
#include "http/mythhttptypes.h"
#include "http/mythhttpresponse.h"

class MythHTTPFile : public QFile, public MythHTTPContent
{
public:
static HTTPFile Create (const QString& ShortName, const QString& FullName);
static HTTPResponse ProcessFile (HTTPRequest2 Request);

protected:
MythHTTPFile(const QString& ShortName, const QString& FullName);

private:
Q_DISABLE_COPY(MythHTTPFile)
};

#endif
122 changes: 122 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpinstance.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// MythTV
#include "mthread.h"
#include "http/mythhttpinstance.h"
#include "http/mythhttproot.h"
#include "http/mythhttpserver.h"

MythHTTPInstance& MythHTTPInstance::Instance()
{
static MythHTTPInstance s_instance;
return s_instance;
}

MythHTTPInstance::MythHTTPInstance()
: m_httpServer(new MythHTTPServer),
m_httpServerThread(new MThread("HTTPServer"))
{
// We need to register some types and this should always be hit at least once
// before they are needed
qRegisterMetaType<HTTPHandlers>();
qRegisterMetaType<HTTPServices>();
qRegisterMetaType<DataPayload>();
qRegisterMetaType<DataPayloads>();
qRegisterMetaType<StringPayload>();

m_httpServer->moveToThread(m_httpServerThread->qthread());
m_httpServerThread->start();
do { QThread::usleep(50); } while (!m_httpServerThread->qthread()->isRunning());
}

MythHTTPInstance::~MythHTTPInstance()
{
if (m_httpServerThread)
{
m_httpServerThread->quit();
m_httpServerThread->wait();
}
delete m_httpServerThread;
delete m_httpServer;
}

/*! \brief Stop and delete the MythHTTPServer instance.
*
* This should be used to cleanup the HTTP server when the application is terminating.
* To start or stop the server during the application lifetime use EnableHTTPService.
*/
void MythHTTPInstance::StopHTTPService()
{
if (Instance().m_httpServerThread)
{
Instance().m_httpServerThread->quit();
Instance().m_httpServerThread->wait();
delete Instance().m_httpServerThread;
Instance().m_httpServerThread = nullptr;
}

delete Instance().m_httpServer;
Instance().m_httpServer = nullptr;
}

/*! \brief Signals to the MythHTTPServer instance whether to start or stop listening.
*
* The server is not deleted.
*/
void MythHTTPInstance::EnableHTTPService(bool Enable)
{
emit Instance().m_httpServer->EnableHTTP(Enable);
}

/*! \brief Add path(s) for the MythHTTPServer instance to handle.
*
* This is the default implementation. The server will serve any files in the
* given path(s) without limit (i.e. no authorisation etc)
*/
void MythHTTPInstance::AddPaths(const QStringList &Paths)
{
emit Instance().m_httpServer->AddPaths(Paths);
}

/*! \brief Remove path(s) from the MythHTTPServer instance.
*/
void MythHTTPInstance::RemovePaths(const QStringList &Paths)
{
emit Instance().m_httpServer->RemovePaths(Paths);
}

/*! \brief Add function handlers for specific paths.
*
* The functions will provide *simple* dynamic content, returning either a valid response
* to be processed or a null response to indicate that the path was not handled.
*/
void MythHTTPInstance::AddHandlers(const HTTPHandlers &Handlers)
{
emit Instance().m_httpServer->AddHandlers(Handlers);
}

void MythHTTPInstance::RemoveHandlers(const HTTPHandlers &Handlers)
{
emit Instance().m_httpServer->RemoveHandlers(Handlers);
}

void MythHTTPInstance::Addservices(const HTTPServices &Services)
{
emit Instance().m_httpServer->AddServices(Services);
}

void MythHTTPInstance::RemoveServices(const HTTPServices &Services)
{
emit Instance().m_httpServer->RemoveServices(Services);
}

/*! \brief A convenience class to manage the lifetime of a MythHTTPInstance
*/
MythHTTPScopedInstance::MythHTTPScopedInstance(const HTTPHandlers& Handlers)
{
MythHTTPInstance::AddHandlers(Handlers);
MythHTTPInstance::EnableHTTPService();
}

MythHTTPScopedInstance::~MythHTTPScopedInstance()
{
MythHTTPInstance::StopHTTPService();
}
41 changes: 41 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpinstance.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#ifndef MYTHHTTPINSTANCE_H
#define MYTHHTTPINSTANCE_H

// MythTV
#include "http/mythhttpservice.h"
#include "http/mythhttptypes.h"

class MThread;
class MythHTTPServer;

class MBASE_PUBLIC MythHTTPInstance
{
public:
static void EnableHTTPService(bool Enable = true);
static void StopHTTPService ();
static void AddPaths (const QStringList& Paths);
static void RemovePaths (const QStringList& Paths);
static void AddHandlers (const HTTPHandlers& Handlers);
static void RemoveHandlers (const HTTPHandlers& Handlers);
static void Addservices (const HTTPServices& Services);
static void RemoveServices (const HTTPServices& Services);

private:
Q_DISABLE_COPY(MythHTTPInstance)

static MythHTTPInstance& Instance();
MythHTTPInstance();
~MythHTTPInstance();

MythHTTPServer* m_httpServer { nullptr };
MThread* m_httpServerThread { nullptr };
};

class MBASE_PUBLIC MythHTTPScopedInstance
{
public:
explicit MythHTTPScopedInstance(const HTTPHandlers& Handlers);
~MythHTTPScopedInstance();
};

#endif
204 changes: 204 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpmetamethod.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// Qt
#include <QDateTime>
#include <QFileInfo>

// MythTV
#include "mythlogging.h"
#include "http/mythhttpmetamethod.h"

#define LOC QString("MetaMethod: ")

/*! \class MythHTTPMetaMethod
*
* \note Instances of this class SHOULD be initialised statically to improve performance but
* MUST NOT be initialised globally. This is because a number of Qt types (e.g. enums)
* and our own custom types will not have been initialised at that point
* (i.e. qRegisterMetatype<>() has not been called).
*/
MythHTTPMetaMethod::MythHTTPMetaMethod(int Index, QMetaMethod& Method, int RequestTypes,
const QString& ReturnName, bool Slot)
: m_index(Index),
m_requestTypes(RequestTypes),
m_method(Method)
{
// Static list of unsupported return types (cannot be serialised (QHash) or of no use (pointers))
static const std::vector<int> s_invalidTypes =
{
QMetaType::UnknownType, QMetaType::VoidStar, QMetaType::QObjectStar,
QMetaType::QVariantHash, QMetaType::QRect, QMetaType::QRectF,
QMetaType::QSize, QMetaType::QSizeF, QMetaType::QLine,
QMetaType::QLineF, QMetaType::QPoint, QMetaType::QPointF
};

// Static list of unsupported parameters (all invalid types plus extras)
static const std::vector<int> s_invalidParams =
{
QMetaType::UnknownType, QMetaType::VoidStar, QMetaType::QObjectStar,
QMetaType::QVariantHash, QMetaType::QRect, QMetaType::QRectF,
QMetaType::QSize, QMetaType::QSizeF, QMetaType::QLine,
QMetaType::QLineF, QMetaType::QPoint, QMetaType::QPointF,
QMetaType::QVariantMap, QMetaType::QStringList, QMetaType::QVariantList
};

int returntype = Method.returnType();

// Discard methods with an unsupported return type
if (std::any_of(s_invalidTypes.cbegin(), s_invalidTypes.cend(), [&returntype](int Type) { return Type == returntype; }))
{
LOG(VB_HTTP, LOG_ERR, LOC + QString("Method '%1' has unsupported return type '%2'").arg(Method.name().constData(), Method.typeName()));
return;
}

// Warn about complicated methods not supported by QMetaMethod - these will
// fail if all arguments are required and used
if (Method.parameterCount() > (Q_METAMETHOD_INVOKE_MAX_ARGS - 1))
{
LOG(VB_GENERAL, LOG_WARNING, LOC + QString("Method '%1' takes more than %2 parameters; will probably fail")
.arg(Method.name().constData()).arg(Q_METAMETHOD_INVOKE_MAX_ARGS - 1));
}

// Decide on the name of the returned type
if (Slot && ValidReturnType(returntype))
{
// Explicitly set via Q_CLASSINFO (name=XXX)
m_returnTypeName = ReturnName;
if (m_returnTypeName.isEmpty())
{
// If this is a user type, we assume its name should be used, otherwise
// prefer deduction from methodname
if (returntype <= QMetaType::User)
if (QString(Method.name()).startsWith(QStringLiteral("Get"), Qt::CaseInsensitive))
m_returnTypeName = Method.name().mid(3);
// Default to the type name
if (m_returnTypeName.isEmpty())
m_returnTypeName = Method.typeName();
// Duplicated from MythXMLSerialiser
if (m_returnTypeName.startsWith("Q"))
m_returnTypeName = m_returnTypeName.mid(1);
m_returnTypeName.remove("DTC::");
m_returnTypeName.remove(QChar('*'));
}
}

// Add method name and return type
m_types.emplace_back(returntype >= 0 ? returntype : 0);
m_names.emplace_back(Method.name());

// Add type/value for each method parameter
auto names = Method.parameterNames();
auto types = Method.parameterTypes();

// Add type/value for each method parameter
for (int i = 0; i < names.size(); ++i)
{
int type = QMetaType::type(types[i]);

// Discard methods that use unsupported parameter types.
// Note: slots only - these are supportable for signals
if (Slot && std::any_of(s_invalidParams.cbegin(), s_invalidParams.cend(), [&type](int Type) { return type == Type; }))
{
LOG(VB_GENERAL, LOG_ERR, LOC + QString("Method '%1' has unsupported parameter type '%2' (%3)")
.arg(Method.name().constData(), types[i].constData()).arg(type));
return;
}

m_names.emplace_back(names[i]);
m_types.emplace_back(type);
}

m_valid = true;
}

HTTPMethodPtr MythHTTPMetaMethod::Create(int Index, QMetaMethod &Method, int RequestTypes,
const QString &ReturnName, bool Slot)
{
HTTPMethodPtr result =
std::shared_ptr<MythHTTPMetaMethod>(new MythHTTPMetaMethod(Index, Method, RequestTypes, ReturnName, Slot));
if (result->m_valid)
return result;
return nullptr;
}

/*! \brief Populate the QMetaType object referenced by Parameter with Value.
*
* \note In the event that the type is not handled, we return the unaltered object
* which may then contain an undefined value.
*/
void* MythHTTPMetaMethod::CreateParameter(void* Parameter, int Type, const QString& Value)
{
// Enum types
if (auto typeflags = QMetaType::typeFlags(Type); (typeflags & QMetaType::IsEnumeration) == QMetaType::IsEnumeration)
{
// QMetaEnum::keyToValue will return -1 for an unrecognised enumerant, so
// default to -1 for all error cases
int value = -1;
QByteArray type = QMetaType::typeName(Type);
if (int index = type.lastIndexOf("::" ); index > -1)
{
QString enumname = type.mid(index + 2);
if (const auto * metaobject = QMetaType::metaObjectForType(Type); metaobject)
{
int enumindex = metaobject->indexOfEnumerator(enumname.toUtf8());
if (enumindex >= 0)
{
QMetaEnum metaEnum = metaobject->enumerator(enumindex);
value = metaEnum.keyToValue(Value.toUtf8());
}
}
}
*(static_cast<int*>(Parameter)) = value;
return Parameter;
}

switch (Type)
{
case QMetaType::QVariant : *(static_cast<QVariant *>(Parameter)) = QVariant(Value); break;
case QMetaType::Bool : *(static_cast<bool *>(Parameter)) = ToBool(Value ); break;
case QMetaType::Char : *(static_cast<char *>(Parameter)) = (Value.length() > 0) ? Value.at(0).toLatin1() : 0; break;
case QMetaType::UChar : *(static_cast<unsigned char*>(Parameter)) = (Value.length() > 0) ? static_cast<unsigned char>(Value.at(0).toLatin1()) : 0; break;
case QMetaType::QChar : *(static_cast<QChar *>(Parameter)) = (Value.length() > 0) ? Value.at(0) : 0; break;
case QMetaType::Short : *(static_cast<short *>(Parameter)) = Value.toShort(); break;
case QMetaType::UShort : *(static_cast<ushort *>(Parameter)) = Value.toUShort(); break;
case QMetaType::Int : *(static_cast<int *>(Parameter)) = Value.toInt(); break;
case QMetaType::UInt : *(static_cast<uint *>(Parameter)) = Value.toUInt(); break;
case QMetaType::Long : *(static_cast<long *>(Parameter)) = Value.toLong(); break;
case QMetaType::ULong : *(static_cast<ulong *>(Parameter)) = Value.toULong(); break;
case QMetaType::LongLong : *(static_cast<qlonglong *>(Parameter)) = Value.toLongLong(); break;
case QMetaType::ULongLong : *(static_cast<qulonglong *>(Parameter)) = Value.toULongLong(); break;
case QMetaType::Double : *(static_cast<double *>(Parameter)) = Value.toDouble(); break;
case QMetaType::Float : *(static_cast<float *>(Parameter)) = Value.toFloat(); break;
case QMetaType::QString : *(static_cast<QString *>(Parameter)) = Value; break;
case QMetaType::QByteArray: *(static_cast<QByteArray *>(Parameter)) = Value.toUtf8(); break;
case QMetaType::QTime : *(static_cast<QTime *>(Parameter)) = QTime::fromString(Value, Qt::ISODate ); break;
case QMetaType::QDate : *(static_cast<QDate *>(Parameter)) = QDate::fromString(Value, Qt::ISODate ); break;
case QMetaType::QDateTime :
{
QDateTime dt = QDateTime::fromString(Value, Qt::ISODate);
dt.setTimeSpec(Qt::UTC);
*(static_cast<QDateTime*>(Parameter)) = dt;
break;
}
default: break;
}
return Parameter;
}

QVariant MythHTTPMetaMethod::CreateReturnValue(int Type, void* Value)
{
if (!(ValidReturnType(Type)))
return QVariant();

// This assumes any user type will be derived from QObject...
// (Exception for QFileInfo)
if (Type == QMetaType::type("QFileInfo"))
return QVariant::fromValue<QFileInfo>(*(static_cast<QFileInfo*>(Value)));

if (Type > QMetaType::User)
{
QObject* object = *(static_cast<QObject**>(Value));
return QVariant::fromValue<QObject*>(object);
}

return QVariant(Type, Value);
}

58 changes: 58 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpmetamethod.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#ifndef MYTHHTTPMETAMETHOD_H
#define MYTHHTTPMETAMETHOD_H

// Qt
#include <QMetaMethod>

// MythTV
#include "http/mythhttptypes.h"

// Std
#include <memory>

class MythHTTPMetaMethod;
using HTTPMethodPtr = std::shared_ptr<MythHTTPMetaMethod>;
using HTTPMethods = std::map<QString,HTTPMethodPtr>;
using HTTPProperties = std::map<int,int>;

class MBASE_PUBLIC MythHTTPMetaMethod
{
public:
static HTTPMethodPtr Create (int Index, QMetaMethod& Method, int RequestTypes,
const QString& ReturnName = {}, bool Slot = true);
void* CreateParameter (void* Parameter, int Type, const QString& Value);
QVariant CreateReturnValue (int Type, void* Value);

bool m_valid { false };
int m_index { 0 };
int m_requestTypes { HTTPUnknown };
QMetaMethod m_method;
std::vector<QString> m_names;
std::vector<int> m_types;
QString m_returnTypeName;

protected:
MythHTTPMetaMethod(int Index, QMetaMethod& Method, int RequestTypes,
const QString& ReturnName, bool Slot);

private:
Q_DISABLE_COPY(MythHTTPMetaMethod)

static inline bool ValidReturnType(int Type)
{
return (Type != QMetaType::UnknownType && Type != QMetaType::Void);
}

static inline bool ToBool(const QString& Value)
{
if (Value.compare("1", Qt::CaseInsensitive) == 0)
return true;
if (Value.compare("y", Qt::CaseInsensitive) == 0)
return true;
if (Value.compare("true", Qt::CaseInsensitive) == 0)
return true;
return false;
}
};

#endif
165 changes: 165 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpmetaservice.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Qt
#include <QMetaClassInfo>

// MythTV
#include "mythlogging.h"
#include "http/mythhttpmetaservice.h"
#include "http/mythhttptypes.h"

/*! \class MythHTTPMetaService
*
* \note Instances of this class SHOULD be initialised statically to improve performance but
* MUST NOT be initialised globally. This is because a number of Qt types (e.g. enums)
* and our own custom types will not have been initialised at that point
* (i.e. qRegisterMetatype<>() has not been called).
*/
MythHTTPMetaService::MythHTTPMetaService(const QString& Name, const QMetaObject& Meta,
HTTPRegisterTypes RegisterCallback,
const QString& MethodsToHide)
: m_meta(Meta),
m_name(Name)
{
// Register any types
if (RegisterCallback != nullptr)
std::invoke(RegisterCallback);

// Static list of signals and slots to avoid
static QString s_defaultHide = { "destroyed,deleteLater,objectNameChanged,RegisterCustomTypes" };

// Retrieve version
auto index = Meta.indexOfClassInfo("Version");
if (index > -1)
m_version = Meta.classInfo(index).value();
else
LOG(VB_GENERAL, LOG_WARNING, QStringLiteral("Service '%1' is missing version information").arg(Name));

// Build complete list of meta objects for class hierarchy
QList<const QMetaObject*> metas;
metas.append(&Meta);
const QMetaObject* super = Meta.superClass();
while (super)
{
metas.append(super);
super = super->superClass();
}

QString hide = s_defaultHide + MethodsToHide;

// Pull out public signals and slots, ignoring 'MethodsToHide'
for (const auto * meta : metas)
{
for (int i = 0; i < meta->methodCount(); ++i)
{
QMetaMethod method = meta->method(i);

// We want only public methods
if (QMetaMethod::Public != method.access())
continue;

// Filtered unwanted
QString name(method.methodSignature());
name = name.section('(', 0, 0);
if (hide.contains(name))
continue;

auto RemoveExisting = [](HTTPMethods& Methods, const HTTPMethodPtr Method, const QString& Search)
{
for (const auto & [_name, _method] : Methods)
{
if ((_name == Search) && (_method->m_method.methodSignature() == Method->m_method.methodSignature()) &&
(_method->m_method.returnType() == Method->m_method.returnType()))
{
Methods.erase(_name);
break;
}
}
};

// Signals
if (QMetaMethod::Signal == method.methodType())
{
auto newmethod = MythHTTPMetaMethod::Create(i, method, HTTPUnknown, {}, false);
if (newmethod)
{
RemoveExisting(m_signals, newmethod, name);
m_signals.emplace(name, newmethod);
}
}
// Slots
else if (QMetaMethod::Slot == method.methodType())
{
QString returnname;
int types = ParseRequestTypes(Meta, name, returnname);
if (types != HTTPUnknown)
{
auto newmethod = MythHTTPMetaMethod::Create(i, method, types, returnname);
if (newmethod)
{
RemoveExisting(m_slots, newmethod, name);
m_slots.emplace(name, newmethod);
}
}
}
}
}

int constpropertyindex = -1;
for (const auto * meta : metas)
{
for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i)
{
QMetaProperty property = meta->property(i);
QString propertyname(property.name());

if (propertyname != QString("objectName") && property.isReadable() &&
((property.hasNotifySignal() && property.notifySignalIndex() > -1) || property.isConstant()))
{
// constant properties are given a signal index < 0
if (property.notifySignalIndex() > -1)
m_properties.emplace(property.notifySignalIndex(), property.propertyIndex());
else
m_properties.emplace(constpropertyindex--, property.propertyIndex());
}
}
}
LOG(VB_GENERAL, LOG_INFO, QString("Service '%1' introspection complete").arg(Name));
}

int MythHTTPMetaService::ParseRequestTypes(const QMetaObject& Meta, const QString& Method, QString& ReturnName)
{
int custom = HTTPUnknown;
int index = Meta.indexOfClassInfo(Method.toLatin1());
if (index > -1)
{
QStringList infos = QString(Meta.classInfo(index).value()).split(';', QString::SkipEmptyParts);
foreach (const QString &info, infos)
{
if (info.startsWith(QStringLiteral("methods=")))
custom |= MythHTTP::RequestsFromString(info.mid(8));
if (info.startsWith(QStringLiteral("name=")))
ReturnName = info.mid(5).trimmed();
}
}

// determine allowed request types
int allowed = HTTPOptions;
if (custom != HTTPUnknown)
{
allowed |= custom;
}
else if (Method.startsWith(QStringLiteral("Get"), Qt::CaseInsensitive))
{
allowed |= HTTPGet | HTTPHead;
}
else if (Method.startsWith(QStringLiteral("Set"), Qt::CaseInsensitive))
{
// Put or Post?
allowed |= HTTPPost;
}
else
{
LOG(VB_GENERAL, LOG_ERR, QString("Failed to get request types for method '%1'- ignoring").arg(Method));
return HTTPUnknown;
}
return allowed;
}
30 changes: 30 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpmetaservice.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#ifndef MYTHHTTPMETASERVICE_H
#define MYTHHTTPMETASERVICE_H

// Qt
#include <QMetaObject>

// MythTV
#include "http/mythhttpmetamethod.h"

class MBASE_PUBLIC MythHTTPMetaService
{
public:
MythHTTPMetaService(const QString& Name, const QMetaObject& Meta,
HTTPRegisterTypes RegisterCallback = nullptr,
const QString& MethodsToHide = {});

static int ParseRequestTypes(const QMetaObject& Meta, const QString& Method, QString& ReturnName);

const QMetaObject& m_meta;
QString m_name;
QString m_version;
HTTPMethods m_signals;
HTTPMethods m_slots;
HTTPProperties m_properties;

private:
Q_DISABLE_COPY(MythHTTPMetaService)
};

#endif
143 changes: 143 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpparser.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Qt
#include <QTcpSocket>

// MythTV
#include "mythlogging.h"
#include "http/mythhttpdata.h"
#include "http/mythhttprequest.h"
#include "http/mythhttpparser.h"

#define LOC QString("HTTPParser: ")

HTTPRequest2 MythHTTPParser::GetRequest(const MythHTTPConfig& Config, QTcpSocket* Socket)
{
// Debug
if (VERBOSE_LEVEL_CHECK(VB_HTTP, LOG_DEBUG))
{
LOG(VB_HTTP, LOG_DEBUG, m_method);
for (auto it = m_headers->cbegin(); it != m_headers->cend(); ++it)
LOG(VB_HTTP, LOG_DEBUG, it.key() + ": " + it.value());
if (m_content.get())
LOG(VB_HTTP, LOG_DEBUG, QString("Content:\r\n") + m_content->constData());
}

// Build the request
auto result = std::make_shared<MythHTTPRequest>(Config, m_method, m_headers, m_content, Socket);

// Reset our internal state
m_started = false;
m_linesRead = 0;
m_headersComplete = false;
m_method.clear();
m_contentLength = 0;
m_headers = std::make_shared<HTTPMap>();
m_content = nullptr;

return result;
}

bool MythHTTPParser::Read(QTcpSocket* Socket, bool& Ready)
{
Ready = false;

// Fail early
if (!Socket || (Socket->state() != QAbstractSocket::ConnectedState))
return false;

// Sanity check the number of headers
if (!m_headersComplete && m_linesRead > 200)
{
LOG(VB_GENERAL, LOG_WARNING, LOC + QString("Read %1 headers - aborting").arg(m_linesRead));
return false;
}

// Filter out non HTTP content quickly. This assumes server side only.
if (!m_started && Socket->bytesAvailable() > 2)
{
QByteArray buf(3, '\0');
if (Socket->peek(buf.data(), 3) == 3)
{
static const std::vector<const char *> s_starters = { "GET", "PUT", "POS", "OPT", "HEA", "DEL" };
if (!std::any_of(s_starters.cbegin(), s_starters.cend(), [&](const char * Starter)
{ return strcmp(Starter, buf.data()) == 0; }))
{
LOG(VB_GENERAL, LOG_WARNING, LOC + QString("Invalid HTTP request start '%1' - quitting").arg(buf.constData()));
return false;
}
}
}

// Read headers
while (!m_headersComplete && Socket->canReadLine())
{
QByteArray line = Socket->readLine().trimmed();
m_linesRead++;

// A large header suggests an error
if (line.size() > 1000)
{
LOG(VB_GENERAL, LOG_WARNING, LOC + "Unusually long header - quitting");
return false;
}

if (line.isEmpty())
{
m_headersComplete = true;
break;
}

if (m_started)
{
int index = line.indexOf(":");
if (index > 0)
{
QByteArray key = line.left(index).trimmed();
QByteArray value = line.mid(index + 1).trimmed();
if (key == "Content-Length")
m_contentLength = value.toLongLong();
m_headers->insert(key.toLower(), value);
}
else
{
LOG(VB_GENERAL, LOG_WARNING, LOC + QString("Invalid header: '%1'").arg(line.constData()));
return false;
}
}
else
{
m_started = true;
m_method = line;
}
}

// Need more data...
if (!m_headersComplete)
return true;

// No body?
if (m_contentLength < 1)
{
Ready = true;
return true;
}

// Create a buffer for the content if needed
if (!m_content)
m_content = MythHTTPData::Create();

// Read contents
while ((Socket->state() == QAbstractSocket::ConnectedState) && Socket->bytesAvailable() &&
(static_cast<int64_t>(m_content->size()) < m_contentLength))
{
int64_t want = m_contentLength - m_content->size();
int64_t have = Socket->bytesAvailable();
m_content->append(Socket->read(qMax(want, qMax(HTTP_CHUNKSIZE, have))));
}

// Need more data...
if (static_cast<int64_t>(m_content->size()) < m_contentLength)
return true;

Ready = true;
return true;
}
30 changes: 30 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpparser.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#ifndef MYTHHTTPPARSER_H
#define MYTHHTTPPARSER_H

// MythTV
#include "http/mythhttptypes.h"

class QTcpSocket;

#define HTTP_CHUNKSIZE 65536L // 64k

class MythHTTPParser
{
public:
MythHTTPParser() = default;
bool Read(QTcpSocket* Socket, bool& Ready);
HTTPRequest2 GetRequest(const MythHTTPConfig& Config, QTcpSocket* Socket);

private:
Q_DISABLE_COPY(MythHTTPParser)

bool m_started { false };
int m_linesRead { 0 };
bool m_headersComplete { false };
QString m_method;
HTTPHeaders m_headers { std::make_shared<HTTPMap>() };
int64_t m_contentLength { 0 };
HTTPData m_content { nullptr };
};

#endif
297 changes: 297 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpranges.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
// MythTV
#include "mythlogging.h"
#include "http/mythhttpranges.h"
#include "http/mythhttpdata.h"
#include "http/mythhttpfile.h"
#include "http/mythhttpresponse.h"

#define LOC QString("HTTPRange: ")

auto sumrange = [](uint64_t Cum, const HTTPRange& Range) { return ((Range.second + 1) - Range.first) + Cum; };

void MythHTTPRanges::HandleRangeRequest(MythHTTPResponse* Response, const QString& Request)
{
if (!Response || Request.isEmpty())
return;

// Check content type and size first
auto data = std::get_if<HTTPData>(&(Response->m_response));
auto file = std::get_if<HTTPFile>(&(Response->m_response));
int64_t size = data ? (*data)->size() : file ? (*file)->size() : 0;
if (size < 1)
return;

// Parse
HTTPRanges& ranges = data ? (*data)->m_ranges : (*file)->m_ranges;
int64_t& partialsize = data ? (*data)->m_partialSize : (*file)->m_partialSize;
MythHTTPStatus status = MythHTTPRanges::ParseRanges(Request, size, ranges, partialsize);
if ((status == HTTPRequestedRangeNotSatisfiable) || (status == HTTPPartialContent))
Response->m_status = status;
}

void MythHTTPRanges::BuildMultipartHeaders(MythHTTPResponse* Response)
{
if (!Response || (Response->m_status != HTTPPartialContent))
return;

auto data = std::get_if<HTTPData>(&(Response->m_response));
auto file = std::get_if<HTTPFile>(&(Response->m_response));
int64_t size = data ? (*data)->size() : file ? (*file)->size() : 0;
if (size < 1)
return;

HTTPRanges& ranges = data ? (*data)->m_ranges : (*file)->m_ranges;
if (ranges.size() < 2)
return;

auto & mime = data ? (*data)->m_mimeType : (*file)->m_mimeType;
HTTPContents headers;
for (auto & range : ranges)
{
auto header = QString("\r\n--%1\r\nContent-Type: %2\r\nContent-Range: %3\r\n\r\n")
.arg(s_multipartBoundary).arg(MythHTTP::GetContentType(mime))
.arg(MythHTTPRanges::GetRangeHeader(range, size));
headers.emplace_back(MythHTTPData::Create(qPrintable(header)));

}
headers.emplace_back(MythHTTPData::Create(qPrintable(QString("\r\n--%1--")
.arg(s_multipartBoundary))));
std::reverse(headers.begin(), headers.end());
int64_t headersize = 0;
for (auto & header : headers)
headersize += header->size();
if (data)
{
(*data)->m_multipartHeaders = headers;
(*data)->m_multipartHeaderSize = headersize;
}
if (file)
{
(*file)->m_multipartHeaders = headers;
(*file)->m_multipartHeaderSize = headersize;
}
}

QString MythHTTPRanges::GetRangeHeader(HTTPRange& Range, int64_t Size)
{
return QString("bytes %1-%2/%3").arg(Range.first).arg(Range.second).arg(Size);
}

QString MythHTTPRanges::GetRangeHeader(HTTPRanges& Ranges, int64_t Size)
{
if (Ranges.empty())
return "ErRoR";
if (Ranges.size() == 1)
return MythHTTPRanges::GetRangeHeader(Ranges[0], Size);
return "multipart/byteranges; boundary=" + s_multipartBoundary;
}

HTTPMulti MythHTTPRanges::HandleRangeWrite(HTTPVariant Data, int64_t Available, int64_t &ToWrite, int64_t &Offset)
{
HTTPMulti result { nullptr, nullptr };
auto data = std::get_if<HTTPData>(&Data);
auto file = std::get_if<HTTPFile>(&Data);
if (!(data || file))
return result;

int64_t partialsize = data ? (*data)->m_partialSize : (*file)->m_partialSize;
uint64_t written = static_cast<uint64_t>(data ? (*data)->m_written : (*file)->m_written);
HTTPRanges& ranges = data ? (*data)->m_ranges : (*file)->m_ranges;
HTTPContents& headers = data ? (*data)->m_multipartHeaders : (*file)->m_multipartHeaders;

uint64_t oldsum = 0;
for (size_t i = 0; i < ranges.size(); ++i)
{
uint64_t newsum = sumrange(oldsum, ranges[i]);
if (oldsum <= written && written < newsum)
{
// This is the start of a multipart range. Add the start headers.
if ((oldsum == written) && !headers.empty())
{
result.first = headers.back();
headers.pop_back();
}

// Restrict the write to the remainder of this range if necessary
ToWrite = std::min(Available, static_cast<int64_t>(newsum - written));

// We need to ensure we send from the correct offset in the data
Offset = static_cast<int64_t>(ranges[i].first - oldsum);
if (file)
Offset += written;

// This is the end of the multipart ranges. Add the closing header
// (We add this first so we can pop the contents when sending the headers)
if (((static_cast<int64_t>(written) + ToWrite) >= partialsize) && !headers.empty())
{
result.second = headers.back();
headers.pop_back();
}

return result;
}
oldsum = newsum;
}
return result;
}

/*! \brief Parse a range request header
*
* \note If we fail to parse the header, we return HTTP 200 and the range request is
* effectively ignored. We return HTTP 416 Requested Range Not Satisfiable where it
* is parsed but is invalid. If parsed and valid, we return HTTP 206 (Partial Content).
*
* \todo Per the specs, sending complicated range requests could potentially be used
* as part of a (D)DOS attack and it is suggested that various complicated requests
* are rejected. Probably unnecessary here.
*
*/
MythHTTPStatus MythHTTPRanges::ParseRanges(const QString& Request, int64_t TotalSize,
HTTPRanges& Ranges, int64_t& PartialSize)
{
MythHTTPStatus result = HTTPOK;
Ranges.clear();

// Just don't...
if (TotalSize < 2)
return result;

LOG(VB_HTTP, LOG_DEBUG, LOC + QString("Parsing: '%1'").arg(Request));

// Split unit and range(s)
QStringList initial = Request.toLower().split("=");
if (initial.size() != 2)
{
LOG(VB_HTTP, LOG_INFO, LOC + QString("Failed to parse ranges: '%1'").arg(Request));
return result;
}

// We only recognise bytes
if (!initial.at(0).contains("bytes"))
{
LOG(VB_HTTP, LOG_INFO, LOC + QString("Unkown range units: '%1'").arg(initial.at(0)));
return result;
}

// Split out individual ranges
#if QT_VERSION < QT_VERSION_CHECK(5,14,0)
QStringList rangelist = initial.at(1).split(",", QString::SkipEmptyParts);
#else
QStringList rangelist = initial.at(1).split(",", Qt::SkipEmptyParts);
#endif

// No ranges
if (rangelist.isEmpty())
{
LOG(VB_HTTP, LOG_INFO, LOC + QString("Failed to find ranges: '%1'").arg(initial.at(1)));
return result;
}

// Iterate over items
HTTPRanges ranges;
for (auto & range : rangelist)
{
QStringList parts = range.split("-");
if (parts.size() != 2)
{
LOG(VB_HTTP, LOG_INFO, LOC + QString("Failed to parse range: '%1'").arg(range));
return result;
}

bool validrange = false;
bool startvalid = false;
bool endvalid = false;
bool startstr = !parts.at(0).isEmpty();
bool endstr = !parts.at(1).isEmpty();
uint64_t start = parts.at(0).toULongLong(&startvalid);
uint64_t end = parts.at(1).toULongLong(&endvalid);

// Regular range
if (startstr && endstr && startvalid && endvalid)
{
validrange = ((end < static_cast<uint64_t>(TotalSize)) && (start <= end));
}
// Start only
else if (startstr && startvalid)
{
end = static_cast<uint64_t>(TotalSize - 1);
validrange = start <= end;
}
// End only
else if (endstr && endvalid)
{
uint64_t size = end;
end = static_cast<uint64_t>(TotalSize) - 1;
start = static_cast<uint64_t>(TotalSize) - size;
validrange = start <= end;
}

if (!validrange)
{
LOG(VB_HTTP, LOG_INFO, LOC + QString("Invalid HTTP range: '%1'").arg(range));
return HTTPRequestedRangeNotSatisfiable;
}

ranges.emplace_back(start, end);
}

// Rationalise so that we have the simplest, most efficient list of ranges:
// - sort
// - merge overlaps (also allowing for minimum multipart header overhead)
// - remove duplicates
static const int s_overhead = 90; // rough worst case overhead per part for multipart requests
if (ranges.size() > 1)
{
auto equals = [](const HTTPRange& First, const HTTPRange& Second)
{ return (First.first == Second.first) && (First.second == Second.second); };
auto lessthan = [](const HTTPRange& First, const HTTPRange& Second)
{ return First.first < Second.first; };

// we MUST sort first
std::sort(ranges.begin(), ranges.end(), lessthan);

if (VERBOSE_LEVEL_CHECK(VB_HTTP, LOG_INFO))
{
QStringList debug;
for (const auto & range : ranges)
debug.append(QString("%1:%2").arg(range.first).arg(range.second));
LOG(VB_HTTP, LOG_INFO, LOC + QString("Sorted ranges: %1").arg(debug.join(" ")));
}

// merge, de-duplicate, repeat...
bool finished = false;
while (!finished)
{
finished = true;
for (uint i = 0; i < (ranges.size() - 1); ++i)
{
if ((ranges[i].second + s_overhead) >= ranges[i + 1].first)
{
finished = false;
ranges[i + 1].first = ranges[i].first;
// N.B we have sorted by start byte - not end
uint64_t end = std::max(ranges[i].second, ranges[i + 1].second);
ranges[i].second = ranges[i + 1].second = end;
}
}

auto last = std::unique(ranges.begin(), ranges.end(), equals);
ranges.erase(last, ranges.end());
}
}

// Sum the expected number of bytes to be sent
PartialSize = std::accumulate(ranges.cbegin(), ranges.cend(), 0, sumrange);
Ranges = ranges;

if (VERBOSE_LEVEL_CHECK(VB_HTTP, LOG_INFO))
{
QStringList debug;
for (const auto & range : ranges)
debug.append(QString("%1:%2").arg(range.first).arg(range.second));
LOG(VB_HTTP, LOG_INFO, LOC + QString("Final ranges : %1").arg(debug.join(" ")));
LOG(VB_HTTP, LOG_INFO, LOC + QString("Bytes to send: %1").arg(PartialSize));
}

return HTTPPartialContent;
}
26 changes: 26 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpranges.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#ifndef MYTHHTTPRANGES_H
#define MYTHHTTPRANGES_H

// MythTV
#include "http/mythhttptypes.h"
#include "http/mythhttpdata.h"
#include "http/mythhttpresponse.h"

using HTTPRange = std::pair<uint64_t,uint64_t>;
using HTTPRanges = std::vector<HTTPRange>;

class MythHTTPRanges
{
public:
static void HandleRangeRequest (MythHTTPResponse* Response, const QString& Request);
static void BuildMultipartHeaders(MythHTTPResponse* Response);
static QString GetRangeHeader (HTTPRanges& Ranges, int64_t Size);
static QString GetRangeHeader (HTTPRange& Range, int64_t Size);
static HTTPMulti HandleRangeWrite (HTTPVariant Data, int64_t Available, int64_t& ToWrite, int64_t& Offset);

protected:
static MythHTTPStatus ParseRanges (const QString& Request, int64_t TotalSize,
HTTPRanges& Ranges, int64_t& PartialSize);
};

#endif
159 changes: 159 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttprequest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Qt
#include <QHash>
#include <QString>
#include <QTcpSocket>

// MythTV
#include "mythlogging.h"
#include "http/mythhttpdata.h"
#include "http/mythhttpencoding.h"
#include "http/mythhttprequest.h"

#define LOC QString("HTTPParse: ")

/*! \class MythHTTPRequest
* \brief Limited parsing of HTTP method and some headers to determine validity of request.
*
* The aim here is to parse the minimum amount of data to determine whether the
* request should be processed and, if not, set enough state to send the appropriate
* error response.
*
* \note If parsing fails early, the connection type remains at 'close' and hence
* the socket will be closed once the error response is sent.
*/
MythHTTPRequest::MythHTTPRequest(const MythHTTPConfig& Config, const QString &Method,
HTTPHeaders Headers, HTTPData Content, QTcpSocket* Socket /*=nullptr*/)
: m_serverName(Config.m_serverName),
m_method(std::move(Method)),
m_headers(Headers),
m_content(Content),
m_root(Config.m_rootDir),
m_timeout(Config.m_timeout)
{
// TODO is the simplified() call here always safe?
#if QT_VERSION < QT_VERSION_CHECK(5,14,0)
QStringList tokens = m_method.simplified().split(' ', QString::SkipEmptyParts);
#else
QStringList tokens = m_method.simplified().split(' ', Qt::SkipEmptyParts);
#endif

// Validation
// Must have verb and url and optional version
if (tokens.size() < 2 || tokens.size() > 3)
{
LOG(VB_GENERAL, LOG_WARNING, LOC + "Failed to parse HTTP method");
return;
}

m_type = MythHTTP::RequestFromString(tokens[0]);
m_rawURL = tokens[1].toUtf8();
m_url = QUrl::fromPercentEncoding(m_rawURL);

// If no version, assume HTTP/1.1
m_version = HTTPOneDotOne;
if (tokens.size() > 2)
m_version = MythHTTP::VersionFromString(tokens[2]);

// Unknown HTTP version
if (m_version == HTTPUnknownVersion)
{
LOG(VB_GENERAL, LOG_WARNING, LOC + "Unknown HTTP version");
m_version = HTTPOneDotOne;
return;
}

// Unknown request type
if (m_type == HTTPUnknown)
{
LOG(VB_GENERAL, LOG_WARNING, LOC + "Unknown HTTP request");
return;
}

// HTTP/1.1 requires the HOST header - even if empty.
bool havehost = m_headers->contains("host");
if ((m_version == HTTPOneDotOne) && !havehost)
{
LOG(VB_GENERAL, LOG_WARNING, LOC + "No host header for HTTP/1.1");
return;
}

// Multiple host headers are also forbidden - assume for any version not just 1/1
if (havehost && m_headers->count("host") > 1)
{
LOG(VB_GENERAL, LOG_WARNING, LOC + "Multiple 'Host' headers forbidden");
return;
}

// If a host is provided, ensure we recognise it. This may be over zealous:)
if (havehost)
{
// N.B. host port is optional - but our host list has both versions
QString host = MythHTTP::GetHeader(m_headers, "host").toLower();
if (!Config.m_hosts.contains(host))
{
LOG(VB_GENERAL, LOG_WARNING, LOC + QString("Invalid 'Host' header. '%1' not recognised")
.arg(host));
return;
}

// TODO Ensure the host address has a port - as below when we add one manually
}
else if (Socket)
{
// Use the socket address to add a host address. This just ensures the
// response always has a valid address for this thread/socket that can be used
// when building a (somewhat dynamic) response.
QHostAddress host = Socket->localAddress();
m_headers->insert("host", QString("%1:%2").arg(MythHTTP::AddressToString(host)).arg(Socket->localPort()));
}

// Need a valid URL
if (!m_url.isValid())
{
LOG(VB_GENERAL, LOG_WARNING, LOC + QString("Invalid URL: '%1'").arg(m_url.toString()));
return;
}

// Parse the URL into its useful components (path/filename) - queries later
m_path = m_url.toString(QUrl::RemoveFilename | QUrl::RemoveFragment | QUrl::RemoveQuery);
m_fileName = m_url.fileName();

// Parse the connection header
// HTTP/1.1 default to KeepAlive, HTTP/1.0 default to close
// HTTP/0.9 is unlikely but assume KeepAlive
m_connection = (m_version == HTTPOneDotZero) ? HTTPConnectionClose : HTTPConnectionKeepAlive;
auto connection = MythHTTP::GetHeader(m_headers, "connection").toLower();
if (connection.contains(QStringLiteral("keep-alive")))
m_connection = HTTPConnectionKeepAlive;
else if (connection.contains(QStringLiteral("close")))
m_connection = HTTPConnectionClose;

// Parse the content type if present - and pull out any form data
if (m_content.get() && m_content->size() && ((m_type == HTTPPut) || (m_type == HTTPPost)))
MythHTTPEncoding::GetContentType(this);

// Only parse queries if we do not have form data
if (m_queries.isEmpty() && m_url.hasQuery())
m_queries = ParseQuery(m_url.query());

m_status = HTTPOK;
}

HTTPQueries MythHTTPRequest::ParseQuery(const QString &Query)
{
HTTPQueries result;
#if QT_VERSION < QT_VERSION_CHECK(5,14,0)
QStringList params = Query.split('&', QString::SkipEmptyParts);
#else
QStringList params = Query.split('&', Qt::SkipEmptyParts);
#endif
for (const auto & param : qAsConst(params))
{
QString key = param.section('=', 0, 0);
QString value = param.section('=', 1);
value.replace("+", " ");
if (!key.isEmpty())
result.insert(key.trimmed(), value);
}
return result;
}
41 changes: 41 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttprequest.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#ifndef MYTHHTTPREQUEST_H
#define MYTHHTTPREQUEST_H

// Qt
#include <QUrl>

// MythTV
#include "http/mythhttptypes.h"

class QTcpSocket;

class MBASE_PUBLIC MythHTTPRequest
{
public:
MythHTTPRequest(const MythHTTPConfig& Config, const QString& Method,
HTTPHeaders Headers, HTTPData Content, QTcpSocket* Socket = nullptr);

QString m_serverName;
QString m_method;
HTTPHeaders m_headers { nullptr };
HTTPData m_content { nullptr };

MythHTTPStatus m_status { HTTPBadRequest };
QByteArray m_rawURL;
QUrl m_url;
QString m_root;
QString m_path;
QString m_fileName;
HTTPQueries m_queries;
MythHTTPVersion m_version { HTTPUnknownVersion };
MythHTTPRequestType m_type { HTTPUnknown };
MythHTTPConnection m_connection { HTTPConnectionClose };
std::chrono::milliseconds m_timeout { HTTP_SOCKET_TIMEOUT_MS };
int m_allowed { HTTP_DEFAULT_ALLOWED };

private:
Q_DISABLE_COPY(MythHTTPRequest)
static HTTPQueries ParseQuery(const QString& Query);
};

#endif
553 changes: 553 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpresponse.cpp

Large diffs are not rendered by default.

54 changes: 54 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpresponse.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#ifndef MYTHHTTPRESPONSE_H
#define MYTHHTTPRESPONSE_H

// MythTV
#include "http/mythhttpfile.h"
#include "http/mythwebsockettypes.h"
#include "http/mythhttptypes.h"
#include "http/mythhttpdata.h"
#include "http/mythhttprequest.h"

// Std
#include <vector>

class QTcpSocket;

class MBASE_PUBLIC MythHTTPResponse
{
public:
MythHTTPResponse() = default;
explicit MythHTTPResponse(const HTTPRequest2 Request);

static HTTPResponse HandleOptions (HTTPRequest2 Request);
static HTTPResponse ErrorResponse (MythHTTPStatus Status, const QString& ServerName);
static HTTPResponse RedirectionResponse (HTTPRequest2 Request, const QString& Redirect);
static HTTPResponse ErrorResponse (HTTPRequest2 Request, const QString& Message = {});
static HTTPResponse OptionsResponse (HTTPRequest2 Request);
static HTTPResponse DataResponse (HTTPRequest2 Request, HTTPData Data);
static HTTPResponse FileResponse (HTTPRequest2 Request, HTTPFile File);
static HTTPResponse EmptyResponse (HTTPRequest2 Request);
static HTTPResponse UpgradeResponse (HTTPRequest2 Request, MythSocketProtocol& Protocol, bool& Testing);

void Finalise (const MythHTTPConfig& Config);
void AddHeader (const QString& Key, const QString& Value);

QString m_serverName;
MythHTTPVersion m_version { HTTPOneDotOne };
MythHTTPConnection m_connection { HTTPConnectionClose };
std::chrono::milliseconds m_timeout { HTTP_SOCKET_TIMEOUT_MS };
MythHTTPStatus m_status { HTTPBadRequest };
MythHTTPRequestType m_requestType { HTTPGet };
int m_allowed { HTTP_DEFAULT_ALLOWED };
HTTPHeaders m_requestHeaders { nullptr };
HTTPContents m_responseHeaders { };
HTTPVariant m_response { std::monostate() };

protected:
void AddDefaultHeaders();
void AddContentHeaders();

private:
Q_DISABLE_COPY(MythHTTPResponse)
};

#endif
57 changes: 57 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttproot.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// MythTV
#include "mythdirs.h"
#include "http/mythhttproot.h"
#include "http/mythhttpdata.h"
#include "http/mythhttpfile.h"
#include "http/mythhttprequest.h"
#include "http/mythhttpresponse.h"

#define INDEX QStringLiteral("index.html")

/*! \brief A convenience method to seemlessly redirect requests for index.html to
* a context specific file.
*
* e.g. assuming 'mythfrontend.html' points to a valid file:
*
* \code{.cpp}
* auto frontend = [](HTTPRequest2 Request, const QString& Root)
* {
* return MythHTTPRoot::RedirectRoot(Request, Root, "mythfrontend.html");
* };
* MythHTTPService::AddHandlers( {{"/", frontend }});
* \endcode
*/
HTTPResponse MythHTTPRoot::RedirectRoot(HTTPRequest2 Request, const QString &File)
{
auto result = static_cast<HTTPResponse>(nullptr);
if (!Request)
return result;

// this is the top level handler. We deal with the empty root request
// and index.html
if (Request->m_fileName.isEmpty())
Request->m_fileName = INDEX;
if (Request->m_fileName != INDEX)
return result;
Request->m_allowed = HTTP_DEFAULT_ALLOWED | HTTPPut | HTTPDelete | HTTPPost;

result = MythHTTPResponse::HandleOptions(Request);
if (result)
return result;

if (!File.isEmpty())
{
Request->m_fileName = File;
result = MythHTTPFile::ProcessFile(Request);
// Rename the file
if (auto file = std::get_if<HTTPFile>(&result->m_response))
(*file)->m_fileName = INDEX;
}
else
{
auto data = MythHTTPData::Create("index.html", s_defaultHTTPPage.arg("MythTV").toUtf8().constData());
result = MythHTTPResponse::DataResponse(Request, data);
}
return result;

}
13 changes: 13 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttproot.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#ifndef MYTHHTTPROOT_H
#define MYTHHTTPROOT_H

// MythTV
#include "http/mythhttprequest.h"

class MBASE_PUBLIC MythHTTPRoot
{
public:
static HTTPResponse RedirectRoot(HTTPRequest2 Request, const QString& File);
};

#endif
141 changes: 141 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttps.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Qt
#include <QFile>

// MythTV
#include "mythlogging.h"
#include "mythcorecontext.h"
#include "mythdirs.h"
#include "mythhttps.h"

#define LOC QString("SSL: ")

#ifndef QT_NO_OPENSSL
bool MythHTTPS::InitSSLServer(QSslConfiguration& Config)
{
if (!QSslSocket::supportsSsl())
return false;

LOG(VB_HTTP, LOG_INFO, LOC + QSslSocket::sslLibraryVersionString());

Config = QSslConfiguration::defaultConfiguration();
Config.setProtocol(QSsl::SecureProtocols); // Includes SSLv3 which is insecure, but can't be helped
Config.setSslOption(QSsl::SslOptionDisableLegacyRenegotiation, true); // Potential DoS multiplier
Config.setSslOption(QSsl::SslOptionDisableCompression, true); // CRIME attack

auto availableCiphers = QSslConfiguration::supportedCiphers();
QList<QSslCipher> secureCiphers;
for (auto it = availableCiphers.begin(); it != availableCiphers.end(); ++it)
{
// Remove weak ciphers from the cipher list
if ((*it).usedBits() < 128)
continue;

if ((*it).name().startsWith("RC4") || // Weak cipher
(*it).name().startsWith("EXP") || // Weak authentication
(*it).name().startsWith("ADH") || // No authentication
(*it).name().contains("NULL")) // No encryption
continue;

secureCiphers.append(*it);
}
Config.setCiphers(secureCiphers);

// Fallback to the config directory if no cert settings
auto configdir = GetConfDir();
while (configdir.endsWith("/"))
configdir.chop(1);
configdir.append(QStringLiteral("/certificates/"));

auto hostKeyPath = gCoreContext->GetSetting("hostSSLKey", "");
if (hostKeyPath.isEmpty())
hostKeyPath = configdir + "key.pem";

QFile hostKeyFile(hostKeyPath);
if (!hostKeyFile.exists() || !hostKeyFile.open(QIODevice::ReadOnly))
{
LOG(VB_GENERAL, LOG_WARNING, LOC +
QString("SSL Host key file (%1) does not exist or is not readable").arg(hostKeyPath));
return false;
}

auto rawHostKey = hostKeyFile.readAll();
auto hostKey = QSslKey(rawHostKey, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey);
if (!hostKey.isNull())
{
Config.setPrivateKey(hostKey);
}
else
{
LOG(VB_GENERAL, LOG_ERR, LOC + QString("Unable to load host key from file (%1)").arg(hostKeyPath));
return false;
}

auto hostCertPath = gCoreContext->GetSetting("hostSSLCertificate", "");
if (hostCertPath.isEmpty())
hostCertPath = configdir + "cert.pem";

QSslCertificate hostCert;
auto certList = QSslCertificate::fromPath(hostCertPath);
if (!certList.isEmpty())
hostCert = certList.first();

if (!hostCert.isNull())
{
if (hostCert.effectiveDate() > QDateTime::currentDateTime())
{
LOG(VB_GENERAL, LOG_ERR, LOC + QString("Host certificate start date in future (%1)").arg(hostCertPath));
return false;
}

if (hostCert.expiryDate() < QDateTime::currentDateTime())
{
LOG(VB_GENERAL, LOG_ERR, LOC + QString("Host certificate has expired (%1)").arg(hostCertPath));
return false;
}

Config.setLocalCertificate(hostCert);
}
else
{
LOG(VB_GENERAL, LOG_ERR, LOC + QString("Unable to load host cert from file (%1)").arg(hostCertPath));
return false;
}

auto caCertPath = gCoreContext->GetSetting("caSSLCertificate", "");
auto CACertList = QSslCertificate::fromPath(caCertPath);
if (!CACertList.isEmpty())
{
Config.setCaCertificates(CACertList);
}
else if (!caCertPath.isEmpty())
{
// Only warn if a path was actually configured, this isn't an error otherwise
LOG(VB_GENERAL, LOG_ERR, LOC + QString("Unable to load CA cert file (%1)").arg(caCertPath));
}
return true;
}

void MythHTTPS::InitSSLSocket(QSslSocket *Socket, QSslConfiguration& Config)
{
if (!Socket)
return;

auto Encrypted = [](const QSslSocket* SslSocket)
{
LOG(VB_HTTP, LOG_INFO, LOC + "Socket encrypted");
if (SslSocket)
LOG(VB_HTTP, LOG_INFO, LOC + QString("Cypher: %1").arg(SslSocket->sessionCipher().name()));
};

auto SSLErrors = [](const QList<QSslError>& Errors)
{
for (const auto & error : Errors)
LOG(VB_GENERAL, LOG_INFO, LOC + QString("SslError: %1").arg(error.errorString()));
};

QObject::connect(Socket, &QSslSocket::encrypted, std::bind(Encrypted, Socket));
QObject::connect(Socket, QOverload<const QList<QSslError> &>::of(&QSslSocket::sslErrors), SSLErrors);
Socket->setSslConfiguration(Config);
Socket->startServerEncryption();
}
#endif
19 changes: 19 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttps.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#ifndef MYTHHTTPS_H
#define MYTHHTTPS_H

#ifndef QT_NO_OPENSSL
#include <QSslConfiguration>
#include <QSslCipher>
#include <QSslKey>
#endif

class MythHTTPS
{
#ifndef QT_NO_OPENSSL
public:
static bool InitSSLServer(QSslConfiguration& Config);
static void InitSSLSocket(QSslSocket* Socket, QSslConfiguration& Config);
#endif
};

#endif
584 changes: 584 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpserver.cpp

Large diffs are not rendered by default.

76 changes: 76 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpserver.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#ifndef MYTHHTTPSERVER_H
#define MYTHHTTPSERVER_H

// Qt
#include <QHostInfo>

// MythTV
#include "http/mythhttptypes.h"
#include "http/mythhttpthreadpool.h"

class BonjourRegister;

class MythHTTPServer : public MythHTTPThreadPool
{
Q_OBJECT

friend class MythHTTPInstance;

signals:
// Inbound
void EnableHTTP (bool Enable);
void AddPaths (const QStringList& Paths);
void RemovePaths (const QStringList& Paths);
void AddHandlers (const HTTPHandlers& Handlers);
void RemoveHandlers (const HTTPHandlers& Handlers);
void AddServices (const HTTPServices& Services);
void RemoveServices (const HTTPServices& Services);
// Outbound
void PathsChanged (const QStringList& Paths);
void HandlersChanged(const HTTPHandlers& Handlers);
void ServicesChanged(const HTTPServices& Services);
void OriginsChanged (const QStringList& Origins);
void HostsChanged (const QStringList& Hosts);
// Internal
void MasterResolved (QHostInfo Info);
void HostResolved (QHostInfo Info);

protected slots:
void newTcpConnection (qt_socket_fd_t Socket) override;
void EnableDisable (bool Enable);
void NewPaths (const QStringList& Paths);
void StalePaths (const QStringList& Paths);
void NewHandlers (const HTTPHandlers& Handlers);
void StaleHandlers (const HTTPHandlers& Handlers);
void NewServices (const HTTPServices& Services);
void StaleServices (const HTTPServices& Services);
void ResolveMaster (QHostInfo Info);
void ResolveHost (QHostInfo Info);

protected:
MythHTTPServer();
~MythHTTPServer() override;

private:
Q_DISABLE_COPY(MythHTTPServer)
void Init();
void Started(bool Tcp, bool Ssl);
void Stopped();
void BuildHosts();
void BuildOrigins();
void DebugHosts();
void DebugOrigins();
bool ReservedPath(const QString& Path);
static QStringList BuildAddressList(QHostInfo& Info);

BonjourRegister* m_bonjour { nullptr };
BonjourRegister* m_bonjourSSL { nullptr };
int m_originLookups { 0 };
int m_hostLookups { 0 };
MythHTTPConfig m_config;
int m_masterStatusPort { 0 };
int m_masterSSLPort { 0 };
QString m_masterIPAddress { };
};

#endif
8 changes: 8 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpserverinstance.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// MythTV
#include "mythhttpserverinstance.h"


MythHTTPServerInstance::MythHTTPServerInstance()
{

}
11 changes: 11 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpserverinstance.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#ifndef MYTHHTTPSERVERINSTANCE_H
#define MYTHHTTPSERVERINSTANCE_H


class MythHTTPServerInstance
{
public:
MythHTTPServerInstance();
};

#endif // MYTHHTTPSERVERINSTANCE_H
164 changes: 164 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpservice.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// MythTV
#include "mythlogging.h"
#include "http/mythwsdl.h"
#include "http/mythhttpservice.h"
#include "http/mythhttprequest.h"
#include "http/mythhttpresponse.h"
#include "http/serialisers/mythserialiser.h"
#include "http/mythhttpencoding.h"
#include "http/mythhttpmetaservice.h"

#define LOC QString("HTTPService: ")

MythHTTPService::MythHTTPService(MythHTTPMetaService *MetaService)
: m_name(MetaService->m_name),
m_staticMetaService(MetaService)
{
}

MythHTTPService::~MythHTTPService()
{
// TODO Signal to clients that the service is closing?
}

/*! \brief Respond to a valid HTTPRequest
*
* \todo Error message always send an HTML version of the error message. This
* should probably be context specific.
*/
HTTPResponse MythHTTPService::HTTPRequest(HTTPRequest2 Request)
{
QString& method = Request->m_fileName;
if (method.isEmpty())
return nullptr;

// WSDL
if (method == "wsdl")
return MythWSDL::GetWSDL(Request, m_staticMetaService);

// Find the method
LOG(VB_HTTP, LOG_DEBUG, LOC + QString("Looking for method '%1'").arg(method));
HTTPMethodPtr handler = nullptr;
for (auto & [path, handle] : m_staticMetaService->m_slots)
if (path == method) { handler = handle; break; }

if (handler == nullptr)
{
// Should we just return not found here rather than falling through
// to all of the other handlers? Do we need other handlers?
LOG(VB_HTTP, LOG_DEBUG, LOC + "Failed to find method");
return nullptr;
}

// Sanity check type count (handler should have the return type at least)
if (handler->m_types.size() < 1)
return nullptr;

// Handle options
Request->m_allowed = handler->m_requestTypes;
if (HTTPResponse options = MythHTTPResponse::HandleOptions(Request))
return options;

// Parse the parameters and match against those expected by the method.
// As for the old code, this allows parameters to be missing and they will
// thus be allocated a default/null/value.
size_t typecount = std::min(handler->m_types.size(), 100UL);

// Build parameters list
// Note: We allow up to 100 args but anything above Q_METAMETHOD_INVOKE_MAX_ARGS
// will be ignored
std::array<void*, 100> param { nullptr};
std::array<int, 100> types { 0 };

// Return type
param[0] = handler->m_types[0] == 0 ? nullptr : QMetaType::create(handler->m_types[0]);
types[0] = handler->m_types[0];

// Parameters
// Iterate over the method's parameters and search for the incoming values...
size_t count = 1;
QString error;
while (count < typecount)
{
auto name = handler->m_names[count];
auto value = Request->m_queries.value(name, "");
auto type = handler->m_types[count];
types[count] = type;
// These should be filtered out in MythHTTPMetaMethod
if (type == 0)
{
error = QString("Unknown parameter type '%1'").arg(name);
break;
}

auto newparam = QMetaType::create(type);
param[count] = handler->CreateParameter(newparam, type, value);
count++;
}

HTTPResponse result = nullptr;
if (count == typecount)
{
// Invoke
if (qt_metacall(QMetaObject::InvokeMetaMethod, handler->m_index, param.data()) >= 0)
LOG(VB_GENERAL, LOG_WARNING, "qt_metacall error");

// Retrieve result
QVariant returnvalue = handler->CreateReturnValue(types[0], param[0]);
if (!returnvalue.isValid())
{
result = MythHTTPResponse::EmptyResponse(Request);
}
else
{
auto accept = MythHTTPEncoding::GetMimeTypes(MythHTTP::GetHeader(Request->m_headers, "accept"));
HTTPData content = MythSerialiser::Serialise(handler->m_returnTypeName, returnvalue, accept);
content->m_cacheType = HTTPETag | HTTPShortLife;
result = MythHTTPResponse::DataResponse(Request, content);

// If the return type is QObject* we need to cleanup
if (returnvalue.canConvert<QObject*>())
{
LOG(VB_GENERAL, LOG_INFO, LOC + "Deleting object");
auto * object = returnvalue.value<QObject*>();
delete object;
}
}
}

// Cleanup
for (size_t i = 0; i < typecount; ++i)
if ((param[i] != nullptr) && (types[i] != 0))
QMetaType::destroy(types[i], param[i]);

// Return the previous error
if (count != typecount)
{
LOG(VB_HTTP, LOG_ERR, LOC + error);
Request->m_status = HTTPBadRequest;
return MythHTTPResponse::ErrorResponse(Request, error);
}

// Valid result...
return result;
}

bool MythHTTPService::Subscribe()
{
return false;
}

void MythHTTPService::Unsubscribe()
{
}

QString& MythHTTPService::Name()
{
return m_name;
}

QVariantMap MythHTTPService::GetServiceDescription()
{
return {};
}

50 changes: 50 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpservice.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#ifndef MYTHHTTPSERVICE_H
#define MYTHHTTPSERVICE_H

// Qt
#include <QObject>

// MythTV
#include "http/mythhttprequest.h"
#include "http/mythhttpresponse.h"

class MythHTTPMetaService;

/*! \class MythHTTPService
*
* This is the base class for a service whose public signals and slots are exported
* as a publicly accessible web interface.
*
*/
class MBASE_PUBLIC MythHTTPService : public QObject
{
Q_OBJECT
Q_CLASSINFO("Subscribe", "methods=GET")
Q_CLASSINFO("Unsubscribe", "methods=GET")

public slots:
bool Subscribe();
void Unsubscribe();
QVariantMap GetServiceDescription();

public:
template<class T> static inline HTTPServicePtr Create() { return std::shared_ptr<MythHTTPService>(new T); }

explicit MythHTTPService(MythHTTPMetaService* MetaService);
~MythHTTPService() override;

virtual HTTPResponse HTTPRequest(HTTPRequest2 Request);
QString& Name();

protected:
QString m_name;
MythHTTPMetaService* m_staticMetaService { nullptr };
};

#define SERVICE_PROPERTY(Type, Name, name) \
Q_PROPERTY(Type Name READ Get##Name MEMBER m_##name USER true) \
public: \
Type Get##Name() const { return m_##name; } \
private: \
Type m_##name { };
#endif
25 changes: 25 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpservices.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// MythTV
#include "mythlogging.h"
#include "http/mythhttpservices.h"
#include "http/mythhttpmetaservice.h"

// This will be initialised in a thread safe manner on first use
Q_GLOBAL_STATIC_WITH_ARGS(MythHTTPMetaService, s_service, (HTTP_SERVICES_DIR, MythHTTPServices::staticMetaObject))

MythHTTPServices::MythHTTPServices()
: MythHTTPService(s_service)
{
}

void MythHTTPServices::UpdateServices(const HTTPServices& Services)
{
m_serviceList.clear();
for (const auto & service : Services)
m_serviceList.append(service.first);
emit ServiceListChanged(m_serviceList);
}

QStringList MythHTTPServices::GetServiceList()
{
return m_serviceList;
}
36 changes: 36 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpservices.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#ifndef MYTHHTTPSERVICES_H
#define MYTHHTTPSERVICES_H

// MythTV
#include "http/mythhttpservice.h"

class MythHTTPServices : public MythHTTPService
{
friend class MythHTTPSocket;

Q_OBJECT
Q_CLASSINFO("Version", "1.0.0")
Q_PROPERTY(QStringList ServiceList READ GetServiceList NOTIFY ServiceListChanged MEMBER m_serviceList)

signals:
void ServiceListChanged(const QStringList& ServiceList);

public slots:
QStringList GetServiceList();

public:
MythHTTPServices();
~MythHTTPServices() override = default;

protected slots:
void UpdateServices(const HTTPServices& Services);

protected:
QStringList m_serviceList;

private:
Q_DISABLE_COPY(MythHTTPServices)
};

#endif

607 changes: 607 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpsocket.cpp

Large diffs are not rendered by default.

76 changes: 76 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpsocket.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#ifndef MYTHHTTPSOCKET_H
#define MYTHHTTPSOCKET_H
// Qt
#include <QObject>
#include <QTimer>
#include <QAbstractSocket>
#include <QElapsedTimer>

// MythTV
#include "mythqtcompat.h"
#include "http/mythhttptypes.h"
#include "http/mythhttpparser.h"

class QTcpSocket;
class QSslSocket;
class MythWebSocket;

class MythHTTPSocket : public QObject
{
Q_OBJECT

signals:
void Finish();
void UpdateServices(const HTTPServices& Services);
void ThreadUpgraded(QThread* Thread);

public slots:
void PathsChanged (const QStringList& Paths);
void HandlersChanged (const HTTPHandlers& Handlers);
void ServicesChanged (const HTTPServices& Services);
void HostsChanged (const QStringList& Hosts);
void OriginsChanged (const QStringList& Origins);
void NewTextMessage (const StringPayload& Text);
void NewRawTextMessage(const DataPayloads& Payloads);
void NewBinaryMessage (const DataPayloads& Payloads);

public:
explicit MythHTTPSocket(qt_socket_fd_t Socket, bool SSL, const MythHTTPConfig& Config);
~MythHTTPSocket() override;
void Respond(HTTPResponse Response);
static void RespondDirect(qt_socket_fd_t Socket, HTTPResponse Response, const MythHTTPConfig& Config);

protected slots:
void Disconnected();
void Timeout();
void Read();
void Stop();
void Write(int64_t Written = 0);
void Error(QAbstractSocket::SocketError Error);

private:
Q_DISABLE_COPY(MythHTTPSocket)
void SetupWebSocket();

qt_socket_fd_t m_socketFD { 0 };
MythHTTPConfig m_config;
HTTPServicePtrs m_activeServices;
bool m_stopping { false };
QTcpSocket* m_socket { nullptr };
MythWebSocket* m_websocket { nullptr };
QString m_peer;
QTimer m_timer;
MythHTTPParser m_parser;
HTTPQueue m_queue;
int64_t m_totalToSend { 0 };
int64_t m_totalWritten { 0 };
int64_t m_totalSent { 0 };
QElapsedTimer m_writeTime;
HTTPData m_writeBuffer { nullptr };
MythHTTPConnection m_nextConnection { HTTPConnectionClose };
MythSocketProtocol m_protocol { ProtHTTP };
// WebSockets only
bool m_testSocket { false };
};

#endif
47 changes: 47 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpthread.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// MythTV
#include "mythlogging.h"
#include "http/mythhttpthreadpool.h"
#include "http/mythhttpserver.h"
#include "http/mythhttpsocket.h"
#include "http/mythhttpthread.h"

#define LOC (QString("%1: ").arg(objectName()))

MythHTTPThread::MythHTTPThread(MythHTTPServer* Server, const MythHTTPConfig &Config,
const QString& ThreadName, qt_socket_fd_t Socket, bool Ssl)
: MThread(ThreadName),
m_server(Server),
m_config(Config),
m_socketFD(Socket),
m_ssl(Ssl)
{
}

void MythHTTPThread::run()
{
RunProlog();
m_socket = new MythHTTPSocket(m_socketFD, m_ssl, m_config);
QObject::connect(m_server, &MythHTTPServer::PathsChanged, m_socket, &MythHTTPSocket::PathsChanged);
QObject::connect(m_server, &MythHTTPServer::HandlersChanged, m_socket, &MythHTTPSocket::HandlersChanged);
QObject::connect(m_server, &MythHTTPServer::ServicesChanged, m_socket, &MythHTTPSocket::ServicesChanged);
QObject::connect(m_server, &MythHTTPServer::HostsChanged, m_socket, &MythHTTPSocket::HostsChanged);
QObject::connect(m_server, &MythHTTPServer::OriginsChanged, m_socket, &MythHTTPSocket::OriginsChanged);
QObject::connect(m_socket, &MythHTTPSocket::ThreadUpgraded, m_server, &MythHTTPServer::ThreadUpgraded);
exec();
delete m_socket;
m_socket = nullptr;
RunEpilog();
}

/*! \brief Tell the socket to complete and disconnect.
*
* We use this mechanism as QThread::quit is a slot and we cannot use it to trigger
* disconnection (and finished is too late for our needs). So tell the socket to
* cleanup and close - and once the socket is disconnected it will trigger a call
* to QThread::quit().
*/
void MythHTTPThread::Quit()
{
if (m_socket)
emit m_socket->Finish();
}
33 changes: 33 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpthread.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#ifndef MYTHHTTPTHREAD_H
#define MYTHHTTPTHREAD_H

// MythTV
#include "mthread.h"
#include "mythqtcompat.h"
#include "http/mythhttptypes.h"

class MythHTTPSocket;
class MythHTTPServer;
class MythHTTPThreadPool;

class MythHTTPThread : public MThread
{
public:
MythHTTPThread(MythHTTPServer* Server, const MythHTTPConfig& Config,
const QString& ThreadName, qt_socket_fd_t Socket, bool Ssl);
void Quit();

protected:
void run() override;

private:
Q_DISABLE_COPY(MythHTTPThread)

MythHTTPServer* m_server { nullptr };
MythHTTPConfig m_config;
qt_socket_fd_t m_socketFD { 0 };
MythHTTPSocket* m_socket { nullptr };
bool m_ssl { false };
};

#endif
99 changes: 99 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpthreadpool.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// MythTV
#include "mythlogging.h"
#include "http/mythhttpthread.h"
#include "http/mythhttpthreadpool.h"

#define LOC QString("HTTPPool: ")

MythHTTPThreadPool::MythHTTPThreadPool()
{
// Number of connections processed concurrently
m_maxThreads = static_cast<size_t>(std::max(QThread::idealThreadCount() * 2, 4));

// Don't allow more connections than we can process, it causes browsers
// to open lots of new connections instead of reusing existing ones
setMaxPendingConnections(static_cast<int>(m_maxThreads));
LOG(VB_GENERAL, LOG_INFO, LOC + QString("Using maximum %1 threads").arg(m_maxThreads));
}

MythHTTPThreadPool::~MythHTTPThreadPool()
{
for (auto * thread : m_threads)
{
thread->Quit();
thread->wait();
delete thread;
}
}

size_t MythHTTPThreadPool::AvailableThreads() const
{
if (m_upgradedThreads.size() > m_threads.size())
LOG(VB_GENERAL, LOG_ERR, LOC + "Threadpool error: upgraded count is higher than total");
return m_maxThreads - (m_threads.size() - m_upgradedThreads.size());
}

size_t MythHTTPThreadPool::MaxThreads() const
{
return m_maxThreads;
}

size_t MythHTTPThreadPool::ThreadCount() const
{
return m_threads.size();
}

void MythHTTPThreadPool::AddThread(MythHTTPThread *Thread)
{
if (Thread)
m_threads.emplace_back(Thread);
}

void MythHTTPThreadPool::ThreadFinished()
{
auto * qthread = dynamic_cast<QThread*>(sender());
auto found = std::find_if(m_threads.cbegin(), m_threads.cend(),
[&qthread](MythHTTPThread* Thread) { return Thread->qthread() == qthread; });
if (found == m_threads.cend())
{
LOG(VB_GENERAL, LOG_ERR, LOC + "Unknown HTTP thread finished");
return;
}

// Remove from list of upgraded threads if necessary
auto upgraded = std::find_if(m_upgradedThreads.cbegin(), m_upgradedThreads.cend(),
[&qthread](MythHTTPThread* Thread) { return Thread->qthread() == qthread; });
if (upgraded != m_upgradedThreads.cend())
m_upgradedThreads.erase(upgraded);

LOG(VB_HTTP, LOG_INFO, LOC + QString("Deleting thread '%1'").arg((*found)->objectName()));
delete *found;
m_threads.erase(found);
}

void MythHTTPThreadPool::ThreadUpgraded(QThread* Thread)
{
auto found = std::find_if(m_threads.cbegin(), m_threads.cend(),
[&Thread](MythHTTPThread* QThread) { return QThread->qthread() == Thread; });
if (found == m_threads.cend())
{
LOG(VB_GENERAL, LOG_ERR, LOC + QString("Unknown thread '%1' upgraded").arg(Thread->objectName()));
return;
}

m_upgradedThreads.emplace_back(*found);
auto standard = m_threads.size() - m_upgradedThreads.size();
auto upgraded = m_upgradedThreads.size();
LOG(VB_HTTP, LOG_INFO, LOC + QString("Thread '%1' upgraded (Standard:%2 Upgraded:%3)")
.arg(Thread->objectName()).arg(standard).arg(upgraded));

// There is no reasonable way of limiting the number of upgraded sockets (i.e.
// websockets) as we have no way of knowing what the client intends to use the
// socket for when connecting. On the other hand, if we there are a sizeable
// number of valid clients - do we want to restrict the number of connections...
if (upgraded > m_maxThreads)
{
LOG(VB_GENERAL, LOG_WARNING, LOC + QString("%1 upgraded sockets - notional max is %2")
.arg(upgraded).arg(m_maxThreads));
}
}
34 changes: 34 additions & 0 deletions mythtv/libs/libmythbase/http/mythhttpthreadpool.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#ifndef MYTHHTTPTHREADPOOL_H
#define MYTHHTTPTHREADPOOL_H

// MythTV
#include "serverpool.h"
#include "mythqtcompat.h"

class MythHTTPThread;

class MythHTTPThreadPool : public ServerPool
{
Q_OBJECT

public:
MythHTTPThreadPool();
~MythHTTPThreadPool() override;

size_t AvailableThreads() const;
size_t MaxThreads() const;
size_t ThreadCount() const;
void AddThread(MythHTTPThread* Thread);

public slots:
void ThreadFinished();
void ThreadUpgraded(QThread* Thread);

private:
Q_DISABLE_COPY(MythHTTPThreadPool)
size_t m_maxThreads { 4 };
std::list<MythHTTPThread*> m_threads { };
std::list<MythHTTPThread*> m_upgradedThreads { };
};

#endif
Loading