| 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("") | ||
| { | ||
| } |
| 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 |
| 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("&", "&"); | ||
| 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; | ||
| } |
| 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 |
| 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; | ||
| } |
| 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 |
| 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(); | ||
| } |
| 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 |
| 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); | ||
| } | ||
|
|
| 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 |
| 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; | ||
| } |
| 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 |
| 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; | ||
| } |
| 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 |
| 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; | ||
| } |
| 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 |
| 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; | ||
| } |
| 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 |
| 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 |
| 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; | ||
|
|
||
| } |
| 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 |
| 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 |
| 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 |
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| // MythTV | ||
| #include "mythhttpserverinstance.h" | ||
|
|
||
|
|
||
| MythHTTPServerInstance::MythHTTPServerInstance() | ||
| { | ||
|
|
||
| } |
| 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 |
| 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 {}; | ||
| } | ||
|
|
| 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 |
| 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; | ||
| } |
| 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 | ||
|
|
| 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 |
| 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(); | ||
| } |
| 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 |
| 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)); | ||
| } | ||
| } |
| 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 |